1 Commits
v0.7.0 ... bats

Author SHA1 Message Date
Vidar Holen
ad1a0da954 Attempted bats @test support 2016-05-22 18:53:35 -07:00
57 changed files with 3630 additions and 9983 deletions

View File

@@ -1,82 +0,0 @@
#!/bin/bash
_cleanup(){
rm -rf dist shellcheck || true
}
build_linux() {
# Linux Docker image
name="$DOCKER_BASE"
DOCKER_BUILDS="$DOCKER_BUILDS $name"
docker build -t "$name:current" .
docker run "$name:current" --version
printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
docker run -v "$PWD:/mnt" "$name:current" myscript
# Copy static executable from docker image
id=$(docker create "$name:current")
docker cp "$id:/bin/shellcheck" "shellcheck"
docker rm "$id"
ls -l shellcheck
./shellcheck myscript
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-x86_64";
done
# Linux Alpine based Docker image
name="$DOCKER_BASE-alpine"
DOCKER_BUILDS="$DOCKER_BUILDS $name"
sed -e '/DELETE-MARKER/,$d' Dockerfile > Dockerfile.alpine
docker build -f Dockerfile.alpine -t "$name:current" .
docker run "$name:current" sh -c 'shellcheck --version'
_cleanup
}
build_aarch64() {
# Linux aarch64 static executable
docker run -v "$PWD:/mnt" koalaman/aarch64-builder 'buildsc'
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-aarch64"
done
}
build_armv6hf() {
# Linux armv6hf static executable
docker run -v "$PWD:/mnt" koalaman/armv6hf-builder -c 'compile-shellcheck'
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-armv6hf";
done
_cleanup
}
build_windows() {
# Windows .exe
docker run --user="$UID" -v "$PWD:/appdata" koalaman/winghc cuib
for tag in $TAGS
do
cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe";
done
_cleanup
}
build_osx() {
# Darwin x86_64 static executable
brew install cabal-install pandoc gnu-tar
sudo ln -s /usr/local/bin/gsha512sum /usr/local/bin/sha512sum
sudo ln -s /usr/local/bin/gtar /usr/local/bin/tar
export PATH="/usr/local/bin:$PATH"
cabal update
cabal install --dependencies-only
cabal build shellcheck
for tag in $TAGS
do
cp "dist/build/shellcheck/shellcheck" "deploy/shellcheck-$tag.darwin-x86_64";
done
_cleanup
}

View File

@@ -1,6 +0,0 @@
*
!LICENSE
!Setup.hs
!ShellCheck.cabal
!shellcheck.hs
!src

2
.ghci
View File

@@ -1 +1 @@
:set -idist/build/autogen -isrc
:set -idist/build/autogen

View File

@@ -1,27 +0,0 @@
#### For bugs
- Rule Id (if any, e.g. SC1000):
- My shellcheck version (`shellcheck --version` or "online"):
- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
- [ ] I tried on shellcheck.net and verified that this is still a problem on the latest commit
#### For new checks and feature suggestions
- [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this
- [ ] I searched through https://github.com/koalaman/shellcheck/issues and didn't find anything related
#### Here's a snippet or screenshot that shows the problem:
```sh
#!/your/interpreter
your script here
```
#### Here's what shellcheck currently says:
#### Here's what I wanted or expected to see:

9
.gitignore vendored
View File

@@ -1,4 +1,4 @@
# Created by https://www.gitignore.io
# Created by http://www.gitignore.io
### Haskell ###
dist
@@ -13,10 +13,3 @@ cabal-dev
cabal.sandbox.config
cabal.config
.stack-work
### Snap ###
/snap/.snapcraft/
/stage/
/parts/
/prime/
*.snap

View File

@@ -1,65 +0,0 @@
#!/bin/bash
# This script packages up Travis compiled binaries
set -ex
shopt -s nullglob
cd deploy
cp ../LICENSE LICENSE.txt
sed -e $'s/$/\r/' > README.txt << END
This is a precompiled ShellCheck binary.
https://www.shellcheck.net/
ShellCheck is a static analysis tool for shell scripts.
It's licensed under the GNU General Public License v3.0.
Information and source code is available on the website.
This binary was compiled on $(date -u).
====== Latest commits ======
$(git log -n 3)
END
for file in ./*.exe
do
zip "${file%.*}.zip" README.txt LICENSE.txt "$file"
done
for file in *.linux-x86_64
do
base="${file%.*}"
cp "$file" "shellcheck"
tar -cJf "$base.linux.x86_64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in *.linux-aarch64
do
base="${file%.*}"
cp "$file" "shellcheck"
tar -cJf "$base.linux.aarch64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in *.linux-armv6hf
do
base="${file%.*}"
cp "$file" "shellcheck"
tar -cJf "$base.linux.armv6hf.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in *.darwin-x86_64
do
base="${file%.*}"
cp "$file" "shellcheck"
tar -cJf "$base.darwin.x86_64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in ./*
do
sha512sum "$file" > "$file.sha512sum"
done

View File

@@ -1,14 +0,0 @@
# In 2015, cabal-install had a http bug triggered when proxies didn't keep
# the connection open. This version made it into Ubuntu Xenial as used by
# Snapcraft. In June 2018, Snapcraft's proxy started triggering this bug.
#
# https://bugs.launchpad.net/launchpad-buildd/+bug/1797809
#
# Workaround: add more proxy
visible_hostname localhost
http_port 8888
cache_peer 10.10.10.1 parent 8222 0 no-query default
cache_peer_domain localhost !.internal
http_access allow all

View File

@@ -1,66 +0,0 @@
sudo: required
language: sh
services:
- docker
matrix:
include:
- os: linux
env: BUILD=linux
- os: linux
env: BUILD=windows
- os: linux
env: BUILD=armv6hf
- os: linux
env: BUILD=aarch64
- os: osx
env: BUILD=osx
before_install: |
DOCKER_BASE="$DOCKER_USERNAME/shellcheck"
DOCKER_BUILDS=""
TAGS=""
test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
echo "Tags are $TAGS"
script:
- mkdir -p deploy
- source ./.compile_binaries
- ./striptests
- set -x; build_"$BUILD"; set +x;
- ./.prepare_deploy
after_success: |
if [ "$BUILD" = "linux" ]; then
docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
for repo in $DOCKER_BUILDS; do
for tag in $TAGS; do
echo "Deploying $repo:current as $repo:$tag...";
docker tag "$repo:current" "$repo:$tag" || exit 1;
docker push "$repo:$tag" || exit 1;
done;
done;
fi
after_failure: |
id
pwd
df -h
find . -name '*.log' -type f -exec grep "" /dev/null {} +
find . -ls
deploy:
provider: gcs
skip_cleanup: true
access_key_id: GOOG7MDN7WEH6IIGBDCA
secret_access_key:
secure: Bcx2cT0/E2ikj7sdamVq52xlLZF9dz9ojGPtoKfPyQhkkZa+McVI4xgUSuyyoSxyKj77sofx2y8m6PJYYumT4g5hREV1tfeUkl0J2DQFMbGDYEt7kxVkXCxojNvhHwTzLFv0ezstrxWWxQm81BfQQ4U9lggRXtndAP4czZnOeHPINPSiue1QNwRAEw05r5UoIUJXy/5xyUrjIxn381pAs+gJqP2COeN9kTKYH53nS/AAws29RprfZFnPlo7xxWmcjRcdS5KPdGXI/c6tQp5zl2iTh510VC1PN2w1Wvnn/oNWhiNdqPyVDsojIX5+sS3nejzJA+KFMxXSBlyXIY3wPpS/MdscU79X6Q5f9ivsFfsm7gNBmxHUPNn0HAvU4ROT/CCE9j6jSbs5PC7QBo3CK4++jxAwE/pd9HUc2rs3k0ofx3rgveJ7txpy5yPKfwIIBi98kVKlC4w7dLvNTOfjW1Imt2yH87XTfsE0UIG9st1WII6s4l/WgBx2GuwKdt6+3QUYiAlCFckkxWi+fAvpHZUEL43Qxub5fN+ZV7Zib1n7opchH4QKGBb6/y0WaDCmtCfu0lppoe/TH6saOTjDFj67NJSElK6ZDxGZ3uw4R+ret2gm6WRKT2Oeub8J33VzSa7VkmFpMPrAAfPa9N1Z4ewBLoTmvxSg2A0dDrCdJio=
bucket: shellcheck
local-dir: deploy
on:
repo: koalaman/shellcheck
all_branches: true

View File

@@ -1,394 +0,0 @@
## v0.7.0 - 2019-07-28
### Added
- Precompiled binaries for macOS and Linux aarch64
- Preliminary support for fix suggestions
- New `-f diff` unified diff format for auto-fixes
- Files containing Bats tests can now be checked
- Directory wide directives can now be placed in a `.shellcheckrc`
- Optional checks: Use `--list-optional` to show a list of tests,
Enable with `-o` flags or `enable=name` directives
- Source paths: Use `-P dir1:dir2` or a `source-path=dir1` directive
to specify search paths for sourced files.
- json1 format like --format=json but treats tabs as single characters
- Recognize FLAGS variables created by the shflags library.
- Site-specific changes can now be made in Custom.hs for ease of patching
- SC2154: Also warn about unassigned uppercase variables (optional)
- SC2252: Warn about `[ $a != x ] || [ $a != y ]`, similar to SC2055
- SC2251: Inform about ineffectual ! in front of commands
- SC2250: Warn about variable references without braces (optional)
- SC2249: Warn about `case` with missing default case (optional)
- SC2248: Warn about unquoted variables without special chars (optional)
- SC2247: Warn about $"(cmd)" and $"{var}"
- SC2246: Warn if a shebang's interpreter ends with /
- SC2245: Warn that Ksh ignores all but the first glob result in `[`
- SC2243/SC2244: Suggest using explicit -n for `[ $foo ]` (optional)
- SC1135: Suggest not ending double quotes just to make $ literal
### Changed
- If a directive or shebang is not specified, a `.bash/.bats/.dash/.ksh`
extension will be used to infer the shell type when present.
- Disabling SC2120 on a function now disables SC2119 on call sites
### Fixed
- SC2183 no longer warns about missing printf args for `%()T`
## v0.6.0 - 2018-12-02
### Added
- Command line option --severity/-S for filtering by minimum severity
- Command line option --wiki-link-count/-W for showing wiki links
- SC2152/SC2151: Warn about bad `exit` values like `1234` and `"foo"`
- SC2236/SC2237: Suggest -n/-z instead of ! -z/-n
- SC2238: Warn when redirecting to a known command name, e.g. ls > rm
- SC2239: Warn if the shebang is not an absolute path, e.g. #!bin/sh
- SC2240: Warn when passing additional arguments to dot (.) in sh/dash
- SC1133: Better diagnostics when starting a line with |/||/&&
### Changed
- Most warnings now have useful end positions
- SC1117 about unknown double-quoted escape sequences has been retired
### Fixed
- SC2021 no longer triggers for equivalence classes like `[=e=]`
- SC2221/SC2222 no longer mistriggers on fall-through case branches
- SC2081 about glob matches in `[ .. ]` now also triggers for `!=`
- SC2086 no longer warns about spaces in `$#`
- SC2164 no longer suggests subshells for `cd ..; cmd; cd ..`
- `read -a` is now correctly considered an array assignment
- SC2039 no longer warns about LINENO now that it's POSIX
## v0.5.0 - 2018-05-31
### Added
- SC2233/SC2234/SC2235: Suggest removing or replacing (..) around tests
- SC2232: Warn about invalid arguments to sudo
- SC2231: Suggest quoting expansions in for loop globs
- SC2229: Warn about 'read $var'
- SC2227: Warn about redirections in the middle of 'find' commands
- SC2224/SC2225/SC2226: Warn when using mv/cp/ln without a destination
- SC2223: Quote warning specific to `: ${var=value}`
- SC1131: Warn when using `elseif` or `elsif`
- SC1128: Warn about blanks/comments before shebang
- SC1127: Warn about C-style comments
### Fixed
- Annotations intended for a command's here documents now work
- Escaped characters inside groups in =~ regexes now parse
- Associative arrays are now respected in arithmetic contexts
- SC1087 about `$var[@]` now correctly triggers on any index
- Bad expansions in here documents are no longer ignored
- FD move operations like {fd}>1- now parse correctly
### Changed
- Here docs are now terminated as per spec, rather than by presumed intent
- SC1073: 'else if' is now parsed correctly and not like 'elif'
- SC2163: 'export $name' can now be silenced with 'export ${name?}'
- SC2183: Now warns when printf arg count is not a multiple of format count
## v0.4.7 - 2017-12-08
### Added
- Statically linked binaries for Linux and Windows (see README.md)!
- `-a` flag to also include warnings in `source`d files
- SC2221/SC2222: Warn about overridden case branches
- SC2220: Warn about unhandled error cases in getopt loops
- SC2218: Warn when using functions before they're defined
- SC2216/SC2217: Warn when piping/redirecting to mv/cp and other non-readers
- SC2215: Warn about commands starting with leading dash
- SC2214: Warn about superfluous getopt flags
- SC2213: Warn about unhandled getopt flags
- SC2212: Suggest `false` over `[ ]`
- SC2211: Warn when using a glob as a command name
- SC2210: Warn when redirecting to an integer, e.g. `foo 1>2`
- SC2206/SC2207: Suggest alternatives when using word splitting in arrays
- SC1117: Warn about double quoted, undefined backslash sequences
- SC1113/SC1114/SC1115: Recognized more malformed shebangs
### Fixed
- `[ -v foo ]` no longer warns if `foo` is undefined
- SC2037 is now suppressed by quotes, e.g. `PAGER="cat" man foo`
- Ksh nested array declarations now parse correctly
- Parameter Expansion without colons are now recognized, e.g. `${foo+bar}`
- The `lastpipe` option is now respected with regard to subshell warnings
- `\(` is now respected for grouping in `[`
- Leading `\` is now ignored for commands, to allow alias suppression
- Comments are now allowed after directives to e.g. explain 'disable'
## v0.4.6 - 2017-03-26
### Added
- SC2204/SC2205: Warn about `( -z foo )` and `( foo -eq bar )`
- SC2200/SC2201: Warn about brace expansion in [/[[
- SC2198/SC2199: Warn about arrays in [/[[
- SC2196/SC2197: Warn about deprected egrep/fgrep
- SC2195: Warn about unmatchable case branches
- SC2194: Warn about constant 'case' statements
- SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables
- SC2188/SC2189: Warn about redirections without commands
- SC2186: Warn about deprecated `tempfile`
- SC1109: Warn when finding `&amp;`/`&gt;`/`&lt;` unquoted
- SC1108: Warn about missing spaces in `[ var= foo ]`
### Changed
- All files are now read as UTF-8 with lenient latin1 fallback, ignoring locale
- Unicode quotes are no longer considered syntactic quotes
- `ash` scripts will now be checked as `dash` with a warning
### Fixed
- `-c` no longer suggested when using `grep -o | wc`
- Comments and whitespace are now allowed before filewide directives
- Here doc delimters with esoteric quoting like `foo""` are now handled
- SC2095 about `ssh` in while read loops is now suppressed when using `-n`
- `%(%Y%M%D)T` now recognized as a single formatter in `printf` checks
- `grep -F` now suppresses regex related suggestions
- Command name checks now recognize busybox applet names
## v0.4.5 - 2016-10-21
### Added
- A Docker build (thanks, kpankonen!)
- SC2185: Suggest explicitly adding path for `find`
- SC2184: Warn about unsetting globs (e.g. `unset foo[1]`)
- SC2183: Warn about `printf` with more formatters than variables
- SC2182: Warn about ignored arguments with `printf`
- SC2181: Suggest using command directly instead of `if [ $? -eq 0 ]`
- SC1106: Warn when using `test` operators in `(( 1 -eq 2 ))`
### Changed
- Unrecognized directives now causes a warning rather than parse failure.
### Fixed
- Indices in associative arrays are now parsed correctly
- Missing shebang warning squashed when specifying with a directive
- Ksh multidimensional arrays are now supported
- Variables in substring ${a:x:y} expansions now count as referenced
- SC1102 now also handles ambiguous `$((`
- Using `$(seq ..)` will no longer suggest quoting
- SC2148 (missing shebang) is now suppressed when using shell directives
- `[ a '>' b ]` is now recognized as being correctly escaped
## v0.4.4 - 2016-05-15
### Added
- Haskell Stack support (thanks, Arguggi!)
- SC2179/SC2178: Warn when assigning/appending strings to arrays
- SC1102: Warn about ambiguous `$(((`
- SC1101: Warn when \\ linebreaks have trailing spaces
### Changed
- Directives directly after the shebang now apply to the entire file
### Fixed
- `{$i..10}` is now flagged similar to `{1..$i}`
## v0.4.3 - 2016-01-13
### Fixed
- Build now works on GHC 7.6.3 as found on Debian Stable/Ubuntu LTS
## v0.4.2 - 2016-01-09
### Added
- First class support for the `dash` shell
- The `--color` flag similar to ls/grep's (thanks, haguenau!)
- SC2174: Warn about unexpected behavior of `mkdir -pm` (thanks, eatnumber1!)
- SC2172: Warn about non-portable use of signal numbers in `trap`
- SC2171: Warn about `]]` without leading `[[`
- SC2168: Warn about `local` outside functions
### Fixed
- Warnings about unchecked `cd` will no longer trigger with `set -e`
- `[ a -nt/-ot/-ef b ]` no longer warns about being constant
- Quoted test operators like `[ foo "<" bar ]` now parse
- Escaped quotes in backticks now parse correctly
## v0.4.1 - 2015-09-05
### Fixed
- Added missing files to Cabal, fixing the build
## v0.4.0 - 2015-09-05
### Added
- Support for following `source`d files
- Support for setting default flags in `SHELLCHECK_OPTS`
- An `--external-sources` flag for following arbitrary `source`d files
- A `source` directive to override the filename to `source`
- SC2166: Suggest using `[ p ] && [ q ]` over `[ p -a q ]`
- SC2165: Warn when nested `for` loops use the same variable name
- SC2164: Warn when using `cd` without checking that it succeeds
- SC2163: Warn about `export $var`
- SC2162: Warn when using `read` without `-r`
- SC2157: Warn about `[ "$var " ]` and similar never-empty string matches
### Fixed
- `cat -vnE file` and similar will no longer flag as UUOC
- Nested trinary operators in `(( ))` now parse correctly
- Ksh `${ ..; }` command expansions now parse
## v0.3.8 - 2015-06-20
### Changed
- ShellCheck's license has changed from AGPLv3 to GPLv3.
### Added
- SC2156: Warn about injecting filenames in `find -exec sh -c "{}" \;`
### Fixed
- Variables and command substitutions in brace expansions are now parsed
- ANSI colors are now disabled on Windows
- Empty scripts now parse
## v0.3.7 - 2015-04-16
### Fixed
- Build now works on GHC 7.10
- Use `regex-tdfa` over `regex-compat` since the latter crashes on OS X.
## v0.3.6 - 2015-03-28
### Added
- SC2155: Warn about masked return values in `export foo=$(exit 1)`
- SC2154: Warn when a lowercase variable is referenced but not assigned
- SC2152/SC2151: Warn about bad `return` values like `1234` and `"foo"`
- SC2150: Warn about `find -exec "shell command" \;`
### Fixed
- `coproc` is now supported
- Trinary operator now recognized in `((..))`
### Removed
- Zsh support has been removed
## v0.3.5 - 2014-11-09
### Added
- SC2148: Warn when not including a shebang
- SC2147: Warn about literal ~ in PATH
- SC1086: Warn about `$` in for loop variables, e.g. `for $i in ..`
- SC1084: Warn when the shebang uses `!#` instead of `#!`
### Fixed
- Empty and comment-only backtick expansions now parse
- Variables used in PS1/PROMPT\_COMMAND/trap now count as referenced
- ShellCheck now skips unreadable files and directories
- `-f gcc` on empty files no longer crashes
- Variables in $".." are now considered quoted
- Warnings about expansions in single quotes now include backticks
## v0.3.4 - 2014-07-08
### Added
- SC2146: Warn about precedence when combining `find -o` with actions
- SC2145: Warn when concatenating arrays and strings
### Fixed
- Case statements now support `;&` and `;;&`
- Indices in array declarations now parse correctly
- `let` expressions now parsed as arithmetic expressions
- Escaping is now respected in here documents
### Changed
- Completely drop Makefile in favor of Cabal (thanks rodrigosetti!)
## v0.3.3 - 2014-05-29
### Added
- SC2144: Warn when using globs in `[/[[`
- SC2143: Suggesting using `grep -q` over `[ "$(.. | grep)" ]`
- SC2142: Warn when referencing positional parameters in aliases
- SC2141: Warn about suspicious IFS assignments like `IFS="\n"`
- SC2140: Warn about bad embedded quotes like `echo "var="value""`
- SC2130: Warn when using `-eq` on strings
- SC2139: Warn about define time expansions in alias definitions
- SC2129: Suggest command grouping over `a >> log; b >> log; c >> log`
- SC2128: Warn when expanding arrays without an index
- SC2126: Suggest `grep -c` over `grep|wc`
- SC2123: Warn about accidentally overriding `$PATH`, e.g. `PATH=/my/dir`
- SC1083: Warn about literal `{/}` outside of quotes
- SC1082: Warn about UTF-8 BOMs
### Fixed
- SC2051 no longer triggers for `{1,$n}`, only `{1..$n}`
- Improved detection of single quoted `sed` variables, e.g. `sed '$s///'`
- Stop warning about single quoted variables in `PS1` and similar
- Support for Zsh short form loops, `=(..)`
### Removed
- SC1000 about unescaped lonely `$`, e.g. `grep "^foo$"`
## v0.3.2 - 2014-03-22
### Added
- SC2121: Warn about trying to `set` variables, e.g. `set var = value`
- SC2120/SC2119: Warn when a function uses `$1..` if none are ever passed
- SC2117: Warn when using `su` in interactive mode, e.g. `su foo; whoami`
- SC2116: Detect useless use of echo, e.g. `for i in $(echo $var)`
- SC2115/SC2114: Detect some catastrophic `rm -r "$empty/"` mistakes
- SC1081: Warn when capitalizing keywords like `While`
- SC1077: Warn when using acute accents instead of backticks
### Fixed
- Shells are now properly recognized in shebangs containing flags
- Stop warning about math on decimals in ksh/zsh
- Stop warning about decimal comparisons with `=`, e.g. `[ $version = 1.2 ]`
- Parsing of `|&`
- `${a[x]}` not counting as a reference of `x`
- `(( x[0] ))` not counting as a reference of `x`
## v0.3.1 - 2014-02-03
### Added
- The `-s` flag to specify shell dialect
- SC2105/SC2104: Warn about `break/continue` outside loops
- SC1076: Detect invalid `[/[[` arithmetic like `[ 1 + 2 = 3 ]`
- SC1075: Suggest using `elif` over `else if`
### Fixed
- Don't warn when comma separating elements in brace expansions
- Improved detection of single quoted `sed` variables, e.g. `sed '$d'`
- Parsing of arithmetic for loops using `{..}` instead of `do..done`
- Don't treat the last pipeline stage as a subshell in ksh/zsh
## v0.3.0 - 2014-01-19
### Added
- A man page (thanks Dridi!)
- GCC compatible error reporting (`shellcheck -f gcc`)
- CheckStyle compatible XML error reporting (`shellcheck -f checkstyle`)
- Error codes for each warning, e.g. SC1234
- Allow disabling warnings with `# shellcheck disable=SC1234`
- Allow disabling warnings with `--exclude`
- SC2103: Suggest using subshells over `cd foo; bar; cd ..`
- SC2102: Warn about duplicates in char ranges, e.g. `[10-15]`
- SC2101: Warn about named classes not inside a char range, e.g. `[:digit:]`
- SC2100/SC2099: Warn about bad math expressions like `i=i+5`
- SC2098/SC2097: Warn about `foo=bar echo $foo`
- SC2095: Warn when using `ssh`/`ffmpeg` in `while read` loops
- Better warnings for missing here doc tokens
### Fixed
- Don't warn when single quoting variables with `ssh/perl/eval`
- `${!var}` is now counted as a variable reference
### Removed
- Suggestions about using parameter expansion over basename
- The `jsoncheck` binary. Use `shellcheck -f json` instead.
## v0.2.0 - 2013-10-27
### Added
- Suggest `./*` instead of `*` when passing globs to commands
- Suggest `pgrep` over `ps | grep`
- Warn about unicode quotes
- Warn about assigned but unused variables
- Inform about client side expansion when using `ssh`
### Fixed
- CLI tool now uses exit codes and stderr canonically
- Parsing of extglobs containing empty patterns
- Parsing of bash style `eval foo=(bar)`
- Parsing of expansions in here documents
- Parsing of function names containing :+-
- Don't warn about `find|xargs` when using `-print0`
## v0.1.0 - 2013-07-23
### Added
- First release

View File

@@ -1,36 +0,0 @@
# Build-only image
FROM ubuntu:18.04 AS build
USER root
WORKDIR /opt/shellCheck
# Install OS deps
RUN apt-get update && apt-get install -y ghc cabal-install
# Install Haskell deps
# (This is a separate copy/run so that source changes don't require rebuilding)
COPY ShellCheck.cabal ./
RUN cabal update && cabal install --dependencies-only --ghc-options="-optlo-Os -split-sections"
# Copy source and build it
COPY LICENSE Setup.hs shellcheck.hs ./
COPY src src
RUN cabal build Paths_ShellCheck && \
ghc -optl-static -optl-pthread -isrc -idist/build/autogen --make shellcheck -split-sections -optc-Wl,--gc-sections -optlo-Os && \
strip --strip-all shellcheck
RUN mkdir -p /out/bin && \
cp shellcheck /out/bin/
# Resulting Alpine image
FROM alpine:latest
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
COPY --from=build /out /
# DELETE-MARKER (Remove everything below to keep the alpine image)
# Resulting ShellCheck image
FROM scratch
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
WORKDIR /mnt
COPY --from=build /out /
ENTRYPOINT ["/bin/shellcheck"]

18
LICENSE
View File

@@ -1,17 +1,7 @@
Employer mandated disclaimer:
I am providing code in the repository to you under an open source license.
Because this is my personal repository, the license you receive to my code is
from me and other individual contributors, and not my employer (Facebook).
- Vidar "koala_man" Holen
----
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
@@ -655,7 +645,7 @@ the "copyright" line and a pointer to where the full notice is found.
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
@@ -674,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/philosophy/why-not-lgpl.html>.
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

483
README.md
View File

@@ -1,76 +1,46 @@
[![Build Status](https://travis-ci.org/koalaman/shellcheck.svg?branch=master)](https://travis-ci.org/koalaman/shellcheck)
# ShellCheck - A shell script static analysis tool
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts:
![Screenshot of a terminal showing problematic shell script lines highlighted](doc/terminal.png)
![Screenshot of a terminal showing problematic shell script lines highlighted](doc/terminal.png).
The goals of ShellCheck are
* To point out and clarify typical beginner's syntax issues that cause a shell
to give cryptic error messages.
- To point out and clarify typical beginner's syntax issues
that cause a shell to give cryptic error messages.
* To point out and clarify typical intermediate level semantic problems that
cause a shell to behave strangely and counter-intuitively.
- To point out and clarify typical intermediate level semantic problems
that cause a shell to behave strangely and counter-intuitively.
* To point out subtle caveats, corner cases and pitfalls that may cause an
advanced user's otherwise working script to fail under future circumstances.
- To point out subtle caveats, corner cases and pitfalls that may cause an
advanced user's otherwise working script to fail under future circumstances.
See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for examples of what ShellCheck can help you identify!
## Table of Contents
* [How to use](#how-to-use)
* [On the web](#on-the-web)
* [From your terminal](#from-your-terminal)
* [In your editor](#in-your-editor)
* [In your build or test suites](#in-your-build-or-test-suites)
* [Installing](#installing)
* [Compiling from source](#compiling-from-source)
* [Installing Cabal](#installing-cabal)
* [Compiling ShellCheck](#compiling-shellcheck)
* [Running tests](#running-tests)
* [Gallery of bad code](#gallery-of-bad-code)
* [Quoting](#quoting)
* [Conditionals](#conditionals)
* [Frequently misused commands](#frequently-misused-commands)
* [Common beginner's mistakes](#common-beginners-mistakes)
* [Style](#style)
* [Data and typing errors](#data-and-typing-errors)
* [Robustness](#robustness)
* [Portability](#portability)
* [Miscellaneous](#miscellaneous)
* [Testimonials](#testimonials)
* [Ignoring issues](#ignoring-issues)
* [Reporting bugs](#reporting-bugs)
* [Contributing](#contributing)
* [Copyright](#copyright)
* [Other Resources](#other-resources)
## How to use
There are a variety of ways to use ShellCheck!
There are a number of ways to use ShellCheck!
### On the web
#### On the web
Paste a shell script on http://www.shellcheck.net for instant feedback.
Paste a shell script on <https://www.shellcheck.net> for instant feedback.
[ShellCheck.net](http://www.shellcheck.net) is always synchronized to the latest git commit, and is the simplest way to give ShellCheck a go. Tell your friends!
[ShellCheck.net](https://www.shellcheck.net) is always synchronized to the latest git commit, and is the easiest way to give ShellCheck a go. Tell your friends!
### From your terminal
#### From your terminal
Run `shellcheck yourscript` in your terminal for instant output, as seen above.
### In your editor
#### In your editor
You can see ShellCheck suggestions directly in a variety of editors.
* Vim, through [ALE](https://github.com/w0rp/ale), [Neomake](https://github.com/neomake/neomake), or [Syntastic](https://github.com/scrooloose/syntastic):
* Vim, through [Syntastic](https://github.com/scrooloose/syntastic):
![Screenshot of Vim showing inlined shellcheck feedback](doc/vim-syntastic.png).
![Screenshot of vim showing inlined shellcheck feedback](doc/vim-syntastic.png).
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck) or [Flymake](https://github.com/federicotdn/flymake-shellcheck):
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck):
![Screenshot of emacs showing inlined shellcheck feedback](doc/emacs-flycheck.png).
@@ -78,52 +48,14 @@ You can see ShellCheck suggestions directly in a variety of editors.
* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
* VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck).
* Most other editors, through [GCC error compatibility](shellcheck.1.md#user-content-formats).
### In your build or test suites
#### In your build or test suites
While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites.
It makes canonical use of exit codes, so you can just add a `shellcheck` command as part of the process.
For example, in a Makefile:
Use ShellCheck's exit code, or its [CheckStyle compatible XML output](shellcheck.1.md#user-content-formats). There's also a simple JSON output format for easy integration.
```Makefile
check-scripts:
# Fail if any of these files have warnings
shellcheck myscripts/*.sh
```
or in a Travis CI `.travis.yml` file:
```yaml
script:
# Fail if any of these files have warnings
- shellcheck myscripts/*.sh
```
Services and platforms that have ShellCheck pre-installed and ready to use:
* [Travis CI](https://travis-ci.org/)
* [Codacy](https://www.codacy.com/)
* [Code Climate](https://codeclimate.com/)
* [Code Factor](https://www.codefactor.io/)
Services and platforms with third party plugins:
* [SonarQube](https://www.sonarqube.org/) through [sonar-shellcheck-plugin](https://github.com/emerald-squad/sonar-shellcheck-plugin)
Most other services, including [GitLab](https://about.gitlab.com/), let you install
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
or by downloading and unpacking a [binary release](#installing-the-shellcheck-binary).
It's a good idea to manually install a specific ShellCheck version regardless. This avoids
any surprise build breaks when a new version with new warnings is published.
For customized filtering or reporting, ShellCheck can output simple JSON, CheckStyle compatible XML,
GCC compatible warnings as well as human readable text (with or without ANSI colors). See the
[Integration](https://github.com/koalaman/shellcheck/wiki/Integration) wiki page for more documentation.
## Installing
@@ -132,345 +64,223 @@ The easiest way to install ShellCheck locally is through your package manager.
On systems with Cabal (installs to `~/.cabal/bin`):
cabal update
cabal install ShellCheck
On systems with Stack (installs to `~/.local/bin`):
stack update
stack install ShellCheck
cabal install shellcheck
On Debian based distros:
apt-get install shellcheck
On Arch Linux based distros:
pacman -S shellcheck
or get the dependency free [shellcheck-static](https://aur.archlinux.org/packages/shellcheck-static/) from the AUR.
On Gentoo based distros:
emerge --ask shellcheck
On EPEL based distros:
yum -y install epel-release
yum install ShellCheck
On Fedora based distros:
dnf install ShellCheck
On FreeBSD:
pkg install hs-ShellCheck
On OS X with homebrew:
brew install shellcheck
On OpenBSD:
On OS X with MacPorts:
pkg_add shellcheck
port install shellcheck
On openSUSE
On openSUSE:Tumbleweed:
zypper in ShellCheck
Or use OneClickInstall - <https://software.opensuse.org/package/ShellCheck>
On other openSUSE distributions:
On Solus:
add OBS devel:languages:haskell repository from https://build.opensuse.org/project/repositories/devel:languages:haskell
eopkg install shellcheck
zypper ar http://download.opensuse.org/repositories/devel:/languages:/haskell/openSUSE_$(version)/devel:languages:haskell.repo
zypper in ShellCheck
On Windows (via [chocolatey](https://chocolatey.org/packages/shellcheck)):
or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
```cmd
C:\> choco install shellcheck
```
Or Windows (via [scoop](http://scoop.sh)):
```cmd
C:\> scoop install shellcheck
```
From Snap Store:
snap install --channel=edge shellcheck
From Docker Hub:
```sh
docker run --rm -v "$PWD:/mnt" koalaman/shellcheck:stable myscript
# Or :v0.4.7 for that version, or :latest for daily builds
```
or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend. It works exactly like a regular Alpine image, but has shellcheck preinstalled.
Alternatively, you can download pre-compiled binaries for the latest release here:
* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
* [Linux, armv6hf](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
* [Linux, aarch64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz) aka ARM64 (statically linked)
* [MacOS, x86_64](https://shellcheck.storage.googleapis.com/shellcheck-stable.darwin.x86_64.tar.xz)
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-stable.zip)
or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds.
Distro packages already come with a `man` page. If you are building from source, it can be installed with:
```console
pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1
sudo mv shellcheck.1 /usr/share/man/man1
```
### Travis CI
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
If you still want to do so in order to upgrade at your leisure or ensure you're
using the latest release, follow the steps below to install a binary version.
### Installing a pre-compiled binary
The pre-compiled binaries come in `tar.xz` files. To decompress them, make sure
`xz` is installed.
On Debian/Ubuntu/Mint, you can `apt install xz-utils`.
On Redhat/Fedora/CentOS, `yum -y install xz`.
A simple installer may do something like:
```bash
scversion="stable" # or "v0.4.7", or "latest"
wget -qO- "https://storage.googleapis.com/shellcheck/shellcheck-${scversion?}.linux.x86_64.tar.xz" | tar -xJv
cp "shellcheck-${scversion}/shellcheck" /usr/bin/
shellcheck --version
```
## Compiling from source
This section describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile.
### Installing Cabal
ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `brew`, `emerge`, `yum`, or `zypper`).
#### Installing Cabal
On MacOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source.
ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `yum`, `zypper` or `brew`).
$ brew install cabal-install
On MacPorts, the package is instead called `hs-cabal-install`, while native Windows users should install the latest version of the Haskell platform from <https://www.haskell.org/platform/>
On MacPorts, the package is instead called `hs-cabal-install`, while native Windows users should install the latest version of the Haskell platform from https://www.haskell.org/platform/
Verify that `cabal` is installed and update its dependency list with
$ cabal update
### Compiling ShellCheck
#### Compiling ShellCheck
`git clone` this repository, and `cd` to the ShellCheck source directory to build/install:
$ cabal install
Or if you intend to run the tests:
$ cabal install --enable-tests
This will compile ShellCheck and install it to your `~/.cabal/bin` directory.
Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
```sh
export PATH="$HOME/.cabal/bin:$PATH"
```
export PATH="$HOME/.cabal/bin:$PATH"
Log out and in again, and verify that your PATH is set up correctly:
```sh
$ which shellcheck
~/.cabal/bin/shellcheck
```
$ which shellcheck
~/.cabal/bin/shellcheck
On native Windows, the `PATH` should already be set up, but the system
may use a legacy codepage. In `cmd.exe`, `powershell.exe` and Powershell ISE,
make sure to use a TrueType font, not a Raster font, and set the active
codepage to UTF-8 (65001) with `chcp`:
```cmd
chcp 65001
```
> chcp 65001
Active code page: 65001
In Powershell ISE, you may need to additionally update the output encoding:
```powershell
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
```
> [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
### Running tests
#### Running tests
To run the unit test suite:
$ cabal test
## Gallery of bad code
## Gallery of bad code
So what kind of things does ShellCheck look for? Here is an incomplete list of detected issues.
### Quoting
#### Quoting
ShellCheck can recognize several types of incorrect quoting:
```sh
echo $1 # Unquoted variables
find . -name *.ogg # Unquoted find/grep patterns
rm "~/my file.txt" # Quoted tilde expansion
v='--verbose="true"'; cmd $v # Literal quotes in variables
for f in "*.ogg" # Incorrectly quoted 'for' loops
touch $@ # Unquoted $@
echo 'Don't forget to restart!' # Singlequote closed by apostrophe
echo 'Don\'t try this at home' # Attempting to escape ' in ''
echo 'Path is $PATH' # Variables in single quotes
trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap
```
echo $1 # Unquoted variables
find . -name *.ogg # Unquoted find/grep patterns
rm "~/my file.txt" # Quoted tilde expansion
v='--verbose="true"'; cmd $v # Literal quotes in variables
for f in "*.ogg" # Incorrectly quoted 'for' loops
touch $@ # Unquoted $@
echo 'Don't forget to restart!' # Singlequote closed by apostrophe
echo 'Don\'t try this at home' # Attempting to escape ' in ''
echo 'Path is $PATH' # Variables in single quotes
trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap
### Conditionals
#### Conditionals
ShellCheck can recognize many types of incorrect test statements.
```sh
[[ n != 0 ]] # Constant test expressions
[[ -e *.mpg ]] # Existence checks of globs
[[ $foo==0 ]] # Always true due to missing spaces
[[ -n "$foo " ]] # Always true due to literals
[[ $foo =~ "fo+" ]] # Quoted regex in =~
[ foo =~ re ] # Unsupported [ ] operators
[ $1 -eq "shellcheck" ] # Numerical comparison of strings
[ $n && $m ] # && in [ .. ]
[ grep -q foo file ] # Command without $(..)
[[ "$$file" == *.jpg ]] # Comparisons that can't succeed
(( 1 -lt 2 )) # Using test operators in ((..))
```
[[ n != 0 ]] # Constant test expressions
[[ -e *.mpg ]] # Existence checks of globs
[[ $foo==0 ]] # Always true due to missing spaces
[[ -n "$foo " ]] # Always true due to literals
[[ $foo =~ "fo+" ]] # Quoted regex in =~
[ foo =~ re ] # Unsupported [ ] operators
[ $1 -eq "shellcheck" ] # Numerical comparison of strings
[ $n && $m ] # && in [ .. ]
[ grep -q foo file ] # Command without $(..)
### Frequently misused commands
#### Frequently misused commands
ShellCheck can recognize instances where commands are used incorrectly:
```sh
grep '*foo*' file # Globs in regex contexts
find . -exec foo {} && bar {} \; # Prematurely terminated find -exec
sudo echo 'Var=42' > /etc/profile # Redirecting sudo
time --format=%s sleep 10 # Passing time(1) flags to time builtin
while read h; do ssh "$h" uptime # Commands eating while loop input
alias archive='mv $1 /backup' # Defining aliases with arguments
tr -cd '[a-zA-Z0-9]' # [] around ranges in tr
exec foo; echo "Done!" # Misused 'exec'
find -name \*.bak -o -name \*~ -delete # Implicit precedence in find
# find . -exec foo > bar \; # Redirections in find
f() { whoami; }; sudo f # External use of internal functions
```
grep '*foo*' file # Globs in regex contexts
find . -exec foo {} && bar {} \; # Prematurely terminated find -exec
sudo echo 'Var=42' > /etc/profile # Redirecting sudo
time --format=%s sleep 10 # Passing time(1) flags to time builtin
while read h; do ssh "$h" uptime # Commands eating while loop input
alias archive='mv $1 /backup' # Defining aliases with arguments
tr -cd '[a-zA-Z0-9]' # [] around ranges in tr
exec foo; echo "Done!" # Misused 'exec'
find -name \*.bak -o -name \*~ -delete # Implicit precedence in find
f() { whoami; }; sudo f # External use of internal functions
### Common beginner's mistakes
#### Common beginner's mistakes
ShellCheck recognizes many common beginner's syntax errors:
```sh
var = 42 # Spaces around = in assignments
$foo=42 # $ in assignments
for $var in *; do ... # $ in for loop variables
var$n="Hello" # Wrong indirect assignment
echo ${var$n} # Wrong indirect reference
var=(1, 2, 3) # Comma separated arrays
array=( [index] = value ) # Incorrect index initialization
echo $var[14] # Missing {} in array references
echo "Argument 10 is $10" # Positional parameter misreference
if $(myfunction); then ..; fi # Wrapping commands in $()
else if othercondition; then .. # Using 'else if'
f; f() { echo "hello world; } # Using function before definition
[ false ] # 'false' being true
if ( -f file ) # Using (..) instead of test
```
var = 42 # Spaces around = in assignments
$foo=42 # $ in assignments
for $var in *; do ... # $ in for loop variables
var$n="Hello" # Wrong indirect assignment
echo ${var$n} # Wrong indirect reference
var=(1, 2, 3) # Comma separated arrays
echo "Argument 10 is $10" # Positional parameter misreference
if $(myfunction); then ..; fi # Wrapping commands in $()
else if othercondition; then .. # Using 'else if'
### Style
#### Style
ShellCheck can make suggestions to improve style:
```sh
[[ -z $(find /tmp | grep mpg) ]] # Use grep -q instead
a >> log; b >> log; c >> log # Use a redirection block instead
echo "The time is `date`" # Use $() instead
cd dir; process *; cd ..; # Use subshells instead
echo $[1+2] # Use standard $((..)) instead of old $[]
echo $(($RANDOM % 6)) # Don't use $ on variables in $((..))
echo "$(date)" # Useless use of echo
cat file | grep foo # Useless use of cat
```
[[ -z $(find /tmp | grep mpg) ]] # Use grep -q instead
a >> log; b >> log; c >> log # Use a redirection block instead
echo "The time is `date`" # Use $() instead
cd dir; process *; cd ..; # Use subshells instead
echo $[1+2] # Use standard $((..)) instead of old $[]
echo $(($RANDOM % 6)) # Don't use $ on variables in $((..))
echo "$(date)" # Useless use of echo
cat file | grep foo # Useless use of cat
### Data and typing errors
#### Data and typing errors
ShellCheck can recognize issues related to data and typing:
```sh
args="$@" # Assigning arrays to strings
files=(foo bar); echo "$files" # Referencing arrays as strings
declare -A arr=(foo bar) # Associative arrays without index
printf "%s\n" "Arguments: $@." # Concatenating strings and arrays
[[ $# > 2 ]] # Comparing numbers as strings
var=World; echo "Hello " var # Unused lowercase variables
echo "Hello $name" # Unassigned lowercase variables
cmd | read bar; echo $bar # Assignments in subshells
cat foo | cp bar # Piping to commands that don't read
printf '%s: %s\n' foo # Mismatches in printf argument count
```
args="$@" # Assigning arrays to strings
files=(foo bar); echo "$files" # Referencing arrays as strings
printf "%s\n" "Arguments: $@." # Concatenating strings and arrays.
[[ $# > 2 ]] # Comparing numbers as strings
var=World; echo "Hello " var # Unused lowercase variables
echo "Hello $name" # Unassigned lowercase variables
cmd | read bar; echo $bar # Assignments in subshells
### Robustness
#### Robustness
ShellCheck can make suggestions for improving the robustness of a script:
```sh
rm -rf "$STEAMROOT/"* # Catastrophic rm
touch ./-l; ls * # Globs that could become options
find . -exec sh -c 'a && b {}' \; # Find -exec shell injection
printf "Hello $name" # Variables in printf format
for f in $(ls *.txt); do # Iterating over ls output
export MYVAR=$(cmd) # Masked exit codes
case $version in 2.*) :;; 2.6.*) # Shadowed case branches
```
rm -rf "$STEAMROOT/"* # Catastrophic rm
touch ./-l; ls * # Globs that could become options
find . -exec sh -c 'a && b {}' \; # Find -exec shell injection
printf "Hello $name" # Variables in printf format
for f in $(ls *.txt); do # Iterating over ls output
export MYVAR=$(cmd) # Masked exit codes
### Portability
#### Portability
ShellCheck will warn when using features not supported by the shebang. For example, if you set the shebang to `#!/bin/sh`, ShellCheck will warn about portability issues similar to `checkbashisms`:
```sh
echo {1..$n} # Works in ksh, but not bash/dash/sh
echo {1..10} # Works in ksh and bash, but not dash/sh
echo -n 42 # Works in ksh, bash and dash, undefined in sh
trap 'exit 42' sigint # Unportable signal spec
cmd &> file # Unportable redirection operator
read foo < /dev/tcp/host/22 # Unportable intercepted files
foo-bar() { ..; } # Undefined/unsupported function name
[ $UID = 0 ] # Variable undefined in dash/sh
local var=value # local is undefined in sh
time sleep 1 | sleep 5 # Undefined uses of 'time'
```
### Miscellaneous
echo {1..$n} # Works in ksh, but not bash/dash/sh
echo {1..10} # Works in ksh and bash, but not dash/sh
echo -n 42 # Works in ksh, bash and dash, undefined in sh
trap 'exit 42' sigint # Unportable signal spec
cmd &> file # Unportable redirection operator
read foo < /dev/tcp/host/22 # Unportable intercepted files
foo-bar() { ..; } # Undefined/unsupported function name
[ $UID = 0 ] # Variable undefined in dash/sh
local var=value # local is undefined in sh
#### Miscellaneous
ShellCheck recognizes a menagerie of other issues:
```sh
PS1='\e[0;32m\$\e[0m ' # PS1 colors not in \[..\]
PATH="$PATH:~/bin" # Literal tilde in $PATH
rm “file” # Unicode quotes
echo "Hello world" # Carriage return / DOS line endings
echo hello \ # Trailing spaces after \
var=42 echo $var # Expansion of inlined environment
#!/bin/bash -x -e # Common shebang errors
echo $((n/180*100)) # Unnecessary loss of precision
ls *[:digit:].txt # Bad character class globs
sed 's/foo/bar/' file > file # Redirecting to input
while getopts "a" f; do case $f in "b") # Unhandled getopts flags
```
PS1='\e[0;32m\$\e[0m ' # PS1 colors not in \[..\]
PATH="$PATH:~/bin" # Literal tilde in $PATH
rm “file” # Unicode quotes
echo "Hello world" # Carriage return / DOS line endings
var=42 echo $var # Expansion of inlined environment
#!/bin/bash -x -e # Common shebang errors
echo $((n/180*100)) # Unnecessary loss of precision
ls *[:digit:].txt # Bad character class globs
sed 's/foo/bar/' file > file # Redirecting to input
## Testimonials
@@ -479,37 +289,26 @@ while getopts "a" f; do case $f in "b") # Unhandled getopts flags
Alexander Tarasikov,
[via Twitter](https://twitter.com/astarasikov/status/568825996532707330)
## Ignoring issues
Issues can be ignored via environmental variable, command line, individually or globally within a file:
<https://github.com/koalaman/shellcheck/wiki/Ignore>
## Reporting bugs
Please use the GitHub issue tracker for any bugs or feature suggestions:
<https://github.com/koalaman/shellcheck/issues>
https://github.com/koalaman/shellcheck/issues
## Contributing
Please submit patches to code or documentation as GitHub pull requests! Check
out the [DevGuide](https://github.com/koalaman/shellcheck/wiki/DevGuide) on the
ShellCheck Wiki.
Please submit patches to code or documentation as GitHub pull requests!
Contributions must be licensed under the GNU GPLv3.
The contributor retains the copyright.
## Copyright
ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE).
Copyright 2012-2019, [Vidar 'koala_man' Holen](https://github.com/koalaman/) and contributors.
Copyright 2012-2015, Vidar 'koala_man' Holen and contributors.
Happy ShellChecking!
## Other Resources
* The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221).
* ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)!

View File

@@ -33,4 +33,4 @@ myPreSDist _ _ = do
putStrLn $ "pandoc exited with " ++ show result
return emptyHookedBuildInfo
where
pandoc_cmd = "pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1"
pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"

View File

@@ -1,12 +1,12 @@
Name: ShellCheck
Version: 0.7.0
Version: 0.4.4
Synopsis: Shell script analysis tool
License: GPL-3
License-file: LICENSE
Category: Static Analysis
Author: Vidar Holen
Maintainer: vidar@vidarholen.net
Homepage: https://www.shellcheck.net/
Homepage: http://www.shellcheck.net/
Build-Type: Custom
Cabal-Version: >= 1.8
Bug-reports: https://github.com/koalaman/shellcheck/issues
@@ -28,39 +28,20 @@ Extra-Source-Files:
shellcheck.1.md
-- built with a cabal sdist hook
shellcheck.1
-- convenience script for stripping tests
striptests
-- tests
test/shellcheck.hs
custom-setup
setup-depends:
base >= 4 && <5,
process >= 1.0 && <1.7,
Cabal >= 1.10 && <2.5
source-repository head
type: git
location: git://github.com/koalaman/shellcheck.git
library
hs-source-dirs: src
if impl(ghc < 8.0)
build-depends:
semigroups
build-depends:
aeson,
array,
-- GHC 7.6.3 (base 4.6.0.1) is buggy (#1131, #1119) in optimized mode.
-- Just disable that version entirely to fail fast.
base > 4.6.0.1 && < 5,
bytestring,
containers >= 0.5,
deepseq >= 1.4.0.0,
Diff >= 0.2.0,
directory >= 1.2.3.0,
base >= 4 && < 5,
containers,
directory,
json,
mtl >= 2.2.1,
filepath,
parsec,
regex-tdfa,
QuickCheck >= 2.7.4,
@@ -74,18 +55,12 @@ library
ShellCheck.AnalyzerLib
ShellCheck.Checker
ShellCheck.Checks.Commands
ShellCheck.Checks.Custom
ShellCheck.Checks.ShellSupport
ShellCheck.Data
ShellCheck.Fixer
ShellCheck.Formatter.Format
ShellCheck.Formatter.CheckStyle
ShellCheck.Formatter.Diff
ShellCheck.Formatter.GCC
ShellCheck.Formatter.JSON
ShellCheck.Formatter.JSON1
ShellCheck.Formatter.TTY
ShellCheck.Formatter.Quiet
ShellCheck.Interface
ShellCheck.Parser
ShellCheck.Regex
@@ -93,42 +68,29 @@ library
Paths_ShellCheck
executable shellcheck
if impl(ghc < 8.0)
build-depends:
semigroups
build-depends:
aeson,
array,
ShellCheck,
base >= 4 && < 5,
bytestring,
containers,
deepseq >= 1.4.0.0,
Diff >= 0.2.0,
directory >= 1.2.3.0,
directory,
json,
mtl >= 2.2.1,
filepath,
parsec >= 3.0,
QuickCheck >= 2.7.4,
parsec,
regex-tdfa,
ShellCheck
QuickCheck >= 2.7.4
main-is: shellcheck.hs
test-suite test-shellcheck
type: exitcode-stdio-1.0
build-depends:
aeson,
array,
ShellCheck,
base >= 4 && < 5,
bytestring,
containers,
deepseq >= 1.4.0.0,
Diff >= 0.2.0,
directory >= 1.2.3.0,
directory,
json,
mtl >= 2.2.1,
filepath,
parsec,
QuickCheck >= 2.7.4,
regex-tdfa,
ShellCheck
QuickCheck >= 2.7.4
main-is: test/shellcheck.hs

View File

@@ -1,8 +1,8 @@
{-
Copyright 2012-2019 Vidar Holen
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -15,51 +15,43 @@
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
module ShellCheck.AST where
import GHC.Generics (Generic)
import Control.Monad
import Control.Monad.Identity
import Control.DeepSeq
import Text.Parsec
import qualified ShellCheck.Regex as Re
import Prelude hiding (id)
newtype Id = Id Int deriving (Show, Eq, Ord, Generic, NFData)
data Id = Id Int deriving (Show, Eq, Ord)
data Quoted = Quoted | Unquoted deriving (Show, Eq)
data Dashed = Dashed | Undashed deriving (Show, Eq)
data AssignmentMode = Assign | Append deriving (Show, Eq)
newtype FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
newtype FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
data FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
newtype Root = Root Token
data Token =
TA_Binary Id String Token Token
| TA_Assignment Id String Token Token
| TA_Variable Id String [Token]
| TA_Expansion Id [Token]
| TA_Index Id Token
| TA_Sequence Id [Token]
| TA_Trinary Id Token Token Token
| TA_Unary Id String Token
| TC_And Id ConditionType String Token Token
| TC_Binary Id ConditionType String Token Token
| TC_Group Id ConditionType Token
| TC_Nullary Id ConditionType Token
| TC_Noary Id ConditionType Token
| TC_Or Id ConditionType String Token Token
| TC_Unary Id ConditionType String Token
| TC_Empty Id ConditionType
| T_AND_IF Id
| T_AndIf Id Token Token
| T_AndIf Id (Token) (Token)
| T_Arithmetic Id Token
| T_Array Id [Token]
| T_IndexedElement Id [Token] Token
-- Store the index as string, and parse as arithmetic or string later
| T_UnparsedIndex Id SourcePos String
| T_Assignment Id AssignmentMode String [Token] Token
| T_IndexedElement Id Token Token
| T_Assignment Id AssignmentMode String (Maybe Token) Token
| T_Backgrounded Id Token
| T_Backticked Id [Token]
| T_Bang Id
@@ -76,7 +68,7 @@ data Token =
| T_DSEMI Id
| T_Do Id
| T_DollarArithmetic Id Token
| T_DollarBraced Id Bool Token
| T_DollarBraced Id Token
| T_DollarBracket Id Token
| T_DollarDoubleQuoted Id [Token]
| T_DollarExpansion Id [Token]
@@ -104,7 +96,6 @@ data Token =
| T_IfExpression Id [([Token],[Token])] [Token]
| T_In Id
| T_IoFile Id Token Token
| T_IoDuplicate Id Token String
| T_LESSAND Id
| T_LESSGREAT Id
| T_Lbrace Id
@@ -114,14 +105,13 @@ data Token =
| T_NEWLINE Id
| T_NormalWord Id [Token]
| T_OR_IF Id
| T_OrIf Id Token Token
| T_ParamSubSpecialChar Id String -- e.g. '%' in ${foo%bar} or '/' in ${foo/bar/baz}
| T_OrIf Id (Token) (Token)
| T_Pipeline Id [Token] [Token] -- [Pipe separators] [Commands]
| T_ProcSub Id String [Token]
| T_Rbrace Id
| T_Redirecting Id [Token] Token
| T_Rparen Id
| T_Script Id Token [Token] -- Shebang T_Literal, followed by script.
| T_Script Id String [Token]
| T_Select Id
| T_SelectIn Id String [Token] [Token]
| T_Semi Id
@@ -137,17 +127,14 @@ data Token =
| T_Pipe Id String
| T_CoProc Id (Maybe String) Token
| T_CoProcBody Id Token
| T_Include Id Token
| T_SourceCommand Id Token Token
| T_Include Id Token Token -- . & source: SimpleCommand T_Script
| T_BatsTest Id Token Token
deriving (Show)
data Annotation =
DisableComment Integer
| EnableComment String
| SourceOverride String
| ShellOverride String
| SourcePath String
deriving (Show, Eq)
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
@@ -159,7 +146,7 @@ tokenEquals a b = kludge a == kludge b
instance Eq Token where
(==) = tokenEquals
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> m Token) -> Token -> m Token
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> Token) -> Token -> m Token
analyze f g i =
round
where
@@ -167,9 +154,14 @@ analyze f g i =
f t
newT <- delve t
g t
i newT
return . i $ newT
roundAll = mapM round
roundMaybe Nothing = return Nothing
roundMaybe (Just v) = do
s <- round v
return (Just s)
dl l v = do
x <- roundAll l
return $ v x
@@ -195,18 +187,14 @@ analyze f g i =
delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id
delve (T_DollarBracket id c) = d1 c $ T_DollarBracket id
delve (T_IoFile id op file) = d2 op file $ T_IoFile id
delve (T_IoDuplicate id op num) = d1 op $ \x -> T_IoDuplicate id x num
delve (T_HereString id word) = d1 word $ T_HereString id
delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v
delve (T_Assignment id mode var indices value) = do
a <- roundAll indices
delve (T_Assignment id mode var index value) = do
a <- roundMaybe index
b <- round value
return $ T_Assignment id mode var a b
delve (T_Array id t) = dl t $ T_Array id
delve (T_IndexedElement id indices t) = do
a <- roundAll indices
b <- round t
return $ T_IndexedElement id a b
delve (T_IndexedElement id t1 t2) = d2 t1 t2 $ T_IndexedElement id
delve (T_Redirecting id redirs cmd) = do
a <- roundAll redirs
b <- round cmd
@@ -253,7 +241,7 @@ analyze f g i =
delve (T_Function id a b name body) = d1 body $ T_Function id a b name
delve (T_Condition id typ token) = d1 token $ T_Condition id typ
delve (T_Extglob id str l) = dl l $ T_Extglob id str
delve (T_DollarBraced id braced op) = d1 op $ T_DollarBraced id braced
delve (T_DollarBraced id op) = d1 op $ T_DollarBraced id
delve (T_HereDoc id d q str l) = dl l $ T_HereDoc id d q str
delve (TC_And id typ str t1 t2) = d2 t1 t2 $ TC_And id typ str
@@ -261,7 +249,7 @@ analyze f g i =
delve (TC_Group id typ token) = d1 token $ TC_Group id typ
delve (TC_Binary id typ op lhs rhs) = d2 lhs rhs $ TC_Binary id typ op
delve (TC_Unary id typ op token) = d1 token $ TC_Unary id typ op
delve (TC_Nullary id typ token) = d1 token $ TC_Nullary id typ
delve (TC_Noary id typ token) = d1 token $ TC_Noary id typ
delve (TA_Binary id op t1 t2) = d2 t1 t2 $ TA_Binary id op
delve (TA_Assignment id op t1 t2) = d2 t1 t2 $ TA_Assignment id op
@@ -273,16 +261,14 @@ analyze f g i =
c <- round t3
return $ TA_Trinary id a b c
delve (TA_Expansion id t) = dl t $ TA_Expansion id
delve (TA_Variable id str t) = dl t $ TA_Variable id str
delve (TA_Index id t) = d1 t $ TA_Index id
delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns
delve (T_CoProc id var body) = d1 body $ T_CoProc id var
delve (T_CoProcBody id t) = d1 t $ T_CoProcBody id
delve (T_Include id script) = d1 script $ T_Include id
delve (T_SourceCommand id includer t_include) = d2 includer t_include $ T_SourceCommand id
delve (T_Include id includer script) = d2 includer script $ T_Include id
delve (T_BatsTest id name t) = d2 name t $ T_BatsTest id
delve t = return t
getId :: Token -> Id
getId t = case t of
T_AND_IF id -> id
T_OR_IF id -> id
@@ -323,13 +309,11 @@ getId t = case t of
T_NormalWord id _ -> id
T_DoubleQuoted id _ -> id
T_DollarExpansion id _ -> id
T_DollarBraced id _ _ -> id
T_DollarBraced id _ -> id
T_DollarArithmetic id _ -> id
T_BraceExpansion id _ -> id
T_ParamSubSpecialChar id _ -> id
T_DollarBraceCommandExpansion id _ -> id
T_IoFile id _ _ -> id
T_IoDuplicate id _ _ -> id
T_HereDoc id _ _ _ _ -> id
T_HereString id _ -> id
T_FdRedirect id _ _ -> id
@@ -362,13 +346,14 @@ getId t = case t of
TC_Group id _ _ -> id
TC_Binary id _ _ _ _ -> id
TC_Unary id _ _ _ -> id
TC_Nullary id _ _ -> id
TC_Noary id _ _ -> id
TA_Binary id _ _ _ -> id
TA_Assignment id _ _ _ -> id
TA_Unary id _ _ -> id
TA_Sequence id _ -> id
TA_Trinary id _ _ _ -> id
TA_Expansion id _ -> id
TA_Index id _ -> id
T_ProcSub id _ _ -> id
T_Glob id _ -> id
T_ForArithmetic id _ _ _ _ -> id
@@ -379,19 +364,12 @@ getId t = case t of
T_Pipe id _ -> id
T_CoProc id _ _ -> id
T_CoProcBody id _ -> id
T_Include id _ -> id
T_SourceCommand id _ _ -> id
T_UnparsedIndex id _ _ -> id
TC_Empty id _ -> id
TA_Variable id _ _ -> id
T_Include id _ _ -> id
T_BatsTest id _ _ -> id
blank :: Monad m => Token -> m ()
blank = const $ return ()
doAnalysis :: Monad m => (Token -> m ()) -> Token -> m Token
doAnalysis f = analyze f blank return
doStackAnalysis :: Monad m => (Token -> m ()) -> (Token -> m ()) -> Token -> m Token
doStackAnalysis startToken endToken = analyze startToken endToken return
doTransform :: (Token -> Token) -> Token -> Token
doTransform i = runIdentity . analyze blank blank (return . i)
doAnalysis f = analyze f blank id
doStackAnalysis startToken endToken = analyze startToken endToken id
doTransform i = runIdentity . analyze blank blank i

253
ShellCheck/ASTLib.hs Normal file
View File

@@ -0,0 +1,253 @@
{-
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
module ShellCheck.ASTLib where
import ShellCheck.AST
import Control.Monad
import Data.List
import Data.Maybe
-- Is this a type of loop?
isLoop t = case t of
T_WhileExpression {} -> True
T_UntilExpression {} -> True
T_ForIn {} -> True
T_ForArithmetic {} -> True
T_SelectIn {} -> True
_ -> False
-- Will this split into multiple words when used as an argument?
willSplit x =
case x of
T_DollarBraced {} -> True
T_DollarExpansion {} -> True
T_Backticked {} -> True
T_BraceExpansion {} -> True
T_Glob {} -> True
T_Extglob {} -> True
T_NormalWord _ l -> any willSplit l
_ -> False
isGlob (T_Extglob {}) = True
isGlob (T_Glob {}) = True
isGlob (T_NormalWord _ l) = any isGlob l
isGlob _ = False
-- Is this shell word a constant?
isConstant token =
case token of
T_NormalWord _ l -> all isConstant l
T_DoubleQuoted _ l -> all isConstant l
T_SingleQuoted _ _ -> True
T_Literal _ _ -> True
_ -> False
-- Is this an empty literal?
isEmpty token =
case token of
T_NormalWord _ l -> all isEmpty l
T_DoubleQuoted _ l -> all isEmpty l
T_SingleQuoted _ "" -> True
T_Literal _ "" -> True
_ -> False
-- Quick&lazy oversimplification of commands, throwing away details
-- and returning a list like ["find", ".", "-name", "${VAR}*" ].
oversimplify token =
case token of
(T_NormalWord _ l) -> [concat (concatMap oversimplify l)]
(T_DoubleQuoted _ l) -> [concat (concatMap oversimplify l)]
(T_SingleQuoted _ s) -> [s]
(T_DollarBraced _ _) -> ["${VAR}"]
(T_DollarArithmetic _ _) -> ["${VAR}"]
(T_DollarExpansion _ _) -> ["${VAR}"]
(T_Backticked _ _) -> ["${VAR}"]
(T_Glob _ s) -> [s]
(T_Pipeline _ _ [x]) -> oversimplify x
(T_Literal _ x) -> [x]
(T_SimpleCommand _ vars words) -> concatMap oversimplify words
(T_Redirecting _ _ foo) -> oversimplify foo
(T_DollarSingleQuoted _ s) -> [s]
(T_Annotation _ _ s) -> oversimplify s
-- Workaround for let "foo = bar" parsing
(TA_Sequence _ [TA_Expansion _ v]) -> concatMap oversimplify v
otherwise -> []
-- Turn a SimpleCommand foo -avz --bar=baz into args "a", "v", "z", "bar",
-- each in a tuple of (token, stringFlag). Non-flag arguments are added with
-- stringFlag == "".
getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
let tokenAndText = map (\x -> (x, concat $ oversimplify x)) args
(flagArgs, rest) = break (stopCondition . snd) tokenAndText
in
concatMap flag flagArgs ++ map (\(t, _) -> (t, "")) rest
where
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
flag (x, '-':args) = map (\v -> (x, [v])) args
flag (x, _) = [ (x, "") ]
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
-- Get all flags in a GNU way, up until --
getAllFlags = getFlagsUntil (== "--")
-- Get all flags in a BSD way, up until first non-flag argument
getLeadingFlags = getFlagsUntil (not . ("-" `isPrefixOf`))
-- Given a T_DollarBraced, return a simplified version of the string contents.
bracedString (T_DollarBraced _ l) = concat $ oversimplify l
bracedString _ = error "Internal shellcheck error, please report! (bracedString on non-variable)"
-- Is this an expansion of multiple items of an array?
isArrayExpansion t@(T_DollarBraced _ _) =
let string = bracedString t in
"@" `isPrefixOf` string ||
not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string
isArrayExpansion _ = False
-- Is it possible that this arg becomes multiple args?
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
where
f t@(T_DollarBraced _ _) =
let string = bracedString t in
"!" `isPrefixOf` string
f (T_DoubleQuoted _ parts) = any f parts
f (T_NormalWord _ parts) = any f parts
f _ = False
-- Is it certain that this word will becomes multiple words?
willBecomeMultipleArgs t = willConcatInAssignment t || f t
where
f (T_Extglob {}) = True
f (T_Glob {}) = True
f (T_BraceExpansion {}) = True
f (T_DoubleQuoted _ parts) = any f parts
f (T_NormalWord _ parts) = any f parts
f _ = False
-- This does token cause implicit concatenation in assignments?
willConcatInAssignment token =
case token of
t@(T_DollarBraced {}) -> isArrayExpansion t
(T_DoubleQuoted _ parts) -> any willConcatInAssignment parts
(T_NormalWord _ parts) -> any willConcatInAssignment parts
_ -> False
-- Maybe get the literal string corresponding to this token
getLiteralString :: Token -> Maybe String
getLiteralString = getLiteralStringExt (const Nothing)
-- Definitely get a literal string, skipping over all non-literals
onlyLiteralString :: Token -> String
onlyLiteralString = fromJust . getLiteralStringExt (const $ return "")
-- Maybe get a literal string, but only if it's an unquoted argument.
getUnquotedLiteral (T_NormalWord _ list) =
liftM concat $ mapM str list
where
str (T_Literal _ s) = return s
str _ = Nothing
getUnquotedLiteral _ = Nothing
-- Maybe get the literal string of this token and any globs in it.
getGlobOrLiteralString = getLiteralStringExt f
where
f (T_Glob _ str) = return str
f _ = Nothing
-- Maybe get the literal value of a token, using a custom function
-- to map unrecognized Tokens into strings.
getLiteralStringExt :: (Token -> Maybe String) -> Token -> Maybe String
getLiteralStringExt more = g
where
allInList = liftM concat . mapM g
g (T_DoubleQuoted _ l) = allInList l
g (T_DollarDoubleQuoted _ l) = allInList l
g (T_NormalWord _ l) = allInList l
g (TA_Expansion _ l) = allInList l
g (T_SingleQuoted _ s) = return s
g (T_Literal _ s) = return s
g x = more x
-- Is this token a string literal?
isLiteral t = isJust $ getLiteralString t
-- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz]
getWordParts (T_NormalWord _ l) = concatMap getWordParts l
getWordParts (T_DoubleQuoted _ l) = l
getWordParts other = [other]
-- Return a list of NormalWords that would result from brace expansion
braceExpand (T_NormalWord id list) = take 1000 $ do
items <- mapM part list
return $ T_NormalWord id items
where
part (T_BraceExpansion id items) = do
item <- items
braceExpand item
part x = return x
-- Maybe get the command name of a token representing a command
getCommandName t =
case t of
T_Redirecting _ _ w -> getCommandName w
T_SimpleCommand _ _ (w:_) -> getLiteralString w
T_Annotation _ _ t -> getCommandName t
otherwise -> Nothing
-- Get the basename of a token representing a command
getCommandBasename = liftM basename . getCommandName
where
basename = reverse . takeWhile (/= '/') . reverse
isAssignment t =
case t of
T_Redirecting _ _ w -> isAssignment w
T_SimpleCommand _ (w:_) [] -> True
T_Assignment {} -> True
T_Annotation _ _ w -> isAssignment w
otherwise -> False
isOnlyRedirection t =
case t of
T_Pipeline _ _ [x] -> isOnlyRedirection x
T_Annotation _ _ w -> isOnlyRedirection w
T_Redirecting _ (_:_) c -> isOnlyRedirection c
T_SimpleCommand _ [] [] -> True
otherwise -> False
isFunction t = case t of T_Function {} -> True; _ -> False
-- Get the list of commands from tokens that contain them, such as
-- the body of while loops and if statements.
getCommandSequences t =
case t of
T_Script _ _ cmds -> [cmds]
T_BraceGroup _ cmds -> [cmds]
T_Subshell _ cmds -> [cmds]
T_WhileExpression _ _ cmds -> [cmds]
T_UntilExpression _ _ cmds -> [cmds]
T_ForIn _ _ _ cmds -> [cmds]
T_ForArithmetic _ _ _ _ cmds -> [cmds]
T_IfExpression _ thens elses -> map snd thens ++ [elses]
otherwise -> []

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
{-
Copyright 2012-2019 Vidar Holen
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -15,37 +15,22 @@
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
module ShellCheck.Analyzer (analyzeScript, ShellCheck.Analyzer.optionalChecks) where
module ShellCheck.Analyzer (analyzeScript) where
import ShellCheck.Analytics
import ShellCheck.AnalyzerLib
import ShellCheck.Interface
import Data.List
import Data.Monoid
import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.Custom
import qualified ShellCheck.Checks.ShellSupport
-- TODO: Clean up the cruft this is layered on
analyzeScript :: AnalysisSpec -> AnalysisResult
analyzeScript spec = newAnalysisResult {
analyzeScript spec = AnalysisResult {
arComments =
filterByAnnotation spec params . nub $
filterByAnnotation (asScript spec) . nub $
runAnalytics spec
++ runChecker params (checkers params)
++ ShellCheck.Checks.Commands.runChecks spec
}
where
params = makeParameters spec
checkers params = mconcat $ map ($ params) [
ShellCheck.Checks.Commands.checker,
ShellCheck.Checks.Custom.checker,
ShellCheck.Checks.ShellSupport.checker
]
optionalChecks = mconcat $ [
ShellCheck.Analytics.optionalChecks
]

646
ShellCheck/AnalyzerLib.hs Normal file
View File

@@ -0,0 +1,646 @@
{-
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
module ShellCheck.AnalyzerLib where
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Regex
import Control.Arrow (first)
import Control.Monad.Identity
import Control.Monad.Reader
import Control.Monad.State
import Control.Monad.Writer
import Data.Char
import Data.List
import Data.Maybe
import qualified Data.Map as Map
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
type Analysis = ReaderT Parameters (Writer [TokenComment]) ()
data Parameters = Parameters {
variableFlow :: [StackData],
parentMap :: Map.Map Id Token,
shellType :: Shell,
shellTypeSpecified :: Bool
}
data Scope = SubshellScope String | NoneScope deriving (Show, Eq)
data StackData =
StackScope Scope
| StackScopeEnd
-- (Base expression, specific position, var name, assigned values)
| Assignment (Token, Token, String, DataType)
| Reference (Token, Token, String)
deriving (Show)
data DataType = DataString DataSource | DataArray DataSource
deriving (Show)
data DataSource = SourceFrom [Token] | SourceExternal | SourceDeclaration | SourceInteger
deriving (Show)
data VariableState = Dead Token String | Alive deriving (Show)
defaultSpec root = AnalysisSpec {
asScript = root,
asShellType = Nothing,
asExecutionMode = Executed
}
pScript s =
let
pSpec = ParseSpec {
psFilename = "script",
psScript = s
}
in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec
makeComment :: Severity -> Id -> Code -> String -> TokenComment
makeComment severity id code note =
TokenComment id $ Comment severity code note
addComment note = tell [note]
warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m ()
warn id code str = addComment $ makeComment WarningC id code str
err id code str = addComment $ makeComment ErrorC id code str
info id code str = addComment $ makeComment InfoC id code str
style id code str = addComment $ makeComment StyleC id code str
makeParameters spec =
let params = Parameters {
shellType = fromMaybe (determineShell root) $ asShellType spec,
shellTypeSpecified = isJust $ asShellType spec,
parentMap = getParentTree root,
variableFlow =
getVariableFlow (shellType params) (parentMap params) root
} in params
where root = asScript spec
prop_determineShell0 = determineShell (fromJust $ pScript "#!/bin/sh") == Sh
prop_determineShell1 = determineShell (fromJust $ pScript "#!/usr/bin/env ksh") == Ksh
prop_determineShell2 = determineShell (fromJust $ pScript "") == Bash
prop_determineShell3 = determineShell (fromJust $ pScript "#!/bin/sh -e") == Sh
prop_determineShell4 = determineShell (fromJust $ pScript
"#!/bin/ksh\n#shellcheck shell=sh\nfoo") == Sh
prop_determineShell5 = determineShell (fromJust $ pScript
"#shellcheck shell=sh\nfoo") == Sh
prop_determineShell6 = determineShell (fromJust $ pScript "#! /bin/sh") == Sh
determineShell t = fromMaybe Bash $ do
shellString <- foldl mplus Nothing $ getCandidates t
shellForExecutable shellString
where
forAnnotation t =
case t of
(ShellOverride s) -> return s
_ -> fail ""
getCandidates :: Token -> [Maybe String]
getCandidates t@(T_Script {}) = [Just $ fromShebang t]
getCandidates (T_Annotation _ annotations s) =
map forAnnotation annotations ++
[Just $ fromShebang s]
fromShebang (T_Script _ s t) = shellFor s
shellFor s | "/env " `isInfixOf` s = head (drop 1 (words s)++[""])
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
--- Context seeking
getParentTree t =
snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
where
pre t = modify (first ((:) t))
post t = do
(_:rest, map) <- get
case rest of [] -> put (rest, map)
(x:_) -> put (rest, Map.insert (getId t) x map)
getTokenMap t =
execState (doAnalysis f t) Map.empty
where
f t = modify (Map.insert (getId t) t)
-- Is this node self quoting for a regular element?
isQuoteFree = isQuoteFreeNode False
-- Is this node striclty self quoting, for array expansions
isStrictlyQuoteFree = isQuoteFreeNode True
isQuoteFreeNode strict tree t =
(isQuoteFreeElement t == Just True) ||
head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False])
where
-- Is this node self-quoting in itself?
isQuoteFreeElement t =
case t of
T_Assignment {} -> return True
T_FdRedirect {} -> return True
_ -> Nothing
-- Are any subnodes inherently self-quoting?
isQuoteFreeContext t =
case t of
TC_Noary _ DoubleBracket _ -> return True
TC_Unary _ DoubleBracket _ _ -> return True
TC_Binary _ DoubleBracket _ _ _ -> return True
TA_Sequence {} -> return True
T_Arithmetic {} -> return True
T_Assignment {} -> return True
T_Redirecting {} -> return $
if strict then False else
-- Not true, just a hack to prevent warning about non-expansion refs
any (isCommand t) ["local", "declare", "typeset", "export", "trap", "readonly"]
T_DoubleQuoted _ _ -> return True
T_DollarDoubleQuoted _ _ -> return True
T_CaseExpression {} -> return True
T_HereDoc {} -> return True
T_DollarBraced {} -> return True
-- When non-strict, pragmatically assume it's desirable to split here
T_ForIn {} -> return (not strict)
T_SelectIn {} -> return (not strict)
_ -> Nothing
isParamTo tree cmd =
go
where
go x = case Map.lookup (getId x) tree of
Nothing -> False
Just parent -> check parent
check t =
case t of
T_SingleQuoted _ _ -> go t
T_DoubleQuoted _ _ -> go t
T_NormalWord _ _ -> go t
T_SimpleCommand {} -> isCommand t cmd
T_Redirecting {} -> isCommand t cmd
_ -> False
getClosestCommand tree t =
msum . map getCommand $ getPath tree t
where
getCommand t@(T_Redirecting {}) = return t
getCommand _ = Nothing
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
where
go currentId (T_NormalWord id [word]:rest)
| currentId == getId word = go id rest
go currentId (T_DoubleQuoted id [word]:rest)
| currentId == getId word = go id rest
go currentId (T_SimpleCommand _ _ (word:_):_)
| currentId == getId word = True
go _ _ = False
-- A list of the element and all its parents
getPath tree t = t :
case Map.lookup (getId t) tree of
Nothing -> []
Just parent -> getPath tree parent
isParentOf tree parent child =
elem (getId parent) . map getId $ getPath tree child
parents params = getPath (parentMap params)
pathTo t = do
parents <- reader parentMap
return $ getPath parents t
-- Check whether a word is entirely output from a single command
tokenIsJustCommandOutput t = case t of
T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds
T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ cmds]] -> check cmds
T_NormalWord id [T_Backticked _ cmds] -> check cmds
T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ cmds]] -> check cmds
_ -> False
where
check [x] = not $ isOnlyRedirection x
check _ = False
-- TODO: Replace this with a proper Control Flow Graph
getVariableFlow shell parents t =
let (_, stack) = runState (doStackAnalysis startScope endScope t) []
in reverse stack
where
startScope t =
let scopeType = leadType shell parents t
in do
when (scopeType /= NoneScope) $ modify (StackScope scopeType:)
when (assignFirst t) $ setWritten t
endScope t =
let scopeType = leadType shell parents t
in do
setRead t
unless (assignFirst t) $ setWritten t
when (scopeType /= NoneScope) $ modify (StackScopeEnd:)
assignFirst (T_ForIn {}) = True
assignFirst (T_SelectIn {}) = True
assignFirst (T_BatsTest {}) = True
assignFirst _ = False
setRead t =
let read = getReferencedVariables parents t
in mapM_ (\v -> modify (Reference v:)) read
setWritten t =
let written = getModifiedVariables t
in mapM_ (\v -> modify (Assignment v:)) written
leadType shell parents t =
case t of
T_DollarExpansion _ _ -> SubshellScope "$(..) expansion"
T_Backticked _ _ -> SubshellScope "`..` expansion"
T_Backgrounded _ _ -> SubshellScope "backgrounding &"
T_Subshell _ _ -> SubshellScope "(..) group"
T_BatsTest {} -> SubshellScope "@bats test"
T_CoProcBody _ _ -> SubshellScope "coproc"
T_Redirecting {} ->
if fromMaybe False causesSubshell
then SubshellScope "pipeline"
else NoneScope
_ -> NoneScope
where
parentPipeline = do
parent <- Map.lookup (getId t) parents
case parent of
T_Pipeline {} -> return parent
_ -> Nothing
causesSubshell = do
(T_Pipeline _ _ list) <- parentPipeline
if length list <= 1
then return False
else if lastCreatesSubshell
then return True
else return . not $ (getId . head $ reverse list) == getId t
lastCreatesSubshell =
case shell of
Bash -> True
Dash -> True
Sh -> True
Ksh -> False
getModifiedVariables t =
case t of
T_SimpleCommand _ vars [] ->
concatMap (\x -> case x of
T_Assignment id _ name _ w ->
[(x, x, name, dataTypeFrom DataString w)]
_ -> []
) vars
c@(T_SimpleCommand {}) ->
getModifiedVariableCommand c
TA_Unary _ "++|" var -> maybeToList $ do
name <- getLiteralString var
return (t, t, name, DataString $ SourceFrom [t])
TA_Unary _ "|++" var -> maybeToList $ do
name <- getLiteralString var
return (t, t, name, DataString $ SourceFrom [t])
TA_Assignment _ op lhs rhs -> maybeToList $ do
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
name <- getLiteralString lhs
return (t, t, name, DataString $ SourceFrom [rhs])
T_BatsTest {} -> [
(t, t, "lines", DataArray SourceExternal),
(t, t, "status", DataString SourceInteger),
(t, t, "output", DataString SourceExternal)
]
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
t@(T_CoProc _ name _) ->
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
--Points to 'for' rather than variable
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
_ -> []
isClosingFileOp op =
case op of
T_IoFile _ (T_GREATAND _) (T_NormalWord _ [T_Literal _ "-"]) -> True
T_IoFile _ (T_LESSAND _) (T_NormalWord _ [T_Literal _ "-"]) -> True
_ -> False
-- Consider 'export/declare -x' a reference, since it makes the var available
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
case x of
"export" -> if "f" `elem` flags
then []
else concatMap getReference rest
"declare" -> if any (`elem` flags) ["x", "p"]
then concatMap getReference rest
else []
"readonly" -> concatMap getReference rest
"trap" ->
case rest of
head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head
_ -> []
_ -> []
where
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
getReference _ = []
flags = map snd $ getAllFlags base
getReferencedVariableCommand _ = []
getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
case x of
"read" ->
let params = map getLiteral rest in
catMaybes . takeWhile isJust . reverse $ params
"getopts" ->
case rest of
opts:var:_ -> maybeToList $ getLiteral var
_ -> []
"let" -> concatMap letParamToLiteral rest
"export" ->
if "f" `elem` flags then [] else concatMap getModifierParamString rest
"declare" -> if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
"typeset" -> declaredVars
"local" -> concatMap getModifierParamString rest
"readonly" -> concatMap getModifierParamString rest
"set" -> maybeToList $ do
params <- getSetParams rest
return (base, base, "@", DataString $ SourceFrom params)
"printf" -> maybeToList $ getPrintfVariable rest
"mapfile" -> maybeToList $ getMapfileArray base rest
"readarray" -> maybeToList $ getMapfileArray base rest
_ -> []
where
flags = map snd $ getAllFlags base
stripEquals s = let rest = dropWhile (/= '=') s in
if rest == "" then "" else tail rest
stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) =
T_NormalWord id1 (T_Literal id2 (stripEquals s):rs)
stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) =
T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
stripEqualsFrom t = t
declaredVars = concatMap (getModifierParam defaultType) rest
where
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
getLiteral t = do
s <- getLiteralString t
when ("-" `isPrefixOf` s) $ fail "argument"
return (base, t, s, DataString SourceExternal)
getModifierParamString = getModifierParam DataString
getModifierParam def t@(T_Assignment _ _ name _ value) =
[(base, t, name, dataTypeFrom def value)]
getModifierParam def t@(T_NormalWord {}) = maybeToList $ do
name <- getLiteralString t
guard $ isVariableName name
return (base, t, name, def SourceDeclaration)
getModifierParam _ _ = []
letParamToLiteral token =
if var == ""
then []
else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])]
where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token
getSetParams (t:_:rest) | getLiteralString t == Just "-o" = getSetParams rest
getSetParams (t:rest) =
let s = getLiteralString t in
case s of
Just "--" -> return rest
Just ('-':_) -> getSetParams rest
_ -> return (t:fromMaybe [] (getSetParams rest))
getSetParams [] = Nothing
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
where
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, var, DataString $ SourceFrom list)
f (_:rest) = f rest
f [] = fail "not found"
-- mapfile has some curious syntax allowing flags plus 0..n variable names
-- where only the first non-option one is used if any. Here we cheat and
-- just get the last one, if it's a variable name.
getMapfileArray base arguments = do
lastArg <- listToMaybe (reverse arguments)
name <- getLiteralString lastArg
guard $ isVariableName name
return (base, lastArg, name, DataArray SourceExternal)
getModifiedVariableCommand _ = []
getIndexReferences s = fromMaybe [] $ do
match <- matchRegex re s
index <- match !!! 0
return $ matchAllStrings variableNameRegex index
where
re = mkRegex "(\\[.*\\])"
getReferencedVariables parents t =
case t of
T_DollarBraced id l -> let str = bracedString t in
(t, t, getBracedReference str) :
map (\x -> (l, l, x)) (getIndexReferences str)
TA_Expansion id _ ->
if isArithmeticAssignment t
then []
else getIfReference t t
T_Assignment id mode str _ word ->
[(t, t, str) | mode == Append] ++ specialReferences str t word
TC_Unary id _ "-v" token -> getIfReference t token
TC_Unary id _ "-R" token -> getIfReference t token
TC_Binary id DoubleBracket op lhs rhs ->
if isDereferencing op
then concatMap (getIfReference t) [lhs, rhs]
else []
T_BatsTest {} -> [ -- pretend @test references vars to avoid warnings
(t, t, "lines"),
(t, t, "status"),
(t, t, "output")
]
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
x -> getReferencedVariableCommand x
where
-- Try to reduce false positives for unused vars only referenced from evaluated vars
specialReferences name base word =
if name `elem` [
"PS1", "PS2", "PS3", "PS4",
"PROMPT_COMMAND"
]
then
map (\x -> (base, base, x)) $
getVariablesFromLiteralToken word
else []
literalizer (TA_Index {}) = return "" -- x[0] becomes a reference of x
literalizer _ = Nothing
getIfReference context token = maybeToList $ do
str <- getLiteralStringExt literalizer token
guard . not $ null str
when (isDigit $ head str) $ fail "is a number"
return (context, token, getBracedReference str)
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
isArithmeticAssignment t = case getPath parents t of
this: TA_Assignment _ "=" _ _ :_ -> True
_ -> False
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
--- Command specific checks
isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `isSuffixOf` cmd)
isUnqualifiedCommand token str = isCommandMatch token (== str)
isCommandMatch token matcher = fromMaybe False $ do
cmd <- getCommandName token
return $ matcher cmd
isConfusedGlobRegex ('*':_) = True
isConfusedGlobRegex [x,'*'] | x /= '\\' = True
isConfusedGlobRegex _ = False
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
isVariableChar x = isVariableStartChar x || isDigit x
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
prop_isVariableName1 = isVariableName "_fo123"
prop_isVariableName2 = not $ isVariableName "4"
prop_isVariableName3 = not $ isVariableName "test: "
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
isVariableName _ = False
getVariablesFromLiteralToken token =
getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token)
-- Try to get referenced variables from a literal string like "$foo"
-- Ignores tons of cases like arithmetic evaluation and array indices.
prop_getVariablesFromLiteral1 =
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
getVariablesFromLiteral string =
map (!! 0) $ matchAllSubgroups variableRegex string
where
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
prop_getBracedReference1 = getBracedReference "foo" == "foo"
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
prop_getBracedReference3 = getBracedReference "#" == "#"
prop_getBracedReference4 = getBracedReference "##" == "#"
prop_getBracedReference5 = getBracedReference "#!" == "!"
prop_getBracedReference6 = getBracedReference "!#" == "#"
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
prop_getBracedReference11= getBracedReference "!os*" == ""
prop_getBracedReference12= getBracedReference "!os?bar**" == ""
getBracedReference s = fromMaybe s $
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
where
noPrefix = dropPrefix s
dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest
dropPrefix "" = ""
takeName s = do
let name = takeWhile isVariableChar s
guard . not $ null name
return name
getSpecial (c:_) =
if c `elem` "*@#?-$!" then return [c] else fail "not special"
getSpecial _ = fail "empty"
nameExpansion ('!':rest) = do -- e.g. ${!foo*bar*}
let suffix = dropWhile isVariableChar rest
guard $ suffix /= rest -- e.g. ${!@}
first <- suffix !!! 0
guard $ first `elem` "*?"
return ""
nameExpansion _ = Nothing
-- Useful generic functions
potentially :: Monad m => Maybe (m ()) -> m ()
potentially = fromMaybe (return ())
headOrDefault _ (a:_) = a
headOrDefault def _ = def
(!!!) list i =
case drop i list of
[] -> Nothing
(r:_) -> Just r
filterByAnnotation token =
filter (not . shouldIgnore)
where
idFor (TokenComment id _) = id
shouldIgnore note =
any (shouldIgnoreFor (getCode note)) $
getPath parents (T_Bang $ idFor note)
shouldIgnoreFor num (T_Annotation _ anns _) =
any hasNum anns
where
hasNum (DisableComment ts) = num == ts
hasNum _ = False
shouldIgnoreFor _ (T_Include {}) = True -- Ignore included files
shouldIgnoreFor _ _ = False
parents = getParentTree token
getCode (TokenComment _ (Comment _ c _)) = c
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

162
ShellCheck/Checker.hs Normal file
View File

@@ -0,0 +1,162 @@
{-
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Analyzer
import Data.Either
import Data.Functor
import Data.List
import Data.Maybe
import Data.Ord
import Control.Monad.Identity
import qualified Data.Map as Map
import qualified System.IO
import Prelude hiding (readFile)
import Control.Monad
import Test.QuickCheck.All
tokenToPosition map (TokenComment id c) = fromMaybe fail $ do
position <- Map.lookup id map
return $ PositionedComment position c
where
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
checkScript sys spec = do
results <- checkScript (csScript spec)
return CheckResult {
crFilename = csFilename spec,
crComments = results
}
where
checkScript contents = do
result <- parseScript sys ParseSpec {
psFilename = csFilename spec,
psScript = contents
}
let parseMessages = prComments result
let analysisMessages =
fromMaybe [] $
(arComments . analyzeScript . analysisSpec)
<$> prRoot result
let translator = tokenToPosition (prTokenPositions result)
return . nub . sortMessages . filter shouldInclude $
(parseMessages ++ map translator analysisMessages)
shouldInclude (PositionedComment _ (Comment _ code _)) =
code `notElem` csExcludedWarnings spec
sortMessages = sortBy (comparing order)
order (PositionedComment pos (Comment severity code message)) =
(posFile pos, posLine pos, posColumn pos, severity, code, message)
getPosition (PositionedComment pos _) = pos
analysisSpec root =
AnalysisSpec {
asScript = root,
asShellType = csShellTypeOverride spec,
asExecutionMode = Executed
}
getErrors sys spec =
sort . map getCode . crComments $
runIdentity (checkScript sys spec)
where
getCode (PositionedComment _ (Comment _ code _)) = code
check = checkWithIncludes []
checkWithIncludes includes src =
getErrors
(mockedSystemInterface includes)
emptyCheckSpec {
csScript = src,
csExcludedWarnings = [2148]
}
prop_findsParseIssue = check "echo \"$12\"" == [1037]
prop_commentDisablesParseIssue1 =
null $ check "#shellcheck disable=SC1037\necho \"$12\""
prop_commentDisablesParseIssue2 =
null $ check "#shellcheck disable=SC1037\n#lol\necho \"$12\""
prop_findsAnalysisIssue =
check "echo $1" == [2086]
prop_commentDisablesAnalysisIssue1 =
null $ check "#shellcheck disable=SC2086\necho $1"
prop_commentDisablesAnalysisIssue2 =
null $ check "#shellcheck disable=SC2086\n#lol\necho $1"
prop_optionDisablesIssue1 =
null $ getErrors
(mockedSystemInterface [])
emptyCheckSpec {
csScript = "echo $1",
csExcludedWarnings = [2148, 2086]
}
prop_optionDisablesIssue2 =
null $ getErrors
(mockedSystemInterface [])
emptyCheckSpec {
csScript = "echo \"$10\"",
csExcludedWarnings = [2148, 1037]
}
prop_canParseDevNull =
[] == check "source /dev/null"
prop_failsWhenNotSourcing =
[1091, 2154] == check "source lol; echo \"$bar\""
prop_worksWhenSourcing =
null $ checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\""
prop_worksWhenDotting =
null $ checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\""
prop_noInfiniteSourcing =
[] == checkWithIncludes [("lib", "source lib")] "source lib"
prop_canSourceBadSyntax =
[1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
prop_cantSourceDynamic =
[1090] == checkWithIncludes [("lib", "")] ". \"$1\""
prop_cantSourceDynamic2 =
[1090] == checkWithIncludes [("lib", "")] "source ~/foo"
prop_canSourceDynamicWhenRedirected =
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
prop_sourceDirectiveDoesntFollowFile =
null $ checkWithIncludes
[("foo", "source bar"), ("bar", "baz=3")]
"#shellcheck source=foo\n. \"$1\"; echo \"$baz\""
return []
runTests = $quickCheckAll

View File

@@ -0,0 +1,560 @@
{-
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
-- This module contains checks that examine specific commands by name.
module ShellCheck.Checks.Commands (runChecks
, ShellCheck.Checks.Commands.runTests
) where
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.AnalyzerLib
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Regex
import Control.Monad
import Control.Monad.Reader
import Control.Monad.Writer
import Data.Char
import Data.List
import Data.Maybe
import qualified Data.Map as Map
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
data CommandName = Exactly String | Basename String
deriving (Eq, Ord)
data CommandCheck =
CommandCheck CommandName (Token -> Analysis)
nullCheck :: Token -> Analysis
nullCheck _ = return ()
verify :: CommandCheck -> String -> Bool
verify f s = producesComments f s == Just True
verifyNot f s = producesComments f s == Just False
producesComments :: CommandCheck -> String -> Maybe Bool
producesComments f s = do
root <- pScript s
return . not . null $ runList (defaultSpec root) [f]
composeChecks f g t = do
f t
g t
arguments (T_SimpleCommand _ _ (cmd:args)) = args
commandChecks :: [CommandCheck]
commandChecks = [
checkTr
,checkFindNameGlob
,checkNeedlessExpr
,checkGrepRe
,checkTrapQuotes
,checkReturn
,checkFindExecWithSingleArgument
,checkUnusedEchoEscapes
,checkInjectableFindSh
,checkFindActionPrecedence
,checkMkdirDashPM
,checkNonportableSignals
,checkInteractiveSu
,checkSshCommandString
,checkPrintfVar
,checkUuoeCmd
,checkSetAssignment
,checkExportedExpansions
,checkAliasesUsesArgs
,checkAliasesExpandEarly
]
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
buildCommandMap = foldl' addCheck Map.empty
where
addCheck map (CommandCheck name function) =
Map.insertWith' composeChecks name function map
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
checkCommand map t@(T_SimpleCommand id _ (cmd:rest)) = fromMaybe (return ()) $ do
name <- getLiteralString cmd
return $
if '/' `elem` name
then
Map.findWithDefault nullCheck (Basename $ basename name) map t
else do
Map.findWithDefault nullCheck (Exactly name) map t
Map.findWithDefault nullCheck (Basename name) map t
where
basename = reverse . takeWhile (/= '/') . reverse
checkCommand _ _ = return ()
runList spec list = notes
where
root = asScript spec
params = makeParameters spec
notes = execWriter $ runReaderT (doAnalysis (checkCommand map) root) params
map = buildCommandMap list
runChecks spec = runList spec commandChecks
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'"
prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'"
prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'"
prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'"
prop_checkTr4 = verifyNot checkTr "ls [a-z]"
prop_checkTr5 = verify checkTr "tr foo bar"
prop_checkTr6 = verify checkTr "tr 'hello' 'world'"
prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
where
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
warn (getId w) 2060 "Quote parameters to tr to prevent glob expansion."
f word =
case getLiteralString word of
Just "a-z" -> info (getId word) 2018 "Use '[:lower:]' to support accents and foreign alphabets."
Just "A-Z" -> info (getId word) 2019 "Use '[:upper:]' to support accents and foreign alphabets."
Just s -> do -- Eliminate false positives by only looking for dupes in SET2?
when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $
info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)."
unless ("[:" `isPrefixOf` s) $
when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $
info (getId word) 2021 "Don't use [] around ranges in tr, it replaces literal square brackets."
Nothing -> return ()
duplicated s =
let relevant = filter isAlpha s
in relevant /= nub relevant
prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php"
prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)"
prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
acceptsGlob _ = False
f [] = return ()
f [x] = return ()
f (a:b:r) = do
when (acceptsGlob (getLiteralString a) && isGlob b) $ do
let (Just s) = getLiteralString a
warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."
f (b:r)
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
prop_checkNeedlessExpr2 = verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``"
prop_checkNeedlessExpr3 = verifyNot checkNeedlessExpr "foo=$(expr foo : regex)"
prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)"
checkNeedlessExpr = CommandCheck (Basename "expr") f where
f t =
when (all (`notElem` exceptions) (words $ arguments t)) $
style (getId t) 2003
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
-- These operators are hard to replicate in POSIX
exceptions = [ ":", "<", ">", "<=", ">=" ]
words = mapMaybe getLiteralString
prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3"
prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3"
prop_checkGrepRe3 = verify checkGrepRe "grep --regex=*.mp3 file"
prop_checkGrepRe4 = verifyNot checkGrepRe "grep foo *.mp3"
prop_checkGrepRe5 = verifyNot checkGrepRe "grep-v --regex=moo *"
prop_checkGrepRe6 = verifyNot checkGrepRe "grep foo \\*.mp3"
prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file"
prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg"
prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"
prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file"
prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo"
checkGrepRe = CommandCheck (Basename "grep") (f . arguments) where
-- --regex=*(extglob) doesn't work. Fixme?
skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s
skippable _ = False
f [] = return ()
f (x:r) | skippable (getLiteralStringExt (const $ return "_") x) = f r
f (re:_) = do
when (isGlob re) $
warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it."
let string = concat $ oversimplify re
if isConfusedGlobRegex string then
warn (getId re) 2063 "Grep uses regex, but this looks like a glob."
else potentially $ do
char <- getSuspiciousRegexWildcard string
return $ info (getId re) 2022 $
"Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'."
wordStartingWith c =
head . filter ([c] `isPrefixOf`) $ candidates
where
candidates =
sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords ++ [c:"test"]
getSuspiciousRegexWildcard str =
if not $ str `matches` contra
then do
match <- matchRegex suspicious str
str <- match !!! 0
str !!! 0
else
fail "looks good"
where
suspicious = mkRegex "([A-Za-z1-9])\\*"
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT"
prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT"
prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
f (x:_) = checkTrap x
f _ = return ()
checkTrap (T_NormalWord _ [T_DoubleQuoted _ rs]) = mapM_ checkExpansions rs
checkTrap _ = return ()
warning id = warn id 2064 "Use single quotes, otherwise this expands now rather than when signalled."
checkExpansions (T_DollarExpansion id _) = warning id
checkExpansions (T_Backticked id _) = warning id
checkExpansions (T_DollarBraced id _) = warning id
checkExpansions (T_DollarArithmetic id _) = warning id
checkExpansions _ = return ()
prop_checkReturn1 = verifyNot checkReturn "return"
prop_checkReturn2 = verifyNot checkReturn "return 1"
prop_checkReturn3 = verifyNot checkReturn "return $var"
prop_checkReturn4 = verifyNot checkReturn "return $((a|b))"
prop_checkReturn5 = verify checkReturn "return -1"
prop_checkReturn6 = verify checkReturn "return 1000"
prop_checkReturn7 = verify checkReturn "return 'hello world'"
checkReturn = CommandCheck (Exactly "return") (f . arguments)
where
f (first:second:_) =
err (getId second) 2151
"Only one integer 0-255 can be returned. Use stdout for other data."
f [value] =
when (isInvalid $ literal value) $
err (getId value) 2152
"Can only return 0-255. Other data should be written to stdout."
f _ = return ()
isInvalid s = s == "" || any (not . isDigit) s || length s > 5
|| let value = (read s :: Integer) in value > 255
literal token = fromJust $ getLiteralStringExt lit token
lit (T_DollarBraced {}) = return "0"
lit (T_DollarArithmetic {}) = return "0"
lit (T_DollarExpansion {}) = return "0"
lit (T_Backticked {}) = return "0"
lit _ = return "WTF"
prop_checkFindExecWithSingleArgument1 = verify checkFindExecWithSingleArgument "find . -exec 'cat {} | wc -l' \\;"
prop_checkFindExecWithSingleArgument2 = verify checkFindExecWithSingleArgument "find . -execdir 'cat {} | wc -l' +"
prop_checkFindExecWithSingleArgument3 = verifyNot checkFindExecWithSingleArgument "find . -exec wc -l {} \\;"
checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments)
where
f = void . sequence . mapMaybe check . tails
check (exec:arg:term:_) = do
execS <- getLiteralString exec
termS <- getLiteralString term
cmdS <- getLiteralStringExt (const $ return " ") arg
guard $ execS `elem` ["-exec", "-execdir"] && termS `elem` [";", "+"]
guard $ cmdS `matches` commandRegex
return $ warn (getId exec) 2150 "-exec does not invoke a shell. Rewrite or use -exec sh -c .. ."
check _ = Nothing
commandRegex = mkRegex "[ |;]"
prop_checkUnusedEchoEscapes1 = verify checkUnusedEchoEscapes "echo 'foo\\nbar\\n'"
prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'"
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
checkUnusedEchoEscapes = CommandCheck (Basename "echo") (f . arguments)
where
isDashE = mkRegex "^-.*e"
hasEscapes = mkRegex "\\\\[rnt]"
f args | concat (concatMap oversimplify allButLast) `matches` isDashE =
return ()
where allButLast = reverse . drop 1 . reverse $ args
f args = mapM_ checkEscapes args
checkEscapes (T_NormalWord _ args) =
mapM_ checkEscapes args
checkEscapes (T_DoubleQuoted id args) =
mapM_ checkEscapes args
checkEscapes (T_Literal id str) = examine id str
checkEscapes (T_SingleQuoted id str) = examine id str
checkEscapes _ = return ()
examine id str =
when (str `matches` hasEscapes) $
info id 2028 "echo won't expand escape sequences. Consider printf."
prop_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
prop_checkInjectableFindSh2 = verify checkInjectableFindSh "find . -execdir bash -c 'rm \"{}\"' ';'"
prop_checkInjectableFindSh3 = verifyNot checkInjectableFindSh "find . -exec sh -c 'rm \"$@\"' _ {} \\;"
checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments)
where
check args = do
let idStrings = map (\x -> (getId x, onlyLiteralString x)) args
match pattern idStrings
match _ [] = return ()
match [] (next:_) = action next
match (p:tests) ((id, arg):args) = do
when (p arg) $ match tests args
match (p:tests) args
pattern = [
(`elem` ["-exec", "-execdir"]),
(`elem` ["sh", "bash", "ksh"]),
(== "-c")
]
action (id, arg) =
when ("{}" `isInfixOf` arg) $
warn id 2156 "Injecting filenames is fragile and insecure. Use parameters."
prop_checkFindActionPrecedence1 = verify checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au' -exec rm {} +"
prop_checkFindActionPrecedence2 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o \\( -name '*.au' -exec rm {} + \\)"
prop_checkFindActionPrecedence3 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au'"
checkFindActionPrecedence = CommandCheck (Basename "find") (f . arguments)
where
pattern = [isMatch, const True, isParam ["-o", "-or"], isMatch, const True, isAction]
f list | length list < length pattern = return ()
f list@(_:rest) =
if and (zipWith ($) pattern list)
then warnFor (list !! (length pattern - 1))
else f rest
isMatch = isParam [ "-name", "-regex", "-iname", "-iregex", "-wholename", "-iwholename" ]
isAction = isParam [ "-exec", "-execdir", "-delete", "-print", "-print0" ]
isParam strs t = fromMaybe False $ do
param <- getLiteralString t
return $ param `elem` strs
warnFor t = warn (getId t) 2146 "This action ignores everything before the -o. Use \\( \\) to group."
prop_checkMkdirDashPM0 = verify checkMkdirDashPM "mkdir -p -m 0755 a/b"
prop_checkMkdirDashPM1 = verify checkMkdirDashPM "mkdir -pm 0755 $dir"
prop_checkMkdirDashPM2 = verify checkMkdirDashPM "mkdir -vpm 0755 a/b"
prop_checkMkdirDashPM3 = verify checkMkdirDashPM "mkdir -pm 0755 -v a/b"
prop_checkMkdirDashPM4 = verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b"
prop_checkMkdirDashPM5 = verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b"
prop_checkMkdirDashPM6 = verify checkMkdirDashPM "mkdir -p --mode=0755 a/b"
prop_checkMkdirDashPM7 = verify checkMkdirDashPM "mkdir --parents -m 0755 a/b"
prop_checkMkdirDashPM8 = verifyNot checkMkdirDashPM "mkdir -p a/b"
prop_checkMkdirDashPM9 = verifyNot checkMkdirDashPM "mkdir -m 0755 a/b"
prop_checkMkdirDashPM10 = verifyNot checkMkdirDashPM "mkdir a/b"
prop_checkMkdirDashPM11 = verifyNot checkMkdirDashPM "mkdir --parents a/b"
prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b"
prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b"
prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"
checkMkdirDashPM = CommandCheck (Basename "mkdir") check
where
check t = potentially $ do
let flags = getAllFlags t
dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
guard $ any couldHaveSubdirs (drop 1 $ arguments t) -- mkdir -pm 0700 dir is fine, but dir/subdir is not.
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
couldHaveSubdirs t = fromMaybe True $ do
name <- getLiteralString t
return $ '/' `elem` name
prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8"
prop_checkNonportableSignals2 = verifyNot checkNonportableSignals "trap f 0"
prop_checkNonportableSignals3 = verifyNot checkNonportableSignals "trap f 14"
prop_checkNonportableSignals4 = verify checkNonportableSignals "trap f SIGKILL"
prop_checkNonportableSignals5 = verify checkNonportableSignals "trap f 9"
prop_checkNonportableSignals6 = verify checkNonportableSignals "trap f stop"
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
where
f = mapM_ check
check param = potentially $ do
str <- getLiteralString param
let id = getId param
return $ sequence_ $ mapMaybe (\f -> f id str) [
checkNumeric,
checkUntrappable
]
checkNumeric id str = do
guard $ not (null str)
guard $ all isDigit str
guard $ str /= "0" -- POSIX exit trap
guard $ str `notElem` ["1", "2", "3", "6", "9", "14", "15" ] -- XSI
return $ warn id 2172
"Trapping signals by number is not well defined. Prefer signal names."
checkUntrappable id str = do
guard $ map toLower str `elem` ["kill", "9", "sigkill", "stop", "sigstop"]
return $ err id 2173
"SIGKILL/SIGSTOP can not be trapped."
prop_checkInteractiveSu1 = verify checkInteractiveSu "su; rm file; su $USER"
prop_checkInteractiveSu2 = verify checkInteractiveSu "su foo; something; exit"
prop_checkInteractiveSu3 = verifyNot checkInteractiveSu "echo rm | su foo"
prop_checkInteractiveSu4 = verifyNot checkInteractiveSu "su root < script"
checkInteractiveSu = CommandCheck (Basename "su") f
where
f cmd = when (length (arguments cmd) <= 1) $ do
path <- pathTo cmd
when (all undirected path) $
info (getId cmd) 2117
"To run commands as another user, use su -c or sudo."
undirected (T_Pipeline _ _ l) = length l <= 1
-- This should really just be modifications to stdin, but meh
undirected (T_Redirecting _ list _) = null list
undirected _ = True
-- This is hard to get right without properly parsing ssh args
prop_checkSshCmdStr1 = verify checkSshCommandString "ssh host \"echo $PS1\""
prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\""
prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""
checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
where
nonOptions =
filter (\x -> not $ "-" `isPrefixOf` concat (oversimplify x))
f args =
case nonOptions args of
(hostport:r@(_:_)) -> checkArg $ last r
_ -> return ()
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
case filter (not . isConstant) parts of
[] -> return ()
(x:_) -> info (getId x) 2029
"Note that, unescaped, this expands on the client side."
checkArg _ = return ()
prop_checkPrintfVar1 = verify checkPrintfVar "printf \"Lol: $s\""
prop_checkPrintfVar2 = verifyNot checkPrintfVar "printf 'Lol: $s'"
prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)"
prop_checkPrintfVar4 = verifyNot checkPrintfVar "printf \"%${count}s\" var"
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
f (format:params) = check format
f _ = return ()
check format =
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
warn (getId format) 2059
"Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"."
prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)"
prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`"
prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\""
prop_checkUuoeCmd4 = verify checkUuoeCmd "echo \"`date`\""
prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\""
prop_checkUuoeCmd6 = verifyNot checkUuoeCmd "echo \"$(<file)\""
checkUuoeCmd = CommandCheck (Exactly "echo") (f . arguments) where
msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'."
f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token)
f _ = return ()
prop_checkSetAssignment1 = verify checkSetAssignment "set foo 42"
prop_checkSetAssignment2 = verify checkSetAssignment "set foo = 42"
prop_checkSetAssignment3 = verify checkSetAssignment "set foo=42"
prop_checkSetAssignment4 = verifyNot checkSetAssignment "set -- if=/dev/null"
prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'"
prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
where
f (var:value:rest) =
let str = literal var in
when (isVariableName str || isAssignment str) $
msg (getId var)
f (var:_) =
when (isAssignment $ literal var) $
msg (getId var)
f _ = return ()
msg id = warn id 2121 "To assign a variable, use just 'var=value', no 'set ..'."
isAssignment str = '=' `elem` str
literal (T_NormalWord _ l) = concatMap literal l
literal (T_Literal _ str) = str
literal _ = "*"
prop_checkExportedExpansions1 = verify checkExportedExpansions "export $foo"
prop_checkExportedExpansions2 = verify checkExportedExpansions "export \"$foo\""
prop_checkExportedExpansions3 = verifyNot checkExportedExpansions "export foo"
checkExportedExpansions = CommandCheck (Exactly "export") (check . arguments)
where
check = mapM_ checkForVariables
checkForVariables f =
case getWordParts f of
[t@(T_DollarBraced {})] ->
warn (getId t) 2163 "Exporting an expansion rather than a variable."
_ -> return ()
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'"
prop_checkAliasesUsesArgs3 = verify checkAliasesUsesArgs "alias a=\"echo \\${@}\""
checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments)
where
re = mkRegex "\\$\\{?[0-9*@]"
f = mapM_ checkArg
checkArg arg =
let string = fromJust $ getLiteralStringExt (const $ return "_") arg in
when ('=' `elem` string && string `matches` re) $
err (getId arg) 2142
"Aliases can't use positional parameters. Use a function."
prop_checkAliasesExpandEarly1 = verify checkAliasesExpandEarly "alias foo=\"echo $PWD\""
prop_checkAliasesExpandEarly2 = verifyNot checkAliasesExpandEarly "alias -p"
prop_checkAliasesExpandEarly3 = verifyNot checkAliasesExpandEarly "alias foo='echo {1..10}'"
checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
where
f = mapM_ checkArg
checkArg arg | '=' `elem` concat (oversimplify arg) =
forM_ (take 1 $ filter (not . isLiteral) $ getWordParts arg) $
\x -> warn (getId x) 2139 "This expands when defined, not when used. Consider escaping."
checkArg _ = return ()
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -33,32 +33,13 @@ internalVariables = [
-- Other
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
"HOSTNAME", "KRB5CCNAME", "XAUTHORITY"
-- Ksh
, ".sh.version"
-- shflags
, "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP",
"FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION",
"flags_error", "flags_return"
]
specialVariablesWithoutSpaces = [
"$", "-", "?", "!", "#"
]
variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
variablesWithoutSpaces = [
"$", "-", "?", "!",
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
-- shflags
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
]
specialVariables = specialVariablesWithoutSpaces ++ ["@", "*"]
unbracedVariables = specialVariables ++ [
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
]
arrayVariables = [
@@ -93,14 +74,6 @@ commonCommands = [
"zcat"
]
nonReadingCommands = [
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
"cp", "du", "echo", "export", "false", "fg", "fuser", "getconf",
"getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
"set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname"
]
sampleWords = [
"alpha", "bravo", "charlie", "delta", "echo", "foxtrot",
"golf", "hotel", "india", "juliett", "kilo", "lima", "mike",
@@ -109,17 +82,6 @@ sampleWords = [
"zulu"
]
binaryTestOps = [
"-nt", "-ot", "-ef", "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le",
"-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="
]
unaryTestOps = [
"!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p",
"-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n",
"-o", "-v", "-R"
]
shellForExecutable :: String -> Maybe Shell
shellForExecutable name =
case name of
@@ -127,7 +89,6 @@ shellForExecutable name =
"bash" -> return Bash
"bats" -> return Bash
"dash" -> return Dash
"ash" -> return Dash -- There's also a warning for this.
"ksh" -> return Ksh
"ksh88" -> return Ksh
"ksh93" -> return Ksh

View File

@@ -1,8 +1,8 @@
{-
Copyright 2012-2019 Vidar Holen
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -15,7 +15,7 @@
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.CheckStyle (format) where
@@ -34,27 +34,14 @@ format = return Formatter {
putStrLn "<checkstyle version='4.3'>",
onFailure = outputError,
onResult = outputResults,
onResult = outputResult,
footer = putStrLn "</checkstyle>"
}
outputResults cr sys =
if null comments
then outputFile (crFilename cr) "" []
else mapM_ outputGroup fileGroups
where
comments = crComments cr
fileGroups = groupWith sourceFile comments
outputGroup group = do
let filename = sourceFile (head group)
result <- (siReadFile sys) filename
let contents = either (const "") id result
outputFile filename contents group
outputFile filename contents warnings = do
let comments = makeNonVirtual warnings contents
putStrLn . formatFile filename $ comments
outputResult result contents = do
let comments = makeNonVirtual (crComments result) contents
putStrLn . formatFile (crFilename result) $ comments
formatFile name comments = concat [
"<file ", attr "name" name, ">\n",

View File

@@ -0,0 +1,61 @@
{-
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.Format where
import ShellCheck.Data
import ShellCheck.Interface
-- A formatter that carries along an arbitrary piece of data
data Formatter = Formatter {
header :: IO (),
onResult :: CheckResult -> String -> IO (),
onFailure :: FilePath -> ErrorMessage -> IO (),
footer :: IO ()
}
lineNo (PositionedComment pos _) = posLine pos
colNo (PositionedComment pos _) = posColumn pos
codeNo (PositionedComment _ (Comment _ code _)) = code
messageText (PositionedComment _ (Comment _ _ t)) = t
severityText :: PositionedComment -> String
severityText (PositionedComment _ (Comment c _ _)) =
case c of
ErrorC -> "error"
WarningC -> "warning"
InfoC -> "info"
StyleC -> "style"
-- Realign comments from a tabstop of 8 to 1
makeNonVirtual comments contents =
map fix comments
where
ls = lines contents
fix c@(PositionedComment pos comment) = PositionedComment pos {
posColumn =
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
else colNo c
} comment
real _ r v target | target <= v = r
real [] r v _ = r -- should never happen
real ('\t':rest) r v target =
real rest (r+1) (v + 8 - (v `mod` 8)) target
real (_:rest) r v target = real rest (r+1) (v+1) target

View File

@@ -1,8 +1,8 @@
{-
Copyright 2012-2019 Vidar Holen
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -15,7 +15,7 @@
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.GCC (format) where
@@ -31,25 +31,14 @@ format = return Formatter {
header = return (),
footer = return (),
onFailure = outputError,
onResult = outputAll
onResult = outputResult
}
outputError file error = hPutStrLn stderr $ file ++ ": " ++ error
outputAll cr sys = mapM_ f groups
where
comments = crComments cr
groups = groupWith sourceFile comments
f :: [PositionedComment] -> IO ()
f group = do
let filename = sourceFile (head group)
result <- (siReadFile sys) filename
let contents = either (const "") id result
outputResult filename contents group
outputResult filename contents warnings = do
let comments = makeNonVirtual warnings contents
mapM_ (putStrLn . formatComment filename) comments
outputResult result contents = do
let comments = makeNonVirtual (crComments result) contents
mapM_ (putStrLn . formatComment (crFilename result)) comments
formatComment filename c = concat [
filename, ":",

View File

@@ -0,0 +1,58 @@
{-
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.JSON (format) where
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Data.IORef
import GHC.Exts
import System.IO
import Text.JSON
format = do
ref <- newIORef []
return Formatter {
header = return (),
onResult = collectResult ref,
onFailure = outputError,
footer = finish ref
}
instance JSON (PositionedComment) where
showJSON comment@(PositionedComment pos (Comment level code string)) = makeObj [
("file", showJSON $ posFile pos),
("line", showJSON $ posLine pos),
("column", showJSON $ posColumn pos),
("level", showJSON $ severityText comment),
("code", showJSON code),
("message", showJSON string)
]
readJSON = undefined
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
collectResult ref result _ =
modifyIORef ref (\x -> crComments result ++ x)
finish ref = do
list <- readIORef ref
putStrLn $ encodeStrict list

View File

@@ -0,0 +1,91 @@
{-
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.TTY (format) where
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Data.List
import GHC.Exts
import System.Info
import System.IO
format :: FormatterOptions -> IO Formatter
format options = return Formatter {
header = return (),
footer = return (),
onFailure = outputError options,
onResult = outputResult options
}
colorForLevel level =
case level of
"error" -> 31 -- red
"warning" -> 33 -- yellow
"info" -> 32 -- green
"style" -> 32 -- green
"message" -> 1 -- bold
"source" -> 0 -- none
otherwise -> 0 -- none
outputError options file error = do
color <- getColorFunc $ foColorOption options
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
outputResult options result contents = do
color <- getColorFunc $ foColorOption options
let comments = crComments result
let fileLines = lines contents
let lineCount = fromIntegral $ length fileLines
let groups = groupWith lineNo comments
mapM_ (\x -> do
let lineNum = lineNo (head x)
let line = if lineNum < 1 || lineNum > lineCount
then ""
else fileLines !! fromIntegral (lineNum - 1)
putStrLn ""
putStrLn $ color "message" $
"In " ++ crFilename result ++" line " ++ show lineNum ++ ":"
putStrLn (color "source" line)
mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) x
putStrLn ""
) groups
cuteIndent :: PositionedComment -> String
cuteIndent comment =
replicate (fromIntegral $ colNo comment - 1) ' ' ++
"^-- " ++ code (codeNo comment) ++ ": " ++ messageText comment
code code = "SC" ++ show code
getColorFunc colorOption = do
term <- hIsTerminalDevice stdout
let windows = "mingw" `isPrefixOf` os
let isUsableTty = term && not windows
let useColor = case colorOption of
ColorAlways -> True
ColorNever -> False
ColorAuto -> isUsableTty
return $ if useColor then colorComment else const id
where
colorComment level comment =
ansi (colorForLevel level) ++ comment ++ clear
clear = ansi 0
ansi n = "\x1B[" ++ show n ++ "m"

116
ShellCheck/Interface.hs Normal file
View File

@@ -0,0 +1,116 @@
{-
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
module ShellCheck.Interface where
import ShellCheck.AST
import Control.Monad.Identity
import qualified Data.Map as Map
data SystemInterface m = SystemInterface {
-- Read a file by filename, or return an error
siReadFile :: String -> m (Either ErrorMessage String)
}
-- ShellCheck input and output
data CheckSpec = CheckSpec {
csFilename :: String,
csScript :: String,
csExcludedWarnings :: [Integer],
csShellTypeOverride :: Maybe Shell
} deriving (Show, Eq)
data CheckResult = CheckResult {
crFilename :: String,
crComments :: [PositionedComment]
} deriving (Show, Eq)
emptyCheckSpec = CheckSpec {
csFilename = "",
csScript = "",
csExcludedWarnings = [],
csShellTypeOverride = Nothing
}
-- Parser input and output
data ParseSpec = ParseSpec {
psFilename :: String,
psScript :: String
} deriving (Show, Eq)
data ParseResult = ParseResult {
prComments :: [PositionedComment],
prTokenPositions :: Map.Map Id Position,
prRoot :: Maybe Token
} deriving (Show, Eq)
-- Analyzer input and output
data AnalysisSpec = AnalysisSpec {
asScript :: Token,
asShellType :: Maybe Shell,
asExecutionMode :: ExecutionMode
}
data AnalysisResult = AnalysisResult {
arComments :: [TokenComment]
}
-- Formatter options
data FormatterOptions = FormatterOptions {
foColorOption :: ColorOption
}
-- Supporting data types
data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
data ExecutionMode = Executed | Sourced deriving (Show, Eq)
type ErrorMessage = String
type Code = Integer
data Severity = ErrorC | WarningC | InfoC | StyleC deriving (Show, Eq, Ord)
data Position = Position {
posFile :: String, -- Filename
posLine :: Integer, -- 1 based source line
posColumn :: Integer -- 1 based source column, where tabs are 8
} deriving (Show, Eq)
data Comment = Comment Severity Code String deriving (Show, Eq)
data PositionedComment = PositionedComment Position Comment deriving (Show, Eq)
data TokenComment = TokenComment Id Comment deriving (Show, Eq)
data ColorOption =
ColorAuto
| ColorAlways
| ColorNever
deriving (Ord, Eq, Show)
-- For testing
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
mockedSystemInterface files = SystemInterface {
siReadFile = rf
}
where
rf file =
case filter ((== file) . fst) files of
[] -> return $ Left "File not included in mock."
[(_, contents)] -> return $ Right contents

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
{-
Copyright 2012-2019 Vidar Holen
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -15,7 +15,7 @@
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
{-# LANGUAGE FlexibleContexts #-}
@@ -30,7 +30,7 @@ import Text.Regex.TDFA
-- Precompile the regex
mkRegex :: String -> Regex
mkRegex str =
let make :: String -> Regex
let make :: RegexMaker Regex CompOption ExecOption String => String -> Regex
make = makeRegex
in
make str

View File

@@ -1,13 +0,0 @@
#!/usr/bin/env bash
# TODO: Find a less trashy way to get the next available error code
if ! shopt -s globstar
then
echo "Error: This script depends on Bash 4." >&2
exit 1
fi
for i in 1 2
do
last=$(grep -hv "^prop" ./**/*.hs | grep -Ewo "$i[0-9]{3}" | sort -n | tail -n 1)
echo "Next ${i}xxx: $((last+1))"
done

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
#!/bin/bash
# quickrun runs ShellCheck in an interpreted mode.
# This allows testing changes without recompiling.
runghc -isrc -idist/build/autogen shellcheck.hs "$@"
runghc -idist/build/autogen shellcheck.hs "$@"

View File

@@ -1,11 +1,17 @@
#!/usr/bin/env bash
#!/bin/bash
# quicktest runs the ShellCheck unit tests in an interpreted mode.
# This allows running tests without compiling, which can be faster.
# 'cabal test' remains the source of truth.
(
var=$(echo 'main' | ghci test/shellcheck.hs 2>&1 | tee /dev/stderr)
if [[ $var == *ExitSuccess* ]]
var=$(echo 'liftM and $ sequence [
ShellCheck.Analytics.runTests
,ShellCheck.Parser.runTests
,ShellCheck.Checker.runTests
,ShellCheck.Checks.Commands.runTests
,ShellCheck.AnalyzerLib.runTests
]' | tr -d '\n' | cabal repl 2>&1 | tee /dev/stderr)
if [[ $var == *$'\nTrue'* ]]
then
exit 0
else

View File

@@ -29,27 +29,15 @@ will warn that decimals are not supported.
+ For scripts starting with `#!/bin/ksh` (or using `-s ksh`), ShellCheck will
not warn at all, as `ksh` supports decimals in arithmetic contexts.
# OPTIONS
**-a**,\ **--check-sourced**
: Emit warnings in sourced files. Normally, `shellcheck` will only warn
about issues in the specified files. With this option, any issues in
sourced files will also be reported.
**-C**[*WHEN*],\ **--color**[=*WHEN*]
: For TTY output, enable colors *always*, *never* or *auto*. The default
is *auto*. **--color** without an argument is equivalent to
**--color=always**.
**-i**\ *CODE1*[,*CODE2*...],\ **--include=***CODE1*[,*CODE2*...]
: Explicitly include only the specified codes in the report. Subsequent **-i**
options are cumulative, but all the codes can be specified at once,
comma-separated as a single argument. Include options override any provided
exclude options.
**-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...]
: Explicitly exclude the specified codes from the report. Subsequent **-e**
@@ -62,49 +50,16 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
standard output. Subsequent **-f** options are ignored, see **FORMATS**
below for more information.
**--list-optional**
: Output a list of known optional checks. These can be enabled with **-o**
flags or **enable** directives.
**--norc**
: Don't try to look for .shellcheckrc configuration files.
**-o**\ *NAME1*[,*NAME2*...],\ **--enable=***NAME1*[,*NAME2*...]
: Enable optional checks. The special name *all* enables all of them.
Subsequent **-o** options accumulate. This is equivalent to specifying
**enable** directives.
**-P**\ *SOURCEPATH*,\ **--source-path=***SOURCEPATH*
: Specify paths to search for sourced files, separated by `:` on Unix and
`;` on Windows. This is equivalent to specifying `search-path`
directives.
**-s**\ *shell*,\ **--shell=***shell*
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
The default is to deduce the shell from the file's `shell` directive,
shebang, or `.bash/.bats/.dash/.ksh` extension, in that order. *sh* refers to
POSIX `sh` (not the system's), and will warn of portability issues.
**-S**\ *SEVERITY*,\ **--severity=***severity*
: Specify minimum severity of errors to consider. Valid values in order of
severity are *error*, *warning*, *info* and *style*.
The default is *style*.
The default is to use the file's shebang, or *bash* if the target shell
can't be determined.
**-V**,\ **--version**
: Print version information and exit.
**-W** *NUM*,\ **--wiki-link-count=NUM**
: For TTY output, show *NUM* wiki links to more information about mentioned
warnings. Set to 0 to disable them entirely.
**-x**,\ **--external-sources**
: Follow 'source' statements even when the file is not specified as input.
@@ -112,11 +67,6 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
line (plus `/dev/null`). This option allows following any file the script
may `source`.
**FILES...**
: One or more script files to check, or "-" for standard input.
# FORMATS
**tty**
@@ -152,59 +102,27 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
...
</checkstyle>
**diff**
: Auto-fixes in unified diff format. Can be piped to `git apply` or `patch -p1`
to automatically apply fixes.
--- a/test.sh
+++ b/test.sh
@@ -2,6 +2,6 @@
## Example of a broken script.
for f in $(ls *.m3u)
do
- grep -qi hq.*mp3 $f \
+ grep -qi hq.*mp3 "$f" \
&& echo -e 'Playlist $f contains a HQ file in mp3 format'
done
**json1**
**json**
: Json is a popular serialization format that is more suitable for web
applications. ShellCheck's json is compact and contains only the bare
minimum. Tabs are counted as 1 character.
{
comments: [
{
"file": "filename",
"line": lineNumber,
"column": columnNumber,
"level": "severitylevel",
"code": errorCode,
"message": "warning message"
},
...
]
}
**json**
: This is a legacy version of the **json1** format. It's a raw array of
comments, and all offsets have a tab stop of 8.
**quiet**
: Suppress all normal output. Exit with zero if no issues are found,
otherwise exit with one. Stops processing after the first issue.
minimum.
[
{
"file": "filename",
"line": lineNumber,
"column": columnNumber,
"level": "severitylevel",
"code": errorCode,
"message": "warning message"
},
...
]
# DIRECTIVES
ShellCheck directives can be specified as comments in the shell script.
If they appear before the first command, they are considered file-wide.
Otherwise, they apply to the immediately following command or block:
ShellCheck directives can be specified as comments in the shell script
before a command or block:
# shellcheck key=value key=value
command-or-structure
@@ -234,64 +152,12 @@ Valid keys are:
The command can be a simple command like `echo foo`, or a compound command
like a function definition, subshell block or loop.
**enable**
: Enable an optional check by name, as listed with **--list-optional**.
Only file-wide `enable` directives are considered.
**source**
: Overrides the filename included by a `source`/`.` statement. This can be
used to tell shellcheck where to look for a file whose name is determined
at runtime, or to skip a source by telling it to use `/dev/null`.
**source-path**
: Add a directory to the search path for `source`/`.` statements (by default,
only ShellCheck's working directory is included). Absolute paths will also
be rooted in these paths. The special path `SCRIPTDIR` can be used to
specify the currently checked script's directory, as in
`source-path=SCRIPTDIR` or `source-path=SCRIPTDIR/../libs`. Multiple
paths accumulate, and `-P` takes precedence over them.
**shell**
: Overrides the shell detected from the shebang. This is useful for
files meant to be included (and thus lacking a shebang), or possibly
as a more targeted alternative to 'disable=2039'.
# RC FILES
Unless `--norc` is used, ShellCheck will look for a file `.shellcheckrc` or
`shellcheckrc` in the script's directory and each parent directory. If found,
it will read `key=value` pairs from it and treat them as file-wide directives.
Here is an example `.shellcheckrc`:
# Look for 'source'd files relative to the checked script,
# and also look for absolute paths in /mnt/chroot
source-path=SCRIPTDIR
source-path=/mnt/chroot
# Turn on warnings for unquoted variables with safe values
enable=quote-safe-variables
# Turn on warnings for unassigned uppercase variables
enable=check-unassigned-uppercase
# Allow using `which` since it gives full paths and is common enough
disable=SC2230
If no `.shellcheckrc` is found in any of the parent directories, ShellCheck
will look in `~/.shellcheckrc` followed by the XDG config directory
(usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on
Windows. Only the first file found will be used.
Note for Snap users: the Snap sandbox disallows access to hidden files.
Use `shellcheckrc` without the dot instead.
Note for Docker users: ShellCheck will only be able to look for files that
are mounted in the container, so `~/.shellcheckrc` will not be read.
# ENVIRONMENT VARIABLES
The environment variable `SHELLCHECK_OPTS` can be set with default flags:
export SHELLCHECK_OPTS='--shell=bash --exclude=SC2016'
@@ -309,32 +175,19 @@ ShellCheck uses the follow exit codes:
+ 3: ShellCheck was invoked with bad syntax (e.g. unknown flag).
+ 4: ShellCheck was invoked with bad options (e.g. unknown formatter).
# LOCALE
This version of ShellCheck is only available in English. All files are
leniently decoded as UTF-8, with a fallback of ISO-8859-1 for invalid
sequences. `LC_CTYPE` is respected for output, and defaults to UTF-8 for
locales where encoding is unspecified (such as the `C` locale).
Windows users seeing `commitBuffer: invalid argument (invalid character)`
should set their terminal to use UTF-8 with `chcp 65001`.
# AUTHORS
ShellCheck is developed and maintained by Vidar Holen, with assistance from a
long list of wonderful contributors.
# AUTHOR
ShellCheck is written and maintained by Vidar Holen.
# REPORTING BUGS
Bugs and issues can be reported on GitHub:
https://github.com/koalaman/shellcheck/issues
# COPYRIGHT
Copyright 2012-2019, Vidar Holen and contributors.
Copyright 2012-2015, Vidar Holen.
Licensed under the GNU General Public License version 3 or later,
see https://gnu.org/licenses/gpl.html
see http://gnu.org/licenses/gpl.html
# SEE ALSO

View File

@@ -1,8 +1,8 @@
{-
Copyright 2012-2019 Vidar Holen
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -15,43 +15,34 @@
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
import qualified ShellCheck.Analyzer
import ShellCheck.Checker
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Regex
import ShellCheck.Data
import ShellCheck.Checker
import ShellCheck.Interface
import ShellCheck.Regex
import ShellCheck.Formatter.Format
import qualified ShellCheck.Formatter.CheckStyle
import ShellCheck.Formatter.Format
import qualified ShellCheck.Formatter.Diff
import qualified ShellCheck.Formatter.GCC
import qualified ShellCheck.Formatter.JSON
import qualified ShellCheck.Formatter.JSON1
import qualified ShellCheck.Formatter.TTY
import qualified ShellCheck.Formatter.Quiet
import Control.Exception
import Control.Monad
import Control.Monad.Except
import Data.Bits
import Data.Char
import Data.Either
import Data.Functor
import Data.IORef
import Data.List
import qualified Data.Map as Map
import Data.Maybe
import Data.Monoid
import Data.Semigroup (Semigroup (..))
import Prelude hiding (catch)
import System.Console.GetOpt
import System.Directory
import System.Environment
import System.Exit
import System.FilePath
import System.IO
import Control.Exception
import Control.Monad
import Control.Monad.Except
import Data.Char
import Data.Functor
import Data.Either
import qualified Data.Map as Map
import Data.Maybe
import Data.Monoid
import Prelude hiding (catch)
import System.Console.GetOpt
import System.Directory
import System.Environment
import System.Exit
import System.IO
data Flag = Flag String String
data Status =
@@ -62,72 +53,40 @@ data Status =
| RuntimeException
deriving (Ord, Eq, Show)
instance Semigroup Status where
(<>) = max
instance Monoid Status where
mempty = NoProblems
mappend = (Data.Semigroup.<>)
mappend = max
data Options = Options {
checkSpec :: CheckSpec,
externalSources :: Bool,
sourcePaths :: [FilePath],
formatterOptions :: FormatterOptions,
minSeverity :: Severity
checkSpec :: CheckSpec,
externalSources :: Bool,
formatterOptions :: FormatterOptions
}
defaultOptions = Options {
checkSpec = emptyCheckSpec,
externalSources = False,
sourcePaths = [],
formatterOptions = newFormatterOptions {
formatterOptions = FormatterOptions {
foColorOption = ColorAuto
},
minSeverity = StyleC
}
}
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
options = [
Option "a" ["check-sourced"]
(NoArg $ Flag "sourced" "false") "Include warnings from sourced files",
Option "e" ["exclude"]
(ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings",
Option "f" ["format"]
(ReqArg (Flag "format") "FORMAT") "output format",
Option "C" ["color"]
(OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN")
"Use color (auto, always, never)",
Option "i" ["include"]
(ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings",
Option "e" ["exclude"]
(ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings",
Option "f" ["format"]
(ReqArg (Flag "format") "FORMAT") $
"Output format (" ++ formatList ++ ")",
Option "" ["list-optional"]
(NoArg $ Flag "list-optional" "true") "List checks disabled by default",
Option "" ["norc"]
(NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc files",
Option "o" ["enable"]
(ReqArg (Flag "enable") "check1,check2..")
"List of optional checks to enable (or 'all')",
Option "P" ["source-path"]
(ReqArg (Flag "source-path") "SOURCEPATHS")
"Specify path when looking for sourced files (\"SCRIPTDIR\" for script's dir)",
Option "s" ["shell"]
(ReqArg (Flag "shell") "SHELLNAME")
"Specify dialect (sh, bash, dash, ksh)",
Option "S" ["severity"]
(ReqArg (Flag "severity") "SEVERITY")
"Minimum severity of errors to consider (error, warning, info, style)",
Option "V" ["version"]
(NoArg $ Flag "version" "true") "Print version information",
Option "W" ["wiki-link-count"]
(ReqArg (Flag "wiki-link-count") "NUM")
"The number of wiki links to show, when applicable",
(ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (sh,bash,dash,ksh)",
Option "x" ["external-sources"]
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES",
Option "" ["help"]
(NoArg $ Flag "help" "true") "Show this usage summary and exit"
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES.",
Option "V" ["version"]
(NoArg $ Flag "version" "true") "Print version information"
]
getUsageInfo = usageInfo usageHeader options
printErr = lift . hPutStrLn stderr
@@ -136,27 +95,20 @@ parseArguments argv =
case getOpt Permute options argv of
(opts, files, []) -> return (opts, files)
(_, _, errors) -> do
printErr $ concat errors ++ "\n" ++ getUsageInfo
printErr $ concat errors ++ "\n" ++ usageInfo usageHeader options
throwError SyntaxFailure
formats :: FormatterOptions -> Map.Map String (IO Formatter)
formats options = Map.fromList [
("checkstyle", ShellCheck.Formatter.CheckStyle.format),
("diff", ShellCheck.Formatter.Diff.format options),
("gcc", ShellCheck.Formatter.GCC.format),
("json", ShellCheck.Formatter.JSON.format),
("json1", ShellCheck.Formatter.JSON1.format),
("tty", ShellCheck.Formatter.TTY.format options),
("quiet", ShellCheck.Formatter.Quiet.format options)
("tty", ShellCheck.Formatter.TTY.format options)
]
formatList = intercalate ", " names
where
names = Map.keys $ formats (formatterOptions defaultOptions)
getOption [] _ = Nothing
getOption [] _ = Nothing
getOption (Flag var val:_) name | name == var = return val
getOption (_:rest) flag = getOption rest flag
getOption (_:rest) flag = getOption rest flag
getOptions options name =
map (\(Flag _ val) -> val) . filter (\(Flag var _) -> var == name) $ options
@@ -170,7 +122,13 @@ split char str =
else split' rest (a:element)
split' [] element = [reverse element]
toStatus = fmap (either id id) . runExceptT
getExclusions options =
let elements = concatMap (split ',') $ getOptions options "exclude"
clean = dropWhile (not . isDigit)
in
map (Prelude.read . clean) elements :: [Int]
toStatus = liftM (either id id) . runExceptT
getEnvArgs = do
opts <- getEnv "SHELLCHECK_OPTS" `catch` cantWaitForLookupEnv
@@ -190,10 +148,10 @@ main = do
statusToCode status =
case status of
NoProblems -> ExitSuccess
SomeProblems -> ExitFailure 1
SyntaxFailure -> ExitFailure 3
SupportFailure -> ExitFailure 4
NoProblems -> ExitSuccess
SomeProblems -> ExitFailure 1
SyntaxFailure -> ExitFailure 3
SupportFailure -> ExitFailure 4
RuntimeException -> ExitFailure 2
process :: [Flag] -> [FilePath] -> ExceptT Status IO Status
@@ -227,50 +185,30 @@ runFormatter sys format options files = do
newStatus <- process file `catch` handler file
return $ status `mappend` newStatus
handler :: FilePath -> IOException -> IO Status
handler file e = reportFailure file (show e)
reportFailure file str = do
onFailure format file str
handler file e = do
onFailure format file (show e)
return RuntimeException
process :: FilePath -> IO Status
process filename = do
input <- siReadFile sys filename
either (reportFailure filename) check input
where
check contents = do
let checkspec = (checkSpec options) {
csFilename = filename,
csScript = contents
}
result <- checkScript sys checkspec
onResult format result sys
return $
if null (crComments result)
then NoProblems
else SomeProblems
contents <- inputFile filename
let checkspec = (checkSpec options) {
csFilename = filename,
csScript = contents
}
result <- checkScript sys checkspec
onResult format result contents
return $
if null (crComments result)
then NoProblems
else SomeProblems
parseEnum name value list =
case filter ((== value) . fst) list of
[(name, value)] -> return value
[] -> do
printErr $ "Unknown value for --" ++ name ++ ". " ++
"Valid options are: " ++ (intercalate ", " $ map fst list)
throwError SupportFailure
parseColorOption value =
parseEnum "color" value [
("auto", ColorAuto),
("always", ColorAlways),
("never", ColorNever)
]
parseSeverityOption value =
parseEnum "severity" value [
("error", ErrorC),
("warning", WarningC),
("info", InfoC),
("style", StyleC)
]
parseColorOption colorOption =
case colorOption of
"auto" -> ColorAuto
"always" -> ColorAlways
"never" -> ColorNever
_ -> error $ "Bad value for --color `" ++ colorOption ++ "'"
parseOption flag options =
case flag of
@@ -284,7 +222,7 @@ parseOption flag options =
}
Flag "exclude" str -> do
new <- mapM parseNum $ filter (not . null) $ split ',' str
new <- mapM parseNum $ split ',' str
let old = csExcludedWarnings . checkSpec $ options
return options {
checkSpec = (checkSpec options) {
@@ -292,92 +230,23 @@ parseOption flag options =
}
}
Flag "include" str -> do
new <- mapM parseNum $ filter (not . null) $ split ',' str
let old = csIncludedWarnings . checkSpec $ options
return options {
checkSpec = (checkSpec options) {
csIncludedWarnings =
if null new
then old
else Just new `mappend` old
}
}
Flag "version" _ -> do
liftIO printVersion
throwError NoProblems
Flag "list-optional" _ -> do
liftIO printOptional
throwError NoProblems
Flag "help" _ -> do
liftIO $ putStrLn getUsageInfo
throwError NoProblems
Flag "externals" _ ->
return options {
externalSources = True
}
Flag "color" color -> do
option <- parseColorOption color
Flag "color" color ->
return options {
formatterOptions = (formatterOptions options) {
foColorOption = option
foColorOption = parseColorOption color
}
}
Flag "source-path" str -> do
let paths = splitSearchPath str
return options {
sourcePaths = (sourcePaths options) ++ paths
}
Flag "sourced" _ ->
return options {
checkSpec = (checkSpec options) {
csCheckSourced = True
}
}
Flag "severity" severity -> do
option <- parseSeverityOption severity
return options {
checkSpec = (checkSpec options) {
csMinSeverity = option
}
}
Flag "wiki-link-count" countString -> do
count <- parseNum countString
return options {
formatterOptions = (formatterOptions options) {
foWikiLinkCount = count
}
}
Flag "norc" _ ->
return options {
checkSpec = (checkSpec options) {
csIgnoreRC = True
}
}
Flag "enable" value ->
let cs = checkSpec options in return options {
checkSpec = cs {
csOptionalChecks = (csOptionalChecks cs) ++ split ',' value
}
}
-- This flag is handled specially in 'process'
Flag "format" _ -> return options
Flag str _ -> do
printErr $ "Internal error for --" ++ str ++ ". Please file a bug :("
return options
_ -> return options
where
die s = do
printErr s
@@ -385,39 +254,22 @@ parseOption flag options =
parseNum ('S':'C':str) = parseNum str
parseNum num = do
unless (all isDigit num) $ do
printErr $ "Invalid number: " ++ num
printErr $ "Bad exclusion: " ++ num
throwError SyntaxFailure
return (Prelude.read num :: Integer)
ioInterface options files = do
inputs <- mapM normalize files
cache <- newIORef emptyCache
configCache <- newIORef ("", Nothing)
return SystemInterface {
siReadFile = get cache inputs,
siFindSource = findSourceFile inputs (sourcePaths options),
siGetConfig = getConfig configCache
siReadFile = get inputs
}
where
emptyCache :: Map.Map FilePath String
emptyCache = Map.empty
get cache inputs file = do
map <- readIORef cache
case Map.lookup file map of
Just x -> return $ Right x
Nothing -> fetch cache inputs file
fetch cache inputs file = do
get inputs file = do
ok <- allowable inputs file
if ok
then (do
(contents, shouldCache) <- inputFile file
when shouldCache $
modifyIORef cache $ Map.insert file contents
return $ Right contents
) `catch` handler
then (Right <$> inputFile file) `catch` handler
else return $ Left (file ++ " was not specified as input (see shellcheck -x).")
where
handler :: IOException -> IO (Either ErrorMessage String)
handler ex = return . Left $ show ex
@@ -435,126 +287,14 @@ ioInterface options files = do
fallback :: FilePath -> IOException -> IO FilePath
fallback path _ = return path
-- Returns the name and contents of .shellcheckrc for the given file
getConfig cache filename = do
path <- normalize filename
let dir = takeDirectory path
(previousPath, result) <- readIORef cache
if dir == previousPath
then return result
else do
paths <- getConfigPaths dir
result <- findConfig paths
writeIORef cache (dir, result)
return result
findConfig paths =
case paths of
(file:rest) -> do
contents <- readConfig file
if isJust contents
then return contents
else findConfig rest
[] -> return Nothing
-- Get a list of candidate filenames. This includes .shellcheckrc
-- in all parent directories, plus the user's home dir and xdg dir.
-- The dot is optional for Windows and Snap users.
getConfigPaths dir = do
let next = takeDirectory dir
rest <- if next /= dir
then getConfigPaths next
else defaultPaths `catch`
((const $ return []) :: IOException -> IO [FilePath])
return $ (dir </> ".shellcheckrc") : (dir </> "shellcheckrc") : rest
defaultPaths = do
home <- getAppUserDataDirectory "shellcheckrc"
xdg <- getXdgDirectory XdgConfig "shellcheckrc"
return [home, xdg]
readConfig file = do
exists <- doesFileExist file
if exists
then do
(contents, _) <- inputFile file `catch` handler file
return $ Just (file, contents)
else
return Nothing
where
handler :: FilePath -> IOException -> IO (String, Bool)
handler file err = do
putStrLn $ file ++ ": " ++ show err
return ("", True)
andM a b arg = do
first <- a arg
if not first then return False else b arg
findSourceFile inputs sourcePathFlag currentScript sourcePathAnnotation original =
if isAbsolute original
then
let (_, relative) = splitDrive original
in find relative original
else
find original original
where
find filename deflt = do
sources <- filterM ((allowable inputs) `andM` doesFileExist) $
(adjustPath filename):(map (</> filename) $ map adjustPath $ sourcePathFlag ++ sourcePathAnnotation)
case sources of
[] -> return deflt
(first:_) -> return first
scriptdir = dropFileName currentScript
adjustPath str =
case (splitDirectories str) of
("SCRIPTDIR":rest) -> joinPath (scriptdir:rest)
_ -> str
inputFile file = do
(handle, shouldCache) <-
contents <-
if file == "-"
then return (stdin, True)
else do
h <- openBinaryFile file ReadMode
reopenable <- hIsSeekable h
return (h, not reopenable)
hSetBinaryMode handle True
contents <- decodeString <$> hGetContents handle -- closes handle
then getContents
else readFile file
seq (length contents) $
return (contents, shouldCache)
-- Decode a char8 string into a utf8 string, with fallback on
-- ISO-8859-1. This avoids depending on additional libraries.
decodeString = decode
where
decode [] = []
decode (c:rest) | isAscii c = c : decode rest
decode (c:rest) =
let num = (fromIntegral $ ord c) :: Int
next = case num of
_ | num >= 0xF8 -> Nothing
| num >= 0xF0 -> construct (num .&. 0x07) 3 rest
| num >= 0xE0 -> construct (num .&. 0x0F) 2 rest
| num >= 0xC0 -> construct (num .&. 0x1F) 1 rest
| True -> Nothing
in
case next of
Just (n, remainder) -> chr n : decode remainder
Nothing -> c : decode rest
construct x 0 rest = do
guard $ x <= 0x10FFFF
return (x, rest)
construct x n (c:rest) =
let num = (fromIntegral $ ord c) :: Int in
if num >= 0x80 && num <= 0xBF
then construct ((x `shiftL` 6) .|. (num .&. 0x3f)) (n-1) rest
else Nothing
construct _ _ _ = Nothing
return contents
verifyFiles files =
when (null files) $ do
@@ -566,15 +306,4 @@ printVersion = do
putStrLn "ShellCheck - shell script analysis tool"
putStrLn $ "version: " ++ shellcheckVersion
putStrLn "license: GNU General Public License, version 3"
putStrLn "website: https://www.shellcheck.net"
printOptional = do
mapM f list
where
list = sortOn cdName ShellCheck.Analyzer.optionalChecks
f item = do
putStrLn $ "name: " ++ cdName item
putStrLn $ "desc: " ++ cdDescription item
putStrLn $ "example: " ++ cdPositive item
putStrLn $ "fix: " ++ cdNegative item
putStrLn ""
putStrLn "website: http://www.shellcheck.net"

View File

@@ -1,54 +0,0 @@
name: shellcheck
summary: A shell script static analysis tool
description: |
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh
shell scripts.
The goals of ShellCheck are
- To point out and clarify typical beginner's syntax issues that cause a
shell to give cryptic error messages.
- To point out and clarify typical intermediate level semantic problems that
cause a shell to behave strangely and counter-intuitively.
- To point out subtle caveats, corner cases and pitfalls that may cause an
advanced user's otherwise working script to fail under future
circumstances.
By default ShellCheck can only check non-hidden files under /home, to make
ShellCheck be able to check files under /media and /run/media you must
connect it to the `removable-media` interface manually:
# snap connect shellcheck:removable-media
version: git
base: core18
grade: stable
confinement: strict
apps:
shellcheck:
command: usr/bin/shellcheck
plugs: [home, removable-media]
parts:
shellcheck:
plugin: dump
source: .
build-packages:
- cabal-install
- squid
override-build: |
# See comments in .snapsquid.conf
[ "$http_proxy" ] && {
squid3 -f .snapsquid.conf
export http_proxy="http://localhost:8888"
sleep 3
}
cabal sandbox init
cabal update || cat /var/log/squid/*
cabal install -j
install -d $SNAPCRAFT_PART_INSTALL/usr/bin
install .cabal-sandbox/bin/shellcheck $SNAPCRAFT_PART_INSTALL/usr/bin

View File

@@ -1,513 +0,0 @@
{-
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.ASTLib where
import ShellCheck.AST
import Control.Monad.Writer
import Control.Monad
import Data.Char
import Data.Functor
import Data.List
import Data.Maybe
-- Is this a type of loop?
isLoop t = case t of
T_WhileExpression {} -> True
T_UntilExpression {} -> True
T_ForIn {} -> True
T_ForArithmetic {} -> True
T_SelectIn {} -> True
_ -> False
-- Will this split into multiple words when used as an argument?
willSplit x =
case x of
T_DollarBraced {} -> True
T_DollarExpansion {} -> True
T_Backticked {} -> True
T_BraceExpansion {} -> True
T_Glob {} -> True
T_Extglob {} -> True
T_NormalWord _ l -> any willSplit l
_ -> False
isGlob T_Extglob {} = True
isGlob T_Glob {} = True
isGlob (T_NormalWord _ l) = any isGlob l
isGlob _ = False
-- Is this shell word a constant?
isConstant token =
case token of
-- This ignores some cases like ~"foo":
T_NormalWord _ (T_Literal _ ('~':_) : _) -> False
T_NormalWord _ l -> all isConstant l
T_DoubleQuoted _ l -> all isConstant l
T_SingleQuoted _ _ -> True
T_Literal _ _ -> True
_ -> False
-- Is this an empty literal?
isEmpty token =
case token of
T_NormalWord _ l -> all isEmpty l
T_DoubleQuoted _ l -> all isEmpty l
T_SingleQuoted _ "" -> True
T_Literal _ "" -> True
_ -> False
-- Quick&lazy oversimplification of commands, throwing away details
-- and returning a list like ["find", ".", "-name", "${VAR}*" ].
oversimplify token =
case token of
(T_NormalWord _ l) -> [concat (concatMap oversimplify l)]
(T_DoubleQuoted _ l) -> [concat (concatMap oversimplify l)]
(T_SingleQuoted _ s) -> [s]
(T_DollarBraced _ _ _) -> ["${VAR}"]
(T_DollarArithmetic _ _) -> ["${VAR}"]
(T_DollarExpansion _ _) -> ["${VAR}"]
(T_Backticked _ _) -> ["${VAR}"]
(T_Glob _ s) -> [s]
(T_Pipeline _ _ [x]) -> oversimplify x
(T_Literal _ x) -> [x]
(T_ParamSubSpecialChar _ x) -> [x]
(T_SimpleCommand _ vars words) -> concatMap oversimplify words
(T_Redirecting _ _ foo) -> oversimplify foo
(T_DollarSingleQuoted _ s) -> [s]
(T_Annotation _ _ s) -> oversimplify s
-- Workaround for let "foo = bar" parsing
(TA_Sequence _ [TA_Expansion _ v]) -> concatMap oversimplify v
_ -> []
-- Turn a SimpleCommand foo -avz --bar=baz into args "a", "v", "z", "bar",
-- each in a tuple of (token, stringFlag). Non-flag arguments are added with
-- stringFlag == "".
getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
let tokenAndText = map (\x -> (x, concat $ oversimplify x)) args
(flagArgs, rest) = break (stopCondition . snd) tokenAndText
in
concatMap flag flagArgs ++ map (\(t, _) -> (t, "")) rest
where
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
flag (x, '-':args) = map (\v -> (x, [v])) args
flag (x, _) = [ (x, "") ]
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
-- Get all flags in a GNU way, up until --
getAllFlags :: Token -> [(Token, String)]
getAllFlags = getFlagsUntil (== "--")
-- Get all flags in a BSD way, up until first non-flag argument or --
getLeadingFlags = getFlagsUntil (\x -> x == "--" || (not $ "-" `isPrefixOf` x))
-- Check if a command has a flag.
hasFlag cmd str = str `elem` (map snd $ getAllFlags cmd)
-- Is this token a word that starts with a dash?
isFlag token =
case getWordParts token of
T_Literal _ ('-':_) : _ -> True
_ -> False
-- Is this token a flag where the - is unquoted?
isUnquotedFlag token = fromMaybe False $ do
str <- getLeadingUnquotedString token
return $ "-" `isPrefixOf` str
-- Given a T_DollarBraced, return a simplified version of the string contents.
bracedString (T_DollarBraced _ _ l) = concat $ oversimplify l
bracedString _ = error "Internal shellcheck error, please report! (bracedString on non-variable)"
-- Is this an expansion of multiple items of an array?
isArrayExpansion t@(T_DollarBraced _ _ _) =
let string = bracedString t in
"@" `isPrefixOf` string ||
not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string
isArrayExpansion _ = False
-- Is it possible that this arg becomes multiple args?
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
where
f t@(T_DollarBraced _ _ _) =
let string = bracedString t in
"!" `isPrefixOf` string
f (T_DoubleQuoted _ parts) = any f parts
f (T_NormalWord _ parts) = any f parts
f _ = False
-- Is it certain that this word will becomes multiple words?
willBecomeMultipleArgs t = willConcatInAssignment t || f t
where
f T_Extglob {} = True
f T_Glob {} = True
f T_BraceExpansion {} = True
f (T_DoubleQuoted _ parts) = any f parts
f (T_NormalWord _ parts) = any f parts
f _ = False
-- This does token cause implicit concatenation in assignments?
willConcatInAssignment token =
case token of
t@T_DollarBraced {} -> isArrayExpansion t
(T_DoubleQuoted _ parts) -> any willConcatInAssignment parts
(T_NormalWord _ parts) -> any willConcatInAssignment parts
_ -> False
-- Maybe get the literal string corresponding to this token
getLiteralString :: Token -> Maybe String
getLiteralString = getLiteralStringExt (const Nothing)
-- Definitely get a literal string, skipping over all non-literals
onlyLiteralString :: Token -> String
onlyLiteralString = fromJust . getLiteralStringExt (const $ return "")
-- Maybe get a literal string, but only if it's an unquoted argument.
getUnquotedLiteral (T_NormalWord _ list) =
concat <$> mapM str list
where
str (T_Literal _ s) = return s
str _ = Nothing
getUnquotedLiteral _ = Nothing
-- Get the last unquoted T_Literal in a word like "${var}foo"THIS
-- or nothing if the word does not end in an unquoted literal.
getTrailingUnquotedLiteral :: Token -> Maybe Token
getTrailingUnquotedLiteral t =
case t of
(T_NormalWord _ list@(_:_)) ->
from (last list)
_ -> Nothing
where
from t =
case t of
T_Literal {} -> return t
_ -> Nothing
-- Get the leading, unquoted, literal string of a token (if any).
getLeadingUnquotedString :: Token -> Maybe String
getLeadingUnquotedString t =
case t of
T_NormalWord _ ((T_Literal _ s) : _) -> return s
_ -> Nothing
-- Maybe get the literal string of this token and any globs in it.
getGlobOrLiteralString = getLiteralStringExt f
where
f (T_Glob _ str) = return str
f _ = Nothing
-- Maybe get the literal value of a token, using a custom function
-- to map unrecognized Tokens into strings.
getLiteralStringExt :: (Token -> Maybe String) -> Token -> Maybe String
getLiteralStringExt more = g
where
allInList = fmap concat . mapM g
g (T_DoubleQuoted _ l) = allInList l
g (T_DollarDoubleQuoted _ l) = allInList l
g (T_NormalWord _ l) = allInList l
g (TA_Expansion _ l) = allInList l
g (T_SingleQuoted _ s) = return s
g (T_Literal _ s) = return s
g (T_ParamSubSpecialChar _ s) = return s
g (T_DollarSingleQuoted _ s) = return $ decodeEscapes s
g x = more x
-- Bash style $'..' decoding
decodeEscapes ('\\':c:cs) =
case c of
'a' -> '\a' : rest
'b' -> '\b' : rest
'e' -> '\x1B' : rest
'f' -> '\f' : rest
'n' -> '\n' : rest
'r' -> '\r' : rest
't' -> '\t' : rest
'v' -> '\v' : rest
'\'' -> '\'' : rest
'"' -> '"' : rest
'\\' -> '\\' : rest
'x' ->
case cs of
(x:y:more) ->
if isHexDigit x && isHexDigit y
then chr (16*(digitToInt x) + (digitToInt y)) : rest
else '\\':c:rest
_ | isOctDigit c ->
let digits = take 3 $ takeWhile isOctDigit (c:cs)
num = parseOct digits
in (if num < 256 then chr num else '?') : rest
_ -> '\\' : c : rest
where
rest = decodeEscapes cs
parseOct = f 0
where
f n "" = n
f n (c:rest) = f (n * 8 + digitToInt c) rest
decodeEscapes (c:cs) = c : decodeEscapes cs
decodeEscapes [] = []
-- Is this token a string literal?
isLiteral t = isJust $ getLiteralString t
-- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz]
getWordParts (T_NormalWord _ l) = concatMap getWordParts l
getWordParts (T_DoubleQuoted _ l) = l
-- TA_Expansion is basically T_NormalWord for arithmetic expressions
getWordParts (TA_Expansion _ l) = concatMap getWordParts l
getWordParts other = [other]
-- Return a list of NormalWords that would result from brace expansion
braceExpand (T_NormalWord id list) = take 1000 $ do
items <- mapM part list
return $ T_NormalWord id items
where
part (T_BraceExpansion id items) = do
item <- items
braceExpand item
part x = return x
-- Maybe get a SimpleCommand from immediate wrappers like T_Redirections
getCommand t =
case t of
T_Redirecting _ _ w -> getCommand w
T_SimpleCommand _ _ (w:_) -> return t
T_Annotation _ _ t -> getCommand t
_ -> Nothing
-- Maybe get the command name string of a token representing a command
getCommandName :: Token -> Maybe String
getCommandName = fst . getCommandNameAndToken
-- Get the command name token from a command, i.e.
-- the token representing 'ls' in 'ls -la 2> foo'.
-- If it can't be determined, return the original token.
getCommandTokenOrThis = snd . getCommandNameAndToken
getCommandNameAndToken :: Token -> (Maybe String, Token)
getCommandNameAndToken t = fromMaybe (Nothing, t) $ do
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
s <- getLiteralString w
if "busybox" `isSuffixOf` s || "builtin" == s
then
case rest of
(applet:_) -> return (getLiteralString applet, applet)
_ -> return (Just s, w)
else
return (Just s, w)
-- If a command substitution is a single command, get its name.
-- $(date +%s) = Just "date"
getCommandNameFromExpansion :: Token -> Maybe String
getCommandNameFromExpansion t =
case t of
T_DollarExpansion _ [c] -> extract c
T_Backticked _ [c] -> extract c
T_DollarBraceCommandExpansion _ [c] -> extract c
_ -> Nothing
where
extract (T_Pipeline _ _ [cmd]) = getCommandName cmd
extract _ = Nothing
-- Get the basename of a token representing a command
getCommandBasename = fmap basename . getCommandName
where
basename = reverse . takeWhile (/= '/') . reverse
isAssignment t =
case t of
T_Redirecting _ _ w -> isAssignment w
T_SimpleCommand _ (w:_) [] -> True
T_Assignment {} -> True
T_Annotation _ _ w -> isAssignment w
_ -> False
isOnlyRedirection t =
case t of
T_Pipeline _ _ [x] -> isOnlyRedirection x
T_Annotation _ _ w -> isOnlyRedirection w
T_Redirecting _ (_:_) c -> isOnlyRedirection c
T_SimpleCommand _ [] [] -> True
_ -> False
isFunction t = case t of T_Function {} -> True; _ -> False
-- Bats tests are functions for the purpose of 'local' and such
isFunctionLike t =
case t of
T_Function {} -> True
T_BatsTest {} -> True
_ -> False
isBraceExpansion t = case t of T_BraceExpansion {} -> True; _ -> False
-- Get the lists of commands from tokens that contain them, such as
-- the body of while loops or branches of if statements.
getCommandSequences :: Token -> [[Token]]
getCommandSequences t =
case t of
T_Script _ _ cmds -> [cmds]
T_BraceGroup _ cmds -> [cmds]
T_Subshell _ cmds -> [cmds]
T_WhileExpression _ _ cmds -> [cmds]
T_UntilExpression _ _ cmds -> [cmds]
T_ForIn _ _ _ cmds -> [cmds]
T_ForArithmetic _ _ _ _ cmds -> [cmds]
T_IfExpression _ thens elses -> map snd thens ++ [elses]
T_Annotation _ _ t -> getCommandSequences t
_ -> []
-- Get a list of names of associative arrays
getAssociativeArrays t =
nub . execWriter $ doAnalysis f t
where
f :: Token -> Writer [String] ()
f t@T_SimpleCommand {} = fromMaybe (return ()) $ do
name <- getCommandName t
let assocNames = ["declare","local","typeset"]
guard $ elem name assocNames
let flags = getAllFlags t
guard $ elem "A" $ map snd flags
let args = map fst . filter ((==) "" . snd) $ flags
let names = mapMaybe (getLiteralStringExt nameAssignments) args
return $ tell names
f _ = return ()
nameAssignments t =
case t of
T_Assignment _ _ name _ _ -> return name
_ -> Nothing
-- A Pseudoglob is a wildcard pattern used for checking if a match can succeed.
-- For example, [[ $(cmd).jpg == [a-z] ]] will give the patterns *.jpg and ?, which
-- can be proven never to match.
data PseudoGlob = PGAny | PGMany | PGChar Char
deriving (Eq, Show)
-- Turn a word into a PG pattern, replacing all unknown/runtime values with
-- PGMany.
wordToPseudoGlob :: Token -> Maybe [PseudoGlob]
wordToPseudoGlob word =
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
where
f x = case x of
T_Literal _ s -> return $ map PGChar s
T_SingleQuoted _ s -> return $ map PGChar s
T_DollarBraced {} -> return [PGMany]
T_DollarExpansion {} -> return [PGMany]
T_Backticked {} -> return [PGMany]
T_Glob _ "?" -> return [PGAny]
T_Glob _ ('[':_) -> return [PGAny]
T_Glob {} -> return [PGMany]
T_Extglob {} -> return [PGMany]
_ -> return [PGMany]
-- Turn a word into a PG pattern, but only if we can preserve
-- exact semantics.
wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob]
wordToExactPseudoGlob word =
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
where
f x = case x of
T_Literal _ s -> return $ map PGChar s
T_SingleQuoted _ s -> return $ map PGChar s
T_Glob _ "?" -> return [PGAny]
T_Glob _ "*" -> return [PGMany]
_ -> fail "Unknown token type"
-- Reorder a PseudoGlob for more efficient matching, e.g.
-- f?*?**g -> f??*g
simplifyPseudoGlob :: [PseudoGlob] -> [PseudoGlob]
simplifyPseudoGlob = f
where
f [] = []
f (x@(PGChar _) : rest ) = x : f rest
f list =
let (anys, rest) = span (\x -> x == PGMany || x == PGAny) list in
order anys ++ f rest
order s = let (any, many) = partition (== PGAny) s in
any ++ take 1 many
-- Check whether the two patterns can ever overlap.
pseudoGlobsCanOverlap :: [PseudoGlob] -> [PseudoGlob] -> Bool
pseudoGlobsCanOverlap = matchable
where
matchable x@(xf:xs) y@(yf:ys) =
case (xf, yf) of
(PGMany, _) -> matchable x ys || matchable xs y
(_, PGMany) -> matchable x ys || matchable xs y
(PGAny, _) -> matchable xs ys
(_, PGAny) -> matchable xs ys
(_, _) -> xf == yf && matchable xs ys
matchable [] [] = True
matchable (PGMany : rest) [] = matchable rest []
matchable (_:_) [] = False
matchable [] r = matchable r []
-- Check whether the first pattern always overlaps the second.
pseudoGlobIsSuperSetof :: [PseudoGlob] -> [PseudoGlob] -> Bool
pseudoGlobIsSuperSetof = matchable
where
matchable x@(xf:xs) y@(yf:ys) =
case (xf, yf) of
(PGMany, PGMany) -> matchable x ys
(PGMany, _) -> matchable x ys || matchable xs y
(_, PGMany) -> False
(PGAny, _) -> matchable xs ys
(_, PGAny) -> False
(_, _) -> xf == yf && matchable xs ys
matchable [] [] = True
matchable (PGMany : rest) [] = matchable rest []
matchable _ _ = False
wordsCanBeEqual x y = fromMaybe True $
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
-- Is this an expansion that can be quoted,
-- e.g. $(foo) `foo` $foo (but not {foo,})?
isQuoteableExpansion t = case t of
T_DollarBraced {} -> True
_ -> isCommandSubstitution t
isCommandSubstitution t = case t of
T_DollarExpansion {} -> True
T_DollarBraceCommandExpansion {} -> True
T_Backticked {} -> True
_ -> False
-- Is this a T_Annotation that ignores a specific code?
isAnnotationIgnoringCode code t =
case t of
T_Annotation _ anns _ -> any hasNum anns
_ -> False
where
hasNum (DisableComment ts) = code == ts
hasNum _ = False

View File

@@ -1,974 +0,0 @@
{-
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.AnalyzerLib where
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Regex
import Control.Arrow (first)
import Control.DeepSeq
import Control.Monad.Identity
import Control.Monad.RWS
import Control.Monad.State
import Control.Monad.Writer
import Data.Char
import Data.List
import Data.Maybe
import Data.Semigroup
import qualified Data.Map as Map
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (maxSuccess, quickCheckWithResult, stdArgs)
type Analysis = AnalyzerM ()
type AnalyzerM a = RWS Parameters [TokenComment] Cache a
nullCheck = const $ return ()
data Checker = Checker {
perScript :: Root -> Analysis,
perToken :: Token -> Analysis
}
runChecker :: Parameters -> Checker -> [TokenComment]
runChecker params checker = notes
where
root = rootNode params
check = perScript checker `composeAnalyzers` (\(Root x) -> void $ doAnalysis (perToken checker) x)
notes = snd $ evalRWS (check $ Root root) params Cache
instance Semigroup Checker where
(<>) x y = Checker {
perScript = perScript x `composeAnalyzers` perScript y,
perToken = perToken x `composeAnalyzers` perToken y
}
instance Monoid Checker where
mempty = Checker {
perScript = nullCheck,
perToken = nullCheck
}
mappend = (Data.Semigroup.<>)
composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis
composeAnalyzers f g x = f x >> g x
data Parameters = Parameters {
-- Whether this script has the 'lastpipe' option set/default.
hasLastpipe :: Bool,
-- Whether this script has 'set -e' anywhere.
hasSetE :: Bool,
-- A linear (bad) analysis of data flow
variableFlow :: [StackData],
-- A map from Id to parent Token
parentMap :: Map.Map Id Token,
-- The shell type, such as Bash or Ksh
shellType :: Shell,
-- True if shell type was forced via flags
shellTypeSpecified :: Bool,
-- The root node of the AST
rootNode :: Token,
-- map from token id to start and end position
tokenPositions :: Map.Map Id (Position, Position)
} deriving (Show)
-- TODO: Cache results of common AST ops here
data Cache = Cache {}
data Scope = SubshellScope String | NoneScope deriving (Show, Eq)
data StackData =
StackScope Scope
| StackScopeEnd
-- (Base expression, specific position, var name, assigned values)
| Assignment (Token, Token, String, DataType)
| Reference (Token, Token, String)
deriving (Show)
data DataType = DataString DataSource | DataArray DataSource
deriving (Show)
data DataSource =
SourceFrom [Token]
| SourceExternal
| SourceDeclaration
| SourceInteger
| SourceChecked
deriving (Show)
data VariableState = Dead Token String | Alive deriving (Show)
defaultSpec pr = spec {
asShellType = Nothing,
asCheckSourced = False,
asExecutionMode = Executed,
asTokenPositions = prTokenPositions pr
} where spec = newAnalysisSpec (fromJust $ prRoot pr)
pScript s =
let
pSpec = newParseSpec {
psFilename = "script",
psScript = s
}
in runIdentity $ parseScript (mockedSystemInterface []) pSpec
-- For testing. If parsed, returns whether there are any comments
producesComments :: Checker -> String -> Maybe Bool
producesComments c s = do
let pr = pScript s
prRoot pr
let spec = defaultSpec pr
let params = makeParameters spec
return . not . null $ runChecker params c
makeComment :: Severity -> Id -> Code -> String -> TokenComment
makeComment severity id code note =
newTokenComment {
tcId = id,
tcComment = newComment {
cSeverity = severity,
cCode = code,
cMessage = note
}
}
addComment note = note `deepseq` tell [note]
warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m ()
warn id code str = addComment $ makeComment WarningC id code str
err id code str = addComment $ makeComment ErrorC id code str
info id code str = addComment $ makeComment InfoC id code str
style id code str = addComment $ makeComment StyleC id code str
warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
warnWithFix = addCommentWithFix WarningC
styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
styleWithFix = addCommentWithFix StyleC
addCommentWithFix :: MonadWriter [TokenComment] m => Severity -> Id -> Code -> String -> Fix -> m ()
addCommentWithFix severity id code str fix =
addComment $ makeCommentWithFix severity id code str fix
makeCommentWithFix :: Severity -> Id -> Code -> String -> Fix -> TokenComment
makeCommentWithFix severity id code str fix =
let comment = makeComment severity id code str
withFix = comment {
tcFix = Just fix
}
in withFix `deepseq` withFix
makeParameters spec =
let params = Parameters {
rootNode = root,
shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec,
hasSetE = containsSetE root,
hasLastpipe =
case shellType params of
Bash -> containsLastpipe root
Dash -> False
Sh -> False
Ksh -> True,
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
parentMap = getParentTree root,
variableFlow = getVariableFlow params root,
tokenPositions = asTokenPositions spec
} in params
where root = asScript spec
-- Does this script mention 'set -e' anywhere?
-- Used as a hack to disable certain warnings.
containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
where
isSetE t =
case t of
T_Script _ (T_Literal _ str) _ -> str `matches` re
T_SimpleCommand {} ->
t `isUnqualifiedCommand` "set" &&
("errexit" `elem` oversimplify t ||
"e" `elem` map snd (getAllFlags t))
_ -> False
re = mkRegex "[[:space:]]-[^-]*e"
-- Does this script mention 'shopt -s lastpipe' anywhere?
-- Also used as a hack.
containsLastpipe root =
isNothing $ doAnalysis (guard . not . isShoptLastPipe) root
where
isShoptLastPipe t =
case t of
T_SimpleCommand {} ->
t `isUnqualifiedCommand` "shopt" &&
("lastpipe" `elem` oversimplify t)
_ -> False
prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh
prop_determineShell1 = determineShellTest "#!/usr/bin/env ksh" == Ksh
prop_determineShell2 = determineShellTest "" == Bash
prop_determineShell3 = determineShellTest "#!/bin/sh -e" == Sh
prop_determineShell4 = determineShellTest "#!/bin/ksh\n#shellcheck shell=sh\nfoo" == Sh
prop_determineShell5 = determineShellTest "#shellcheck shell=sh\nfoo" == Sh
prop_determineShell6 = determineShellTest "#! /bin/sh" == Sh
prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh
determineShellTest = determineShellTest' Nothing
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
determineShell fallbackShell t = fromMaybe Bash $ do
shellString <- foldl mplus Nothing $ getCandidates t
shellForExecutable shellString `mplus` fallbackShell
where
forAnnotation t =
case t of
(ShellOverride s) -> return s
_ -> fail ""
getCandidates :: Token -> [Maybe String]
getCandidates t@T_Script {} = [Just $ fromShebang t]
getCandidates (T_Annotation _ annotations s) =
map forAnnotation annotations ++
[Just $ fromShebang s]
fromShebang (T_Script _ (T_Literal _ s) _) = executableFromShebang s
-- Given a string like "/bin/bash" or "/usr/bin/env dash",
-- return the shell basename like "bash" or "dash"
executableFromShebang :: String -> String
executableFromShebang = shellFor
where
shellFor s | "/env " `isInfixOf` s = head (drop 1 (words s)++[""])
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
-- Given a root node, make a map from Id to parent Token.
-- This is used to populate parentMap in Parameters
getParentTree :: Token -> Map.Map Id Token
getParentTree t =
snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
where
pre t = modify (first ((:) t))
post t = do
(x, map) <- get
case x of
_:rest -> case rest of [] -> put (rest, map)
(x:_) -> put (rest, Map.insert (getId t) x map)
-- Given a root node, make a map from Id to Token
getTokenMap :: Token -> Map.Map Id Token
getTokenMap t =
execState (doAnalysis f t) Map.empty
where
f t = modify (Map.insert (getId t) t)
-- Is this token in a quoting free context? (i.e. would variable expansion split)
-- True: Assignments, [[ .. ]], here docs, already in double quotes
-- False: Regular words
isStrictlyQuoteFree = isQuoteFreeNode True
-- Like above, but also allow some cases where splitting may be desired.
-- True: Like above + for loops
-- False: Like above
isQuoteFree = isQuoteFreeNode False
isQuoteFreeNode strict tree t =
(isQuoteFreeElement t == Just True) ||
head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False])
where
-- Is this node self-quoting in itself?
isQuoteFreeElement t =
case t of
T_Assignment {} -> return True
T_FdRedirect {} -> return True
_ -> Nothing
-- Are any subnodes inherently self-quoting?
isQuoteFreeContext t =
case t of
TC_Nullary _ DoubleBracket _ -> return True
TC_Unary _ DoubleBracket _ _ -> return True
TC_Binary _ DoubleBracket _ _ _ -> return True
TA_Sequence {} -> return True
T_Arithmetic {} -> return True
T_Assignment {} -> return True
T_Redirecting {} -> return False
T_DoubleQuoted _ _ -> return True
T_DollarDoubleQuoted _ _ -> return True
T_CaseExpression {} -> return True
T_HereDoc {} -> return True
T_DollarBraced {} -> return True
-- When non-strict, pragmatically assume it's desirable to split here
T_ForIn {} -> return (not strict)
T_SelectIn {} -> return (not strict)
_ -> Nothing
-- Check if a token is a parameter to a certain command by name:
-- Example: isParamTo (parentMap params) "sed" t
isParamTo :: Map.Map Id Token -> String -> Token -> Bool
isParamTo tree cmd =
go
where
go x = case Map.lookup (getId x) tree of
Nothing -> False
Just parent -> check parent
check t =
case t of
T_SingleQuoted _ _ -> go t
T_DoubleQuoted _ _ -> go t
T_NormalWord _ _ -> go t
T_SimpleCommand {} -> isCommand t cmd
T_Redirecting {} -> isCommand t cmd
_ -> False
-- Get the parent command (T_Redirecting) of a Token, if any.
getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token
getClosestCommand tree t =
findFirst findCommand $ getPath tree t
where
findCommand t =
case t of
T_Redirecting {} -> return True
T_Script {} -> return False
_ -> Nothing
-- Like above, if koala_man knew Haskell when starting this project.
getClosestCommandM t = do
tree <- asks parentMap
return $ getClosestCommand tree t
-- Is the token used as a command name (the first word in a T_SimpleCommand)?
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
where
go currentId (T_NormalWord id [word]:rest)
| currentId == getId word = go id rest
go currentId (T_DoubleQuoted id [word]:rest)
| currentId == getId word = go id rest
go currentId (T_SimpleCommand _ _ (word:_):_)
| currentId == getId word = True
go _ _ = False
-- A list of the element and all its parents up to the root node.
getPath tree t = t :
case Map.lookup (getId t) tree of
Nothing -> []
Just parent -> getPath tree parent
-- Version of the above taking the map from the current context
-- Todo: give this the name "getPath"
getPathM t = do
map <- asks parentMap
return $ getPath map t
isParentOf tree parent child =
elem (getId parent) . map getId $ getPath tree child
parents params = getPath (parentMap params)
-- Find the first match in a list where the predicate is Just True.
-- Stops if it's Just False and ignores Nothing.
findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a
findFirst p l =
case l of
[] -> Nothing
(x:xs) ->
case p x of
Just True -> return x
Just False -> Nothing
Nothing -> findFirst p xs
-- Check whether a word is entirely output from a single command
tokenIsJustCommandOutput t = case t of
T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds
T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ cmds]] -> check cmds
T_NormalWord id [T_Backticked _ cmds] -> check cmds
T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ cmds]] -> check cmds
_ -> False
where
check [x] = not $ isOnlyRedirection x
check _ = False
-- TODO: Replace this with a proper Control Flow Graph
getVariableFlow params t =
let (_, stack) = runState (doStackAnalysis startScope endScope t) []
in reverse stack
where
startScope t =
let scopeType = leadType params t
in do
when (scopeType /= NoneScope) $ modify (StackScope scopeType:)
when (assignFirst t) $ setWritten t
endScope t =
let scopeType = leadType params t
in do
setRead t
unless (assignFirst t) $ setWritten t
when (scopeType /= NoneScope) $ modify (StackScopeEnd:)
assignFirst T_ForIn {} = True
assignFirst T_SelectIn {} = True
assignFirst (T_BatsTest {}) = True
assignFirst _ = False
setRead t =
let read = getReferencedVariables (parentMap params) t
in mapM_ (\v -> modify (Reference v:)) read
setWritten t =
let written = getModifiedVariables t
in mapM_ (\v -> modify (Assignment v:)) written
leadType params t =
case t of
T_DollarExpansion _ _ -> SubshellScope "$(..) expansion"
T_Backticked _ _ -> SubshellScope "`..` expansion"
T_Backgrounded _ _ -> SubshellScope "backgrounding &"
T_Subshell _ _ -> SubshellScope "(..) group"
T_BatsTest {} -> SubshellScope "@bats test"
T_CoProcBody _ _ -> SubshellScope "coproc"
T_Redirecting {} ->
if fromMaybe False causesSubshell
then SubshellScope "pipeline"
else NoneScope
_ -> NoneScope
where
parentPipeline = do
parent <- Map.lookup (getId t) (parentMap params)
case parent of
T_Pipeline {} -> return parent
_ -> Nothing
causesSubshell = do
(T_Pipeline _ _ list) <- parentPipeline
if length list <= 1
then return False
else if not $ hasLastpipe params
then return True
else return . not $ (getId . head $ reverse list) == getId t
getModifiedVariables t =
case t of
T_SimpleCommand _ vars [] ->
concatMap (\x -> case x of
T_Assignment id _ name _ w ->
[(x, x, name, dataTypeFrom DataString w)]
_ -> []
) vars
c@T_SimpleCommand {} ->
getModifiedVariableCommand c
TA_Unary _ "++|" v@(TA_Variable _ name _) ->
[(t, v, name, DataString $ SourceFrom [v])]
TA_Unary _ "|++" v@(TA_Variable _ name _) ->
[(t, v, name, DataString $ SourceFrom [v])]
TA_Assignment _ op (TA_Variable _ name _) rhs -> maybeToList $ do
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
return (t, t, name, DataString $ SourceFrom [rhs])
T_BatsTest {} -> [
(t, t, "lines", DataArray SourceExternal),
(t, t, "status", DataString SourceInteger),
(t, t, "output", DataString SourceExternal)
]
-- Count [[ -v foo ]] as an "assignment".
-- This is to prevent [ -v foo ] being unassigned or unused.
TC_Unary id _ "-v" token -> maybeToList $ do
str <- fmap (takeWhile (/= '[')) $ -- Quoted index
flip getLiteralStringExt token $ \x ->
case x of
T_Glob _ s -> return s -- Unquoted index
_ -> Nothing
guard . not . null $ str
return (t, token, str, DataString SourceChecked)
T_DollarBraced _ _ l -> maybeToList $ do
let string = bracedString t
let modifier = getBracedModifier string
guard $ ":=" `isPrefixOf` modifier
return (t, t, getBracedReference string, DataString $ SourceFrom [l])
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
t@(T_CoProc _ name _) ->
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
--Points to 'for' rather than variable
T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)]
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
_ -> []
isClosingFileOp op =
case op of
T_IoDuplicate _ (T_GREATAND _) "-" -> True
T_IoDuplicate _ (T_LESSAND _) "-" -> True
_ -> False
-- Consider 'export/declare -x' a reference, since it makes the var available
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
case x of
"export" -> if "f" `elem` flags
then []
else concatMap getReference rest
"declare" -> if
any (`elem` flags) ["x", "p"] &&
(not $ any (`elem` flags) ["f", "F"])
then concatMap getReference rest
else []
"trap" ->
case rest of
head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head
_ -> []
_ -> []
where
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
getReference _ = []
flags = map snd $ getAllFlags base
getReferencedVariableCommand _ = []
-- The function returns a tuple consisting of four items describing an assignment.
-- Given e.g. declare foo=bar
-- (
-- BaseCommand :: Token, -- The command/structure assigning the variable, i.e. declare foo=bar
-- AssignmentToken :: Token, -- The specific part that assigns this variable, i.e. foo=bar
-- VariableName :: String, -- The variable name, i.e. foo
-- VariableValue :: DataType -- A description of the value being assigned, i.e. "Literal string with value foo"
-- )
getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
case x of
"read" ->
let params = map getLiteral rest
readArrayVars = getReadArrayVariables rest
in
catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params
"getopts" ->
case rest of
opts:var:_ -> maybeToList $ getLiteral var
_ -> []
"let" -> concatMap letParamToLiteral rest
"export" ->
if "f" `elem` flags then [] else concatMap getModifierParamString rest
"declare" -> if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
"typeset" -> declaredVars
"local" -> concatMap getModifierParamString rest
"readonly" ->
if any (`elem` flags) ["f", "p"]
then []
else concatMap getModifierParamString rest
"set" -> maybeToList $ do
params <- getSetParams rest
return (base, base, "@", DataString $ SourceFrom params)
"printf" -> maybeToList $ getPrintfVariable rest
"mapfile" -> maybeToList $ getMapfileArray base rest
"readarray" -> maybeToList $ getMapfileArray base rest
"DEFINE_boolean" -> maybeToList $ getFlagVariable rest
"DEFINE_float" -> maybeToList $ getFlagVariable rest
"DEFINE_integer" -> maybeToList $ getFlagVariable rest
"DEFINE_string" -> maybeToList $ getFlagVariable rest
_ -> []
where
flags = map snd $ getAllFlags base
stripEquals s = let rest = dropWhile (/= '=') s in
if rest == "" then "" else tail rest
stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) =
T_NormalWord id1 (T_Literal id2 (stripEquals s):rs)
stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) =
T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
stripEqualsFrom t = t
declaredVars = concatMap (getModifierParam defaultType) rest
where
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
getLiteralOfDataType t d = do
s <- getLiteralString t
when ("-" `isPrefixOf` s) $ fail "argument"
return (base, t, s, d)
getLiteral t = getLiteralOfDataType t (DataString SourceExternal)
getLiteralArray t = getLiteralOfDataType t (DataArray SourceExternal)
getModifierParamString = getModifierParam DataString
getModifierParam def t@(T_Assignment _ _ name _ value) =
[(base, t, name, dataTypeFrom def value)]
getModifierParam def t@T_NormalWord {} = maybeToList $ do
name <- getLiteralString t
guard $ isVariableName name
return (base, t, name, def SourceDeclaration)
getModifierParam _ _ = []
letParamToLiteral token =
if var == ""
then []
else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])]
where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token
getSetParams (t:_:rest) | getLiteralString t == Just "-o" = getSetParams rest
getSetParams (t:rest) =
let s = getLiteralString t in
case s of
Just "--" -> return rest
Just ('-':_) -> getSetParams rest
_ -> return (t:fromMaybe [] (getSetParams rest))
getSetParams [] = Nothing
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
where
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, varName, varType $ SourceFrom list)
where
(varName, varType) = case elemIndex '[' var of
Just i -> (take i var, DataArray)
Nothing -> (var, DataString)
f (_:rest) = f rest
f [] = fail "not found"
-- mapfile has some curious syntax allowing flags plus 0..n variable names
-- where only the first non-option one is used if any. Here we cheat and
-- just get the last one, if it's a variable name.
getMapfileArray base arguments = do
lastArg <- listToMaybe (reverse arguments)
name <- getLiteralString lastArg
guard $ isVariableName name
return (base, lastArg, name, DataArray SourceExternal)
-- get all the array variables used in read, e.g. read -a arr
getReadArrayVariables args =
map (getLiteralArray . snd)
(filter (isArrayFlag . fst) (zip args (tail args)))
isArrayFlag x = fromMaybe False $ do
str <- getLiteralString x
return $ case str of
'-':'-':_ -> False
'-':str -> 'a' `elem` str
_ -> False
-- get the FLAGS_ variable created by a shflags DEFINE_ call
getFlagVariable (n:v:_) = do
name <- getLiteralString n
return (base, n, "FLAGS_" ++ name, DataString $ SourceExternal)
getFlagVariable _ = Nothing
getModifiedVariableCommand _ = []
getIndexReferences s = fromMaybe [] $ do
match <- matchRegex re s
index <- match !!! 0
return $ matchAllStrings variableNameRegex index
where
re = mkRegex "(\\[.*\\])"
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
getOffsetReferences mods = fromMaybe [] $ do
-- if mods start with [, then drop until ]
match <- matchRegex re mods
offsets <- match !!! 1
return $ matchAllStrings variableNameRegex offsets
where
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
getReferencedVariables parents t =
case t of
T_DollarBraced id _ l -> let str = bracedString t in
(t, t, getBracedReference str) :
map (\x -> (l, l, x)) (
getIndexReferences str
++ getOffsetReferences (getBracedModifier str))
TA_Variable id name _ ->
if isArithmeticAssignment t
then []
else [(t, t, name)]
T_Assignment id mode str _ word ->
[(t, t, str) | mode == Append] ++ specialReferences str t word
TC_Unary id _ "-v" token -> getIfReference t token
TC_Unary id _ "-R" token -> getIfReference t token
TC_Binary id DoubleBracket op lhs rhs ->
if isDereferencing op
then concatMap (getIfReference t) [lhs, rhs]
else []
T_BatsTest {} -> [ -- pretend @test references vars to avoid warnings
(t, t, "lines"),
(t, t, "status"),
(t, t, "output")
]
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
x -> getReferencedVariableCommand x
where
-- Try to reduce false positives for unused vars only referenced from evaluated vars
specialReferences name base word =
if name `elem` [
"PS1", "PS2", "PS3", "PS4",
"PROMPT_COMMAND"
]
then
map (\x -> (base, base, x)) $
getVariablesFromLiteralToken word
else []
literalizer t = case t of
T_Glob _ s -> return s -- Also when parsed as globs
_ -> Nothing
getIfReference context token = maybeToList $ do
str <- getLiteralStringExt literalizer token
guard . not $ null str
when (isDigit $ head str) $ fail "is a number"
return (context, token, getBracedReference str)
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
isArithmeticAssignment t = case getPath parents t of
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
_ -> False
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
--- Command specific checks
-- Compare a command to a string: t `isCommand` "sed" (also matches /usr/bin/sed)
isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `isSuffixOf` cmd)
-- Compare a command to a literal. Like above, but checks full path.
isUnqualifiedCommand token str = isCommandMatch token (== str)
isCommandMatch token matcher = fromMaybe False $
fmap matcher (getCommandName token)
-- Does this regex look like it was intended as a glob?
-- True: *foo*
-- False: .*foo.*
isConfusedGlobRegex :: String -> Bool
isConfusedGlobRegex ('*':_) = True
isConfusedGlobRegex [x,'*'] | x `notElem` "\\." = True
isConfusedGlobRegex _ = False
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
isVariableChar x = isVariableStartChar x || isDigit x
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
prop_isVariableName1 = isVariableName "_fo123"
prop_isVariableName2 = not $ isVariableName "4"
prop_isVariableName3 = not $ isVariableName "test: "
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
isVariableName _ = False
getVariablesFromLiteralToken token =
getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token)
-- Try to get referenced variables from a literal string like "$foo"
-- Ignores tons of cases like arithmetic evaluation and array indices.
prop_getVariablesFromLiteral1 =
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
getVariablesFromLiteral string =
map (!! 0) $ matchAllSubgroups variableRegex string
where
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
-- Get the variable name from an expansion like ${var:-foo}
prop_getBracedReference1 = getBracedReference "foo" == "foo"
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
prop_getBracedReference3 = getBracedReference "#" == "#"
prop_getBracedReference4 = getBracedReference "##" == "#"
prop_getBracedReference5 = getBracedReference "#!" == "!"
prop_getBracedReference6 = getBracedReference "!#" == "#"
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
prop_getBracedReference11= getBracedReference "!os*" == ""
prop_getBracedReference12= getBracedReference "!os?bar**" == ""
prop_getBracedReference13= getBracedReference "foo[bar]" == "foo"
getBracedReference s = fromMaybe s $
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
where
noPrefix = dropPrefix s
dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest
dropPrefix "" = ""
takeName s = do
let name = takeWhile isVariableChar s
guard . not $ null name
return name
getSpecial (c:_) =
if c `elem` "*@#?-$!" then return [c] else fail "not special"
getSpecial _ = fail "empty"
nameExpansion ('!':rest) = do -- e.g. ${!foo*bar*}
let suffix = dropWhile isVariableChar rest
guard $ suffix /= rest -- e.g. ${!@}
first <- suffix !!! 0
guard $ first `elem` "*?"
return ""
nameExpansion _ = Nothing
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
getBracedModifier s = fromMaybe "" . listToMaybe $ do
let var = getBracedReference s
a <- dropModifier s
dropPrefix var a
where
dropPrefix [] t = return t
dropPrefix (a:b) (c:d) | a == c = dropPrefix b d
dropPrefix _ _ = []
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
dropModifier x = [x]
-- Useful generic functions.
-- Run an action in a Maybe (or do nothing).
-- Example:
-- potentially $ do
-- s <- getLiteralString cmd
-- guard $ s `elem` ["--recursive", "-r"]
-- return $ warn .. "Something something recursive"
potentially :: Monad m => Maybe (m ()) -> m ()
potentially = fromMaybe (return ())
-- Get element 0 or a default. Like `head` but safe.
headOrDefault _ (a:_) = a
headOrDefault def _ = def
--- Get element n of a list, or Nothing. Like `!!` but safe.
(!!!) list i =
case drop i list of
[] -> Nothing
(r:_) -> Just r
-- Run a command if the shell is in the given list
whenShell l c = do
shell <- asks shellType
when (shell `elem` l ) c
filterByAnnotation asSpec params =
filter (not . shouldIgnore)
where
token = asScript asSpec
shouldIgnore note =
any (shouldIgnoreFor (getCode note)) $
getPath parents (T_Bang $ tcId note)
shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec
shouldIgnoreFor code t = isAnnotationIgnoringCode code t
parents = parentMap params
getCode = cCode . tcComment
shouldIgnoreCode params code t =
any (isAnnotationIgnoringCode code) $
getPath (parentMap params) t
-- Is this a ${#anything}, to get string length or array count?
isCountingReference (T_DollarBraced id _ token) =
case concat $ oversimplify token of
'#':_ -> True
_ -> False
isCountingReference _ = False
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
isQuotedAlternativeReference t =
case t of
T_DollarBraced _ _ _ ->
getBracedModifier (bracedString t) `matches` re
_ -> False
where
re = mkRegex "(^|\\]):?\\+"
-- getGnuOpts "erd:u:" will parse a SimpleCommand like
-- read -re -d : -u 3 bar
-- into
-- Just [("r", -re), ("e", -re), ("d", :), ("u", 3), ("", bar)]
-- where flags with arguments map to arguments, while others map to themselves.
-- Any unrecognized flag will result in Nothing.
getGnuOpts = getOpts getAllFlags
getBsdOpts = getOpts getLeadingFlags
getOpts :: (Token -> [(Token, String)]) -> String -> Token -> Maybe [(String, Token)]
getOpts flagTokenizer string cmd = process flags
where
flags = flagTokenizer cmd
flagList (c:':':rest) = ([c], True) : flagList rest
flagList (c:rest) = ([c], False) : flagList rest
flagList [] = []
flagMap = Map.fromList $ ("", False) : flagList string
process [] = return []
process [(token, flag)] = do
takesArg <- Map.lookup flag flagMap
guard $ not takesArg
return [(flag, token)]
process ((token1, flag1):rest2@((token2, flag2):rest)) = do
takesArg <- Map.lookup flag1 flagMap
if takesArg
then do
guard $ flag2 == ""
more <- process rest
return $ (flag1, token2) : more
else do
more <- process rest2
return $ (flag1, token1) : more
supportsArrays shell = shell == Bash || shell == Ksh
-- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh)
isBashLike :: Parameters -> Bool
isBashLike params =
case shellType params of
Bash -> True
Ksh -> True
Dash -> False
Sh -> False
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -1,394 +0,0 @@
{-
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Analyzer
import Data.Either
import Data.Functor
import Data.List
import Data.Maybe
import Data.Ord
import Control.Monad.Identity
import qualified Data.Map as Map
import qualified System.IO
import Prelude hiding (readFile)
import Control.Monad
import Test.QuickCheck.All
tokenToPosition startMap t = fromMaybe fail $ do
span <- Map.lookup (tcId t) startMap
return $ newPositionedComment {
pcStartPos = fst span,
pcEndPos = snd span,
pcComment = tcComment t,
pcFix = tcFix t
}
where
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
shellFromFilename filename = foldl mplus Nothing candidates
where
shellExtensions = [(".ksh", Ksh)
,(".bash", Bash)
,(".bats", Bash)
,(".dash", Dash)]
-- The `.sh` is too generic to determine the shell:
-- We fallback to Bash in this case and emit SC2148 if there is no shebang
candidates =
map (\(ext,sh) -> if ext `isSuffixOf` filename then Just sh else Nothing) shellExtensions
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
checkScript sys spec = do
results <- checkScript (csScript spec)
return emptyCheckResult {
crFilename = csFilename spec,
crComments = results
}
where
checkScript contents = do
result <- parseScript sys newParseSpec {
psFilename = csFilename spec,
psScript = contents,
psCheckSourced = csCheckSourced spec,
psIgnoreRC = csIgnoreRC spec,
psShellTypeOverride = csShellTypeOverride spec
}
let parseMessages = prComments result
let tokenPositions = prTokenPositions result
let analysisSpec root =
as {
asScript = root,
asShellType = csShellTypeOverride spec,
asFallbackShell = shellFromFilename $ csFilename spec,
asCheckSourced = csCheckSourced spec,
asExecutionMode = Executed,
asTokenPositions = tokenPositions,
asOptionalChecks = csOptionalChecks spec
} where as = newAnalysisSpec root
let analysisMessages =
fromMaybe [] $
(arComments . analyzeScript . analysisSpec)
<$> prRoot result
let translator = tokenToPosition tokenPositions
return . nub . sortMessages . filter shouldInclude $
(parseMessages ++ map translator analysisMessages)
shouldInclude pc =
severity <= csMinSeverity spec &&
case csIncludedWarnings spec of
Nothing -> code `notElem` csExcludedWarnings spec
Just includedWarnings -> code `elem` includedWarnings
where
code = cCode (pcComment pc)
severity = cSeverity (pcComment pc)
sortMessages = sortBy (comparing order)
order pc =
let pos = pcStartPos pc
comment = pcComment pc in
(posFile pos,
posLine pos,
posColumn pos,
cSeverity comment,
cCode comment,
cMessage comment)
getPosition = pcStartPos
getErrors sys spec =
sort . map getCode . crComments $
runIdentity (checkScript sys spec)
where
getCode = cCode . pcComment
check = checkWithIncludes []
checkWithSpec includes =
getErrors (mockedSystemInterface includes)
checkWithIncludes includes src =
checkWithSpec includes emptyCheckSpec {
csScript = src,
csExcludedWarnings = [2148]
}
checkRecursive includes src =
checkWithSpec includes emptyCheckSpec {
csScript = src,
csExcludedWarnings = [2148],
csCheckSourced = True
}
checkOptionIncludes includes src =
checkWithSpec [] emptyCheckSpec {
csScript = src,
csIncludedWarnings = includes,
csCheckSourced = True
}
checkWithRc rc = getErrors
(mockRcFile rc $ mockedSystemInterface [])
checkWithIncludesAndSourcePath includes mapper = getErrors
(mockedSystemInterface includes) {
siFindSource = mapper
}
prop_findsParseIssue = check "echo \"$12\"" == [1037]
prop_commentDisablesParseIssue1 =
null $ check "#shellcheck disable=SC1037\necho \"$12\""
prop_commentDisablesParseIssue2 =
null $ check "#shellcheck disable=SC1037\n#lol\necho \"$12\""
prop_findsAnalysisIssue =
check "echo $1" == [2086]
prop_commentDisablesAnalysisIssue1 =
null $ check "#shellcheck disable=SC2086\necho $1"
prop_commentDisablesAnalysisIssue2 =
null $ check "#shellcheck disable=SC2086\n#lol\necho $1"
prop_optionDisablesIssue1 =
null $ getErrors
(mockedSystemInterface [])
emptyCheckSpec {
csScript = "echo $1",
csExcludedWarnings = [2148, 2086]
}
prop_optionDisablesIssue2 =
null $ getErrors
(mockedSystemInterface [])
emptyCheckSpec {
csScript = "echo \"$10\"",
csExcludedWarnings = [2148, 1037]
}
prop_wontParseBadShell =
[1071] == check "#!/usr/bin/python\ntrue $1\n"
prop_optionDisablesBadShebang =
null $ getErrors
(mockedSystemInterface [])
emptyCheckSpec {
csScript = "#!/usr/bin/python\ntrue\n",
csShellTypeOverride = Just Sh
}
prop_annotationDisablesBadShebang =
[] == check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
prop_canParseDevNull =
[] == check "source /dev/null"
prop_failsWhenNotSourcing =
[1091, 2154] == check "source lol; echo \"$bar\""
prop_worksWhenSourcing =
null $ checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\""
prop_worksWhenSourcingWithDashDash =
null $ checkWithIncludes [("lib", "bar=1")] "source -- lib; echo \"$bar\""
prop_worksWhenDotting =
null $ checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\""
-- FIXME: This should really be giving [1093], "recursively sourced"
prop_noInfiniteSourcing =
[] == checkWithIncludes [("lib", "source lib")] "source lib"
prop_canSourceBadSyntax =
[1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
prop_cantSourceDynamic =
[1090] == checkWithIncludes [("lib", "")] ". \"$1\""
prop_cantSourceDynamic2 =
[1090] == checkWithIncludes [("lib", "")] "source ~/foo"
prop_canSourceDynamicWhenRedirected =
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
prop_recursiveAnalysis =
[2086] == checkRecursive [("lib", "echo $1")] "source lib"
prop_recursiveParsing =
[1037] == checkRecursive [("lib", "echo \"$10\"")] "source lib"
prop_nonRecursiveAnalysis =
[] == checkWithIncludes [("lib", "echo $1")] "source lib"
prop_nonRecursiveParsing =
[] == checkWithIncludes [("lib", "echo \"$10\"")] "source lib"
prop_sourceDirectiveDoesntFollowFile =
null $ checkWithIncludes
[("foo", "source bar"), ("bar", "baz=3")]
"#shellcheck source=foo\n. \"$1\"; echo \"$baz\""
prop_filewideAnnotationBase = [2086] == check "#!/bin/sh\necho $1"
prop_filewideAnnotation1 = null $
check "#!/bin/sh\n# shellcheck disable=2086\necho $1"
prop_filewideAnnotation2 = null $
check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation3 = null $
check "#!/bin/sh\n#unrelated\n# shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation4 = null $
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
prop_filewideAnnotation5 = null $
check "#!/bin/sh\n\n\n\n#shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation6 = null $
check "#shellcheck shell=sh\n#unrelated\n#shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation7 = null $
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
prop_filewideAnnotationBase2 = [2086, 2181] == check "true\n[ $? == 0 ] && echo $1"
prop_filewideAnnotation8 = null $
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
prop_spinBug1413 = null $ check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n"
prop_deducesTypeFromExtension = null result
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.ksh",
csScript = "(( 3.14 ))"
}
prop_deducesTypeFromExtension2 = result == [2079]
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.bash",
csScript = "(( 3.14 ))"
}
prop_shExtensionDoesntMatter = result == [2148]
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.sh",
csScript = "echo 'hello world'"
}
prop_sourcedFileUsesOriginalShellExtension = result == [2079]
where
result = checkWithSpec [("file.ksh", "(( 3.14 ))")] emptyCheckSpec {
csFilename = "file.bash",
csScript = "source file.ksh",
csCheckSourced = True
}
prop_canEnableOptionalsWithSpec = result == [2244]
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.sh",
csScript = "#!/bin/sh\n[ \"$1\" ]",
csOptionalChecks = ["avoid-nullary-conditions"]
}
prop_optionIncludes1 =
-- expect 2086, but not included, so nothing reported
null $ checkOptionIncludes (Just [2080]) "#!/bin/sh\n var='a b'\n echo $var"
prop_optionIncludes2 =
-- expect 2086, included, so it is reported
[2086] == checkOptionIncludes (Just [2086]) "#!/bin/sh\n var='a b'\n echo $var"
prop_optionIncludes3 =
-- expect 2086, no inclusions provided, so it is reported
[2086] == checkOptionIncludes Nothing "#!/bin/sh\n var='a b'\n echo $var"
prop_optionIncludes4 =
-- expect 2086 & 2154, only 2154 included, so only that's reported
[2154] == checkOptionIncludes (Just [2154]) "#!/bin/sh\n var='a b'\n echo $var\n echo $bar"
prop_readsRcFile = result == []
where
result = checkWithRc "disable=2086" emptyCheckSpec {
csScript = "#!/bin/sh\necho $1",
csIgnoreRC = False
}
prop_canUseNoRC = result == [2086]
where
result = checkWithRc "disable=2086" emptyCheckSpec {
csScript = "#!/bin/sh\necho $1",
csIgnoreRC = True
}
prop_NoRCWontLookAtFile = result == [2086]
where
result = checkWithRc (error "Fail") emptyCheckSpec {
csScript = "#!/bin/sh\necho $1",
csIgnoreRC = True
}
prop_brokenRcGetsWarning = result == [1134, 2086]
where
result = checkWithRc "rofl" emptyCheckSpec {
csScript = "#!/bin/sh\necho $1",
csIgnoreRC = False
}
prop_canEnableOptionalsWithRc = result == [2244]
where
result = checkWithRc "enable=avoid-nullary-conditions" emptyCheckSpec {
csScript = "#!/bin/sh\n[ \"$1\" ]"
}
prop_sourcePathRedirectsName = result == [2086]
where
f "dir/myscript" _ "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource lib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_sourcePathAddsAnnotation = result == [2086]
where
f "dir/myscript" ["mypath"] "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source-path=mypath\nsource lib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_sourcePathRedirectsDirective = result == [2086]
where
f "dir/myscript" _ "lib" = return "foo/lib"
f _ _ _ = return "/dev/null"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source=lib\nsource kittens",
csFilename = "dir/myscript",
csCheckSourced = True
}
return []
runTests = $quickCheckAll

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +0,0 @@
{-
This empty file is provided for ease of patching in site specific checks.
However, there are no guarantees regarding compatibility between versions.
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Checks.Custom (checker, ShellCheck.Checks.Custom.runTests) where
import ShellCheck.AnalyzerLib
import Test.QuickCheck
checker :: Parameters -> Checker
checker params = Checker {
perScript = const $ return (),
perToken = const $ return ()
}
prop_CustomTestsWork = True
return []
runTests = $quickCheckAll

View File

@@ -1,545 +0,0 @@
{-
Copyright 2012-2016 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.AnalyzerLib
import ShellCheck.Interface
import ShellCheck.Regex
import Control.Monad
import Control.Monad.RWS
import Data.Char
import Data.List
import Data.Maybe
import qualified Data.Map as Map
import qualified Data.Set as Set
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
data ForShell = ForShell [Shell] (Token -> Analysis)
getChecker params list = Checker {
perScript = nullCheck,
perToken = foldl composeAnalyzers nullCheck $ mapMaybe include list
}
where
shell = shellType params
include (ForShell list a) = do
guard $ shell `elem` list
return a
checker params = getChecker params checks
checks = [
checkForDecimals
,checkBashisms
,checkEchoSed
,checkBraceExpansionVars
,checkMultiDimensionalArrays
,checkPS1Assignments
]
testChecker (ForShell _ t) =
Checker {
perScript = nullCheck,
perToken = t
}
verify c s = producesComments (testChecker c) s == Just True
verifyNot c s = producesComments (testChecker c) s == Just False
prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
checkForDecimals = ForShell [Sh, Dash, Bash] f
where
f t@(TA_Expansion id _) = potentially $ do
str <- getLiteralString t
first <- str !!! 0
guard $ isDigit first && '.' `elem` str
return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
f _ = return ()
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"
prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
prop_checkBashisms5 = verify checkBashisms "source file"
prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}"
prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}"
prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}"
prop_checkBashisms13= verify checkBashisms "exec -c env"
prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \""
prop_checkBashisms15= verify checkBashisms "let n++"
prop_checkBashisms16= verify checkBashisms "echo $RANDOM"
prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))"
prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"
prop_checkBashisms19= verify checkBashisms "foo > file*.txt"
prop_checkBashisms20= verify checkBashisms "read -ra foo"
prop_checkBashisms21= verify checkBashisms "[ -a foo ]"
prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]"
prop_checkBashisms23= verify checkBashisms "trap mything ERR INT"
prop_checkBashisms24= verifyNot checkBashisms "trap mything INT TERM"
prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123"
prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM"
prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*"
prop_checkBashisms28= verify checkBashisms "exec {n}>&2"
prop_checkBashisms29= verify checkBashisms "echo ${!var}"
prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\""
prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\""
prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo"
prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo"
prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo"
prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM"
prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }"
prop_checkBashisms40= verify checkBashisms "echo $(<file)"
prop_checkBashisms41= verify checkBashisms "echo `<file`"
prop_checkBashisms42= verify checkBashisms "trap foo int"
prop_checkBashisms43= verify checkBashisms "trap foo sigint"
prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"
prop_checkBashisms55= verify checkBashisms "#!/bin/sh\necho ${@%foo}"
prop_checkBashisms56= verifyNot checkBashisms "#!/bin/sh\necho ${##}"
prop_checkBashisms57= verifyNot checkBashisms "#!/bin/dash\nulimit -c 0"
prop_checkBashisms58= verify checkBashisms "#!/bin/sh\nulimit -c 0"
prop_checkBashisms59 = verify checkBashisms "#!/bin/sh\njobs -s"
prop_checkBashisms60 = verifyNot checkBashisms "#!/bin/sh\njobs -p"
prop_checkBashisms61 = verifyNot checkBashisms "#!/bin/sh\njobs -lp"
prop_checkBashisms62 = verify checkBashisms "#!/bin/sh\nexport -f foo"
prop_checkBashisms63 = verifyNot checkBashisms "#!/bin/sh\nexport -p"
prop_checkBashisms64 = verify checkBashisms "#!/bin/sh\nreadonly -a"
prop_checkBashisms65 = verifyNot checkBashisms "#!/bin/sh\nreadonly -p"
prop_checkBashisms66 = verifyNot checkBashisms "#!/bin/sh\ncd -P ."
prop_checkBashisms67 = verify checkBashisms "#!/bin/sh\ncd -P -e ."
prop_checkBashisms68 = verify checkBashisms "#!/bin/sh\numask -p"
prop_checkBashisms69 = verifyNot checkBashisms "#!/bin/sh\numask -S"
prop_checkBashisms70 = verify checkBashisms "#!/bin/sh\ntrap -l"
prop_checkBashisms71 = verify checkBashisms "#!/bin/sh\ntype -a ls"
prop_checkBashisms72 = verifyNot checkBashisms "#!/bin/sh\ntype ls"
prop_checkBashisms73 = verify checkBashisms "#!/bin/sh\nunset -n namevar"
prop_checkBashisms74 = verifyNot checkBashisms "#!/bin/sh\nunset -f namevar"
prop_checkBashisms75 = verifyNot checkBashisms "#!/bin/sh\necho \"-n foo\""
prop_checkBashisms76 = verifyNot checkBashisms "#!/bin/sh\necho \"-ne foo\""
prop_checkBashisms77 = verifyNot checkBashisms "#!/bin/sh\necho -Q foo"
prop_checkBashisms78 = verify checkBashisms "#!/bin/sh\necho -ne foo"
prop_checkBashisms79 = verify checkBashisms "#!/bin/sh\nhash -l"
prop_checkBashisms80 = verifyNot checkBashisms "#!/bin/sh\nhash -r"
prop_checkBashisms81 = verifyNot checkBashisms "#!/bin/dash\nhash -v"
prop_checkBashisms82 = verifyNot checkBashisms "#!/bin/sh\nset -v +o allexport -o errexit -C"
prop_checkBashisms83 = verifyNot checkBashisms "#!/bin/sh\nset --"
prop_checkBashisms84 = verify checkBashisms "#!/bin/sh\nset -o pipefail"
prop_checkBashisms85 = verify checkBashisms "#!/bin/sh\nset -B"
prop_checkBashisms86 = verifyNot checkBashisms "#!/bin/dash\nset -o emacs"
prop_checkBashisms87 = verify checkBashisms "#!/bin/sh\nset -o emacs"
prop_checkBashisms88 = verifyNot checkBashisms "#!/bin/sh\nset -- wget -o foo 'https://some.url'"
prop_checkBashisms89 = verifyNot checkBashisms "#!/bin/sh\nopts=$-\nset -\"$opts\""
prop_checkBashisms90 = verifyNot checkBashisms "#!/bin/sh\nset -o \"$opt\""
prop_checkBashisms91 = verify checkBashisms "#!/bin/sh\nwait -n"
prop_checkBashisms92 = verify checkBashisms "#!/bin/sh\necho $((16#FF))"
prop_checkBashisms93 = verify checkBashisms "#!/bin/sh\necho $(( 10#$(date +%m) ))"
prop_checkBashisms94 = verify checkBashisms "#!/bin/sh\n[ -v var ]"
prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_"
prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
checkBashisms = ForShell [Sh, Dash] $ \t -> do
params <- ask
kludge params t
where
-- This code was copy-pasted from Analytics where params was a variable
kludge params = bashism
where
isDash = shellType params == Dash
warnMsg id s =
if isDash
then warn id 2169 $ "In dash, " ++ s ++ " not supported."
else warn id 2039 $ "In POSIX sh, " ++ s ++ " undefined."
bashism (T_ProcSub id _ _) = warnMsg id "process substitution is"
bashism (T_Extglob id _ _) = warnMsg id "extglob is"
bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is"
bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\" is"
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id "arithmetic for loops are"
bashism (T_Arithmetic id _) = warnMsg id "standalone ((..)) is"
bashism (T_DollarBracket id _) = warnMsg id "$[..] in place of $((..)) is"
bashism (T_SelectIn id _ _ _) = warnMsg id "select loops are"
bashism (T_BraceExpansion id _) = warnMsg id "brace expansion is"
bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is"
bashism (T_HereString id _) = warnMsg id "here-strings are"
bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
unless isDash $ warnMsg id $ "lexicographical " ++ op ++ " is"
bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "-nt", "-ef" ] =
unless isDash $ warnMsg id $ op ++ " is"
bashism (TC_Binary id SingleBracket "==" _ _) =
warnMsg id "== in place of = is"
bashism (TC_Binary id SingleBracket "=~" _ _) =
warnMsg id "=~ regex matching is"
bashism (TC_Unary id SingleBracket "-v" _) =
warnMsg id "unary -v (in place of [ -n \"${var+x}\" ]) is"
bashism (TC_Unary id _ "-a" _) =
warnMsg id "unary -a in place of -e is"
bashism (TA_Unary id op _)
| op `elem` [ "|++", "|--", "++|", "--|"] =
warnMsg id $ filter (/= '|') op ++ " is"
bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are"
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is"
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id ">& is"
bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are"
bashism (T_FdRedirect id num _)
| all isDigit num && length num > 1 = warnMsg id "FDs outside 0-9 are"
bashism (T_Assignment id Append _ _ _) =
warnMsg id "+= is"
bashism (T_IoFile id _ word) | isNetworked =
warnMsg id "/dev/{tcp,udp} is"
where
file = onlyLiteralString word
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
bashism (T_Glob id str) | "[^" `isInfixOf` str =
warnMsg id "^ in place of ! in glob bracket expressions is"
bashism t@(TA_Variable id str _) | isBashVariable str =
warnMsg id $ str ++ " is"
bashism t@(T_DollarBraced id _ token) = do
mapM_ check expansion
when (isBashVariable var) $
warnMsg id $ var ++ " is"
where
str = bracedString t
var = getBracedReference str
check (regex, feature) =
when (isJust $ matchRegex regex str) $ warnMsg id feature
bashism t@(T_Pipe id "|&") =
warnMsg id "|& in place of 2>&1 | is"
bashism (T_Array id _) =
warnMsg id "arrays are"
bashism (T_IoFile id _ t) | isGlob t =
warnMsg id "redirecting to/from globs is"
bashism (T_CoProc id _ _) =
warnMsg id "coproc is"
bashism (T_Function id _ _ str _) | not (isVariableName str) =
warnMsg id "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
warnMsg id "$(<file) to read files is"
bashism (T_Backticked id [x]) | isOnlyRedirection x =
warnMsg id "`<file` to read files is"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "echo" && argString `matches` flagRegex =
if isDash
then
when (argString /= "-n") $
warnMsg (getId arg) "echo flags besides -n"
else
warnMsg (getId arg) "echo flags are"
where
argString = concat $ oversimplify arg
flagRegex = mkRegex "^-[eEsn]+$"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
warnMsg (getId arg) "exec flags are"
bashism t@(T_SimpleCommand id _ _)
| t `isCommand` "let" = warnMsg id "'let' is"
bashism t@(T_SimpleCommand _ _ (cmd:args))
| t `isCommand` "set" = unless isDash $
checkOptions $ getLiteralArgs args
where
-- Get the literal options from a list of arguments,
-- up until the first non-literal one
getLiteralArgs :: [Token] -> [(Id, String)]
getLiteralArgs (first:rest) = fromMaybe [] $ do
str <- getLiteralString first
return $ (getId first, str) : getLiteralArgs rest
getLiteralArgs [] = []
-- Check a flag-option pair (such as -o errexit)
checkOptions (flag@(fid,flag') : opt@(oid,opt') : rest)
| flag' `matches` oFlagRegex = do
when (opt' `notElem` longOptions) $
warnMsg oid $ "set option " <> opt' <> " is"
checkFlags (flag:rest)
| otherwise = checkFlags (flag:opt:rest)
checkOptions (flag:rest) = checkFlags (flag:rest)
checkOptions _ = return ()
-- Check that each option in a sequence of flags
-- (such as -aveo) is valid
checkFlags (flag@(fid, flag'):rest)
| startsOption flag' = do
unless (flag' `matches` validFlagsRegex) $
forM_ (tail flag') $ \letter ->
when (letter `notElem` optionsSet) $
warnMsg fid $ "set flag " <> ('-':letter:" is")
checkOptions rest
| beginsWithDoubleDash flag' = do
warnMsg fid $ "set flag " <> flag' <> " is"
checkOptions rest
-- Either a word that doesn't start with a dash, or simply '--',
-- so stop checking.
| otherwise = return ()
checkFlags [] = return ()
options = "abCefhmnuvxo"
optionsSet = Set.fromList options
startsOption = (`matches` mkRegex "^(\\+|-[^-])")
oFlagRegex = mkRegex $ "^[-+][" <> options <> "]*o$"
validFlagsRegex = mkRegex $ "^[-+]([" <> options <> "]+o?|o)$"
beginsWithDoubleDash = (`matches` mkRegex "^--.+$")
longOptions = Set.fromList
[ "allexport", "errexit", "ignoreeof", "monitor", "noclobber"
, "noexec", "noglob", "nolog", "notify" , "nounset", "verbose"
, "vi", "xtrace" ]
bashism t@(T_SimpleCommand id _ (cmd:rest)) =
let name = fromMaybe "" $ getCommandName t
flags = getLeadingFlags t
in do
when (name `elem` unsupportedCommands) $
warnMsg id $ "'" ++ name ++ "' is"
potentially $ do
allowed' <- Map.lookup name allowedFlags
allowed <- allowed'
(word, flag) <- listToMaybe $
filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"
when (name == "source") $ warnMsg id "'source' in place of '.' is"
when (name == "trap") $
let
check token = potentially $ do
str <- getLiteralString token
let upper = map toUpper str
return $ do
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
warnMsg (getId token) $ "trapping " ++ str ++ " is"
when ("SIG" `isPrefixOf` upper) $
warnMsg (getId token)
"prefixing signal names with 'SIG' is"
when (not isDash && upper /= str) $
warnMsg (getId token)
"using lower/mixed case for signal names is"
in
mapM_ check (drop 1 rest)
when (name == "printf") $ potentially $ do
format <- rest !!! 0 -- flags are covered by allowedFlags
let literal = onlyLiteralString format
guard $ "%q" `isInfixOf` literal
return $ warnMsg (getId format) "printf %q is"
where
unsupportedCommands = [
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
"typeset"
] ++ if not isDash then ["local"] else []
allowedFlags = Map.fromList [
("cd", Just ["L", "P"]),
("exec", Just []),
("export", Just ["p"]),
("hash", Just $ if isDash then ["r", "v"] else ["r"]),
("jobs", Just ["l", "p"]),
("printf", Just []),
("read", Just $ if isDash then ["r", "p"] else ["r"]),
("readonly", Just ["p"]),
("trap", Just []),
("type", Just []),
("ulimit", if isDash then Nothing else Just ["f"]),
("umask", Just ["S"]),
("unset", Just ["f", "v"]),
("wait", Just [])
]
bashism t@(T_SourceCommand id src _) =
let name = fromMaybe "" $ getCommandName src
in when (name == "source") $ warnMsg id "'source' in place of '.' is"
bashism (TA_Expansion _ (T_Literal id str : _)) | str `matches` radix =
when (str `matches` radix) $ warnMsg id "arithmetic base conversion is"
where
radix = mkRegex "^[0-9]+#"
bashism _ = return ()
varChars="_0-9a-zA-Z"
expansion = let re = mkRegex in [
(re $ "^![" ++ varChars ++ "]", "indirect expansion is"),
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", "string indexing is"),
(re $ "^([*@][%#]|#[@*])", "string operations on $@/$* are"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")
]
bashVars = [
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS",
"_"
]
bashDynamicVars = [ "RANDOM", "SECONDS" ]
dashVars = [ "_" ]
isBashVariable var =
(var `elem` bashDynamicVars
|| var `elem` bashVars && not (isAssigned var))
&& not (isDash && var `elem` dashVars)
isAssigned var = any f (variableFlow params)
where
f x = case x of
Assignment (_, _, name, _) -> name == var
_ -> False
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
prop_checkEchoSed1b= verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")"
prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
prop_checkEchoSed2b= verify checkEchoSed "rm $(sed -e 's,foo,bar,' <<< $cow)"
checkEchoSed = ForShell [Bash, Ksh] f
where
f (T_Redirecting id lefts r) =
when (any redirectHereString lefts) $
checkSed id rcmd
where
redirectHereString :: Token -> Bool
redirectHereString t = case t of
(T_FdRedirect _ _ T_HereString{}) -> True
_ -> False
rcmd = oversimplify r
f (T_Pipeline id _ [a, b]) =
when (acmd == ["echo", "${VAR}"]) $
checkSed id bcmd
where
acmd = oversimplify a
bcmd = oversimplify b
f _ = return ()
checkSed id ["sed", v] = checkIn id v
checkSed id ["sed", "-e", v] = checkIn id v
checkSed _ _ = return ()
-- This should have used backreferences, but TDFA doesn't support them
sedRe = mkRegex "^s(.)([^\n]*)g?$"
isSimpleSed s = fromMaybe False $ do
[first,rest] <- matchRegex sedRe s
let delimiters = filter (== head first) rest
guard $ length delimiters == 2
return True
checkIn id s =
when (isSimpleSed s) $
style id 2001 "See if you can use ${variable//search/replace} instead."
prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}"
prop_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}"
prop_checkBraceExpansionVars3 = verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg"
prop_checkBraceExpansionVars4 = verify checkBraceExpansionVars "echo {$i..100}"
checkBraceExpansionVars = ForShell [Bash] f
where
f t@(T_BraceExpansion id list) = mapM_ check list
where
check element =
when (any (`isInfixOf` toString element) ["$..", "..$"]) $ do
c <- isEvaled element
if c
then style id 2175 "Quote this invalid brace expansion since it should be passed literally to eval."
else warn id 2051 "Bash doesn't support variables in brace range expansions."
f _ = return ()
literalExt t =
case t of
T_DollarBraced {} -> return "$"
T_DollarExpansion {} -> return "$"
T_DollarArithmetic {} -> return "$"
otherwise -> return "-"
toString t = fromJust $ getLiteralStringExt literalExt t
isEvaled t = do
cmd <- getClosestCommandM t
return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval"
prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3"
prop_checkMultiDimensionalArrays2 = verifyNot checkMultiDimensionalArrays "foo[a]=3"
prop_checkMultiDimensionalArrays3 = verify checkMultiDimensionalArrays "foo=( [a][b]=c )"
prop_checkMultiDimensionalArrays4 = verifyNot checkMultiDimensionalArrays "foo=( [a]=c )"
prop_checkMultiDimensionalArrays5 = verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}"
prop_checkMultiDimensionalArrays6 = verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}"
checkMultiDimensionalArrays = ForShell [Bash] f
where
f token =
case token of
T_Assignment _ _ name (first:second:_) _ -> about second
T_IndexedElement _ (first:second:_) _ -> about second
T_DollarBraced {} ->
when (isMultiDim token) $ about token
_ -> return ()
about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays."
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
isMultiDim t = getBracedModifier (bracedString t) `matches` re
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '"
prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '"
prop_checkPS14a= verify checkPS1Assignments "export PS1=$'\\e[3m; '"
prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"
prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'"
prop_checkPS18 = verifyNot checkPS1Assignments "PS1='\\[\\e\\]'"
checkPS1Assignments = ForShell [Bash] f
where
f token = case token of
(T_Assignment _ _ "PS1" _ word) -> warnFor word
_ -> return ()
warnFor word =
let contents = concat $ oversimplify word in
when (containsUnescaped contents) $
info (getId word) 2025 "Make sure all escape sequences are enclosed in \\[..\\] to prevent line wrapping issues"
containsUnescaped s =
let unenclosed = subRegex enclosedRegex s "" in
isJust $ matchRegex escapeRegex unenclosed
enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -1,409 +0,0 @@
{-
Copyright 2018-2019 Vidar Holen, Ng Zhi An
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Fixer (applyFix, removeTabStops, mapPositions, Ranged(..), runTests) where
import ShellCheck.Interface
import Control.Monad.State
import Data.Array
import Data.List
import Data.Semigroup
import GHC.Exts (sortWith)
import Test.QuickCheck
-- The Ranged class is used for types that has a start and end position.
class Ranged a where
start :: a -> Position
end :: a -> Position
overlap :: a -> a -> Bool
overlap x y =
(yStart >= xStart && yStart < xEnd) || (yStart < xStart && yEnd > xStart)
where
yStart = start y
yEnd = end y
xStart = start x
xEnd = end x
-- Set a new start and end position on a Ranged
setRange :: (Position, Position) -> a -> a
-- Tests auto-verify that overlap commutes
assertOverlap x y = overlap x y && overlap y x
assertNoOverlap x y = not (overlap x y) && not (overlap y x)
prop_overlap_contiguous = assertNoOverlap
(tFromStart 10 12 "foo" 1)
(tFromStart 12 14 "bar" 2)
prop_overlap_adjacent_zerowidth = assertNoOverlap
(tFromStart 3 3 "foo" 1)
(tFromStart 3 3 "bar" 2)
prop_overlap_enclosed = assertOverlap
(tFromStart 3 5 "foo" 1)
(tFromStart 1 10 "bar" 2)
prop_overlap_partial = assertOverlap
(tFromStart 1 5 "foo" 1)
(tFromStart 3 7 "bar" 2)
instance Ranged PositionedComment where
start = pcStartPos
end = pcEndPos
setRange (s, e) pc = pc {
pcStartPos = s,
pcEndPos = e
}
instance Ranged Replacement where
start = repStartPos
end = repEndPos
setRange (s, e) r = r {
repStartPos = s,
repEndPos = e
}
-- The Monoid instance for Fix merges fixes that do not conflict.
-- TODO: Make an efficient 'mconcat'
instance Monoid Fix where
mempty = newFix
mappend = (<>)
instance Semigroup Fix where
f1 <> f2 =
-- FIXME: This might need to also discard adjacent zero-width ranges for
-- when two fixes change the same AST node, e.g. `foo` -> "$(foo)"
if or [ r2 `overlap` r1 | r1 <- fixReplacements f1, r2 <- fixReplacements f2 ]
then f1
else newFix {
fixReplacements = fixReplacements f1 ++ fixReplacements f2
}
-- Conveniently apply a transformation to positions in a Fix
mapPositions :: (Position -> Position) -> Fix -> Fix
mapPositions f = adjustFix
where
adjustReplacement rep =
rep {
repStartPos = f $ repStartPos rep,
repEndPos = f $ repEndPos rep
}
adjustFix fix =
fix {
fixReplacements = map adjustReplacement $ fixReplacements fix
}
-- Rewrite a Ranged from a tabstop of 8 to 1
removeTabStops :: Ranged a => a -> Array Int String -> a
removeTabStops range ls =
let startColumn = realignColumn lineNo colNo range
endColumn = realignColumn endLineNo endColNo range
startPosition = (start range) { posColumn = startColumn }
endPosition = (end range) { posColumn = endColumn } in
setRange (startPosition, endPosition) range
where
realignColumn lineNo colNo c =
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
then real (ls ! fromIntegral (lineNo c)) 0 0 (colNo c)
else colNo c
real _ r v target | target <= v = r
-- hit this case at the end of line, and if we don't hit the target
-- return real + (target - v)
real [] r v target = r + (target - v)
real ('\t':rest) r v target = real rest (r+1) (v + 8 - (v `mod` 8)) target
real (_:rest) r v target = real rest (r+1) (v+1) target
lineNo = posLine . start
endLineNo = posLine . end
colNo = posColumn . start
endColNo = posColumn . end
-- A replacement that spans multiple line is applied by:
-- 1. merging the affected lines into a single string using `unlines`
-- 2. apply the replacement as if it only spanned a single line
-- The tricky part is adjusting the end column of the replacement
-- (the end line doesn't matter because there is only one line)
--
-- aaS <--- start of replacement (row 1 column 3)
-- bbbb
-- cEc
-- \------- end of replacement (row 3 column 2)
--
-- a flattened string will look like:
--
-- "aaS\nbbbb\ncEc\n"
--
-- The column of E has to be adjusted by:
-- 1. lengths of lines to be replaced, except the end row itself
-- 2. end column of the replacement
-- 3. number of '\n' by `unlines`
multiToSingleLine :: [Fix] -> Array Int String -> ([Fix], String)
multiToSingleLine fixes lines =
(map (mapPositions adjust) fixes, unlines $ elems lines)
where
-- A prefix sum tree from line number to column shift.
-- FIXME: The tree will be totally unbalanced.
shiftTree :: PSTree Int
shiftTree =
foldl (\t (n,s) -> addPSValue (n+1) (length s + 1) t) newPSTree $
assocs lines
singleString = unlines $ elems lines
adjust pos =
pos {
posLine = 1,
posColumn = (posColumn pos) +
(fromIntegral $ getPrefixSum (fromIntegral $ posLine pos) shiftTree)
}
-- Apply a fix and return resulting lines.
-- The number of lines can increase or decrease with no obvious mapping back, so
-- the function does not return an array.
applyFix :: Fix -> Array Int String -> [String]
applyFix fix fileLines =
let
untabbed = fix {
fixReplacements =
map (\c -> removeTabStops c fileLines) $
fixReplacements fix
}
(adjustedFixes, singleLine) = multiToSingleLine [untabbed] fileLines
in
lines . runFixer $ applyFixes2 adjustedFixes singleLine
-- start and end comes from pos, which is 1 based
prop_doReplace1 = doReplace 0 0 "1234" "A" == "A1234" -- technically not valid
prop_doReplace2 = doReplace 1 1 "1234" "A" == "A1234"
prop_doReplace3 = doReplace 1 2 "1234" "A" == "A234"
prop_doReplace4 = doReplace 3 3 "1234" "A" == "12A34"
prop_doReplace5 = doReplace 4 4 "1234" "A" == "123A4"
prop_doReplace6 = doReplace 5 5 "1234" "A" == "1234A"
doReplace start end o r =
let si = fromIntegral (start-1)
ei = fromIntegral (end-1)
(x, xs) = splitAt si o
(y, z) = splitAt (ei - si) xs
in
x ++ r ++ z
-- Fail if the 'expected' string is not result when applying 'fixes' to 'original'.
testFixes :: String -> String -> [Fix] -> Bool
testFixes expected original fixes =
actual == expected
where
actual = runFixer (applyFixes2 fixes original)
-- A Fixer allows doing repeated modifications of a string where each
-- replacement automatically accounts for shifts from previous ones.
type Fixer a = State (PSTree Int) a
-- Apply a single replacement using its indices into the original string.
-- It does not handle multiple lines, all line indices must be 1.
applyReplacement2 :: Replacement -> String -> Fixer String
applyReplacement2 rep string = do
tree <- get
let transform pos = pos + getPrefixSum pos tree
let originalPos = (repStartPos rep, repEndPos rep)
(oldStart, oldEnd) = tmap (fromInteger . posColumn) originalPos
(newStart, newEnd) = tmap transform (oldStart, oldEnd)
let (l1, l2) = tmap posLine originalPos in
when (l1 /= 1 || l2 /= 1) $
error "ShellCheck internal error, please report: bad cross-line fix"
let replacer = repString rep
let shift = (length replacer) - (oldEnd - oldStart)
let insertionPoint =
case repInsertionPoint rep of
InsertBefore -> oldStart
InsertAfter -> oldEnd+1
put $ addPSValue insertionPoint shift tree
return $ doReplace newStart newEnd string replacer
where
tmap f (a,b) = (f a, f b)
-- Apply a list of Replacements in the correct order
applyReplacements2 :: [Replacement] -> String -> Fixer String
applyReplacements2 reps str =
foldM (flip applyReplacement2) str $
reverse $ sortWith repPrecedence reps
-- Apply all fixes with replacements in the correct order
applyFixes2 :: [Fix] -> String -> Fixer String
applyFixes2 fixes = applyReplacements2 (concatMap fixReplacements fixes)
-- Get the final value of a Fixer.
runFixer :: Fixer a -> a
runFixer f = evalState f newPSTree
-- A Prefix Sum Tree that lets you look up the sum of values at and below an index.
-- It's implemented essentially as a Fenwick tree without the bit-based balancing.
-- The last Num is the sum of the left branch plus current element.
data PSTree n = PSBranch n (PSTree n) (PSTree n) n | PSLeaf
deriving (Show)
newPSTree :: Num n => PSTree n
newPSTree = PSLeaf
-- Get the sum of values whose keys are <= 'target'
getPrefixSum :: (Ord n, Num n) => n -> PSTree n -> n
getPrefixSum = f 0
where
f sum _ PSLeaf = sum
f sum target (PSBranch pivot left right cumulative) =
case () of
_ | target < pivot -> f sum target left
_ | target > pivot -> f (sum+cumulative) target right
_ -> sum+cumulative
-- Add a value to the Prefix Sum tree at the given index.
-- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5
addPSValue :: (Ord n, Num n) => n -> n -> PSTree n -> PSTree n
addPSValue key value tree = if value == 0 then tree else f tree
where
f PSLeaf = PSBranch key PSLeaf PSLeaf value
f (PSBranch pivot left right sum) =
case () of
_ | key < pivot -> PSBranch pivot (f left) right (sum + value)
_ | key > pivot -> PSBranch pivot left (f right) sum
_ -> PSBranch pivot left right (sum + value)
prop_pstreeSumsCorrectly kvs targets =
let
-- Trivial O(n * m) implementation
dumbPrefixSums :: [(Int, Int)] -> [Int] -> [Int]
dumbPrefixSums kvs targets =
let prefixSum target = sum . map snd . filter (\(k,v) -> k <= target) $ kvs
in map prefixSum targets
-- PSTree O(n * log m) implementation
smartPrefixSums :: [(Int, Int)] -> [Int] -> [Int]
smartPrefixSums kvs targets =
let tree = foldl (\tree (pos, shift) -> addPSValue pos shift tree) PSLeaf kvs
in map (\x -> getPrefixSum x tree) targets
in smartPrefixSums kvs targets == dumbPrefixSums kvs targets
-- Semi-convenient functions for constructing tests.
testFix :: [Replacement] -> Fix
testFix list = newFix {
fixReplacements = list
}
tFromStart :: Int -> Int -> String -> Int -> Replacement
tFromStart start end repl order =
newReplacement {
repStartPos = newPosition {
posLine = 1,
posColumn = fromIntegral start
},
repEndPos = newPosition {
posLine = 1,
posColumn = fromIntegral end
},
repString = repl,
repPrecedence = order,
repInsertionPoint = InsertAfter
}
tFromEnd start end repl order =
(tFromStart start end repl order) {
repInsertionPoint = InsertBefore
}
prop_simpleFix1 = testFixes "hello world" "hell world" [
testFix [
tFromEnd 5 5 "o" 1
]]
prop_anchorsLeft = testFixes "-->foobar<--" "--><--" [
testFix [
tFromStart 4 4 "foo" 1,
tFromStart 4 4 "bar" 2
]]
prop_anchorsRight = testFixes "-->foobar<--" "--><--" [
testFix [
tFromEnd 4 4 "bar" 1,
tFromEnd 4 4 "foo" 2
]]
prop_anchorsBoth1 = testFixes "-->foobar<--" "--><--" [
testFix [
tFromStart 4 4 "bar" 2,
tFromEnd 4 4 "foo" 1
]]
prop_anchorsBoth2 = testFixes "-->foobar<--" "--><--" [
testFix [
tFromEnd 4 4 "foo" 2,
tFromStart 4 4 "bar" 1
]]
prop_composeFixes1 = testFixes "cd \"$1\" || exit" "cd $1" [
testFix [
tFromStart 4 4 "\"" 10,
tFromEnd 6 6 "\"" 10
],
testFix [
tFromEnd 6 6 " || exit" 5
]]
prop_composeFixes2 = testFixes "$(\"$1\")" "`$1`" [
testFix [
tFromStart 1 2 "$(" 5,
tFromEnd 4 5 ")" 5
],
testFix [
tFromStart 2 2 "\"" 10,
tFromEnd 4 4 "\"" 10
]]
prop_composeFixes3 = testFixes "(x)[x]" "xx" [
testFix [
tFromStart 1 1 "(" 4,
tFromEnd 2 2 ")" 3,
tFromStart 2 2 "[" 2,
tFromEnd 3 3 "]" 1
]]
prop_composeFixes4 = testFixes "(x)[x]" "xx" [
testFix [
tFromStart 1 1 "(" 4,
tFromStart 2 2 "[" 3,
tFromEnd 2 2 ")" 2,
tFromEnd 3 3 "]" 1
]]
prop_composeFixes5 = testFixes "\"$(x)\"" "`x`" [
testFix [
tFromStart 1 2 "$(" 2,
tFromEnd 3 4 ")" 2,
tFromStart 1 1 "\"" 1,
tFromEnd 4 4 "\"" 1
]]
return []
runTests = $quickCheckAll

View File

@@ -1,255 +0,0 @@
{-
Copyright 2019 Vidar 'koala_man' Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Formatter.Diff (format, ShellCheck.Formatter.Diff.runTests) where
import ShellCheck.Interface
import ShellCheck.Fixer
import ShellCheck.Formatter.Format
import Control.Monad
import Data.Algorithm.Diff
import Data.Array
import Data.IORef
import Data.List
import qualified Data.Monoid as Monoid
import Data.Maybe
import qualified Data.Map as M
import GHC.Exts (sortWith)
import System.IO
import System.FilePath
import Test.QuickCheck
import Debug.Trace
ltt x = trace (show x) x
format :: FormatterOptions -> IO Formatter
format options = do
didOutput <- newIORef False
shouldColor <- shouldOutputColor (foColorOption options)
let color = if shouldColor then colorize else nocolor
return Formatter {
header = return (),
footer = checkFooter didOutput color,
onFailure = reportFailure color,
onResult = reportResult didOutput color
}
contextSize = 3
red = 31
green = 32
yellow = 33
cyan = 36
bold = 1
nocolor n = id
colorize n s = (ansi n) ++ s ++ (ansi 0)
ansi n = "\x1B[" ++ show n ++ "m"
printErr :: ColorFunc -> String -> IO ()
printErr color = hPutStrLn stderr . color bold . color red
reportFailure color file msg = printErr color $ file ++ ": " ++ msg
checkFooter didOutput color = do
output <- readIORef didOutput
unless output $
printErr color "Issues were detected, but none were auto-fixable. Use another format to see them."
type ColorFunc = (Int -> String -> String)
data LFStatus = LinefeedMissing | LinefeedOk
data DiffDoc a = DiffDoc String LFStatus [DiffRegion a]
data DiffRegion a = DiffRegion (Int, Int) (Int, Int) [Diff a]
reportResult :: (IORef Bool) -> ColorFunc -> CheckResult -> SystemInterface IO -> IO ()
reportResult didOutput color result sys = do
let comments = crComments result
let suggestedFixes = mapMaybe pcFix comments
let fixmap = buildFixMap suggestedFixes
mapM_ output $ M.toList fixmap
where
output (name, fix) = do
file <- (siReadFile sys) name
case file of
Right contents -> do
putStrLn $ formatDoc color $ makeDiff name contents fix
writeIORef didOutput True
Left msg -> reportFailure color name msg
hasTrailingLinefeed str =
case str of
[] -> True
_ -> last str == '\n'
coversLastLine regions =
case regions of
[] -> False
_ -> (fst $ last regions)
-- TODO: Factor this out into a unified diff library because we're doing a lot
-- of the heavy lifting anyways.
makeDiff :: String -> String -> Fix -> DiffDoc String
makeDiff name contents fix = do
let hunks = groupDiff $ computeDiff contents fix
let lf = if coversLastLine hunks && not (hasTrailingLinefeed contents)
then LinefeedMissing
else LinefeedOk
DiffDoc name lf $ findRegions hunks
computeDiff :: String -> Fix -> [Diff String]
computeDiff contents fix =
let old = lines contents
array = listArray (1, fromIntegral $ (length old)) old
new = applyFix fix array
in getDiff old new
-- Group changes into hunks
groupDiff :: [Diff a] -> [(Bool, [Diff a])]
groupDiff = filter (\(_, l) -> not (null l)) . hunt []
where
-- Churn through 'Both's until we find a difference
hunt current [] = [(False, reverse current)]
hunt current (x@Both {}:rest) = hunt (x:current) rest
hunt current list =
let (context, previous) = splitAt contextSize current
in (False, reverse previous) : gather context 0 list
-- Pick out differences until we find a run of Both's
gather current n [] =
let (extras, patch) = splitAt (max 0 $ n - contextSize) current
in [(True, reverse patch), (False, reverse extras)]
gather current n list@(Both {}:_) | n == contextSize*2 =
let (context, previous) = splitAt contextSize current
in (True, reverse previous) : hunt context list
gather current n (x@Both {}:rest) = gather (x:current) (n+1) rest
gather current n (x:rest) = gather (x:current) 0 rest
-- Get line numbers for hunks
findRegions :: [(Bool, [Diff String])] -> [DiffRegion String]
findRegions = find' 1 1
where
find' _ _ [] = []
find' left right ((output, run):rest) =
let (dl, dr) = countDelta run
remainder = find' (left+dl) (right+dr) rest
in
if output
then DiffRegion (left, dl) (right, dr) run : remainder
else remainder
-- Get left/right line counts for a hunk
countDelta :: [Diff a] -> (Int, Int)
countDelta = count' 0 0
where
count' left right [] = (left, right)
count' left right (x:rest) =
case x of
Both {} -> count' (left+1) (right+1) rest
First {} -> count' (left+1) right rest
Second {} -> count' left (right+1) rest
formatRegion :: ColorFunc -> LFStatus -> DiffRegion String -> String
formatRegion color lf (DiffRegion left right diffs) =
let header = color cyan ("@@ -" ++ (tup left) ++ " +" ++ (tup right) ++" @@")
in
unlines $ header : reverse (getStrings lf (reverse diffs))
where
noLF = "\\ No newline at end of file"
getStrings LinefeedOk list = map format list
getStrings LinefeedMissing list@((Both _ _):_) = noLF : map format list
getStrings LinefeedMissing list@((First _):_) = noLF : map format list
getStrings LinefeedMissing (last:rest) = format last : getStrings LinefeedMissing rest
tup (a,b) = (show a) ++ "," ++ (show b)
format (Both x _) = ' ':x
format (First x) = color red $ '-':x
format (Second x) = color green $ '+':x
splitLast [] = ([], [])
splitLast x =
let (last, rest) = splitAt 1 $ reverse x
in (reverse rest, last)
formatDoc color (DiffDoc name lf regions) =
let (most, last) = splitLast regions
in
(color bold $ "--- " ++ ("a" </> name)) ++ "\n" ++
(color bold $ "+++ " ++ ("b" </> name)) ++ "\n" ++
concatMap (formatRegion color LinefeedOk) most ++
concatMap (formatRegion color lf) last
-- Create a Map from filename to Fix
buildFixMap :: [Fix] -> M.Map String Fix
buildFixMap fixes = perFile
where
splitFixes = concatMap splitFixByFile fixes
perFile = groupByMap (posFile . repStartPos . head . fixReplacements) splitFixes
-- There are currently no multi-file fixes, but let's handle it anyways
splitFixByFile :: Fix -> [Fix]
splitFixByFile fix = map makeFix $ groupBy sameFile (fixReplacements fix)
where
sameFile rep1 rep2 = (posFile $ repStartPos rep1) == (posFile $ repStartPos rep2)
makeFix reps = newFix { fixReplacements = reps }
groupByMap :: (Ord k, Monoid v) => (v -> k) -> [v] -> M.Map k v
groupByMap f = M.fromListWith Monoid.mappend . map (\x -> (f x, x))
-- For building unit tests
b n = Both n n
l = First
r = Second
prop_identifiesProperContext = groupDiff [b 1, b 2, b 3, b 4, l 5, b 6, b 7, b 8, b 9] ==
[(False, [b 1]), -- Omitted
(True, [b 2, b 3, b 4, l 5, b 6, b 7, b 8]), -- A change with three lines of context
(False, [b 9])] -- Omitted
prop_includesContextFromStartIfNecessary = groupDiff [b 4, l 5, b 6, b 7, b 8, b 9] ==
[ -- Nothing omitted
(True, [b 4, l 5, b 6, b 7, b 8]), -- A change with three lines of context
(False, [b 9])] -- Omitted
prop_includesContextUntilEndIfNecessary = groupDiff [b 4, l 5] ==
[ -- Nothing omitted
(True, [b 4, l 5])
] -- Nothing Omitted
prop_splitsIntoMultipleHunks = groupDiff [l 1, b 1, b 2, b 3, b 4, b 5, b 6, b 7, r 8] ==
[ -- Nothing omitted
(True, [l 1, b 1, b 2, b 3]),
(False, [b 4]),
(True, [b 5, b 6, b 7, r 8])
] -- Nothing Omitted
prop_splitsIntoMultipleHunksUnlessTouching = groupDiff [l 1, b 1, b 2, b 3, b 4, b 5, b 6, r 7] ==
[
(True, [l 1, b 1, b 2, b 3, b 4, b 5, b 6, r 7])
]
prop_countDeltasWorks = countDelta [b 1, l 2, r 3, r 4, b 5] == (3,4)
prop_countDeltasWorks2 = countDelta [] == (0,0)
return []
runTests = $quickCheckAll

View File

@@ -1,79 +0,0 @@
{-
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.Format where
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Fixer
import Control.Monad
import Data.Array
import Data.List
import System.IO
import System.Info
-- A formatter that carries along an arbitrary piece of data
data Formatter = Formatter {
header :: IO (),
onResult :: CheckResult -> SystemInterface IO -> IO (),
onFailure :: FilePath -> ErrorMessage -> IO (),
footer :: IO ()
}
sourceFile = posFile . pcStartPos
lineNo = posLine . pcStartPos
endLineNo = posLine . pcEndPos
colNo = posColumn . pcStartPos
endColNo = posColumn . pcEndPos
codeNo = cCode . pcComment
messageText = cMessage . pcComment
severityText :: PositionedComment -> String
severityText pc =
case cSeverity (pcComment pc) of
ErrorC -> "error"
WarningC -> "warning"
InfoC -> "info"
StyleC -> "style"
-- Realign comments from a tabstop of 8 to 1
makeNonVirtual comments contents =
map fix comments
where
list = lines contents
arr = listArray (1, length list) list
untabbedFix f = newFix {
fixReplacements = map (\r -> removeTabStops r arr) (fixReplacements f)
}
fix c = (removeTabStops c arr) {
pcFix = fmap untabbedFix (pcFix c)
}
shouldOutputColor :: ColorOption -> IO Bool
shouldOutputColor colorOption = do
term <- hIsTerminalDevice stdout
let windows = "mingw" `isPrefixOf` os
let isUsableTty = term && not windows
let useColor = case colorOption of
ColorAlways -> True
ColorNever -> False
ColorAuto -> isUsableTty
return useColor

View File

@@ -1,110 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
{-
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.JSON (format) where
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Data.Aeson
import Data.IORef
import Data.Monoid
import GHC.Exts
import System.IO
import qualified Data.ByteString.Lazy.Char8 as BL
format :: IO Formatter
format = do
ref <- newIORef []
return Formatter {
header = return (),
onResult = collectResult ref,
onFailure = outputError,
footer = finish ref
}
instance ToJSON Replacement where
toJSON replacement =
let start = repStartPos replacement
end = repEndPos replacement
str = repString replacement in
object [
"precedence" .= repPrecedence replacement,
"insertionPoint" .=
case repInsertionPoint replacement of
InsertBefore -> "beforeStart" :: String
InsertAfter -> "afterEnd",
"line" .= posLine start,
"column" .= posColumn start,
"endLine" .= posLine end,
"endColumn" .= posColumn end,
"replacement" .= str
]
instance ToJSON PositionedComment where
toJSON comment =
let start = pcStartPos comment
end = pcEndPos comment
c = pcComment comment in
object [
"file" .= posFile start,
"line" .= posLine start,
"endLine" .= posLine end,
"column" .= posColumn start,
"endColumn" .= posColumn end,
"level" .= severityText comment,
"code" .= cCode c,
"message" .= cMessage c,
"fix" .= pcFix comment
]
toEncoding comment =
let start = pcStartPos comment
end = pcEndPos comment
c = pcComment comment in
pairs (
"file" .= posFile start
<> "line" .= posLine start
<> "endLine" .= posLine end
<> "column" .= posColumn start
<> "endColumn" .= posColumn end
<> "level" .= severityText comment
<> "code" .= cCode c
<> "message" .= cMessage c
<> "fix" .= pcFix comment
)
instance ToJSON Fix where
toJSON fix = object [
"replacements" .= fixReplacements fix
]
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
collectResult ref cr sys = mapM_ f groups
where
comments = crComments cr
groups = groupWith sourceFile comments
f :: [PositionedComment] -> IO ()
f group = modifyIORef ref (\x -> comments ++ x)
finish ref = do
list <- readIORef ref
BL.putStrLn $ encode list

View File

@@ -1,127 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
{-
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.JSON1 (format) where
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Data.Aeson
import Data.IORef
import Data.Monoid
import GHC.Exts
import System.IO
import qualified Data.ByteString.Lazy.Char8 as BL
format :: IO Formatter
format = do
ref <- newIORef []
return Formatter {
header = return (),
onResult = collectResult ref,
onFailure = outputError,
footer = finish ref
}
data Json1Output = Json1Output {
comments :: [PositionedComment]
}
instance ToJSON Json1Output where
toJSON result = object [
"comments" .= comments result
]
toEncoding result = pairs (
"comments" .= comments result
)
instance ToJSON Replacement where
toJSON replacement =
let start = repStartPos replacement
end = repEndPos replacement
str = repString replacement in
object [
"precedence" .= repPrecedence replacement,
"insertionPoint" .=
case repInsertionPoint replacement of
InsertBefore -> "beforeStart" :: String
InsertAfter -> "afterEnd",
"line" .= posLine start,
"column" .= posColumn start,
"endLine" .= posLine end,
"endColumn" .= posColumn end,
"replacement" .= str
]
instance ToJSON PositionedComment where
toJSON comment =
let start = pcStartPos comment
end = pcEndPos comment
c = pcComment comment in
object [
"file" .= posFile start,
"line" .= posLine start,
"endLine" .= posLine end,
"column" .= posColumn start,
"endColumn" .= posColumn end,
"level" .= severityText comment,
"code" .= cCode c,
"message" .= cMessage c,
"fix" .= pcFix comment
]
toEncoding comment =
let start = pcStartPos comment
end = pcEndPos comment
c = pcComment comment in
pairs (
"file" .= posFile start
<> "line" .= posLine start
<> "endLine" .= posLine end
<> "column" .= posColumn start
<> "endColumn" .= posColumn end
<> "level" .= severityText comment
<> "code" .= cCode c
<> "message" .= cMessage c
<> "fix" .= pcFix comment
)
instance ToJSON Fix where
toJSON fix = object [
"replacements" .= fixReplacements fix
]
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
collectResult ref cr sys = mapM_ f groups
where
comments = crComments cr
groups = groupWith sourceFile comments
f :: [PositionedComment] -> IO ()
f group = do
let filename = sourceFile (head group)
result <- siReadFile sys filename
let contents = either (const "") id result
let comments' = makeNonVirtual comments contents
modifyIORef ref (\x -> comments' ++ x)
finish ref = do
list <- readIORef ref
BL.putStrLn $ encode $ Json1Output { comments = list }

View File

@@ -1,36 +0,0 @@
{-
Copyright 2019 Austin Voecks
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.Quiet (format) where
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Control.Monad
import Data.IORef
import System.Exit
format :: FormatterOptions -> IO Formatter
format options =
return Formatter {
header = return (),
footer = return (),
onFailure = \ _ _ -> exitFailure,
onResult = \ result _ -> unless (null $ crComments result) exitFailure
}

View File

@@ -1,197 +0,0 @@
{-
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.TTY (format) where
import ShellCheck.Fixer
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Control.Monad
import Data.Array
import Data.Foldable
import Data.Ord
import Data.IORef
import Data.List
import Data.Maybe
import GHC.Exts
import System.IO
import System.Info
wikiLink = "https://www.shellcheck.net/wiki/"
-- An arbitrary Ord thing to order warnings
type Ranking = (Char, Severity, Integer)
-- Ansi coloring function
type ColorFunc = (String -> String -> String)
format :: FormatterOptions -> IO Formatter
format options = do
topErrorRef <- newIORef []
return Formatter {
header = return (),
footer = outputWiki topErrorRef,
onFailure = outputError options,
onResult = outputResult options topErrorRef
}
colorForLevel level =
case level of
"error" -> 31 -- red
"warning" -> 33 -- yellow
"info" -> 32 -- green
"style" -> 32 -- green
"verbose" -> 32 -- green
"message" -> 1 -- bold
"source" -> 0 -- none
_ -> 0 -- none
rankError :: PositionedComment -> Ranking
rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err)
where
ranking =
if cCode (pcComment err) `elem` uninteresting
then 'Z'
else 'A'
-- A list of the most generic, least directly helpful
-- error codes to downrank.
uninteresting = [
1009, -- Mentioned parser error was..
1019, -- Expected this to be an argument
1036, -- ( is invalid here
1047, -- Expected 'fi'
1062, -- Expected 'done'
1070, -- Parsing stopped here (generic)
1072, -- Missing/unexpected ..
1073, -- Couldn't parse this ..
1088, -- Parsing stopped here (paren)
1089 -- Parsing stopped here (keyword)
]
appendComments errRef comments max = do
previous <- readIORef errRef
let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments
writeIORef errRef . take max . nubBy equal . sort $ previous ++ current
where
fst3 (x,_,_) = x
equal x y = fst3 x == fst3 y
outputWiki :: IORef [(Ranking, Integer, String)] -> IO ()
outputWiki errRef = do
issues <- readIORef errRef
unless (null issues) $ do
putStrLn "For more information:"
mapM_ showErr issues
where
showErr (_, code, msg) =
putStrLn $ " " ++ wikiLink ++ "SC" ++ show code ++ " -- " ++ shorten msg
limit = 36
shorten msg =
if length msg < limit
then msg
else (take (limit-3) msg) ++ "..."
outputError options file error = do
color <- getColorFunc $ foColorOption options
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
outputResult options ref result sys = do
color <- getColorFunc $ foColorOption options
let comments = crComments result
appendComments ref comments (fromIntegral $ foWikiLinkCount options)
let fileGroups = groupWith sourceFile comments
mapM_ (outputForFile color sys) fileGroups
outputForFile color sys comments = do
let fileName = sourceFile (head comments)
result <- (siReadFile sys) fileName
let contents = either (const "") id result
let fileLinesList = lines contents
let lineCount = length fileLinesList
let fileLines = listArray (1, lineCount) fileLinesList
let groups = groupWith lineNo comments
mapM_ (\commentsForLine -> do
let lineNum = fromIntegral $ lineNo (head commentsForLine)
let line = if lineNum < 1 || lineNum > lineCount
then ""
else fileLines ! fromIntegral lineNum
putStrLn ""
putStrLn $ color "message" $
"In " ++ fileName ++" line " ++ show lineNum ++ ":"
putStrLn (color "source" line)
mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) commentsForLine
putStrLn ""
showFixedString color commentsForLine (fromIntegral lineNum) fileLines
) groups
-- Pick out only the lines necessary to show a fix in action
sliceFile :: Fix -> Array Int String -> (Fix, Array Int String)
sliceFile fix lines =
(mapPositions adjust fix, sliceLines lines)
where
(minLine, maxLine) =
foldl (\(mm, mx) pos -> ((min mm $ fromIntegral $ posLine pos), (max mx $ fromIntegral $ posLine pos)))
(maxBound, minBound) $
concatMap (\x -> [repStartPos x, repEndPos x]) $ fixReplacements fix
sliceLines :: Array Int String -> Array Int String
sliceLines = ixmap (1, maxLine - minLine + 1) (\x -> x + minLine - 1)
adjust pos =
pos {
posLine = posLine pos - (fromIntegral minLine) + 1
}
showFixedString :: ColorFunc -> [PositionedComment] -> Int -> Array Int String -> IO ()
showFixedString color comments lineNum fileLines =
let line = fileLines ! fromIntegral lineNum in
case mapMaybe pcFix comments of
[] -> return ()
fixes -> do
-- Folding automatically removes overlap
let mergedFix = fold fixes
-- We show the complete, associated fixes, whether or not it includes this
-- and/or other unrelated lines.
let (excerptFix, excerpt) = sliceFile mergedFix fileLines
-- in the spirit of error prone
putStrLn $ color "message" "Did you mean: "
putStrLn $ unlines $ applyFix excerptFix excerpt
cuteIndent :: PositionedComment -> String
cuteIndent comment =
replicate (fromIntegral $ colNo comment - 1) ' ' ++
makeArrow ++ " " ++ code (codeNo comment) ++ ": " ++ messageText comment
where
arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^"
makeArrow =
let sameLine = lineNo comment == endLineNo comment
delta = endColNo comment - colNo comment
in
if sameLine && delta > 2 && delta < 32 then arrow delta else "^--"
code num = "SC" ++ show num
getColorFunc :: ColorOption -> IO ColorFunc
getColorFunc colorOption = do
useColor <- shouldOutputColor colorOption
return $ if useColor then colorComment else const id
where
colorComment level comment =
ansi (colorForLevel level) ++ comment ++ clear
clear = ansi 0
ansi n = "\x1B[" ++ show n ++ "m"

View File

@@ -1,328 +0,0 @@
{-
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
module ShellCheck.Interface
(
SystemInterface(..)
, CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csOptionalChecks)
, CheckResult(crFilename, crComments)
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
, ParseResult(prComments, prTokenPositions, prRoot)
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asOptionalChecks)
, AnalysisResult(arComments)
, FormatterOptions(foColorOption, foWikiLinkCount)
, Shell(Ksh, Sh, Bash, Dash)
, ExecutionMode(Executed, Sourced)
, ErrorMessage
, Code
, Severity(ErrorC, WarningC, InfoC, StyleC)
, Position(posFile, posLine, posColumn)
, Comment(cSeverity, cCode, cMessage)
, PositionedComment(pcStartPos , pcEndPos , pcComment, pcFix)
, ColorOption(ColorAuto, ColorAlways, ColorNever)
, TokenComment(tcId, tcComment, tcFix)
, emptyCheckResult
, newParseResult
, newAnalysisSpec
, newAnalysisResult
, newFormatterOptions
, newPosition
, newTokenComment
, mockedSystemInterface
, mockRcFile
, newParseSpec
, emptyCheckSpec
, newPositionedComment
, newComment
, Fix(fixReplacements)
, newFix
, InsertionPoint(InsertBefore, InsertAfter)
, Replacement(repStartPos, repEndPos, repString, repPrecedence, repInsertionPoint)
, newReplacement
, CheckDescription(cdName, cdDescription, cdPositive, cdNegative)
, newCheckDescription
) where
import ShellCheck.AST
import Control.DeepSeq
import Control.Monad.Identity
import Data.List
import Data.Monoid
import Data.Ord
import Data.Semigroup
import GHC.Generics (Generic)
import qualified Data.Map as Map
data SystemInterface m = SystemInterface {
-- Read a file by filename, or return an error
siReadFile :: String -> m (Either ErrorMessage String),
-- Given:
-- the current script,
-- a list of source-path annotations in effect,
-- and a sourced file,
-- find the sourced file
siFindSource :: String -> [String] -> String -> m FilePath,
-- Get the configuration file (name, contents) for a filename
siGetConfig :: String -> m (Maybe (FilePath, String))
}
-- ShellCheck input and output
data CheckSpec = CheckSpec {
csFilename :: String,
csScript :: String,
csCheckSourced :: Bool,
csIgnoreRC :: Bool,
csExcludedWarnings :: [Integer],
csIncludedWarnings :: Maybe [Integer],
csShellTypeOverride :: Maybe Shell,
csMinSeverity :: Severity,
csOptionalChecks :: [String]
} deriving (Show, Eq)
data CheckResult = CheckResult {
crFilename :: String,
crComments :: [PositionedComment]
} deriving (Show, Eq)
emptyCheckResult :: CheckResult
emptyCheckResult = CheckResult {
crFilename = "",
crComments = []
}
emptyCheckSpec :: CheckSpec
emptyCheckSpec = CheckSpec {
csFilename = "",
csScript = "",
csCheckSourced = False,
csIgnoreRC = False,
csExcludedWarnings = [],
csIncludedWarnings = Nothing,
csShellTypeOverride = Nothing,
csMinSeverity = StyleC,
csOptionalChecks = []
}
newParseSpec :: ParseSpec
newParseSpec = ParseSpec {
psFilename = "",
psScript = "",
psCheckSourced = False,
psIgnoreRC = False,
psShellTypeOverride = Nothing
}
-- Parser input and output
data ParseSpec = ParseSpec {
psFilename :: String,
psScript :: String,
psCheckSourced :: Bool,
psIgnoreRC :: Bool,
psShellTypeOverride :: Maybe Shell
} deriving (Show, Eq)
data ParseResult = ParseResult {
prComments :: [PositionedComment],
prTokenPositions :: Map.Map Id (Position, Position),
prRoot :: Maybe Token
} deriving (Show, Eq)
newParseResult :: ParseResult
newParseResult = ParseResult {
prComments = [],
prTokenPositions = Map.empty,
prRoot = Nothing
}
-- Analyzer input and output
data AnalysisSpec = AnalysisSpec {
asScript :: Token,
asShellType :: Maybe Shell,
asFallbackShell :: Maybe Shell,
asExecutionMode :: ExecutionMode,
asCheckSourced :: Bool,
asOptionalChecks :: [String],
asTokenPositions :: Map.Map Id (Position, Position)
}
newAnalysisSpec token = AnalysisSpec {
asScript = token,
asShellType = Nothing,
asFallbackShell = Nothing,
asExecutionMode = Executed,
asCheckSourced = False,
asOptionalChecks = [],
asTokenPositions = Map.empty
}
newtype AnalysisResult = AnalysisResult {
arComments :: [TokenComment]
}
newAnalysisResult = AnalysisResult {
arComments = []
}
-- Formatter options
data FormatterOptions = FormatterOptions {
foColorOption :: ColorOption,
foWikiLinkCount :: Integer
}
newFormatterOptions = FormatterOptions {
foColorOption = ColorAuto,
foWikiLinkCount = 3
}
data CheckDescription = CheckDescription {
cdName :: String,
cdDescription :: String,
cdPositive :: String,
cdNegative :: String
}
newCheckDescription = CheckDescription {
cdName = "",
cdDescription = "",
cdPositive = "",
cdNegative = ""
}
-- Supporting data types
data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
data ExecutionMode = Executed | Sourced deriving (Show, Eq)
type ErrorMessage = String
type Code = Integer
data Severity = ErrorC | WarningC | InfoC | StyleC
deriving (Show, Eq, Ord, Generic, NFData)
data Position = Position {
posFile :: String, -- Filename
posLine :: Integer, -- 1 based source line
posColumn :: Integer -- 1 based source column, where tabs are 8
} deriving (Show, Eq, Generic, NFData, Ord)
newPosition :: Position
newPosition = Position {
posFile = "",
posLine = 1,
posColumn = 1
}
data Comment = Comment {
cSeverity :: Severity,
cCode :: Code,
cMessage :: String
} deriving (Show, Eq, Generic, NFData)
newComment :: Comment
newComment = Comment {
cSeverity = StyleC,
cCode = 0,
cMessage = ""
}
-- only support single line for now
data Replacement = Replacement {
repStartPos :: Position,
repEndPos :: Position,
repString :: String,
-- Order in which the replacements should happen: highest precedence first.
repPrecedence :: Int,
-- Whether to insert immediately before or immediately after the specified region.
repInsertionPoint :: InsertionPoint
} deriving (Show, Eq, Generic, NFData)
data InsertionPoint = InsertBefore | InsertAfter
deriving (Show, Eq, Generic, NFData)
instance Ord Replacement where
compare r1 r2 = (repStartPos r1) `compare` (repStartPos r2)
newReplacement = Replacement {
repStartPos = newPosition,
repEndPos = newPosition,
repString = "",
repPrecedence = 1,
repInsertionPoint = InsertAfter
}
data Fix = Fix {
fixReplacements :: [Replacement]
} deriving (Show, Eq, Generic, NFData)
newFix = Fix {
fixReplacements = []
}
data PositionedComment = PositionedComment {
pcStartPos :: Position,
pcEndPos :: Position,
pcComment :: Comment,
pcFix :: Maybe Fix
} deriving (Show, Eq, Generic, NFData)
newPositionedComment :: PositionedComment
newPositionedComment = PositionedComment {
pcStartPos = newPosition,
pcEndPos = newPosition,
pcComment = newComment,
pcFix = Nothing
}
data TokenComment = TokenComment {
tcId :: Id,
tcComment :: Comment,
tcFix :: Maybe Fix
} deriving (Show, Eq, Generic, NFData)
newTokenComment = TokenComment {
tcId = Id 0,
tcComment = newComment,
tcFix = Nothing
}
data ColorOption =
ColorAuto
| ColorAlways
| ColorNever
deriving (Ord, Eq, Show)
-- For testing
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
mockedSystemInterface files = SystemInterface {
siReadFile = rf,
siFindSource = fs,
siGetConfig = const $ return Nothing
}
where
rf file =
case filter ((== file) . fst) files of
[] -> return $ Left "File not included in mock."
[(_, contents)] -> return $ Right contents
fs _ _ file = return file
mockRcFile rcfile mock = mock {
siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
}

View File

@@ -1,8 +1,8 @@
# This file was automatically generated by stack init
# For more information, see: https://docs.haskellstack.org/en/stable/yaml_configuration/
# For more information, see: http://docs.haskellstack.org/en/stable/yaml_configuration/
# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
resolver: lts-13.26
resolver: lts-5.5
# Local packages, usually specified by relative directory name
packages:

View File

@@ -1,78 +0,0 @@
#!/usr/bin/env bash
# This file strips all unit tests from ShellCheck, removing
# the dependency on QuickCheck and Template Haskell and
# reduces the binary size considerably.
set -o pipefail
sponge() {
local data
data="$(cat)"
printf '%s\n' "$data" > "$1"
}
modify() {
if ! "${@:2}" < "$1" | sponge "$1"
then
{
printf 'Failed to modify %s: ' "$1"
printf '%q ' "${@:2}"
printf '\n'
} >&2
exit 1
fi
}
detestify() {
printf '%s\n' '-- AUTOGENERATED from ShellCheck by striptests. Do not modify.'
awk '
BEGIN {
state = 0;
}
/LANGUAGE TemplateHaskell/ { next; }
/^import.*Test\./ { next; }
/^module/ {
sub(/,[^,)]*runTests/, "");
}
# Delete tests
/^prop_/ { state = 1; next; }
# ..and any blank lines following them.
state == 1 && /^ / { next; }
# Template Haskell marker
/^return / {
exit;
}
{ state = 0; print; }
'
}
if [[ ! -e 'ShellCheck.cabal' ]]
then
echo "Run me from the ShellCheck directory." >&2
exit 1
fi
if [[ -d '.git' ]] && ! git diff --exit-code > /dev/null 2>&1
then
echo "You have local changes! These may be overwritten." >&2
exit 2
fi
modify 'ShellCheck.cabal' sed -e '
/QuickCheck/d
/^test-suite/{ s/.*//; q; }
'
find . -name '.git' -prune -o -type f -name '*.hs' -print |
while IFS= read -r file
do
modify "$file" detestify
done

View File

@@ -1,35 +0,0 @@
#!/bin/bash
# This script configures, builds and runs tests.
# It's meant for automatic cross-distro testing.
die() { echo "$*" >&2; exit 1; }
[ -e "ShellCheck.cabal" ] ||
die "ShellCheck.cabal not in current dir"
command -v cabal ||
die "cabal is missing"
cabal update ||
die "can't update"
cabal install --dependencies-only --enable-tests ||
die "can't install dependencies"
cabal configure --enable-tests ||
die "configure failed"
cabal build ||
die "build failed"
cabal test ||
die "test failed"
dist/build/shellcheck/shellcheck - << 'EOF' || die "execution failed"
#!/bin/sh
echo "Hello World"
EOF
dist/build/shellcheck/shellcheck - << 'EOF' && die "negative execution failed"
#!/bin/sh
echo $1
EOF
echo "Success"
exit 0

View File

@@ -1,75 +0,0 @@
#!/usr/bin/env bash
failed=0
fail() {
echo "$(tput setaf 1)$*$(tput sgr0)"
failed=1
}
if git diff | grep -q ""
then
fail "There are uncommited changes"
fi
current=$(git tag --points-at)
if [[ -z "$current" ]]
then
fail "No git tag on the current commit"
echo "Create one with: git tag -a v0.0.0"
fi
if [[ "$current" != v* ]]
then
fail "Bad tag format: expected v0.0.0"
fi
if [[ "$(git cat-file -t "$current")" != "tag" ]]
then
fail "Current tag is not annotated (required for Snap)."
fi
if [[ "$(git tag --points-at master)" != "$current" ]]
then
fail "You are not on master"
fi
version=${current#v}
if ! grep "Version:" ShellCheck.cabal | grep -qFw "$version"
then
fail "The cabal file does not match tag version $version"
fi
if ! grep -qF "## $current" CHANGELOG.md
then
fail "CHANGELOG.md does not contain '## $current'"
fi
if [[ $(git log -1 --pretty=%B) != "Stable version "* ]]
then
fail "Expected git log message to be 'Stable version ...'"
fi
i=1 j=1
cat << EOF
Manual Checklist
$((i++)). Make sure none of the automated checks above failed
$((i++)). Make sure Travis build currently passes: https://travis-ci.org/koalaman/shellcheck
$((i++)). Make sure SnapCraft build currently works: https://build.snapcraft.io/user/koalaman
$((i++)). Run test/distrotest to ensure that most distros can build OOTB.
$((i++)). Format and read over the manual for bad formatting and outdated info.
$((i++)). Make sure the Hackage package builds, so that all files are
Release Steps
$((j++)). \`cabal sdist\` to generate a Hackage package
$((j++)). \`git push --follow-tags\` to push commit
$((j++)). Wait for Travis to build
$((j++)). Verify release:
a. Check that the new versions are uploaded: https://shellcheck.storage.googleapis.com/index.html
b. Check that the docker images have version tags: https://hub.docker.com/u/koalaman
$((j++)). If no disaster, upload to Hackage: http://hackage.haskell.org/upload
$((j++)). Push a new commit that updates CHANGELOG.md
EOF
exit "$failed"

View File

@@ -1,80 +0,0 @@
#!/bin/bash
# This script runs 'buildtest' on each of several distros
# via Docker.
set -o pipefail
exec 3>&1 4>&2
die() { echo "$*" >&4; exit 1; }
[ -e "ShellCheck.cabal" ] || die "ShellCheck.cabal not in this dir"
[ "$1" = "--run" ] || {
cat << EOF
This script pulls multiple distros via Docker and compiles
ShellCheck and dependencies for each one. It takes hours,
and is still highly experimental.
Make sure you're plugged in and have screen/tmux in place,
then re-run with $0 --run to continue.
Also note that 'dist' will be deleted.
EOF
exit 0
}
echo "Deleting 'dist'..."
rm -rf dist
log=$(mktemp) || die "Can't create temp file"
date >> "$log" || die "Can't write to log"
echo "Logging to $log" >&3
exec >> "$log" 2>&1
final=0
while read -r distro setup
do
[[ "$distro" = "#"* || -z "$distro" ]] && continue
printf '%s ' "$distro" >&3
docker pull "$distro" || die "Can't pull $distro"
printf 'pulled. ' >&3
tmp=$(mktemp -d) || die "Can't make temp dir"
cp -r . "$tmp/" || die "Can't populate test dir"
printf 'Result: ' >&3
< /dev/null docker run -v "$tmp:/mnt" "$distro" sh -c "
$setup
cd /mnt || exit 1
test/buildtest
"
ret=$?
if [ "$ret" = 0 ]
then
echo "OK" >&3
else
echo "FAIL with $ret. See $log" >&3
final=1
fi
rm -rf "$tmp"
done << EOF
# Docker tag Setup command
debian:stable apt-get update && apt-get install -y cabal-install
debian:testing apt-get update && apt-get install -y cabal-install
ubuntu:latest apt-get update && apt-get install -y cabal-install
opensuse/leap:latest zypper install -y cabal-install ghc
# Other Ubuntu versions we want to support
ubuntu:19.04 apt-get update && apt-get install -y cabal-install
ubuntu:18.10 apt-get update && apt-get install -y cabal-install
# Misc Haskell including current and latest Stack build
ubuntu:18.10 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
haskell:latest true
# Known to currently fail
centos:latest yum install -y epel-release && yum install -y cabal-install
fedora:latest dnf install -y cabal-install
archlinux/base:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
EOF
exit "$final"

View File

@@ -2,28 +2,20 @@ module Main where
import Control.Monad
import System.Exit
import qualified ShellCheck.Checker
import qualified ShellCheck.Analytics
import qualified ShellCheck.AnalyzerLib
import qualified ShellCheck.Checker
import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.Custom
import qualified ShellCheck.Checks.ShellSupport
import qualified ShellCheck.Fixer
import qualified ShellCheck.Formatter.Diff
import qualified ShellCheck.Parser
import qualified ShellCheck.Checks.Commands
main = do
putStrLn "Running ShellCheck tests..."
results <- sequence [
ShellCheck.Analytics.runTests
,ShellCheck.AnalyzerLib.runTests
,ShellCheck.Checker.runTests
,ShellCheck.Checks.Commands.runTests
,ShellCheck.Checks.Custom.runTests
,ShellCheck.Checks.ShellSupport.runTests
,ShellCheck.Fixer.runTests
,ShellCheck.Formatter.Diff.runTests
,ShellCheck.Parser.runTests
ShellCheck.Checker.runTests,
ShellCheck.Checks.Commands.runTests,
ShellCheck.Analytics.runTests,
ShellCheck.AnalyzerLib.runTests,
ShellCheck.Parser.runTests
]
if and results
then exitSuccess

View File

@@ -1,27 +0,0 @@
#!/bin/bash
# This script builds ShellCheck through `stack` using
# various resolvers. It's run via distrotest.
resolvers=(
nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
)
die() { echo "$*" >&2; exit 1; }
[ -e "ShellCheck.cabal" ] ||
die "ShellCheck.cabal not in current dir"
[ -e "stack.yaml" ] ||
die "stack.yaml not in current dir"
command -v stack ||
die "stack is missing"
stack setup || die "Failed to setup with default resolver"
stack build --test || die "Failed to build/test with default resolver"
for resolver in "${resolvers[@]}"
do
stack --resolver="$resolver" setup || die "Failed to setup $resolver"
stack --resolver="$resolver" build --test || die "Failed build/test with $resolver!"
done
echo "Success"