Merge branch 'master' into autofix

This commit is contained in:
Simon Yeadon 2025-05-27 19:03:36 +10:00
commit de7e02b5d5
78 changed files with 8904 additions and 1208 deletions

6
.dockerignore Normal file
View file

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

View file

@ -1,11 +1,11 @@
#### For bugs #### For bugs
- Rule Id (if any, e.g. SC1000): - Rule Id (if any, e.g. SC1000):
- My shellcheck version (`shellcheck --version` or "online"): - My shellcheck version (`shellcheck --version` or "online"):
- [ ] I read the issue's wiki page, e.g. https://github.com/koalaman/shellcheck/wiki/SC2086 - [ ] 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 - [ ] I tried on https://www.shellcheck.net/ and verified that this is still a problem on the latest commit
#### For new checks and feature suggestions #### For new checks and feature suggestions
- [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this - [ ] https://www.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 - [ ] I searched through https://github.com/koalaman/shellcheck/issues and didn't find anything related

7
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

159
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,159 @@
name: Build ShellCheck
# Run this workflow every time a new commit pushed to your repository
on: push
jobs:
package_source:
name: Package Source Code
runs-on: ubuntu-latest
steps:
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-mark manual ghc # Don't bother installing ghc just to tar up source
sudo apt-get install cabal-install
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Deduce tags
run: |
mkdir source
echo "latest" > source/tags
if tag=$(git describe --exact-match --tags)
then
echo "stable" >> source/tags
echo "$tag" >> source/tags
fi
cat source/tags
- name: Package Source
run: |
grep "stable" source/tags || ./setgitversion
cabal sdist
mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: source
path: source/
run_tests:
name: Run tests
needs: package_source
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
- name: Install dependencies
run: |
sudo apt-get update && sudo apt-get install ghc cabal-install
cabal update
- name: Unpack source
run: |
cd source
tar xvf source.tar.gz --strip-components=1
- name: Build and run tests
run: |
cd source
cabal test
build_source:
name: Build
needs: package_source
strategy:
matrix:
build: [linux.x86_64, linux.aarch64, linux.armv6hf, linux.riscv64, darwin.x86_64, darwin.aarch64, windows.x86_64]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
- name: Build source
run: |
mkdir -p bin
mkdir -p bin/${{matrix.build}}
( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} )
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{matrix.build}}.bin
path: bin/
package_binary:
name: Package Binaries
needs: build_source
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
- name: Work around GitHub permissions bug
run: chmod +x *.bin/*/shellcheck*
- name: Package binaries
run: |
export TAGS="$(cat source/tags)"
mkdir -p deploy
cp -r *.bin/* deploy
cd deploy
../.prepare_deploy
rm -rf */ README* LICENSE*
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: deploy
path: deploy/
deploy:
name: Deploy binaries
needs: package_binary
runs-on: ubuntu-latest
environment: Deploy
steps:
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install hub
- name: Checkout repository
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
- name: Upload to GitHub
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
export TAGS="$(cat source/tags)"
./.github_deploy
- name: Waiting for GitHub to replicate uploaded releases
run: |
sleep 300
- name: Upload to Docker Hub
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_EMAIL: ${{ secrets.DOCKER_EMAIL }}
DOCKER_BASE: ${{ secrets.DOCKER_USERNAME }}/shellcheck
run: |
export TAGS="$(cat source/tags)"
( source ./.multi_arch_docker && set -eux && multi_arch_docker::main )

28
.github_deploy Executable file
View file

@ -0,0 +1,28 @@
#!/bin/bash
set -x
shopt -s extglob
export EDITOR="touch"
# Sanity check
gh --version || exit 1
hub release show latest || exit 1
for tag in $TAGS
do
if ! hub release show "$tag"
then
echo "Creating new release $tag"
git show --no-patch --format='format:%B' > description
hub release create -F description "$tag"
fi
files=()
for file in deploy/*
do
[[ $file == *.@(xz|gz|zip) ]] || continue
[[ $file == *"$tag"* ]] || continue
files+=("$file")
done
gh release upload "$tag" "${files[@]}" --clobber || exit 1
done

1
.gitignore vendored
View file

@ -20,3 +20,4 @@ cabal.config
/parts/ /parts/
/prime/ /prime/
*.snap *.snap
/dist-newstyle/

89
.multi_arch_docker Executable file
View file

@ -0,0 +1,89 @@
#!/bin/bash
# This script builds and deploys multi-architecture docker images from the
# binaries previously built and deployed to GitHub.
function multi_arch_docker::install_docker_buildx() {
# Install QEMU multi-architecture support for docker buildx.
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
# Instantiate docker buildx builder with multi-architecture support.
docker buildx create --name mybuilder
docker buildx use mybuilder
# Start up buildx and verify that all is OK.
docker buildx inspect --bootstrap
}
# Log in to Docker Hub for deployment.
function multi_arch_docker::login_to_docker_hub() {
echo "$DOCKER_PASSWORD" | docker login -u="$DOCKER_USERNAME" --password-stdin
}
# Run buildx build and push. Passed in arguments augment the command line.
function multi_arch_docker::buildx() {
mkdir -p /tmp/empty
docker buildx build \
--platform "${DOCKER_PLATFORMS// /,}" \
--push \
--progress plain \
-f Dockerfile.multi-arch \
"$@" \
/tmp/empty
rmdir /tmp/empty
}
# Build and push plain and alpine docker images for all tags.
function multi_arch_docker::build_and_push_all() {
for tag in $TAGS; do
multi_arch_docker::buildx -t "$DOCKER_BASE:$tag" --build-arg "tag=$tag"
multi_arch_docker::buildx -t "$DOCKER_BASE-alpine:$tag" \
--build-arg "tag=$tag" --target alpine
done
}
# Test all pushed docker images.
function multi_arch_docker::test_all() {
printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
for platform in $DOCKER_PLATFORMS; do
for tag in $TAGS; do
for ext in '-alpine' ''; do
image="${DOCKER_BASE}${ext}:${tag}"
msg="Testing docker image $image on platform $platform"
line="${msg//?/=}"
printf '\n%s\n%s\n%s\n' "${line}" "${msg}" "${line}"
docker pull -q --platform "$platform" "$image"
if [ -n "$ext" ]; then
echo -n "Image architecture: "
docker run --rm --entrypoint /bin/sh "$image" -c 'uname -m'
version=$(docker run --rm "$image" shellcheck --version \
| grep 'version:')
else
version=$(docker run --rm "$image" --version | grep 'version:')
fi
version=${version/#version: /v}
echo "shellcheck version: $version"
if [[ ! ("$tag" =~ ^(latest|stable)$) && "$tag" != "$version" ]]; then
echo "Version mismatch: shellcheck $version tagged as $tag"
exit 1
fi
if [ -n "$ext" ]; then
docker run --rm -v "$PWD:/mnt" -w /mnt "$image" shellcheck myscript
else
docker run --rm -v "$PWD:/mnt" "$image" myscript
fi
done
done
done
}
function multi_arch_docker::main() {
export DOCKER_PLATFORMS='linux/amd64'
DOCKER_PLATFORMS+=' linux/arm64'
DOCKER_PLATFORMS+=' linux/arm/v6'
DOCKER_PLATFORMS+=' linux/riscv64'
multi_arch_docker::install_docker_buildx
multi_arch_docker::login_to_docker_hub
multi_arch_docker::build_and_push_all
multi_arch_docker::test_all
}

View file

@ -1,8 +1,9 @@
#!/bin/bash #!/bin/bash
# This script packages up Travis compiled binaries # This script packages up compiled binaries
set -ex set -ex
shopt -s nullglob shopt -s nullglob extglob
cd deploy
ls -l
cp ../LICENSE LICENSE.txt cp ../LICENSE LICENSE.txt
sed -e $'s/$/\r/' > README.txt << END sed -e $'s/$/\r/' > README.txt << END
@ -22,29 +23,32 @@ This binary was compiled on $(date -u).
$(git log -n 3) $(git log -n 3)
END END
for file in ./*.exe for dir in */
do do
zip "${file%.*}.zip" README.txt LICENSE.txt "$file" cp LICENSE.txt README.txt "$dir"
done done
for file in *.linux-x86_64 echo "Tags are $TAGS"
for tag in $TAGS
do do
base="${file%.*}"
cp "$file" "shellcheck" for dir in windows.*/
tar -cJf "$base.linux.x86_64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck do
rm "shellcheck" ( cd "$dir" && zip "../shellcheck-$tag.zip" * )
done done
for file in *.linux-armv6hf for dir in {linux,darwin}.*/
do do
base="${file%.*}" base="${dir%/}"
cp "$file" "shellcheck" ( cd "$dir" && tar -cJf "../shellcheck-$tag.$base.tar.xz" --transform="s:^:shellcheck-$tag/:" * )
tar -cJf "$base.linux.armv6hf.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck done
rm "shellcheck"
done done
for file in ./* for file in ./*
do do
[[ -f "$file" ]] || continue
sha512sum "$file" > "$file.sha512sum" sha512sum "$file" > "$file.sha512sum"
done done
ls -l

View file

@ -1,80 +0,0 @@
sudo: required
language: sh
services:
- docker
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 deploy
# Remove all tests to reduce binary size
- ./striptests
# 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'
# 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
- rm -f shellcheck || true
# 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
- rm -rf dist shellcheck || true
# Misc packaging
- ./.prepare_deploy
after_success:
- 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;
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,17 +1,213 @@
## ??? ## Git
### Added
- SC2327/SC2328: Warn about capturing the output of redirected commands.
- SC2329: Warn when (non-escaping) functions are never invoked.
- SC2330: Warn about unsupported glob matches with [[ .. ]] in BusyBox.
- SC2331: Suggest using standard -e instead of unary -a in tests.
- SC2332: Warn about `[ ! -o opt ]` being unconditionally true in Bash.
- SC3062: Warn about bashism `[ -o opt ]`.
- Precompiled binaries for Linux riscv64 (linux.riscv64)
### Changed
- SC2002 about Useless Use Of Cat is now disabled by default. It can be
re-enabled with `--enable=useless-use-of-cat` or equivalent directive.
- SC2015 about `A && B || C` no longer triggers when B is a test command.
- SC3012: Do not warn about `\<` and `\>` in test/[] as specified in POSIX.1-2024
### Fixed
- SC2218 about function use-before-define is now more accurate.
- SC2317 about unreachable commands is now less spammy for nested ones.
- SC2292, optional suggestion for [[ ]], now triggers for Busybox.
### Removed
- SC3013: removed since the operators `-ot/-nt/-ef` are specified in POSIX.1-2024
## v0.10.0 - 2024-03-07
### Added
- Precompiled binaries for macOS ARM64 (darwin.aarch64)
- Added support for busybox sh
- Added flag --rcfile to specify an rc file by name.
- Added `extended-analysis=true` directive to enable/disable dataflow analysis
(with a corresponding --extended-analysis flag).
- SC2324: Warn when x+=1 appends instead of increments
- SC2325: Warn about multiple `!`s in dash/sh.
- SC2326: Warn about `foo | ! bar` in bash/dash/sh.
- SC3012: Warn about lexicographic-compare bashism in test like in [ ]
- SC3013: Warn bashism `test _ -op/-nt/-ef _` like in [ ]
- SC3014: Warn bashism `test _ == _` like in [ ]
- SC3015: Warn bashism `test _ =~ _` like in [ ]
- SC3016: Warn bashism `test -v _` like in [ ]
- SC3017: Warn bashism `test -a _` like in [ ]
### Fixed
- source statements with here docs now work correctly
- "(Array.!): undefined array element" error should no longer occur
## v0.9.0 - 2022-12-12
### Added
- SC2316: Warn about 'local readonly foo' and similar (thanks, patrickxia!)
- SC2317: Warn about unreachable commands
- SC2318: Warn about backreferences in 'declare x=1 y=$x'
- SC2319/SC2320: Warn when $? refers to echo/printf/[ ]/[[ ]]/test
- SC2321: Suggest removing $((..)) in array[$((idx))]=val
- SC2322: Suggest collapsing double parentheses in arithmetic contexts
- SC2323: Suggest removing wrapping parentheses in a[(x+1)]=val
### Fixed
- SC2086: Now uses DFA to make more accurate predictions about values
- SC2086: No longer warns about values declared as integer with declare -i
### Changed
- ShellCheck now has a Data Flow Analysis engine to make smarter decisions
based on control flow rather than just syntax. Existing checks will
gradually start using it, which may cause them to trigger differently
(but more accurately).
- Values in directives/shellcheckrc can now be quoted with '' or ""
## v0.8.0 - 2021-11-06
### Added
- `disable=all` now conveniently disables all warnings
- `external-sources=true` directive can be added to .shellcheckrc to make
shellcheck behave as if `-x` was specified.
- Optional `check-extra-masked-returns` for pointing out commands with
suppressed exit codes (SC2312).
- Optional `require-double-brackets` for recommending \[\[ ]] (SC2292).
- SC2286-SC2288: Warn when command name ends in a symbol like `/.)'"`
- SC2289: Warn when command name contains tabs or linefeeds
- SC2291: Warn about repeated unquoted spaces between words in echo
- SC2292: Suggest [[ over [ in Bash/Ksh scripts (optional)
- SC2293/SC2294: Warn when calling `eval` with arrays
- SC2295: Warn about "${x#$y}" treating $y as a pattern when not quoted
- SC2296-SC2301: Improved warnings for bad parameter expansions
- SC2302/SC2303: Warn about loops over array values when using them as keys
- SC2304-SC2306: Warn about unquoted globs in expr arguments
- SC2307: Warn about insufficient number of arguments to expr
- SC2308: Suggest other approaches for non-standard expr extensions
- SC2313: Warn about `read` with unquoted, array indexed variable
### Fixed
- SC2102 about repetitions in ranges no longer triggers on [[ -v arr[xx] ]]
- SC2155 now recognizes `typeset` and local read-only `declare` statements
- SC2181 now tries to avoid triggering for error handling functions
- SC2290: Warn about misused = in declare & co, which were not caught by SC2270+
- The flag --color=auto no longer outputs color when TERM is "dumb" or unset
### Changed
- SC2048: Warning about $\* now also applies to ${array[\*]}
- SC2181 now only triggers on single condition tests like `[ $? = 0 ]`.
- Quote warnings are now emitted for declaration utilities in sh
- Leading `_` can now be used to suppress warnings about unused variables
- TTY output now includes warning level in text as well as color
### Removed
- SC1004: Literal backslash+linefeed in '' was found to be usually correct
## v0.7.2 - 2021-04-19
### Added
- `disable` directives can now be a range, e.g. `disable=SC3000-SC4000`
- SC1143: Warn about line continuations in comments
- SC2259/SC2260: Warn when redirections override pipes
- SC2261: Warn about multiple competing redirections
- SC2262/SC2263: Warn about aliases declared and used in the same parsing unit
- SC2264: Warn about wrapper functions that blatantly recurse
- SC2265/SC2266: Warn when using & or | with test statements
- SC2267: Warn when using xargs -i instead of -I
- SC2268: Warn about unnecessary x-comparisons like `[ x$var = xval ]`
### Fixed
- SC1072/SC1073 now respond to disable annotations, though ignoring parse errors
is still purely cosmetic and does not allow ShellCheck to continue.
- Improved error reporting for trailing tokens after ]/]] and compound commands
- `#!/usr/bin/env -S shell` is now handled correctly
- Here docs with \r are now parsed correctly and give better warnings
### Changed
- Assignments are now parsed to spec, without leniency for leading $ or spaces
- POSIX/dash unsupported feature warnings now have individual SC3xxx codes
- SC1090: A leading `$x/` or `$(x)/` is now treated as `./` when locating files
- SC2154: Variables appearing in -z/-n tests are no longer considered unassigned
- SC2270-SC2285: Improved warnings about misused `=`, e.g. `${var}=42`
## v0.7.1 - 2020-04-04
### Fixed
- `-f diff` no longer claims that it found more issues when it didn't
- Known empty variables now correctly trigger SC2086
- ShellCheck should now be compatible with Cabal 3
- SC2154 and all command-specific checks now trigger for builtins
called with `builtin`
### Added
- SC1136: Warn about unexpected characters after ]/]]
- SC2254: Suggest quoting expansions in case statements
- SC2255: Suggest using `$((..))` in `[ 2*3 -eq 6 ]`
- SC2256: Warn about translated strings that are known variables
- SC2257: Warn about arithmetic mutation in redirections
- SC2258: Warn about trailing commas in for loop elements
### Changed
- SC2230: 'command -v' suggestion is now off by default (-i deprecate-which)
- SC1081: Keywords are now correctly parsed case sensitively, with a warning
## 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 ### Added
- Command line option --severity/-S for filtering by minimum severity - Command line option --severity/-S for filtering by minimum severity
- Command line option --wiki-link-count/-W for showing wiki links - 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 - SC2236/SC2237: Suggest -n/-z instead of ! -z/-n
- SC2238: Warn when redirecting to a known command name, e.g. ls > rm - 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 - 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 |/||/&& - SC1133: Better diagnostics when starting a line with |/||/&&
### Changed ### Changed
- Most warnings now have useful end positions - Most warnings now have useful end positions
- SC1117 about unknown double-quoted escape sequences has been retired - SC1117 about unknown double-quoted escape sequences has been retired
### Fixed ### Fixed
- SC2021 no longer triggers for equivalence classes like '[=e=]' - SC2021 no longer triggers for equivalence classes like `[=e=]`
- SC2221/SC2222 no longer mistriggers on fall-through case branches - 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 ## v0.5.0 - 2018-05-31
### Added ### Added
@ -74,7 +270,7 @@
- SC2204/SC2205: Warn about `( -z foo )` and `( foo -eq bar )` - SC2204/SC2205: Warn about `( -z foo )` and `( foo -eq bar )`
- SC2200/SC2201: Warn about brace expansion in [/[[ - SC2200/SC2201: Warn about brace expansion in [/[[
- SC2198/SC2199: Warn about arrays in [/[[ - SC2198/SC2199: Warn about arrays in [/[[
- SC2196/SC2197: Warn about deprected egrep/fgrep - SC2196/SC2197: Warn about deprecated egrep/fgrep
- SC2195: Warn about unmatchable case branches - SC2195: Warn about unmatchable case branches
- SC2194: Warn about constant 'case' statements - SC2194: Warn about constant 'case' statements
- SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables - SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables
@ -91,7 +287,7 @@
### Fixed ### Fixed
- `-c` no longer suggested when using `grep -o | wc` - `-c` no longer suggested when using `grep -o | wc`
- Comments and whitespace are now allowed before filewide directives - Comments and whitespace are now allowed before filewide directives
- Here doc delimters with esoteric quoting like `foo""` are now handled - Here doc delimiters with esoteric quoting like `foo""` are now handled
- SC2095 about `ssh` in while read loops is now suppressed when using `-n` - 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 - `%(%Y%M%D)T` now recognized as a single formatter in `printf` checks
- `grep -F` now suppresses regex related suggestions - `grep -F` now suppresses regex related suggestions

View file

@ -1,36 +0,0 @@
# Build-only image
FROM ubuntu:17.10 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"]

26
Dockerfile.multi-arch Normal file
View file

@ -0,0 +1,26 @@
# Alpine image
FROM alpine:latest AS alpine
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
ARG tag
# Put the right binary for each architecture into place for the
# multi-architecture docker image.
RUN set -x; \
arch="$(uname -m)"; \
echo "arch is $arch"; \
if [ "${arch}" = 'armv7l' ]; then \
arch='armv6hf'; \
fi; \
url_base='https://github.com/koalaman/shellcheck/releases/download/'; \
tar_file="${tag}/shellcheck-${tag}.linux.${arch}.tar.xz"; \
wget "${url_base}${tar_file}" -O - | tar xJf -; \
mv "shellcheck-${tag}/shellcheck" /bin/; \
rm -rf "shellcheck-${tag}"; \
ls -laF /bin/shellcheck
# ShellCheck image
FROM scratch
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
WORKDIR /mnt
COPY --from=alpine /bin/shellcheck /bin/
ENTRYPOINT ["/bin/shellcheck"]

12
LICENSE
View file

@ -1,13 +1,3 @@
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 GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007 Version 3, 29 June 2007
@ -681,4 +671,4 @@ into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with 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 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 Public License instead of this License. But first, please read
<https://www.gnu.org/philosophy/why-not-lgpl.html>. <https://www.gnu.org/licenses/why-not-lgpl.html>.

260
README.md
View file

@ -1,4 +1,5 @@
[![Build Status](https://travis-ci.org/koalaman/shellcheck.svg?branch=master)](https://travis-ci.org/koalaman/shellcheck) [![Build Status](https://github.com/koalaman/shellcheck/actions/workflows/build.yml/badge.svg)](https://github.com/koalaman/shellcheck/actions/workflows/build.yml)
# ShellCheck - A shell script static analysis tool # ShellCheck - A shell script static analysis tool
@ -8,45 +9,45 @@ ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell
The goals of ShellCheck are The goals of ShellCheck are
- To point out and clarify typical beginner's syntax issues that cause a shell * To point out and clarify typical beginner's syntax issues that cause a shell
to give cryptic error messages. to give cryptic error messages.
- To point out and clarify typical intermediate level semantic problems that * To point out and clarify typical intermediate level semantic problems that
cause a shell to behave strangely and counter-intuitively. cause a shell to behave strangely and counter-intuitively.
- To point out subtle caveats, corner cases and pitfalls that may cause an * To point out subtle caveats, corner cases and pitfalls that may cause an
advanced user's otherwise working script to fail under future circumstances. 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! 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 ## Table of Contents
- [How to use](#how-to-use) * [How to use](#how-to-use)
- [On the web](#on-the-web) * [On the web](#on-the-web)
- [From your terminal](#from-your-terminal) * [From your terminal](#from-your-terminal)
- [In your editor](#in-your-editor) * [In your editor](#in-your-editor)
- [In your build or test suites](#in-your-build-or-test-suites) * [In your build or test suites](#in-your-build-or-test-suites)
- [Installing](#installing) * [Installing](#installing)
- [Travis CI](#travis-ci) * [Compiling from source](#compiling-from-source)
- [Compiling from source](#compiling-from-source) * [Installing Cabal](#installing-cabal)
- [Installing Cabal](#installing-cabal) * [Compiling ShellCheck](#compiling-shellcheck)
- [Compiling ShellCheck](#compiling-shellcheck) * [Running tests](#running-tests)
- [Running tests](#running-tests) * [Gallery of bad code](#gallery-of-bad-code)
- [Gallery of bad code](#gallery-of-bad-code) * [Quoting](#quoting)
- [Quoting](#quoting) * [Conditionals](#conditionals)
- [Conditionals](#conditionals) * [Frequently misused commands](#frequently-misused-commands)
- [Frequently misused commands](#frequently-misused-commands) * [Common beginner's mistakes](#common-beginners-mistakes)
- [Common beginner's mistakes](#common-beginners-mistakes) * [Style](#style)
- [Style](#style) * [Data and typing errors](#data-and-typing-errors)
- [Data and typing errors](#data-and-typing-errors) * [Robustness](#robustness)
- [Robustness](#robustness) * [Portability](#portability)
- [Portability](#portability) * [Miscellaneous](#miscellaneous)
- [Miscellaneous](#miscellaneous) * [Testimonials](#testimonials)
- [Testimonials](#testimonials) * [Ignoring issues](#ignoring-issues)
- [Ignoring issues](#ignoring-issues) * [Reporting bugs](#reporting-bugs)
- [Reporting bugs](#reporting-bugs) * [Contributing](#contributing)
- [Contributing](#contributing) * [Copyright](#copyright)
- [Copyright](#copyright) * [Other Resources](#other-resources)
## How to use ## How to use
@ -54,7 +55,7 @@ There are a number of ways to use ShellCheck!
### On the web ### On the web
Paste a shell script on https://www.shellcheck.net for instant feedback. Paste a shell script on <https://www.shellcheck.net> for instant feedback.
[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! [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!
@ -76,7 +77,7 @@ You can see ShellCheck suggestions directly in a variety of editors.
* Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck). * Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck).
* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck). * Pulsar Edit (former Atom), through [linter-shellcheck-pulsar](https://github.com/pulsar-cooperative/linter-shellcheck-pulsar).
* VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck). * VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck).
@ -85,8 +86,46 @@ You can see ShellCheck suggestions directly in a variety of editors.
### 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. 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.
ShellCheck makes canonical use of exit codes, and 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. For example, in a Makefile:
```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/)
* [Codety](https://www.codety.io/) via the [Codety Scanner](https://github.com/codetyio/codety-scanner)
* [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck)
* [Github](https://github.com/features/actions) (only Linux)
* [Trunk Check](https://trunk.io/products/check) (universal linter; [allows you to explicitly version your shellcheck install](https://github.com/trunk-io/plugins/blob/bcbb361dcdbe4619af51ea7db474d7fb87540d20/.trunk/trunk.yaml#L32)) via the [shellcheck plugin](https://github.com/trunk-io/plugins/blob/main/linters/shellcheck/plugin.yaml)
* [CodeRabbit](https://coderabbit.ai/)
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-a-pre-compiled-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 ## Installing
@ -104,13 +143,13 @@ On systems with Stack (installs to `~/.local/bin`):
On Debian based distros: On Debian based distros:
apt-get install shellcheck sudo apt install shellcheck
On Arch Linux based distros: On Arch Linux based distros:
pacman -S shellcheck pacman -S shellcheck
or get the dependency free [shellcheck-static](https://aur.archlinux.org/packages/shellcheck-static/) from the AUR. or get the dependency free [shellcheck-bin](https://aur.archlinux.org/packages/shellcheck-bin/) from the AUR.
On Gentoo based distros: On Gentoo based distros:
@ -118,8 +157,8 @@ On Gentoo based distros:
On EPEL based distros: On EPEL based distros:
yum -y install epel-release sudo yum -y install epel-release
yum install ShellCheck sudo yum install ShellCheck
On Fedora based distros: On Fedora based distros:
@ -129,23 +168,49 @@ On FreeBSD:
pkg install hs-ShellCheck pkg install hs-ShellCheck
On OS X with homebrew: On macOS (OS X) with Homebrew:
brew install shellcheck brew install shellcheck
Or with MacPorts:
sudo port install shellcheck
On OpenBSD:
pkg_add shellcheck
On openSUSE On openSUSE
zypper in ShellCheck zypper in ShellCheck
Or use OneClickInstall - https://software.opensuse.org/package/ShellCheck Or use OneClickInstall - <https://software.opensuse.org/package/ShellCheck>
On Solus: On Solus:
eopkg install shellcheck eopkg install shellcheck
On Windows (via [scoop](http://scoop.sh)): On Windows (via [chocolatey](https://chocolatey.org/packages/shellcheck)):
scoop install shellcheck ```cmd
C:\> choco install shellcheck
```
Or Windows (via [winget](https://github.com/microsoft/winget-pkgs)):
```cmd
C:\> winget install --id koalaman.shellcheck
```
Or Windows (via [scoop](http://scoop.sh)):
```cmd
C:\> scoop install shellcheck
```
From [conda-forge](https://anaconda.org/conda-forge/shellcheck):
conda install -c conda-forge shellcheck
From Snap Store: From Snap Store:
@ -154,37 +219,77 @@ From Snap Store:
From Docker Hub: From Docker Hub:
```sh ```sh
docker pull koalaman/shellcheck:stable # Or :v0.4.7 for that version, or :latest for daily builds docker run --rm -v "$PWD:/mnt" koalaman/shellcheck:stable myscript
docker run -v "$PWD:/mnt" koalaman/shellcheck 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. 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.
Using the [nix package manager](https://nixos.org/nix):
```sh
nix-env -iA nixpkgs.shellcheck
```
Using the [Flox package manager](https://flox.dev/)
```sh
flox install shellcheck
```
Alternatively, you can download pre-compiled binaries for the latest release here: 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, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/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, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-stable.zip) * [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)
* [macOS, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.aarch64.tar.xz)
* [macOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz)
* [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/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. or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
(including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds).
## Travis CI There are currently no official binaries for Apple Silicon, but third party builds are available via
[ShellCheck for Visual Studio Code](https://github.com/vscode-shellcheck/shellcheck-binaries/releases).
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
```
### pre-commit
To run ShellCheck via [pre-commit](https://pre-commit.com/), add the hook to your `.pre-commit-config.yaml`:
```
repos:
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.7.2
hooks:
- id: shellcheck
# args: ["--severity=warning"] # Optionally only show errors and warnings
```
### Travis CI
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it. 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 the latest release, follow the steps to install the shellcheck binary, bellow. 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 the shellcheck binary ### Installing a pre-compiled binary
*Pre-requisite*: the program 'xz' needs to be installed on the system. The pre-compiled binaries come in `tar.xz` files. To decompress them, make sure
To install it on debian/ubuntu/linux mint, run `apt install xz-utils`. `xz` is installed.
To install it on Redhat/Fedora/CentOS, run `yum -y install xz`. 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 ```bash
export scversion="stable" # or "v0.4.7", or "latest" scversion="stable" # or "v0.4.7", or "latest"
wget "https://storage.googleapis.com/shellcheck/shellcheck-${scversion}.linux.x86_64.tar.xz" wget -qO- "https://github.com/koalaman/shellcheck/releases/download/${scversion?}/shellcheck-${scversion?}.linux.x86_64.tar.xz" | tar -xJv
tar --xz -xvf shellcheck-"${scversion}".linux.x86_64.tar.xz cp "shellcheck-${scversion}/shellcheck" /usr/bin/
cp shellcheck-"${scversion}"/shellcheck /usr/bin/
shellcheck --version shellcheck --version
``` ```
@ -196,13 +301,11 @@ This section describes how to build ShellCheck from a source directory. ShellChe
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`). 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`).
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. 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.
brew install cask $ brew install cabal-install
brew cask install haskell-platform
cabal 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 Verify that `cabal` is installed and update its dependency list with
@ -214,10 +317,6 @@ Verify that `cabal` is installed and update its dependency list with
$ cabal 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. 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`): Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
@ -238,12 +337,15 @@ 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 make sure to use a TrueType font, not a Raster font, and set the active
codepage to UTF-8 (65001) with `chcp`: codepage to UTF-8 (65001) with `chcp`:
> chcp 65001 ```cmd
Active code page: 65001 chcp 65001
```
In Powershell ISE, you may need to additionally update the output encoding: In Powershell ISE, you may need to additionally update the output encoding:
> [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 ```powershell
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
```
### Running tests ### Running tests
@ -270,6 +372,7 @@ echo 'Don't forget to restart!' # Singlequote closed by apostrophe
echo 'Don\'t try this at home' # Attempting to escape ' in '' echo 'Don\'t try this at home' # Attempting to escape ' in ''
echo 'Path is $PATH' # Variables in single quotes echo 'Path is $PATH' # Variables in single quotes
trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap
unset var[i] # Array index treated as glob
``` ```
### Conditionals ### Conditionals
@ -288,6 +391,7 @@ ShellCheck can recognize many types of incorrect test statements.
[ grep -q foo file ] # Command without $(..) [ grep -q foo file ] # Command without $(..)
[[ "$$file" == *.jpg ]] # Comparisons that can't succeed [[ "$$file" == *.jpg ]] # Comparisons that can't succeed
(( 1 -lt 2 )) # Using test operators in ((..)) (( 1 -lt 2 )) # Using test operators in ((..))
[ x ] & [ y ] | [ z ] # Accidental backgrounding and piping
``` ```
### Frequently misused commands ### Frequently misused commands
@ -359,6 +463,8 @@ echo "Hello $name" # Unassigned lowercase variables
cmd | read bar; echo $bar # Assignments in subshells cmd | read bar; echo $bar # Assignments in subshells
cat foo | cp bar # Piping to commands that don't read cat foo | cp bar # Piping to commands that don't read
printf '%s: %s\n' foo # Mismatches in printf argument count printf '%s: %s\n' foo # Mismatches in printf argument count
eval "${array[@]}" # Lost word boundaries in array eval
for i in "${x[@]}"; do ${x[$i]} # Using array value as key
``` ```
### Robustness ### Robustness
@ -383,6 +489,7 @@ ShellCheck will warn when using features not supported by the shebang. For examp
echo {1..$n} # Works in ksh, but not bash/dash/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 {1..10} # Works in ksh and bash, but not dash/sh
echo -n 42 # Works in ksh, bash and dash, undefined in sh echo -n 42 # Works in ksh, bash and dash, undefined in sh
expr match str regex # Unportable alias for `expr str : regex`
trap 'exit 42' sigint # Unportable signal spec trap 'exit 42' sigint # Unportable signal spec
cmd &> file # Unportable redirection operator cmd &> file # Unportable redirection operator
read foo < /dev/tcp/host/22 # Unportable intercepted files read foo < /dev/tcp/host/22 # Unportable intercepted files
@ -403,10 +510,15 @@ rm “file” # Unicode quotes
echo "Hello world" # Carriage return / DOS line endings echo "Hello world" # Carriage return / DOS line endings
echo hello \ # Trailing spaces after \ echo hello \ # Trailing spaces after \
var=42 echo $var # Expansion of inlined environment var=42 echo $var # Expansion of inlined environment
#!/bin/bash -x -e # Common shebang errors !# bin/bash -x -e # Common shebang errors
echo $((n/180*100)) # Unnecessary loss of precision echo $((n/180*100)) # Unnecessary loss of precision
ls *[:digit:].txt # Bad character class globs ls *[:digit:].txt # Bad character class globs
sed 's/foo/bar/' file > file # Redirecting to input sed 's/foo/bar/' file > file # Redirecting to input
var2=$var2 # Variable assigned to itself
[ x$var = xval ] # Antiquated x-comparisons
ls() { ls -l "$@"; } # Infinitely recursive wrapper
alias ls='ls -l'; ls foo # Alias used before it takes effect
for x; do for x; do # Nested loop uses same variable
while getopts "a" f; do case $f in "b") # Unhandled getopts flags while getopts "a" f; do case $f in "b") # Unhandled getopts flags
``` ```
@ -421,13 +533,13 @@ Alexander Tarasikov,
Issues can be ignored via environmental variable, command line, individually or globally within a file: Issues can be ignored via environmental variable, command line, individually or globally within a file:
https://github.com/koalaman/shellcheck/wiki/Ignore <https://github.com/koalaman/shellcheck/wiki/Ignore>
## Reporting bugs ## Reporting bugs
Please use the GitHub issue tracker for any bugs or feature suggestions: 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 ## Contributing
@ -442,11 +554,11 @@ The contributor retains the copyright.
ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE). ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE).
Copyright 2012-2018, Vidar 'koala_man' Holen and contributors. Copyright 2012-2019, [Vidar 'koala_man' Holen](https://github.com/koalaman/) and contributors.
Happy ShellChecking! Happy ShellChecking!
## Other Resources ## 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). * 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)! * 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

@ -1,36 +0,0 @@
import Distribution.PackageDescription (
HookedBuildInfo,
emptyHookedBuildInfo )
import Distribution.Simple (
Args,
UserHooks ( preSDist ),
defaultMainWithHooks,
simpleUserHooks )
import Distribution.Simple.Setup ( SDistFlags )
import System.Process ( system )
main = defaultMainWithHooks myHooks
where
myHooks = simpleUserHooks { preSDist = myPreSDist }
-- | This hook will be executed before e.g. @cabal sdist@. It runs
-- pandoc to create the man page from shellcheck.1.md. If the pandoc
-- command is not found, this will fail with an error message:
--
-- /bin/sh: pandoc: command not found
--
-- Since the man page is listed in the Extra-Source-Files section of
-- our cabal file, a failure here should result in a failure to
-- create the distribution tarball (that's a good thing).
--
myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo
myPreSDist _ _ = do
putStrLn "Building the man page (shellcheck.1) with pandoc..."
putStrLn pandoc_cmd
result <- system pandoc_cmd
putStrLn $ "pandoc exited with " ++ show result
return emptyHookedBuildInfo
where
pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"

17
build/README.md Normal file
View file

@ -0,0 +1,17 @@
This directory contains Dockerfiles for all builds.
A build image will:
* Run on Linux x86\_64 with vanilla Docker (no exceptions)
* Not contain any software that would restrict easy modification or copying
* Take a `cabal sdist` style tar.gz of the ShellCheck directory on stdin
* Output a tar.gz of artifacts on stdout, in a directory named for the arch
This makes it simple to build any release without exotic hardware or software.
An image can be built and tagged using `build_builder`,
and run on a source tarball using `run_builder`.
Tip: Are you developing an image that relies on QEmu usermode emulation?
It's easy to accidentally depend on binfmt\_misc on the host OS.
Do a `echo 0 | sudo tee /proc/sys/fs/binfmt_misc/status` before testing.

12
build/build_builder Executable file
View file

@ -0,0 +1,12 @@
#!/bin/sh
if [ $# -eq 0 ]
then
echo >&2 "No build image directories specified"
echo >&2 "Example: $0 build/*/"
exit 1
fi
for dir
do
( cd "$dir" && docker build -t "$(cat tag)" . ) || exit 1
done

View file

@ -0,0 +1,40 @@
FROM ghcr.io/shepherdjerred/macos-cross-compiler:latest
ENV TARGET aarch64-apple-darwin22
ENV TARGETNAME darwin.aarch64
# Build dependencies
USER root
ENV DEBIAN_FRONTEND noninteractive
ENV LC_ALL C.utf8
# Install basic deps
RUN apt-get update && apt-get install -y automake autoconf build-essential curl xz-utils qemu-user-static
# Install a more suitable host compiler
WORKDIR /host-ghc
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin
RUN curl -L 'https://downloads.haskell.org/~ghc/8.10.7/ghc-8.10.7-x86_64-deb10-linux.tar.xz' | tar xJ --strip-components=1
RUN ./configure && make install
# Build GHC. We have to use an old version because cross-compilation across OS has since broken.
WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.7/ghc-8.10.7-src.tar.xz" | tar xJ --strip-components=1
RUN apt-get install -y llvm-12
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
RUN make install
# Due to an apparent cabal bug, we specify our options directly to cabal
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
ENV CABALOPTS "--ghc-options;-optc-Os -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;--constraint=hashable==1.3.5.0"
# Prebuild the dependencies
RUN cabal update
RUN IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

16
build/darwin.aarch64/build Executable file
View file

@ -0,0 +1,16 @@
#!/bin/sh
set -xe
{
tar xzv --strip-components=1
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
( IFS=';'; cabal build $CABALOPTS )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
# Stripping invalidates the code signature and the build image does
# not appear to have anything similar to the 'codesign' tool.
# "$TARGET-strip" "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
file "$TARGETNAME/shellcheck" | grep "Mach-O 64-bit arm64 executable"
} >&2
tar czv "$TARGETNAME"

1
build/darwin.aarch64/tag Normal file
View file

@ -0,0 +1 @@
koalaman/scbuilder-darwin-aarch64

View file

@ -0,0 +1,33 @@
FROM liushuyu/osxcross@sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76
ENV TARGET x86_64-apple-darwin18
ENV TARGETNAME darwin.x86_64
# Build dependencies
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list
RUN apt-get update
RUN apt-get dist-upgrade -y
RUN apt-get install -y ghc automake autoconf llvm curl alex happy
# Build GHC
WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/9.2.5/ghc-9.2.5-src.tar.xz" | tar xJ --strip-components=1
RUN ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
RUN make install
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin
# Due to an apparent cabal bug, we specify our options directly to cabal
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
ENV CABALOPTS "--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
# Prebuild the dependencies
RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

13
build/darwin.x86_64/build Executable file
View file

@ -0,0 +1,13 @@
#!/bin/sh
set -xe
{
tar xzv --strip-components=1
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
( IFS=';'; cabal build $CABALOPTS )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
"$TARGET-strip" -Sx "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
} >&2
tar czv "$TARGETNAME"

1
build/darwin.x86_64/tag Normal file
View file

@ -0,0 +1 @@
koalaman/scbuilder-darwin-x86_64

View file

@ -0,0 +1,40 @@
FROM ubuntu:20.04
ENV TARGET aarch64-linux-gnu
ENV TARGETNAME linux.aarch64
# Build dependencies
USER root
ENV DEBIAN_FRONTEND noninteractive
# These deps are from 20.04, because GHC's compiler/llvm support moves slowly
RUN apt-get update && apt-get install -y llvm gcc-$TARGET
# The rest are from 22.10
RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list
# Kinetic does not receive updates anymore, switch to last available
RUN sed -e 's/archive.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list
RUN sed -e 's/security.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list
RUN apt-get update && apt-get install -y ghc alex happy automake autoconf build-essential curl qemu-user-static
# Build GHC
WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/9.2.8/ghc-9.2.8-src.tar.xz" | tar xJ --strip-components=1
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
RUN make install
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin
# Due to an apparent cabal bug, we specify our options directly to cabal
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;-c;hashable -arch-native"
# Prebuild the dependencies
RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

14
build/linux.aarch64/build Executable file
View file

@ -0,0 +1,14 @@
#!/bin/sh
set -xe
{
tar xzv --strip-components=1
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
"$TARGET-strip" -s "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
qemu-aarch64-static "$TARGETNAME/shellcheck" --version
} >&2
tar czv "$TARGETNAME"

1
build/linux.aarch64/tag Normal file
View file

@ -0,0 +1 @@
koalaman/scbuilder-linux-aarch64

View file

@ -0,0 +1,42 @@
# This Docker file uses a custom QEmu fork with patches to follow execve
# to build all of ShellCheck emulated.
FROM ubuntu:24.04
ENV TARGETNAME linux.armv6hf
# Build QEmu with execve follow support
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
RUN apt-get install -y --no-install-recommends build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev python3-setuptools ca-certificates debootstrap
WORKDIR /qemu
RUN git clone --depth 1 https://github.com/koalaman/qemu .
RUN ./configure --static --disable-werror && cd build && ninja qemu-arm
ENV QEMU_EXECVE 1
# Convenience utility
COPY scutil /bin/scutil
COPY scutil /chroot/bin/scutil
RUN chmod +x /bin/scutil /chroot/bin/scutil
# Set up an armv6 userspace
WORKDIR /
RUN debootstrap --arch armhf --variant=minbase --foreign bookworm /chroot http://mirrordirector.raspbian.org/raspbian
RUN cp /qemu/build/qemu-arm /chroot/bin/qemu
RUN scutil emu /debootstrap/debootstrap --second-stage
# Install deps in the chroot
RUN scutil emu apt-get update
RUN scutil emu apt-get install -y --no-install-recommends ghc cabal-install
RUN scutil emu cabal update
# Finally we can build the current dependencies. This takes hours.
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--gcc-options;-Os -Wl,--gc-sections -ffunction-sections -fdata-sections"
# Generated with `cabal freeze --constraint 'hashable -arch-native'`
COPY cabal.project.freeze /chroot/etc
RUN IFS=";" && scutil install_from_freeze /chroot/etc/cabal.project.freeze emu cabal install $CABALOPTS
# Copy the build script
COPY build /chroot/bin
ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"]

17
build/linux.armv6hf/build Executable file
View file

@ -0,0 +1,17 @@
#!/bin/sh
set -xe
mkdir /scratch && cd /scratch
{
tar xzv --strip-components=1
cp /etc/cabal.project.freeze .
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
# This script does not cabal update because compiling anything new is slow
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
strip -s "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
"$TARGETNAME/shellcheck" --version
} >&2
tar czv "$TARGETNAME"

View file

@ -0,0 +1,93 @@
active-repositories: hackage.haskell.org:merge
constraints: any.Diff ==0.5,
any.OneTuple ==0.4.2,
any.QuickCheck ==2.14.3,
QuickCheck -old-random +templatehaskell,
any.StateVar ==1.2.2,
any.aeson ==2.2.3.0,
aeson +ordered-keymap,
any.array ==0.5.4.0,
any.assoc ==1.1.1,
assoc -tagged,
any.base ==4.15.1.0,
any.base-orphans ==0.9.2,
any.bifunctors ==5.6.2,
bifunctors +tagged,
any.binary ==0.8.8.0,
any.bytestring ==0.10.12.1,
any.character-ps ==0.1,
any.comonad ==5.0.8,
comonad +containers +distributive +indexed-traversable,
any.containers ==0.6.4.1,
any.contravariant ==1.5.5,
contravariant +semigroups +statevar +tagged,
any.data-array-byte ==0.1.0.1,
any.data-fix ==0.3.3,
any.deepseq ==1.4.5.0,
any.directory ==1.3.6.2,
any.distributive ==0.6.2.1,
distributive +semigroups +tagged,
any.dlist ==1.0,
dlist -werror,
any.exceptions ==0.10.4,
any.fgl ==5.8.2.0,
fgl +containers042,
any.filepath ==1.4.2.1,
any.foldable1-classes-compat ==0.1,
foldable1-classes-compat +tagged,
any.generically ==0.1.1,
any.ghc-bignum ==1.1,
any.ghc-boot-th ==9.0.2,
any.ghc-prim ==0.7.0,
any.hashable ==1.4.6.0,
hashable -arch-native +integer-gmp -random-initial-seed,
any.indexed-traversable ==0.1.4,
any.indexed-traversable-instances ==0.1.2,
any.integer-conversion ==0.1.1,
any.integer-logarithms ==1.0.3.1,
integer-logarithms -check-bounds +integer-gmp,
any.mtl ==2.2.2,
any.network-uri ==2.6.4.2,
any.parsec ==3.1.14.0,
any.pretty ==1.1.3.6,
any.primitive ==0.9.0.0,
any.process ==1.6.13.2,
any.random ==1.2.1.2,
any.regex-base ==0.94.0.2,
any.regex-tdfa ==1.3.2.2,
regex-tdfa +doctest -force-o2,
any.rts ==1.0.2,
any.scientific ==0.3.8.0,
scientific -integer-simple,
any.semialign ==1.3.1,
semialign +semigroupoids,
any.semigroupoids ==6.0.1,
semigroupoids +comonad +containers +contravariant +distributive +tagged +unordered-containers,
any.splitmix ==0.1.0.5,
splitmix -optimised-mixer,
any.stm ==2.5.0.0,
any.strict ==0.5,
any.tagged ==0.8.8,
tagged +deepseq +transformers,
any.template-haskell ==2.17.0.0,
any.text ==1.2.5.0,
any.text-iso8601 ==0.1.1,
any.text-short ==0.1.6,
text-short -asserts,
any.th-abstraction ==0.7.0.0,
any.th-compat ==0.1.5,
any.these ==1.2.1,
any.time ==1.9.3,
any.time-compat ==1.9.7,
any.transformers ==0.5.6.2,
any.transformers-compat ==0.7.2,
transformers-compat -five +five-three -four +generic-deriving +mtl -three -two,
any.unix ==2.7.2.2,
any.unordered-containers ==0.2.20,
unordered-containers -debug,
any.uuid-types ==1.0.6,
any.vector ==0.13.1.0,
vector +boundschecks -internalchecks -unsafechecks -wall,
any.vector-stream ==0.1.0.1,
any.witherable ==0.5
index-state: hackage.haskell.org 2024-06-18T02:21:19Z

View file

@ -0,0 +1,48 @@
#!/bin/dash
# Various ShellCheck build utility functions
# Generally set a ulimit to avoid QEmu using too much memory
ulimit -v "$((10*1024*1024))"
# If we happen to invoke or run under QEmu, make sure to follow execve.
# This requires a patched QEmu.
export QEMU_EXECVE=1
# Retry a command until it succeeds
# Usage: scutil retry 3 mycmd
retry() {
n="$1"
ret=1
shift
while [ "$n" -gt 0 ]
do
"$@"
ret=$?
[ "$ret" = 0 ] && break
n=$((n-1))
done
return "$ret"
}
# Install all dependencies from a freeze file
# Usage: scutil install_from_freeze /path/cabal.project.freeze cabal install
install_from_freeze() {
linefeed=$(printf '\nx')
linefeed=${linefeed%x}
flags=$(
sed 's/constraints:/&\n /' "$1" |
grep -vw -e rts -e base |
sed -n -e 's/^ *\([^,]*\).*/\1/p' |
sed -e 's/any\.\([^ ]*\) ==\(.*\)/\1-\2/; te; s/.*/--constraint\n&/; :e')
shift
# shellcheck disable=SC2086
( IFS=$linefeed; set -x; "$@" $flags )
}
# Run a command under emulation.
# This assumes the correct emulator is named 'qemu' and the chroot is /chroot
# Usage: scutil emu echo "Hello World"
emu() {
chroot /chroot /bin/qemu /usr/bin/env "$@"
}
"$@"

1
build/linux.armv6hf/tag Normal file
View file

@ -0,0 +1 @@
koalaman/scbuilder-linux-armv6hf

View file

@ -0,0 +1,46 @@
FROM ubuntu:24.04
ENV TARGETNAME linux.riscv64
ENV TARGET riscv64-linux-gnu
USER root
ENV DEBIAN_FRONTEND noninteractive
# Init base
RUN apt-get update -y
# Install qemu
RUN apt-get install -y --no-install-recommends build-essential ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev curl ca-certificates python3-virtualenv git python3-setuptools debootstrap
WORKDIR /qemu
RUN git clone --depth 1 https://github.com/koalaman/qemu .
RUN ./configure --target-list=riscv64-linux-user --static --disable-system --disable-pie --disable-werror
RUN cd build && ninja qemu-riscv64
ENV QEMU_EXECVE 1
# Convenience utility
COPY scutil /bin/scutil
# We have to copy to /usr/bin because debootstrap will try to symlink /bin and fail if it exists
COPY scutil /chroot/usr/bin/scutil
RUN chmod +x /bin/scutil /chroot/usr/bin/scutil
# Set up a riscv64 userspace
WORKDIR /
RUN debootstrap --arch=riscv64 --variant=minbase --components=main,universe --foreign noble /chroot http://ports.ubuntu.com/ubuntu-ports
RUN cp /qemu/build/qemu-riscv64 /chroot/bin/qemu
RUN scutil emu /debootstrap/debootstrap --second-stage
# Install deps in the chroot
RUN scutil emu apt-get update
RUN scutil emu apt-get install -y --no-install-recommends ghc cabal-install
RUN scutil emu cabal update
# Generated with: cabal freeze -c 'hashable -arch-native'. We put it in /etc so cabal won't find it.
COPY cabal.project.freeze /chroot/etc
# Build all dependencies from the freeze file. The emulator segfaults at random,
# so retry a few times.
RUN scutil install_from_freeze /chroot/etc/cabal.project.freeze retry 5 emu cabal install --keep-going
# Copy the build script
COPY build /chroot/bin/build
ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"]

21
build/linux.riscv64/build Executable file
View file

@ -0,0 +1,21 @@
#!/bin/sh
set -xe
IFS=';'
{
mkdir -p /tmp/scratch
cd /tmp/scratch
tar xzv --strip-components=1
chmod +x striptests && ./striptests
# Use a freeze file to ensure we use the same dependencies we cached during
# the docker image build. We don't want to spend time compiling anything new.
cp /etc/cabal.project.freeze .
mkdir "$TARGETNAME"
# Retry in case of random segfault
scutil retry 3 cabal build --enable-executable-static
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
"$TARGET-strip" -s "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
"$TARGETNAME/shellcheck" --version
} >&2
tar czv "$TARGETNAME"

View file

@ -0,0 +1,93 @@
active-repositories: hackage.haskell.org:merge
constraints: any.Diff ==0.5,
any.OneTuple ==0.4.2,
any.QuickCheck ==2.14.3,
QuickCheck -old-random +templatehaskell,
any.StateVar ==1.2.2,
any.aeson ==2.2.3.0,
aeson +ordered-keymap,
any.array ==0.5.4.0,
any.assoc ==1.1.1,
assoc -tagged,
any.base ==4.17.2.0,
any.base-orphans ==0.9.2,
any.bifunctors ==5.6.2,
bifunctors +tagged,
any.binary ==0.8.9.1,
any.bytestring ==0.11.5.2,
any.character-ps ==0.1,
any.comonad ==5.0.8,
comonad +containers +distributive +indexed-traversable,
any.containers ==0.6.7,
any.contravariant ==1.5.5,
contravariant +semigroups +statevar +tagged,
any.data-fix ==0.3.3,
any.deepseq ==1.4.8.0,
any.directory ==1.3.7.1,
any.distributive ==0.6.2.1,
distributive +semigroups +tagged,
any.dlist ==1.0,
dlist -werror,
any.exceptions ==0.10.5,
any.fgl ==5.8.2.0,
fgl +containers042,
any.filepath ==1.4.2.2,
any.foldable1-classes-compat ==0.1,
foldable1-classes-compat +tagged,
any.generically ==0.1.1,
any.ghc-bignum ==1.3,
any.ghc-boot-th ==9.4.7,
any.ghc-prim ==0.9.1,
any.hashable ==1.4.6.0,
hashable -arch-native +integer-gmp -random-initial-seed,
any.indexed-traversable ==0.1.4,
any.indexed-traversable-instances ==0.1.2,
any.integer-conversion ==0.1.1,
any.integer-logarithms ==1.0.3.1,
integer-logarithms -check-bounds +integer-gmp,
any.mtl ==2.2.2,
any.network-uri ==2.6.4.2,
any.os-string ==2.0.3,
any.parsec ==3.1.16.1,
any.pretty ==1.1.3.6,
any.primitive ==0.9.0.0,
any.process ==1.6.17.0,
any.random ==1.2.1.2,
any.regex-base ==0.94.0.2,
any.regex-tdfa ==1.3.2.2,
regex-tdfa +doctest -force-o2,
any.rts ==1.0.2,
any.scientific ==0.3.8.0,
scientific -integer-simple,
any.semialign ==1.3.1,
semialign +semigroupoids,
any.semigroupoids ==6.0.1,
semigroupoids +comonad +containers +contravariant +distributive +tagged +unordered-containers,
any.splitmix ==0.1.0.5,
splitmix -optimised-mixer,
any.stm ==2.5.1.0,
any.strict ==0.5,
any.tagged ==0.8.8,
tagged +deepseq +transformers,
any.template-haskell ==2.19.0.0,
any.text ==2.0.2,
any.text-iso8601 ==0.1.1,
any.text-short ==0.1.6,
text-short -asserts,
any.th-abstraction ==0.7.0.0,
any.th-compat ==0.1.5,
any.these ==1.2.1,
any.time ==1.12.2,
any.time-compat ==1.9.7,
any.transformers ==0.5.6.2,
any.transformers-compat ==0.7.2,
transformers-compat -five +five-three -four +generic-deriving +mtl -three -two,
any.unix ==2.7.3,
any.unordered-containers ==0.2.20,
unordered-containers -debug,
any.uuid-types ==1.0.6,
any.vector ==0.13.1.0,
vector +boundschecks -internalchecks -unsafechecks -wall,
any.vector-stream ==0.1.0.1,
any.witherable ==0.5
index-state: hackage.haskell.org 2024-06-17T00:48:51Z

1
build/linux.riscv64/tag Normal file
View file

@ -0,0 +1 @@
koalaman/scbuilder-linux-riscv64

View file

@ -0,0 +1,24 @@
FROM alpine:3.16
# alpine:3.16 (GHC 9.0.1): 5.8 megabytes
# alpine:3.17 (GHC 9.0.2): 15.0 megabytes
# alpine:3.18 (GHC 9.4.4): 29.0 megabytes
# alpine:3.19 (GHC 9.4.7): 29.0 megabytes
ENV TARGETNAME linux.x86_64
# Install GHC and cabal
USER root
RUN apk add ghc cabal g++ libffi-dev curl bash
# Use ld.bfd instead of ld.gold due to
# x86_64-linux-gnu/libpthread.a(pthread_cond_init.o)(.note.stapsdt+0x14): error:
# relocation refers to local symbol "" [2], which is defined in a discarded section
ENV CABALOPTS "--ghc-options;-optl-Wl,-fuse-ld=bfd -split-sections -optc-Os -optc-Wl,--gc-sections"
# Other archs pre-build dependencies here, but this one doesn't to detect ecosystem movement
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

15
build/linux.x86_64/build Executable file
View file

@ -0,0 +1,15 @@
#!/bin/sh
set -xe
{
tar xzv --strip-components=1
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
strip -s "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
"$TARGETNAME/shellcheck" --version
} >&2
tar czv "$TARGETNAME"

1
build/linux.x86_64/tag Normal file
View file

@ -0,0 +1 @@
koalaman/scbuilder-linux-x86_64

30
build/run_builder Executable file
View file

@ -0,0 +1,30 @@
#!/bin/bash
if [ $# -lt 2 ]
then
echo >&2 "This script builds a source archive (as produced by cabal sdist)"
echo >&2 "Usage: $0 sourcefile.tar.gz builddir..."
exit 1
fi
file=$(realpath "$1")
shift
if [ ! -e "$file" ]
then
echo >&2 "$file does not exist"
exit 1
fi
set -ex -o pipefail
for dir
do
tagfile="$dir/tag"
if [ ! -e "$tagfile" ]
then
echo >&2 "$tagfile does not exist"
exit 2
fi
docker run -i "$(< "$tagfile")" < "$file" | tar xz
done

View file

@ -0,0 +1,27 @@
FROM ubuntu:20.04
ENV TARGETNAME windows.x86_64
# We don't need wine32, even though it complains
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y curl busybox wine winbind
# Fetch Windows version, will be available under z:\haskell
WORKDIR /haskell
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-x86_64-unknown-mingw32.tar.xz" | tar xJ --strip-components=1
WORKDIR /haskell/bin
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-mingw32.zip" | busybox unzip -
RUN curl -L "https://curl.se/windows/dl-8.7.1_7/curl-8.7.1_7-win64-mingw.zip" | busybox unzip - && mv curl-*-win64-mingw/bin/* .
ENV WINEPATH /haskell/bin
# It's unknown whether Cabal on Windows suffers from the same issue
# that necessitated this but I don't care enough to find out
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections"
# Precompile some deps to speed up later builds
RUN wine /haskell/bin/cabal.exe update && IFS=';' && wine /haskell/bin/cabal.exe install --lib --dependencies-only $CABALOPTS ShellCheck
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

18
build/windows.x86_64/build Executable file
View file

@ -0,0 +1,18 @@
#!/bin/sh
cabal() {
wine /haskell/bin/cabal.exe "$@"
}
set -xe
{
tar xzv --strip-components=1
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
( IFS=';'; cabal build $CABALOPTS )
find dist*/ -name shellcheck.exe -type f -ls -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
wine "/haskell/mingw/bin/strip.exe" -s "$TARGETNAME/shellcheck.exe"
ls -l "$TARGETNAME"
wine "$TARGETNAME/shellcheck.exe" --version
} >&2
tar czv "$TARGETNAME"

1
build/windows.x86_64/tag Normal file
View file

@ -0,0 +1 @@
koalaman/scbuilder-windows-x86_64

294
doc/shellcheck_logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 244 KiB

4
manpage Executable file
View file

@ -0,0 +1,4 @@
#!/bin/sh
echo >&2 "Generating man page using pandoc"
pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1 || exit
echo >&2 "Done. You can read it with: man ./shellcheck.1"

View file

@ -6,8 +6,8 @@ then
exit 1 exit 1
fi fi
for i in 1 2 for i in 1 2 3
do do
last=$(grep -hv "^prop" ./**/*.hs | grep -Ewo "$i[0-9]{3}" | sort -n | tail -n 1) last=$(grep -hv "^prop" ./**/*.hs | grep -Ewo "${i}[0-9]{3}" | sort -n | tail -n 1)
echo "Next ${i}xxx: $((last+1))" echo "Next ${i}xxx: $((last+1))"
done done

View file

@ -2,4 +2,12 @@
# quickrun runs ShellCheck in an interpreted mode. # quickrun runs ShellCheck in an interpreted mode.
# This allows testing changes without recompiling. # This allows testing changes without recompiling.
runghc -isrc -idist/build/autogen shellcheck.hs "$@" path=$(find . -type f -path './dist*/Paths_ShellCheck.hs' | sort | head -n 1)
if [ -z "$path" ]
then
echo >&2 "Unable to find Paths_ShellCheck.hs. Please 'cabal build' once."
exit 1
fi
path="${path%/*}"
exec runghc -isrc -i"$path" shellcheck.hs "$@"

View file

@ -3,16 +3,18 @@
# This allows running tests without compiling, which can be faster. # This allows running tests without compiling, which can be faster.
# 'cabal test' remains the source of truth. # 'cabal test' remains the source of truth.
path=$(find . -type f -path './dist*/Paths_ShellCheck.hs' | sort | head -n 1)
if [ -z "$path" ]
then
echo >&2 "Unable to find Paths_ShellCheck.hs. Please 'cabal build' once."
exit 1
fi
path="${path%/*}"
( (
var=$(echo 'liftM and $ sequence [ var=$(echo 'main' | ghci -isrc -i"$path" test/shellcheck.hs 2>&1 | tee /dev/stderr)
ShellCheck.Analytics.runTests if [[ $var == *ExitSuccess* ]]
,ShellCheck.Parser.runTests
,ShellCheck.Checker.runTests
,ShellCheck.Checks.Commands.runTests
,ShellCheck.Checks.ShellSupport.runTests
,ShellCheck.AnalyzerLib.runTests
]' | tr -d '\n' | cabal repl 2>&1 | tee /dev/stderr)
if [[ $var == *$'\nTrue'* ]]
then then
exit 0 exit 0
else else

11
setgitversion Executable file
View file

@ -0,0 +1,11 @@
#!/bin/sh -xe
# This script hardcodes the `git describe` version as ShellCheck's version number.
# This is done to allow shellcheck --version to differ from the cabal version when
# building git snapshots.
file="src/ShellCheck/Data.hs"
test -e "$file"
tmp=$(mktemp)
version=$(git describe)
sed -e "s/=.*VERSIONSTRING.*/= \"$version\" -- VERSIONSTRING, DO NOT SUBMIT/" "$file" > "$tmp"
mv "$tmp" "$file"

View file

@ -29,14 +29,13 @@ will warn that decimals are not supported.
+ For scripts starting with `#!/bin/ksh` (or using `-s ksh`), ShellCheck will + For scripts starting with `#!/bin/ksh` (or using `-s ksh`), ShellCheck will
not warn at all, as `ksh` supports decimals in arithmetic contexts. not warn at all, as `ksh` supports decimals in arithmetic contexts.
# OPTIONS # OPTIONS
**-a**,\ **--check-sourced** **-a**,\ **--check-sourced**
: Emit warnings in sourced files. Normally, `shellcheck` will only warn : Emit warnings in sourced files. Normally, `shellcheck` will only warn
about issues in the specified files. With this option, any issues in about issues in the specified files. With this option, any issues in
sourced files files will also be reported. sourced files will also be reported.
**-C**[*WHEN*],\ **--color**[=*WHEN*] **-C**[*WHEN*],\ **--color**[=*WHEN*]
@ -44,28 +43,71 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
is *auto*. **--color** without an argument is equivalent to is *auto*. **--color** without an argument is equivalent to
**--color=always**. **--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*...] **-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...]
: Explicitly exclude the specified codes from the report. Subsequent **-e** : Explicitly exclude the specified codes from the report. Subsequent **-e**
options are cumulative, but all the codes can be specified at once, options are cumulative, but all the codes can be specified at once,
comma-separated as a single argument. comma-separated as a single argument.
**--extended-analysis=true/false**
: Enable/disable Dataflow Analysis to identify more issues (default true). If
ShellCheck uses too much CPU/RAM when checking scripts with several
thousand lines of code, extended analysis can be disabled with this flag
or a directive. This flag overrides directives and rc files.
**-f** *FORMAT*, **--format=***FORMAT* **-f** *FORMAT*, **--format=***FORMAT*
: Specify the output format of shellcheck, which prints its results in the : Specify the output format of shellcheck, which prints its results in the
standard output. Subsequent **-f** options are ignored, see **FORMATS** standard output. Subsequent **-f** options are ignored, see **FORMATS**
below for more information. below for more information.
**-S**\ *SEVERITY*,\ **--severity=***severity* **--list-optional**
: Specify minimum severity of errors to consider. Valid values are *error*, : Output a list of known optional checks. These can be enabled with **-o**
*warning*, *info* and *style*. The default is *style*. flags or **enable** directives.
**--norc**
: Don't try to look for .shellcheckrc configuration files.
**--rcfile** *RCFILE*
: Prefer the specified configuration file over searching for one
in the default locations.
**-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* **-s**\ *shell*,\ **--shell=***shell*
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*. : Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash*, *ksh*,
The default is to use the file's shebang, or *bash* if the target shell and *busybox*.
can't be determined. 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*.
**-V**,\ **--version** **-V**,\ **--version**
@ -78,11 +120,18 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
**-x**,\ **--external-sources** **-x**,\ **--external-sources**
: Follow 'source' statements even when the file is not specified as input. : Follow `source` statements even when the file is not specified as input.
By default, `shellcheck` will only follow files specified on the command By default, `shellcheck` will only follow files specified on the command
line (plus `/dev/null`). This option allows following any file the script line (plus `/dev/null`). This option allows following any file the script
may `source`. may `source`.
This option may also be enabled using `external-sources=true` in
`.shellcheckrc`. This flag takes precedence.
**FILES...**
: One or more script files to check, or "-" for standard input.
# FORMATS # FORMATS
@ -119,13 +168,31 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
... ...
</checkstyle> </checkstyle>
**json** **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 is a popular serialization format that is more suitable for web : Json is a popular serialization format that is more suitable for web
applications. ShellCheck's json is compact and contains only the bare applications. ShellCheck's json is compact and contains only the bare
minimum. minimum. Tabs are counted as 1 character.
[ {
comments: [
{ {
"file": "filename", "file": "filename",
"line": lineNumber, "line": lineNumber,
@ -136,10 +203,24 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
}, },
... ...
] ]
}
**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.
# DIRECTIVES # DIRECTIVES
ShellCheck directives can be specified as comments in the shell script
before a command or block: 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 key=value key=value # shellcheck key=value key=value
command-or-structure command-or-structure
@ -167,19 +248,88 @@ Valid keys are:
**disable** **disable**
: Disables a comma separated list of error codes for the following command. : Disables a comma separated list of error codes for the following command.
The command can be a simple command like `echo foo`, or a compound command The command can be a simple command like `echo foo`, or a compound command
like a function definition, subshell block or loop. like a function definition, subshell block or loop. A range can be
be specified with a dash, e.g. `disable=SC3000-SC4000` to exclude 3xxx.
All warnings can be disabled with `disable=all`.
**enable**
: Enable an optional check by name, as listed with **--list-optional**.
Only file-wide `enable` directives are considered.
**extended-analysis**
: Set to true/false to enable/disable dataflow analysis. Specifying
`# shellcheck extended-analysis=false` in particularly large (2000+ line)
auto-generated scripts will reduce ShellCheck's resource usage at the
expense of certain checks. Extended analysis is enabled by default.
**external-sources**
: Set to `true` in `.shellcheckrc` to always allow ShellCheck to open
arbitrary files from 'source' statements (the way most tools do).
This option defaults to `false` only due to ShellCheck's origin as a
remote service for checking untrusted scripts. It can safely be enabled
for normal development.
**source** **source**
: Overrides the filename included by a `source`/`.` statement. This can be : 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 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`. 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** **shell**
: Overrides the shell detected from the shebang. This is useful for : Overrides the shell detected from the shebang. This is useful for
files meant to be included (and thus lacking a shebang), or possibly files meant to be included (and thus lacking a shebang), or possibly
as a more targeted alternative to 'disable=2039'. as a more targeted alternative to 'disable=SC2039'.
# 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
# Since 0.9.0, values can be quoted with '' or "" to allow spaces
source-path="My Documents/scripts"
# Allow opening any 'source'd file, even if not specified as input
external-sources=true
# 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 [ ! -z foo ] instead of suggesting -n
disable=SC2236
If no `.shellcheckrc` is found in any of the parent directories, ShellCheck
will look in `~/.shellcheckrc` followed by the `$XDG_CONFIG_HOME`
(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 # ENVIRONMENT VARIABLES
The environment variable `SHELLCHECK_OPTS` can be set with default flags: The environment variable `SHELLCHECK_OPTS` can be set with default flags:
export SHELLCHECK_OPTS='--shell=bash --exclude=SC2016' export SHELLCHECK_OPTS='--shell=bash --exclude=SC2016'
@ -189,7 +339,7 @@ invocation.
# RETURN VALUES # RETURN VALUES
ShellCheck uses the follow exit codes: ShellCheck uses the following exit codes:
+ 0: All files successfully scanned with no issues. + 0: All files successfully scanned with no issues.
+ 1: All files successfully scanned with some issues. + 1: All files successfully scanned with some issues.
@ -198,6 +348,7 @@ ShellCheck uses the follow exit codes:
+ 4: ShellCheck was invoked with bad options (e.g. unknown formatter). + 4: ShellCheck was invoked with bad options (e.g. unknown formatter).
# LOCALE # LOCALE
This version of ShellCheck is only available in English. All files are 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 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 sequences. `LC_CTYPE` is respected for output, and defaults to UTF-8 for
@ -206,20 +357,50 @@ locales where encoding is unspecified (such as the `C` locale).
Windows users seeing `commitBuffer: invalid argument (invalid character)` Windows users seeing `commitBuffer: invalid argument (invalid character)`
should set their terminal to use UTF-8 with `chcp 65001`. should set their terminal to use UTF-8 with `chcp 65001`.
# AUTHOR # KNOWN INCOMPATIBILITIES
ShellCheck is written and maintained by Vidar Holen.
(If nothing in this section makes sense, you are unlikely to be affected by it)
To avoid confusing and misguided suggestions, ShellCheck requires function
bodies to be either `{ brace groups; }` or `( subshells )`, and function names
containing `[]*=!` are only recognized after a `function` keyword.
The following unconventional function definitions are identical in Bash,
but ShellCheck only recognizes the latter.
[x!=y] () [[ $1 ]]
function [x!=y] () { [[ $1 ]]; }
Shells without the `function` keyword do not allow these characters in function
names to begin with. Function names containing `{}` are not supported at all.
Further, if ShellCheck sees `[x!=y]` it will assume this is an invalid
comparison. To invoke the above function, quote the command as in `'[x!=y]'`,
or to retain the same globbing behavior, use `command [x!=y]`.
ShellCheck imposes additional restrictions on the `[` command to help diagnose
common invalid uses. While `[ $x= 1 ]` is defined in POSIX, ShellCheck will
assume it was intended as the much more likely comparison `[ "$x" = 1 ]` and
fail accordingly. For unconventional or dynamic uses of the `[` command, use
`test` or `\[` instead.
# REPORTING BUGS # REPORTING BUGS
Bugs and issues can be reported on GitHub: Bugs and issues can be reported on GitHub:
https://github.com/koalaman/shellcheck/issues https://github.com/koalaman/shellcheck/issues
# AUTHORS
ShellCheck is developed and maintained by Vidar Holen, with assistance from a
long list of wonderful contributors.
# COPYRIGHT # COPYRIGHT
Copyright 2012-2015, Vidar Holen.
Copyright 2012-2024, Vidar Holen and contributors.
Licensed under the GNU General Public License version 3 or later, Licensed under the GNU General Public License version 3 or later,
see https://gnu.org/licenses/gpl.html see https://gnu.org/licenses/gpl.html
# SEE ALSO # SEE ALSO
sh(1) bash(1) sh(1) bash(1) dash(1) ksh(1)

View file

@ -1,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@ -17,6 +17,7 @@
You should have received a copy of the GNU General Public License 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 <https://www.gnu.org/licenses/>.
-} -}
import qualified ShellCheck.Analyzer
import ShellCheck.Checker import ShellCheck.Checker
import ShellCheck.Data import ShellCheck.Data
import ShellCheck.Interface import ShellCheck.Interface
@ -24,12 +25,17 @@ import ShellCheck.Regex
import qualified ShellCheck.Formatter.CheckStyle import qualified ShellCheck.Formatter.CheckStyle
import ShellCheck.Formatter.Format import ShellCheck.Formatter.Format
import qualified ShellCheck.Formatter.Diff
import qualified ShellCheck.Formatter.GCC import qualified ShellCheck.Formatter.GCC
import qualified ShellCheck.Formatter.JSON import qualified ShellCheck.Formatter.JSON
import qualified ShellCheck.Formatter.JSON1
import qualified ShellCheck.Formatter.TTY import qualified ShellCheck.Formatter.TTY
import qualified ShellCheck.Formatter.Quiet
import Control.Exception import Control.Exception
import Control.Monad import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Control.Monad.Except import Control.Monad.Except
import Data.Bits import Data.Bits
import Data.Char import Data.Char
@ -46,6 +52,7 @@ import System.Console.GetOpt
import System.Directory import System.Directory
import System.Environment import System.Environment
import System.Exit import System.Exit
import System.FilePath
import System.IO import System.IO
data Flag = Flag String String data Flag = Flag String String
@ -67,17 +74,21 @@ instance Monoid Status where
data Options = Options { data Options = Options {
checkSpec :: CheckSpec, checkSpec :: CheckSpec,
externalSources :: Bool, externalSources :: Bool,
sourcePaths :: [FilePath],
formatterOptions :: FormatterOptions, formatterOptions :: FormatterOptions,
minSeverity :: Severity minSeverity :: Severity,
rcfile :: Maybe FilePath
} }
defaultOptions = Options { defaultOptions = Options {
checkSpec = emptyCheckSpec, checkSpec = emptyCheckSpec,
externalSources = False, externalSources = False,
sourcePaths = [],
formatterOptions = newFormatterOptions { formatterOptions = newFormatterOptions {
foColorOption = ColorAuto foColorOption = ColorAuto
}, },
minSeverity = StyleC minSeverity = StyleC,
rcfile = Nothing
} }
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..." usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
@ -87,14 +98,31 @@ options = [
Option "C" ["color"] Option "C" ["color"]
(OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN") (OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN")
"Use color (auto, always, never)", "Use color (auto, always, never)",
Option "i" ["include"]
(ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings",
Option "e" ["exclude"] Option "e" ["exclude"]
(ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings", (ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings",
Option "" ["extended-analysis"]
(ReqArg (Flag "extended-analysis") "bool") "Perform dataflow analysis (default true)",
Option "f" ["format"] Option "f" ["format"]
(ReqArg (Flag "format") "FORMAT") $ (ReqArg (Flag "format") "FORMAT") $
"Output format (" ++ formatList ++ ")", "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 "" ["rcfile"]
(ReqArg (Flag "rcfile") "RCFILE")
"Prefer the specified configuration file over searching for one",
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"] Option "s" ["shell"]
(ReqArg (Flag "shell") "SHELLNAME") (ReqArg (Flag "shell") "SHELLNAME")
"Specify dialect (sh, bash, dash, ksh)", "Specify dialect (sh, bash, dash, ksh, busybox)",
Option "S" ["severity"] Option "S" ["severity"]
(ReqArg (Flag "severity") "SEVERITY") (ReqArg (Flag "severity") "SEVERITY")
"Minimum severity of errors to consider (error, warning, info, style)", "Minimum severity of errors to consider (error, warning, info, style)",
@ -102,10 +130,13 @@ options = [
(NoArg $ Flag "version" "true") "Print version information", (NoArg $ Flag "version" "true") "Print version information",
Option "W" ["wiki-link-count"] Option "W" ["wiki-link-count"]
(ReqArg (Flag "wiki-link-count") "NUM") (ReqArg (Flag "wiki-link-count") "NUM")
"The number of wiki links to show, when applicable.", "The number of wiki links to show, when applicable",
Option "x" ["external-sources"] Option "x" ["external-sources"]
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES" (NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES",
Option "" ["help"]
(NoArg $ Flag "help" "true") "Show this usage summary and exit"
] ]
getUsageInfo = usageInfo usageHeader options
printErr = lift . hPutStrLn stderr printErr = lift . hPutStrLn stderr
@ -114,15 +145,18 @@ parseArguments argv =
case getOpt Permute options argv of case getOpt Permute options argv of
(opts, files, []) -> return (opts, files) (opts, files, []) -> return (opts, files)
(_, _, errors) -> do (_, _, errors) -> do
printErr $ concat errors ++ "\n" ++ usageInfo usageHeader options printErr $ concat errors ++ "\n" ++ getUsageInfo
throwError SyntaxFailure throwError SyntaxFailure
formats :: FormatterOptions -> Map.Map String (IO Formatter) formats :: FormatterOptions -> Map.Map String (IO Formatter)
formats options = Map.fromList [ formats options = Map.fromList [
("checkstyle", ShellCheck.Formatter.CheckStyle.format), ("checkstyle", ShellCheck.Formatter.CheckStyle.format),
("diff", ShellCheck.Formatter.Diff.format options),
("gcc", ShellCheck.Formatter.GCC.format), ("gcc", ShellCheck.Formatter.GCC.format),
("json", ShellCheck.Formatter.JSON.format), ("json", ShellCheck.Formatter.JSON.format),
("tty", ShellCheck.Formatter.TTY.format options) ("json1", ShellCheck.Formatter.JSON1.format),
("tty", ShellCheck.Formatter.TTY.format options),
("quiet", ShellCheck.Formatter.Quiet.format options)
] ]
formatList = intercalate ", " names formatList = intercalate ", " names
@ -200,7 +234,7 @@ runFormatter sys format options files = do
f :: Status -> FilePath -> IO Status f :: Status -> FilePath -> IO Status
f status file = do f status file = do
newStatus <- process file `catch` handler file newStatus <- process file `catch` handler file
return $ status `mappend` newStatus return $! status `mappend` newStatus
handler :: FilePath -> IOException -> IO Status handler :: FilePath -> IOException -> IO Status
handler file e = reportFailure file (show e) handler file e = reportFailure file (show e)
reportFailure file str = do reportFailure file str = do
@ -209,7 +243,7 @@ runFormatter sys format options files = do
process :: FilePath -> IO Status process :: FilePath -> IO Status
process filename = do process filename = do
input <- siReadFile sys filename input <- siReadFile sys Nothing filename
either (reportFailure filename) check input either (reportFailure filename) check input
where where
check contents = do check contents = do
@ -225,9 +259,9 @@ runFormatter sys format options files = do
else SomeProblems else SomeProblems
parseEnum name value list = parseEnum name value list =
case filter ((== value) . fst) list of case lookup value list of
[(name, value)] -> return value Just value -> return value
[] -> do Nothing -> do
printErr $ "Unknown value for --" ++ name ++ ". " ++ printErr $ "Unknown value for --" ++ name ++ ". " ++
"Valid options are: " ++ (intercalate ", " $ map fst list) "Valid options are: " ++ (intercalate ", " $ map fst list)
throwError SupportFailure throwError SupportFailure
@ -267,10 +301,30 @@ 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 Flag "version" _ -> do
liftIO printVersion liftIO printVersion
throwError NoProblems throwError NoProblems
Flag "list-optional" _ -> do
liftIO printOptional
throwError NoProblems
Flag "help" _ -> do
liftIO $ putStrLn getUsageInfo
throwError NoProblems
Flag "externals" _ -> Flag "externals" _ ->
return options { return options {
externalSources = True externalSources = True
@ -284,6 +338,12 @@ parseOption flag options =
} }
} }
Flag "source-path" str -> do
let paths = splitSearchPath str
return options {
sourcePaths = (sourcePaths options) ++ paths
}
Flag "sourced" _ -> Flag "sourced" _ ->
return options { return options {
checkSpec = (checkSpec options) { checkSpec = (checkSpec options) {
@ -307,7 +367,39 @@ parseOption flag options =
} }
} }
_ -> return options Flag "norc" _ ->
return options {
checkSpec = (checkSpec options) {
csIgnoreRC = True
}
}
Flag "rcfile" str -> do
return options {
rcfile = Just str
}
Flag "enable" value ->
let cs = checkSpec options in return options {
checkSpec = cs {
csOptionalChecks = (csOptionalChecks cs) ++ split ',' value
}
}
Flag "extended-analysis" str -> do
value <- parseBool str
return options {
checkSpec = (checkSpec options) {
csExtendedAnalysis = Just 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
where where
die s = do die s = do
printErr s printErr s
@ -319,23 +411,36 @@ parseOption flag options =
throwError SyntaxFailure throwError SyntaxFailure
return (Prelude.read num :: Integer) return (Prelude.read num :: Integer)
parseBool str = do
case str of
"true" -> return True
"false" -> return False
_ -> do
printErr $ "Invalid boolean, expected true/false: " ++ str
throwError SyntaxFailure
ioInterface :: Options -> [FilePath] -> IO (SystemInterface IO)
ioInterface options files = do ioInterface options files = do
inputs <- mapM normalize files inputs <- mapM normalize files
cache <- newIORef emptyCache cache <- newIORef emptyCache
return SystemInterface { configCache <- newIORef ("", Nothing)
siReadFile = get cache inputs return (newSystemInterface :: SystemInterface IO) {
siReadFile = get cache inputs,
siFindSource = findSourceFile inputs (sourcePaths options),
siGetConfig = getConfig configCache
} }
where where
emptyCache :: Map.Map FilePath String emptyCache :: Map.Map FilePath String
emptyCache = Map.empty emptyCache = Map.empty
get cache inputs file = do
get cache inputs rcSuggestsExternal file = do
map <- readIORef cache map <- readIORef cache
case Map.lookup file map of case Map.lookup file map of
Just x -> return $ Right x Just x -> return $ Right x
Nothing -> fetch cache inputs file Nothing -> fetch cache inputs rcSuggestsExternal file
fetch cache inputs file = do fetch cache inputs rcSuggestsExternal file = do
ok <- allowable inputs file ok <- allowable rcSuggestsExternal inputs file
if ok if ok
then (do then (do
(contents, shouldCache) <- inputFile file (contents, shouldCache) <- inputFile file
@ -343,14 +448,16 @@ ioInterface options files = do
modifyIORef cache $ Map.insert file contents modifyIORef cache $ Map.insert file contents
return $ Right contents return $ Right contents
) `catch` handler ) `catch` handler
else
if rcSuggestsExternal == Just False
then return $ Left (file ++ " was not specified as input, and external files were disabled via directive.")
else return $ Left (file ++ " was not specified as input (see shellcheck -x).") else return $ Left (file ++ " was not specified as input (see shellcheck -x).")
where where
handler :: IOException -> IO (Either ErrorMessage String) handler :: IOException -> IO (Either ErrorMessage String)
handler ex = return . Left $ show ex handler ex = return . Left $ show ex
allowable inputs x = allowable rcSuggestsExternal inputs x =
if externalSources options if fromMaybe (externalSources options) rcSuggestsExternal
then return True then return True
else do else do
path <- normalize x path <- normalize x
@ -362,6 +469,103 @@ ioInterface options files = do
fallback :: FilePath -> IOException -> IO FilePath fallback :: FilePath -> IOException -> IO FilePath
fallback path _ = return path fallback path _ = return path
-- Returns the name and contents of .shellcheckrc for the given file
getConfig cache filename =
case rcfile options of
Just file -> do
-- We have a specified rcfile. Ignore normal rcfile resolution.
(path, result) <- readIORef cache
if path == "/"
then return result
else do
result <- readConfig file
when (isNothing result) $
hPutStrLn stderr $ "Warning: unable to read --rcfile " ++ file
writeIORef cache ("/", result)
return result
Nothing -> 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
hPutStrLn stderr $ file ++ ": " ++ show err
return ("", True)
andM a b arg = do
first <- a arg
if not first then return False else b arg
findM p = foldr go (pure Nothing)
where
go x acc = do
b <- p x
if b then pure (Just x) else acc
findSourceFile inputs sourcePathFlag currentScript rcSuggestsExternal sourcePathAnnotation original =
if isAbsolute original
then
let (_, relative) = splitDrive original
in find relative original
else
find original original
where
find filename deflt = do
sources <- findM ((allowable rcSuggestsExternal inputs) `andM` doesFileExist) $
(adjustPath filename):(map ((</> filename) . adjustPath) $ sourcePathFlag ++ sourcePathAnnotation)
case sources of
Nothing -> return deflt
Just first -> return first
scriptdir = dropFileName currentScript
adjustPath str =
case (splitDirectories str) of
("SCRIPTDIR":rest) -> joinPath (scriptdir:rest)
_ -> str
inputFile file = do inputFile file = do
(handle, shouldCache) <- (handle, shouldCache) <-
if file == "-" if file == "-"
@ -418,3 +622,14 @@ printVersion = do
putStrLn $ "version: " ++ shellcheckVersion putStrLn $ "version: " ++ shellcheckVersion
putStrLn "license: GNU General Public License, version 3" putStrLn "license: GNU General Public License, version 3"
putStrLn "website: https://www.shellcheck.net" 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 ""

View file

@ -23,24 +23,34 @@ description: |
# snap connect shellcheck:removable-media # snap connect shellcheck:removable-media
version: git version: git
grade: devel base: core20
grade: stable
confinement: strict confinement: strict
apps: apps:
shellcheck: shellcheck:
command: usr/bin/shellcheck command: usr/bin/shellcheck
plugs: [home, removable-media] plugs: [home, removable-media]
environment:
LANG: C.UTF-8
parts: parts:
shellcheck: shellcheck:
plugin: dump plugin: dump
source: ./ source: .
build-packages: build-packages:
- cabal-install - cabal-install
build: | stage-packages:
- libatomic1
override-build: |
# Give ourselves enough memory to build
dd if=/dev/zero of=/tmp/swap bs=1M count=2000
mkswap /tmp/swap
swapon /tmp/swap
cabal sandbox init cabal sandbox init
cabal update cabal update
cabal install -j cabal install -j
install: |
install -d $SNAPCRAFT_PART_INSTALL/usr/bin install -d $SNAPCRAFT_PART_INSTALL/usr/bin
install .cabal-sandbox/bin/shellcheck $SNAPCRAFT_PART_INSTALL/usr/bin install .cabal-sandbox/bin/shellcheck $SNAPCRAFT_PART_INSTALL/usr/bin

View file

@ -1,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2021 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@ -17,16 +17,27 @@
You should have received a copy of the GNU General Public License 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 <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.ASTLib where module ShellCheck.ASTLib where
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.Prelude
import ShellCheck.Regex
import Control.Monad.Writer import Control.Monad.Writer
import Control.Monad import Control.Monad
import Data.Char import Data.Char
import Data.Functor import Data.Functor
import Data.Functor.Identity
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map
import Numeric (showHex)
import Test.QuickCheck
arguments (T_SimpleCommand _ _ (cmd:args)) = args
-- Is this a type of loop? -- Is this a type of loop?
isLoop t = case t of isLoop t = case t of
@ -46,13 +57,32 @@ willSplit x =
T_BraceExpansion {} -> True T_BraceExpansion {} -> True
T_Glob {} -> True T_Glob {} -> True
T_Extglob {} -> True T_Extglob {} -> True
T_DoubleQuoted _ l -> any willBecomeMultipleArgs l
T_NormalWord _ l -> any willSplit l T_NormalWord _ l -> any willSplit l
_ -> False _ -> False
isGlob T_Extglob {} = True isGlob t = case t of
isGlob T_Glob {} = True T_Extglob {} -> True
isGlob (T_NormalWord _ l) = any isGlob l T_Glob {} -> True
isGlob _ = False T_NormalWord _ l -> any isGlob l || hasSplitRange l
_ -> False
where
-- foo[x${var}y] gets parsed as foo,[,x,$var,y],
-- so check if there's such an interval
hasSplitRange l =
let afterBracket = dropWhile (not . isHalfOpenRange) l
in any isClosingRange afterBracket
isHalfOpenRange t =
case t of
T_Literal _ "[" -> True
_ -> False
isClosingRange t =
case t of
T_Literal _ str -> ']' `elem` str
_ -> False
-- Is this shell word a constant? -- Is this shell word a constant?
isConstant token = isConstant token =
@ -81,7 +111,7 @@ oversimplify token =
(T_NormalWord _ l) -> [concat (concatMap oversimplify l)] (T_NormalWord _ l) -> [concat (concatMap oversimplify l)]
(T_DoubleQuoted _ l) -> [concat (concatMap oversimplify l)] (T_DoubleQuoted _ l) -> [concat (concatMap oversimplify l)]
(T_SingleQuoted _ s) -> [s] (T_SingleQuoted _ s) -> [s]
(T_DollarBraced _ _) -> ["${VAR}"] (T_DollarBraced _ _ _) -> ["${VAR}"]
(T_DollarArithmetic _ _) -> ["${VAR}"] (T_DollarArithmetic _ _) -> ["${VAR}"]
(T_DollarExpansion _ _) -> ["${VAR}"] (T_DollarExpansion _ _) -> ["${VAR}"]
(T_Backticked _ _) -> ["${VAR}"] (T_Backticked _ _) -> ["${VAR}"]
@ -110,7 +140,7 @@ getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ] flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
flag (x, '-':args) = map (\v -> (x, [v])) args flag (x, '-':args) = map (\v -> (x, [v])) args
flag (x, _) = [ (x, "") ] flag (x, _) = [ (x, "") ]
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)" getFlagsUntil _ _ = error $ pleaseReport "getFlags on non-command"
-- Get all flags in a GNU way, up until -- -- Get all flags in a GNU way, up until --
getAllFlags :: Token -> [(Token, String)] getAllFlags :: Token -> [(Token, String)]
@ -128,30 +158,146 @@ isFlag token =
_ -> False _ -> False
-- Is this token a flag where the - is unquoted? -- Is this token a flag where the - is unquoted?
isUnquotedFlag token = fromMaybe False $ do isUnquotedFlag token =
str <- getLeadingUnquotedString token case getLeadingUnquotedString token of
return $ "-" `isPrefixOf` str Just ('-':_) -> True
_ -> False
-- getGnuOpts "erd:u:" will parse a list of arguments tokens like `read`
-- -re -d : -u 3 bar
-- into
-- Just [("r", (-re, -re)), ("e", (-re, -re)), ("d", (-d,:)), ("u", (-u,3)), ("", (bar,bar))]
--
-- Each string flag maps to a tuple of (flag, argument), where argument=flag if it
-- doesn't take a specific one.
--
-- Any unrecognized flag will result in Nothing. The exception is if arbitraryLongOpts
-- is set, in which case --anything will map to "anything".
getGnuOpts :: String -> [Token] -> Maybe [(String, (Token, Token))]
getGnuOpts str args = getOpts (True, False) str [] args
-- As above, except the first non-arg string will treat the rest as arguments
getBsdOpts :: String -> [Token] -> Maybe [(String, (Token, Token))]
getBsdOpts str args = getOpts (False, False) str [] args
-- Tests for this are in Commands.hs where it's more frequently used
getOpts ::
-- Behavioral config: gnu style, allow arbitrary long options
(Bool, Bool)
-- A getopts style string
-> String
-- List of long options and whether they take arguments
-> [(String, Bool)]
-- List of arguments (excluding command)
-> [Token]
-- List of flags to tuple of (optionToken, valueToken)
-> Maybe [(String, (Token, Token))]
getOpts (gnu, arbitraryLongOpts) string longopts args = process args
where
flagList (c:':':rest) = ([c], True) : flagList rest
flagList (c:rest) = ([c], False) : flagList rest
flagList [] = longopts
flagMap = Map.fromList $ ("", False) : flagList string
process [] = return []
process (token:rest) = do
case getLiteralStringDef "\0" token of
"--" -> return $ listToArgs rest
'-':'-':word -> do
let (name, arg) = span (/= '=') word
needsArg <-
if arbitraryLongOpts
then return $ Map.findWithDefault False name flagMap
else Map.lookup name flagMap
if needsArg && null arg
then
case rest of
(arg:rest2) -> do
more <- process rest2
return $ (name, (token, arg)) : more
_ -> fail "Missing arg"
else do
more <- process rest
-- Consider splitting up token to get arg
return $ (name, (token, token)) : more
'-':opts -> shortToOpts opts token rest
arg ->
if gnu
then do
more <- process rest
return $ ("", (token, token)):more
else return $ listToArgs (token:rest)
shortToOpts opts token args =
case opts of
c:rest -> do
needsArg <- Map.lookup [c] flagMap
case () of
_ | needsArg && null rest -> do
(next:restArgs) <- return args
more <- process restArgs
return $ ([c], (token, next)):more
_ | needsArg -> do
more <- process args
return $ ([c], (token, token)):more
_ -> do
more <- shortToOpts rest token args
return $ ([c], (token, token)):more
[] -> process args
listToArgs = map (\x -> ("", (x, x)))
-- Generic getOpts that doesn't rely on a format string, but may also be inaccurate.
-- This provides a best guess interpretation instead of failing when new options are added.
--
-- "--" is treated as end of arguments
-- "--anything[=foo]" is treated as a long option without argument
-- "-any" is treated as -a -n -y, with the next arg as an option to -y unless it starts with -
-- anything else is an argument
getGenericOpts :: [Token] -> [(String, (Token, Token))]
getGenericOpts = process
where
process (token:rest) =
case getLiteralStringDef "\0" token of
"--" -> map (\c -> ("", (c,c))) rest
'-':'-':word -> (takeWhile (`notElem` "\0=") word, (token, token)) : process rest
'-':optString ->
let opts = takeWhile (/= '\0') optString
in
case rest of
next:_ | "-" `isPrefixOf` getLiteralStringDef "\0" next ->
map (\c -> ([c], (token, token))) opts ++ process rest
next:remainder ->
case reverse opts of
last:initial ->
map (\c -> ([c], (token, token))) (reverse initial)
++ [([last], (token, next))]
++ process remainder
[] -> process remainder
[] -> map (\c -> ([c], (token, token))) opts
_ -> ("", (token, token)) : process rest
process [] = []
-- 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? -- Is this an expansion of multiple items of an array?
isArrayExpansion t@(T_DollarBraced _ _) = isArrayExpansion (T_DollarBraced _ _ l) =
let string = bracedString t in let string = concat $ oversimplify l in
"@" `isPrefixOf` string || "@" `isPrefixOf` string ||
not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string
isArrayExpansion _ = False isArrayExpansion _ = False
-- Is it possible that this arg becomes multiple args? -- Is it possible that this arg becomes multiple args?
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f False t
where where
f t@(T_DollarBraced _ _) = f quoted (T_DollarBraced _ _ l) =
let string = bracedString t in let string = concat $ oversimplify l in
"!" `isPrefixOf` string not quoted || "!" `isPrefixOf` string
f (T_DoubleQuoted _ parts) = any f parts f quoted (T_DoubleQuoted _ parts) = any (f True) parts
f (T_NormalWord _ parts) = any f parts f quoted (T_NormalWord _ parts) = any (f quoted) parts
f _ = False f _ _ = False
-- Is it certain that this word will becomes multiple words? -- Is it certain that this word will becomes multiple words?
willBecomeMultipleArgs t = willConcatInAssignment t || f t willBecomeMultipleArgs t = willConcatInAssignment t || f t
@ -159,7 +305,6 @@ willBecomeMultipleArgs t = willConcatInAssignment t || f t
f T_Extglob {} = True f T_Extglob {} = True
f T_Glob {} = True f T_Glob {} = True
f T_BraceExpansion {} = True f T_BraceExpansion {} = True
f (T_DoubleQuoted _ parts) = any f parts
f (T_NormalWord _ parts) = any f parts f (T_NormalWord _ parts) = any f parts
f _ = False f _ = False
@ -175,9 +320,13 @@ willConcatInAssignment token =
getLiteralString :: Token -> Maybe String getLiteralString :: Token -> Maybe String
getLiteralString = getLiteralStringExt (const Nothing) getLiteralString = getLiteralStringExt (const Nothing)
-- Definitely get a literal string, with a given default for all non-literals
getLiteralStringDef :: String -> Token -> String
getLiteralStringDef x = runIdentity . getLiteralStringExt (const $ return x)
-- Definitely get a literal string, skipping over all non-literals -- Definitely get a literal string, skipping over all non-literals
onlyLiteralString :: Token -> String onlyLiteralString :: Token -> String
onlyLiteralString = fromJust . getLiteralStringExt (const $ return "") onlyLiteralString = getLiteralStringDef ""
-- Maybe get a literal string, but only if it's an unquoted argument. -- Maybe get a literal string, but only if it's an unquoted argument.
getUnquotedLiteral (T_NormalWord _ list) = getUnquotedLiteral (T_NormalWord _ list) =
@ -187,6 +336,12 @@ getUnquotedLiteral (T_NormalWord _ list) =
str _ = Nothing str _ = Nothing
getUnquotedLiteral _ = Nothing getUnquotedLiteral _ = Nothing
isQuotes t =
case t of
T_DoubleQuoted {} -> True
T_SingleQuoted {} -> True
_ -> False
-- Get the last unquoted T_Literal in a word like "${var}foo"THIS -- 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. -- or nothing if the word does not end in an unquoted literal.
getTrailingUnquotedLiteral :: Token -> Maybe Token getTrailingUnquotedLiteral :: Token -> Maybe Token
@ -205,8 +360,11 @@ getTrailingUnquotedLiteral t =
getLeadingUnquotedString :: Token -> Maybe String getLeadingUnquotedString :: Token -> Maybe String
getLeadingUnquotedString t = getLeadingUnquotedString t =
case t of case t of
T_NormalWord _ ((T_Literal _ s) : _) -> return s T_NormalWord _ ((T_Literal _ s) : rest) -> return $ s ++ from rest
_ -> Nothing _ -> Nothing
where
from ((T_Literal _ s):rest) = s ++ from rest
from _ = ""
-- Maybe get the literal string of this token and any globs in it. -- Maybe get the literal string of this token and any globs in it.
getGlobOrLiteralString = getLiteralStringExt f getGlobOrLiteralString = getLiteralStringExt f
@ -214,9 +372,24 @@ getGlobOrLiteralString = getLiteralStringExt f
f (T_Glob _ str) = return str f (T_Glob _ str) = return str
f _ = Nothing f _ = Nothing
prop_getLiteralString1 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x01") == Just "\1"
prop_getLiteralString2 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\xyz") == Just "\\xyz"
prop_getLiteralString3 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x1") == Just "\x1"
prop_getLiteralString4 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x1y") == Just "\x1y"
prop_getLiteralString5 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\xy") == Just "\\xy"
prop_getLiteralString6 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x") == Just "\\x"
prop_getLiteralString7 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1x") == Just "\1x"
prop_getLiteralString8 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\12x") == Just "\o12x"
prop_getLiteralString9 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\123x") == Just "\o123x"
prop_getLiteralString10 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1234") == Just "\o123\&4"
prop_getLiteralString11 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1") == Just "\1"
prop_getLiteralString12 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\12") == Just "\o12"
prop_getLiteralString13 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\123") == Just "\o123"
-- Maybe get the literal value of a token, using a custom function -- Maybe get the literal value of a token, using a custom function
-- to map unrecognized Tokens into strings. -- to map unrecognized Tokens into strings.
getLiteralStringExt :: (Token -> Maybe String) -> Token -> Maybe String getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String
getLiteralStringExt more = g getLiteralStringExt more = g
where where
allInList = fmap concat . mapM g allInList = fmap concat . mapM g
@ -246,14 +419,15 @@ getLiteralStringExt more = g
'\\' -> '\\' : rest '\\' -> '\\' : rest
'x' -> 'x' ->
case cs of case cs of
(x:y:more) -> (x:y:more) | isHexDigit x && isHexDigit y ->
if isHexDigit x && isHexDigit y chr (16*(digitToInt x) + (digitToInt y)) : decodeEscapes more
then chr (16*(digitToInt x) + (digitToInt y)) : rest (x:more) | isHexDigit x ->
else '\\':c:rest chr (digitToInt x) : decodeEscapes more
more -> '\\' : 'x' : decodeEscapes more
_ | isOctDigit c -> _ | isOctDigit c ->
let digits = take 3 $ takeWhile isOctDigit (c:cs) let (digits, more) = spanMax isOctDigit 3 (c:cs)
num = parseOct digits num = (parseOct digits) `mod` 256
in (if num < 256 then chr num else '?') : rest in (chr num) : decodeEscapes more
_ -> '\\' : c : rest _ -> '\\' : c : rest
where where
rest = decodeEscapes cs rest = decodeEscapes cs
@ -261,12 +435,54 @@ getLiteralStringExt more = g
where where
f n "" = n f n "" = n
f n (c:rest) = f (n * 8 + digitToInt c) rest f n (c:rest) = f (n * 8 + digitToInt c) rest
spanMax f n list =
let (first, second) = span f list
(prefix, suffix) = splitAt n first
in
(prefix, suffix ++ second)
decodeEscapes (c:cs) = c : decodeEscapes cs decodeEscapes (c:cs) = c : decodeEscapes cs
decodeEscapes [] = [] decodeEscapes [] = []
-- Is this token a string literal? -- Is this token a string literal?
isLiteral t = isJust $ getLiteralString t isLiteral t = isJust $ getLiteralString t
-- Is this token a string literal number?
isLiteralNumber t = fromMaybe False $ do
s <- getLiteralString t
guard $ all isDigit s
return True
-- Escape user data for messages.
-- Messages generally avoid repeating user data, but sometimes it's helpful.
e4m = escapeForMessage
escapeForMessage :: String -> String
escapeForMessage str = concatMap f str
where
f '\\' = "\\\\"
f '\n' = "\\n"
f '\r' = "\\r"
f '\t' = "\\t"
f '\x1B' = "\\e"
f c =
if shouldEscape c
then
if ord c < 256
then "\\x" ++ (pad0 2 $ toHex c)
else "\\U" ++ (pad0 4 $ toHex c)
else [c]
shouldEscape c =
(not $ isPrint c)
|| (not (isAscii c) && not (isLetter c))
pad0 :: Int -> String -> String
pad0 n s =
let l = length s in
if l < n
then (replicate (n-l) '0') ++ s
else s
toHex :: Char -> String
toHex c = map toUpper $ showHex (ord c) ""
-- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz] -- 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_NormalWord _ l) = concatMap getWordParts l
@ -295,25 +511,48 @@ getCommand t =
-- Maybe get the command name string of a token representing a command -- Maybe get the command name string of a token representing a command
getCommandName :: Token -> Maybe String getCommandName :: Token -> Maybe String
getCommandName = fst . getCommandNameAndToken getCommandName = fst . getCommandNameAndToken False
-- Maybe get the name+arguments of a command.
getCommandArgv t = do
(T_SimpleCommand _ _ args@(_:_)) <- getCommand t
return args
-- Get the command name token from a command, i.e. -- Get the command name token from a command, i.e.
-- the token representing 'ls' in 'ls -la 2> foo'. -- the token representing 'ls' in 'ls -la 2> foo'.
-- If it can't be determined, return the original token. -- If it can't be determined, return the original token.
getCommandTokenOrThis = snd . getCommandNameAndToken getCommandTokenOrThis = snd . getCommandNameAndToken False
getCommandNameAndToken :: Token -> (Maybe String, Token) -- Given a command, get the string and token that represents the command name.
getCommandNameAndToken t = fromMaybe (Nothing, t) $ do -- If direct, return the actual command (e.g. exec in 'exec ls')
(T_SimpleCommand _ _ (w:rest)) <- getCommand t -- If not, return the logical command (e.g. 'ls' in 'exec ls')
getCommandNameAndToken :: Bool -> Token -> (Maybe String, Token)
getCommandNameAndToken direct t = fromMaybe (Nothing, t) $ do
cmd@(T_SimpleCommand _ _ (w:rest)) <- getCommand t
s <- getLiteralString w s <- getLiteralString w
if "busybox" `isSuffixOf` s || "builtin" == s return $ fromMaybe (Just s, w) $ do
then guard $ not direct
case rest of actual <- getEffectiveCommandToken s cmd rest
(applet:_) -> return (getLiteralString applet, applet) return (getLiteralString actual, actual)
_ -> return (Just s, w) where
else getEffectiveCommandToken str cmd args =
return (Just s, w) let
firstArg = do
arg <- listToMaybe args
guard . not $ isFlag arg
return arg
in
case str of
"busybox" -> firstArg
"builtin" -> firstArg
"command" -> firstArg
"run" -> firstArg -- Used by bats
"exec" -> do
opts <- getBsdOpts "cla:" args
(_, (t, _)) <- find (null . fst) opts
return t
_ -> fail ""
-- If a command substitution is a single command, get its name. -- If a command substitution is a single command, get its name.
-- $(date +%s) = Just "date" -- $(date +%s) = Just "date"
@ -330,7 +569,7 @@ getCommandNameFromExpansion t =
-- Get the basename of a token representing a command -- Get the basename of a token representing a command
getCommandBasename = fmap basename . getCommandName getCommandBasename = fmap basename . getCommandName
where
basename = reverse . takeWhile (/= '/') . reverse basename = reverse . takeWhile (/= '/') . reverse
isAssignment t = isAssignment t =
@ -351,22 +590,34 @@ isOnlyRedirection t =
isFunction t = case t of T_Function {} -> 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 isBraceExpansion t = case t of T_BraceExpansion {} -> True; _ -> False
-- Get the lists of commands from tokens that contain them, such as -- Get the lists of commands from tokens that contain them, such as
-- the body of while loops or branches of if statements. -- the conditions and bodies of while loops or branches of if statements.
getCommandSequences :: Token -> [[Token]] getCommandSequences :: Token -> [[Token]]
getCommandSequences t = getCommandSequences t =
case t of case t of
T_Script _ _ cmds -> [cmds] T_Script _ _ cmds -> [cmds]
T_BraceGroup _ cmds -> [cmds] T_BraceGroup _ cmds -> [cmds]
T_Subshell _ cmds -> [cmds] T_Subshell _ cmds -> [cmds]
T_WhileExpression _ _ cmds -> [cmds] T_WhileExpression _ cond cmds -> [cond, cmds]
T_UntilExpression _ _ cmds -> [cmds] T_UntilExpression _ cond cmds -> [cond, cmds]
T_ForIn _ _ _ cmds -> [cmds] T_ForIn _ _ _ cmds -> [cmds]
T_ForArithmetic _ _ _ _ cmds -> [cmds] T_ForArithmetic _ _ _ _ cmds -> [cmds]
T_IfExpression _ thens elses -> map snd thens ++ [elses] T_IfExpression _ thens elses -> (concatMap (\(a,b) -> [a,b]) thens) ++ [elses]
T_Annotation _ _ t -> getCommandSequences t T_Annotation _ _ t -> getCommandSequences t
T_DollarExpansion _ cmds -> [cmds]
T_DollarBraceCommandExpansion _ cmds -> [cmds]
T_Backticked _ cmds -> [cmds]
_ -> [] _ -> []
-- Get a list of names of associative arrays -- Get a list of names of associative arrays
@ -374,13 +625,13 @@ getAssociativeArrays t =
nub . execWriter $ doAnalysis f t nub . execWriter $ doAnalysis f t
where where
f :: Token -> Writer [String] () f :: Token -> Writer [String] ()
f t@T_SimpleCommand {} = fromMaybe (return ()) $ do f t@T_SimpleCommand {} = sequence_ $ do
name <- getCommandName t name <- getCommandName t
let assocNames = ["declare","local","typeset"] let assocNames = ["declare","local","typeset"]
guard $ elem name assocNames guard $ name `elem` assocNames
let flags = getAllFlags t let flags = getAllFlags t
guard $ elem "A" $ map snd flags guard $ "A" `elem` map snd flags
let args = map fst . filter ((==) "" . snd) $ flags let args = [arg | (arg, "") <- flags]
let names = mapMaybe (getLiteralStringExt nameAssignments) args let names = mapMaybe (getLiteralStringExt nameAssignments) args
return $ tell names return $ tell names
f _ = return () f _ = return ()
@ -398,38 +649,36 @@ data PseudoGlob = PGAny | PGMany | PGChar Char
-- Turn a word into a PG pattern, replacing all unknown/runtime values with -- Turn a word into a PG pattern, replacing all unknown/runtime values with
-- PGMany. -- PGMany.
wordToPseudoGlob :: Token -> Maybe [PseudoGlob] wordToPseudoGlob :: Token -> [PseudoGlob]
wordToPseudoGlob word = wordToPseudoGlob = fromMaybe [PGMany] . wordToPseudoGlob' False
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 -- Turn a word into a PG pattern, but only if we can preserve
-- exact semantics. -- exact semantics.
wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob] wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob]
wordToExactPseudoGlob word = wordToExactPseudoGlob = wordToPseudoGlob' True
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
wordToPseudoGlob' :: Bool -> Token -> Maybe [PseudoGlob]
wordToPseudoGlob' exact word =
simplifyPseudoGlob <$> toGlob word
where where
toGlob :: Token -> Maybe [PseudoGlob]
toGlob word =
case word of
T_NormalWord _ (T_Literal _ ('~':str):rest) -> do
guard $ not exact
let this = (PGMany : (map PGChar $ dropWhile (/= '/') str))
tail <- concat <$> (mapM f $ concatMap getWordParts rest)
return $ this ++ tail
_ -> concat <$> (mapM f $ getWordParts word)
f x = case x of f x = case x of
T_Literal _ s -> return $ map PGChar s T_Literal _ s -> return $ map PGChar s
T_SingleQuoted _ s -> return $ map PGChar s T_SingleQuoted _ s -> return $ map PGChar s
T_Glob _ "?" -> return [PGAny] T_Glob _ "?" -> return [PGAny]
T_Glob _ "*" -> return [PGMany] T_Glob _ "*" -> return [PGMany]
_ -> fail "Unknown token type" T_Glob _ ('[':_) | not exact -> return [PGAny]
_ -> if exact then fail "" else return [PGMany]
-- Reorder a PseudoGlob for more efficient matching, e.g. -- Reorder a PseudoGlob for more efficient matching, e.g.
-- f?*?**g -> f??*g -- f?*?**g -> f??*g
@ -479,14 +728,199 @@ pseudoGlobIsSuperSetof = matchable
matchable (PGMany : rest) [] = matchable rest [] matchable (PGMany : rest) [] = matchable rest []
matchable _ _ = False matchable _ _ = False
wordsCanBeEqual x y = fromMaybe True $ wordsCanBeEqual x y = pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
-- Is this an expansion that can be quoted, -- Is this an expansion that can be quoted,
-- e.g. $(foo) `foo` $foo (but not {foo,})? -- e.g. $(foo) `foo` $foo (but not {foo,})?
isQuoteableExpansion t = case t of isQuoteableExpansion t = case t of
T_DollarBraced {} -> True
_ -> isCommandSubstitution t
isCommandSubstitution t = case t of
T_DollarExpansion {} -> True T_DollarExpansion {} -> True
T_DollarBraceCommandExpansion {} -> True T_DollarBraceCommandExpansion {} -> True
T_Backticked {} -> True T_Backticked {} -> True
T_DollarBraced {} -> True
_ -> False _ -> False
-- Is this an expansion that results in a simple string?
isStringExpansion t = isCommandSubstitution t || case t of
T_DollarArithmetic {} -> True
T_DollarBraced {} -> not (isArrayExpansion t)
_ -> 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 from to) = code >= from && code < to
hasNum _ = False
prop_executableFromShebang1 = executableFromShebang "/bin/sh" == "sh"
prop_executableFromShebang2 = executableFromShebang "/bin/bash" == "bash"
prop_executableFromShebang3 = executableFromShebang "/usr/bin/env ksh" == "ksh"
prop_executableFromShebang4 = executableFromShebang "/usr/bin/env -S foo=bar bash -x" == "bash"
prop_executableFromShebang5 = executableFromShebang "/usr/bin/env --split-string=bash -x" == "bash"
prop_executableFromShebang6 = executableFromShebang "/usr/bin/env --split-string=foo=bar bash -x" == "bash"
prop_executableFromShebang7 = executableFromShebang "/usr/bin/env --split-string bash -x" == "bash"
prop_executableFromShebang8 = executableFromShebang "/usr/bin/env --split-string foo=bar bash -x" == "bash"
prop_executableFromShebang9 = executableFromShebang "/usr/bin/env foo=bar dash" == "dash"
prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "busybox sh"
prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "busybox ash"
-- Get the shell executable from a string like '/usr/bin/env bash'
executableFromShebang :: String -> String
executableFromShebang = shellFor
where
re = mkRegex "/env +(-S|--split-string=?)? *(.*)"
shellFor s | s `matches` re =
case matchRegex re s of
Just [flag, shell] -> fromEnvArgs (words shell)
_ -> ""
shellFor sb =
case words sb of
[] -> ""
[x] -> basename x
(first:second:args) | basename first == "busybox" ->
case basename second of
"sh" -> "busybox sh"
"ash" -> "busybox ash"
x -> x
(first:args) | basename first == "env" ->
fromEnvArgs args
(first:_) -> basename first
fromEnvArgs args = fromMaybe "" $ find (notElem '=') $ skipFlags args
basename s = reverse . takeWhile (/= '/') . reverse $ s
skipFlags = dropWhile ("-" `isPrefixOf`)
-- Determining if a name is a variable
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
isVariableChar x = isVariableStartChar x || isDigit x
isSpecialVariableChar = (`elem` "*@#?-$!")
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
-- 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_getBracedReference11b = 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) | c `elem` "!#" = rest
dropPrefix cs = cs
takeName s = do
let name = takeWhile isVariableChar s
guard . not $ null name
return name
getSpecial (c:_) | isSpecialVariableChar c = return [c]
getSpecial _ = fail "empty or not special"
nameExpansion ('!':next:rest) = do -- e.g. ${!foo*bar*}
guard $ isVariableChar next -- e.g. ${!@}
first <- find (not . isVariableChar) rest
guard $ first `elem` "*?@"
return ""
nameExpansion _ = Nothing
-- Get the variable modifier like /a/b in ${var/a/b}
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
prop_getBracedModifier4 = getBracedModifier "foo[@]@Q" == "[@]@Q"
prop_getBracedModifier5 = getBracedModifier "@@Q" == "@Q"
getBracedModifier s = headOrDefault "" $ 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]
-- Get the variables from indices like ["x", "y"] in ${var[x+y+1]}
prop_getIndexReferences1 = getIndexReferences "var[x+y+1]" == ["x", "y"]
getIndexReferences s = fromMaybe [] $ do
index:_ <- matchRegex re s
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 ]
_:offsets:_ <- matchRegex re mods
return $ matchAllStrings variableNameRegex offsets
where
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
-- Returns whether a token is a parameter expansion without any modifiers.
-- True for $var ${var} $1 $#
-- False for ${#var} ${var[x]} ${var:-0}
isUnmodifiedParameterExpansion t =
case t of
T_DollarBraced _ False _ -> True
T_DollarBraced _ _ list ->
let str = concat $ oversimplify list
in getBracedReference str == str
_ -> False
-- Return the referenced variable if (and only if) it's an unmodified parameter expansion.
getUnmodifiedParameterExpansion t =
case t of
T_DollarBraced _ _ list -> do
let str = concat $ oversimplify list
guard $ getBracedReference str == str
return str
_ -> Nothing
--- A list of the element and all its parents up to the root node.
getPath tree = NE.unfoldr $ \t -> (t, Map.lookup (getId t) tree)
isClosingFileOp op =
case op of
T_IoDuplicate _ (T_GREATAND _) "-" -> True
T_IoDuplicate _ (T_LESSAND _) "-" -> True
_ -> False
getEnableDirectives root =
case root of
T_Annotation _ list _ -> [s | EnableComment s <- list]
_ -> []
getExtendedAnalysisDirective :: Token -> Maybe Bool
getExtendedAnalysisDirective root =
case root of
T_Annotation _ list _ -> listToMaybe $ [s | ExtendedAnalysis s <- list]
_ -> Nothing
return []
runTests = $quickCheckAll

View file

@ -1,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2022 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@ -17,7 +17,7 @@
You should have received a copy of the GNU General Public License 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 <https://www.gnu.org/licenses/>.
-} -}
module ShellCheck.Analyzer (analyzeScript) where module ShellCheck.Analyzer (analyzeScript, ShellCheck.Analyzer.optionalChecks) where
import ShellCheck.Analytics import ShellCheck.Analytics
import ShellCheck.AnalyzerLib import ShellCheck.AnalyzerLib
@ -25,6 +25,8 @@ import ShellCheck.Interface
import Data.List import Data.List
import Data.Monoid import Data.Monoid
import qualified ShellCheck.Checks.Commands import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.ControlFlow
import qualified ShellCheck.Checks.Custom
import qualified ShellCheck.Checks.ShellSupport import qualified ShellCheck.Checks.ShellSupport
@ -33,13 +35,21 @@ analyzeScript :: AnalysisSpec -> AnalysisResult
analyzeScript spec = newAnalysisResult { analyzeScript spec = newAnalysisResult {
arComments = arComments =
filterByAnnotation spec params . nub $ filterByAnnotation spec params . nub $
runAnalytics spec runChecker params (checkers spec params)
++ runChecker params (checkers params)
} }
where where
params = makeParameters spec params = makeParameters spec
checkers params = mconcat $ map ($ params) [ checkers spec params = mconcat $ map ($ params) [
ShellCheck.Checks.Commands.checker, ShellCheck.Analytics.checker spec,
ShellCheck.Checks.Commands.checker spec,
ShellCheck.Checks.ControlFlow.checker spec,
ShellCheck.Checks.Custom.checker,
ShellCheck.Checks.ShellSupport.checker ShellCheck.Checks.ShellSupport.checker
] ]
optionalChecks = mconcat $ [
ShellCheck.Analytics.optionalChecks,
ShellCheck.Checks.Commands.optionalChecks,
ShellCheck.Checks.ControlFlow.optionalChecks
]

1316
src/ShellCheck/CFG.hs Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,101 @@
{-
Copyright 2022 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 #-}
-- Checks that run on the Control Flow Graph (as opposed to the AST)
-- This is scaffolding for a work in progress.
module ShellCheck.Checks.ControlFlow (checker, optionalChecks, ShellCheck.Checks.ControlFlow.runTests) where
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.CFG hiding (cfgAnalysis)
import ShellCheck.CFGAnalysis
import ShellCheck.AnalyzerLib
import ShellCheck.Data
import ShellCheck.Interface
import Control.Monad
import Control.Monad.Reader
import Data.Graph.Inductive.Graph
import qualified Data.Map as M
import qualified Data.Set as S
import Data.List
import Data.Maybe
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
optionalChecks :: [CheckDescription]
optionalChecks = []
-- A check that runs on the entire graph
type ControlFlowCheck = Analysis
-- A check invoked once per node, with its (pre,post) data
type ControlFlowNodeCheck = LNode CFNode -> (ProgramState, ProgramState) -> Analysis
-- A check invoked once per effect, with its node's (pre,post) data
type ControlFlowEffectCheck = IdTagged CFEffect -> Node -> (ProgramState, ProgramState) -> Analysis
checker :: AnalysisSpec -> Parameters -> Checker
checker spec params = Checker {
perScript = const $ sequence_ controlFlowChecks,
perToken = const $ return ()
}
controlFlowChecks :: [ControlFlowCheck]
controlFlowChecks = [
runNodeChecks controlFlowNodeChecks
]
controlFlowNodeChecks :: [ControlFlowNodeCheck]
controlFlowNodeChecks = [
runEffectChecks controlFlowEffectChecks
]
controlFlowEffectChecks :: [ControlFlowEffectCheck]
controlFlowEffectChecks = [
]
runNodeChecks :: [ControlFlowNodeCheck] -> ControlFlowCheck
runNodeChecks perNode = do
cfg <- asks cfgAnalysis
mapM_ runOnAll cfg
where
getData datas n@(node, label) = do
(pre, post) <- M.lookup node datas
return (n, (pre, post))
runOn :: (LNode CFNode, (ProgramState, ProgramState)) -> Analysis
runOn (node, prepost) = mapM_ (\c -> c node prepost) perNode
runOnAll cfg = mapM_ runOn $ mapMaybe (getData $ nodeToData cfg) $ labNodes (graph cfg)
runEffectChecks :: [ControlFlowEffectCheck] -> ControlFlowNodeCheck
runEffectChecks list = checkNode
where
checkNode (node, label) prepost =
case label of
CFApplyEffects effects -> mapM_ (\effect -> mapM_ (\c -> c effect node prepost) list) effects
_ -> return ()
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View file

@ -0,0 +1,21 @@
{-
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,5 +1,5 @@
{- {-
Copyright 2012-2016 Vidar Holen Copyright 2012-2020 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@ -19,20 +19,24 @@
-} -}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ViewPatterns #-}
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.ASTLib import ShellCheck.ASTLib
import ShellCheck.AnalyzerLib import ShellCheck.AnalyzerLib
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Prelude
import ShellCheck.Regex import ShellCheck.Regex
import Control.Monad import Control.Monad
import Control.Monad.RWS import Control.Monad.RWS
import Data.Char import Data.Char
import Data.Functor.Identity
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Set as Set
import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess) import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
@ -57,6 +61,9 @@ checks = [
,checkBraceExpansionVars ,checkBraceExpansionVars
,checkMultiDimensionalArrays ,checkMultiDimensionalArrays
,checkPS1Assignments ,checkPS1Assignments
,checkMultipleBangs
,checkBangAfterPipe
,checkNegatedUnaryOps
] ]
testChecker (ForShell _ t) = testChecker (ForShell _ t) =
@ -70,22 +77,24 @@ verifyNot c s = producesComments (testChecker c) s == Just False
prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))" prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar" prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar" prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
checkForDecimals = ForShell [Sh, Dash, Bash] f checkForDecimals = ForShell [Sh, Dash, BusyboxSh, Bash] f
where where
f t@(TA_Expansion id _) = potentially $ do f t@(TA_Expansion id _) = sequence_ $ do
str <- getLiteralString t first:rest <- getLiteralString t
first <- str !!! 0 guard $ isDigit first && '.' `elem` rest
guard $ isDigit first && '.' `elem` str
return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk." return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
f _ = return () f _ = return ()
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)" prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]" prop_checkBashisms2 = verifyNot checkBashisms "[ foo -nt bar ]"
prop_checkBashisms3 = verify checkBashisms "echo $((i++))" prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)" prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
prop_checkBashisms5 = verify checkBashisms "source file" prop_checkBashisms5 = verify checkBashisms "source file"
prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]" prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
prop_checkBashisms6b = verify checkBashisms "test \"$a\" == 42"
prop_checkBashisms6c = verify checkBashisms "[ foo =~ bar ]"
prop_checkBashisms6d = verify checkBashisms "test foo =~ bar"
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}" prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}" prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}" prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
@ -101,6 +110,7 @@ prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"
prop_checkBashisms19 = verify checkBashisms "foo > file*.txt" prop_checkBashisms19 = verify checkBashisms "foo > file*.txt"
prop_checkBashisms20 = verify checkBashisms "read -ra foo" prop_checkBashisms20 = verify checkBashisms "read -ra foo"
prop_checkBashisms21 = verify checkBashisms "[ -a foo ]" prop_checkBashisms21 = verify checkBashisms "[ -a foo ]"
prop_checkBashisms21b = verify checkBashisms "test -a foo"
prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]" prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]"
prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT" prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT"
prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM" prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM"
@ -132,181 +142,351 @@ prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
prop_checkBashisms50 = verify checkBashisms "#!/bin/sh\ncmd >& file" prop_checkBashisms50 = verify checkBashisms "#!/bin/sh\ncmd >& file"
prop_checkBashisms51 = verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1" prop_checkBashisms51 = verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
prop_checkBashisms52 = verifyNot checkBashisms "#!/bin/sh\ncmd >&2" prop_checkBashisms52 = verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
prop_checkBashisms52b = verifyNot checkBashisms "#!/bin/sh\ncmd >& $var"
prop_checkBashisms52c = verify checkBashisms "#!/bin/sh\ncmd >& $dir/$var"
prop_checkBashisms53 = verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n" prop_checkBashisms53 = verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
prop_checkBashisms54 = verify checkBashisms "#!/bin/sh\nfoo+=bar" prop_checkBashisms54 = verify checkBashisms "#!/bin/sh\nfoo+=bar"
prop_checkBashisms55 = verify checkBashisms "#!/bin/sh\necho ${@%foo}" prop_checkBashisms55 = verify checkBashisms "#!/bin/sh\necho ${@%foo}"
prop_checkBashisms56 = verifyNot checkBashisms "#!/bin/sh\necho ${##}" prop_checkBashisms56 = verifyNot checkBashisms "#!/bin/sh\necho ${##}"
checkBashisms = ForShell [Sh, Dash] $ \t -> do 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 $_"
prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}"
prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}"
prop_checkBashisms99 = verify checkBashisms "#!/bin/dash\necho [^f]oo"
prop_checkBashisms100 = verify checkBashisms "read -r"
prop_checkBashisms101 = verify checkBashisms "read"
prop_checkBashisms102 = verifyNot checkBashisms "read -r foo"
prop_checkBashisms103 = verifyNot checkBashisms "read foo"
prop_checkBashisms104 = verifyNot checkBashisms "read ''"
prop_checkBashisms105 = verifyNot checkBashisms "#!/bin/busybox sh\nset -o pipefail"
prop_checkBashisms106 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[[ \"$x\" = \"$x\" ]]"
prop_checkBashisms107 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[ \"$x\" == \"$x\" ]"
prop_checkBashisms108 = verifyNot checkBashisms "#!/bin/busybox sh\necho magic &> /dev/null"
prop_checkBashisms109 = verifyNot checkBashisms "#!/bin/busybox sh\ntrap stop EXIT SIGTERM"
prop_checkBashisms110 = verifyNot checkBashisms "#!/bin/busybox sh\nsource /dev/null"
prop_checkBashisms111 = verify checkBashisms "#!/bin/dash\nx='test'\n${x:0:3}" -- SC3057
prop_checkBashisms112 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x:0:3}" -- SC3057
prop_checkBashisms113 = verify checkBashisms "#!/bin/dash\nx='test'\n${x/st/xt}" -- SC3060
prop_checkBashisms114 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x/st/xt}" -- SC3060
prop_checkBashisms115 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x}" -- SC3053
prop_checkBashisms116 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x[1]}" -- SC3054
prop_checkBashisms117 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x[@]}" -- SC3055
prop_checkBashisms118 = verify checkBashisms "#!/bin/busybox sh\nxyz=1\n${!x*}" -- SC3056
prop_checkBashisms119 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x^^[t]}" -- SC3059
prop_checkBashisms120 = verify checkBashisms "#!/bin/sh\n[ x == y ]"
prop_checkBashisms121 = verifyNot checkBashisms "#!/bin/sh\n# shellcheck shell=busybox\n[ x == y ]"
prop_checkBashisms122 = verify checkBashisms "#!/bin/dash\n$'a'"
prop_checkBashisms123 = verifyNot checkBashisms "#!/bin/busybox sh\n$'a'"
prop_checkBashisms124 = verify checkBashisms "#!/bin/dash\ntype -p test"
prop_checkBashisms125 = verifyNot checkBashisms "#!/bin/busybox sh\ntype -p test"
prop_checkBashisms126 = verifyNot checkBashisms "#!/bin/busybox sh\nread -p foo -r bar"
prop_checkBashisms127 = verifyNot checkBashisms "#!/bin/busybox sh\necho -ne foo"
prop_checkBashisms128 = verify checkBashisms "#!/bin/dash\ntype -p test"
prop_checkBashisms129 = verify checkBashisms "#!/bin/sh\n[ -k /tmp ]"
prop_checkBashisms130 = verifyNot checkBashisms "#!/bin/dash\ntest -k /tmp"
prop_checkBashisms131 = verify checkBashisms "#!/bin/sh\n[ -o errexit ]"
checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
params <- ask params <- ask
kludge params t kludge params t
where where
-- This code was copy-pasted from Analytics where params was a variable -- This code was copy-pasted from Analytics where params was a variable
kludge params = bashism kludge params = bashism
where where
isDash = shellType params == Dash isBusyboxSh = shellType params == BusyboxSh
warnMsg id s = isDash = shellType params == Dash || isBusyboxSh
warnMsg id code s =
if isDash if isDash
then warn id 2169 $ "In dash, " ++ s ++ " not supported." then err id code $ "In dash, " ++ s ++ " not supported."
else warn id 2039 $ "In POSIX sh, " ++ s ++ " undefined." else warn id code $ "In POSIX sh, " ++ s ++ " undefined."
asStr = getLiteralString
bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is"
bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is"
bashism (T_DollarSingleQuoted id _) =
unless isBusyboxSh $ warnMsg id 3003 "$'..' is"
bashism (T_DollarDoubleQuoted id _) = warnMsg id 3004 "$\"..\" is"
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id 3005 "arithmetic for loops are"
bashism (T_Arithmetic id _) = warnMsg id 3006 "standalone ((..)) is"
bashism (T_DollarBracket id _) = warnMsg id 3007 "$[..] in place of $((..)) is"
bashism (T_SelectIn id _ _ _) = warnMsg id 3008 "select loops are"
bashism (T_BraceExpansion id _) = warnMsg id 3009 "brace expansion is"
bashism (T_Condition id DoubleBracket _) =
unless isBusyboxSh $ warnMsg id 3010 "[[ ]] is"
bashism (T_HereString id _) = warnMsg id 3011 "here-strings are"
bashism (TC_Binary id _ op _ _) =
checkTestOp bashismBinaryTestFlags op id
bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs]) =
checkTestOp bashismBinaryTestFlags op id
bashism (TC_Unary id _ op _) =
checkTestOp bashismUnaryTestFlags op id
bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just op, _]) =
checkTestOp bashismUnaryTestFlags op id
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 _ "-a" _) =
warnMsg id "unary -a in place of -e is"
bashism (TA_Unary id op _) bashism (TA_Unary id op _)
| op `elem` [ "|++", "|--", "++|", "--|"] = | op `elem` [ "|++", "|--", "++|", "--|"] =
warnMsg id $ filter (/= '|') op ++ " is" warnMsg id 3018 $ filter (/= '|') op ++ " is"
bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are" bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is" bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) =
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id ">& is" unless isBusyboxSh $ warnMsg id 3020 "&> is"
bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are" bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) file)) =
unless (all isDigit $ onlyLiteralString file) $ warnMsg id 3021 ">& filename (as opposed to >& fd) is"
bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are"
bashism (T_FdRedirect id num _) bashism (T_FdRedirect id num _)
| all isDigit num && length num > 1 = warnMsg id "FDs outside 0-9 are" | all isDigit num && length num > 1 = warnMsg id 3023 "FDs outside 0-9 are"
bashism (T_Assignment id Append _ _ _) = bashism (T_Assignment id Append _ _ _) =
warnMsg id "+= is" warnMsg id 3024 "+= is"
bashism (T_IoFile id _ word) | isNetworked = bashism (T_IoFile id _ word) | isNetworked =
warnMsg id "/dev/{tcp,udp} is" warnMsg id 3025 "/dev/{tcp,udp} is"
where where
file = onlyLiteralString word file = onlyLiteralString word
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"] isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
bashism (T_Glob id str) | "[^" `isInfixOf` str = bashism (T_Glob id str) | "[^" `isInfixOf` str =
warnMsg id "^ in place of ! in glob bracket expressions is" warnMsg id 3026 "^ in place of ! in glob bracket expressions is"
bashism t@(TA_Variable id str _) | isBashVariable str = bashism t@(TA_Variable id str _) | isBashVariable str =
warnMsg id $ str ++ " is" warnMsg id 3028 $ str ++ " is"
bashism t@(T_DollarBraced id token) = do bashism t@(T_DollarBraced id _ token) = do
mapM_ check expansion unless isBusyboxSh $ mapM_ check simpleExpansions
mapM_ check advancedExpansions
when (isBashVariable var) $ when (isBashVariable var) $
warnMsg id $ var ++ " is" warnMsg id 3028 $ var ++ " is"
where where
str = bracedString t str = concat $ oversimplify token
var = getBracedReference str var = getBracedReference str
check (regex, feature) = check (regex, code, feature) =
when (isJust $ matchRegex regex str) $ warnMsg id feature when (isJust $ matchRegex regex str) $ warnMsg id code feature
bashism t@(T_Pipe id "|&") = bashism t@(T_Pipe id "|&") =
warnMsg id "|& in place of 2>&1 | is" warnMsg id 3029 "|& in place of 2>&1 | is"
bashism (T_Array id _) = bashism (T_Array id _) =
warnMsg id "arrays are" warnMsg id 3030 "arrays are"
bashism (T_IoFile id _ t) | isGlob t = bashism (T_IoFile id _ t) | isGlob t =
warnMsg id "redirecting to/from globs is" warnMsg id 3031 "redirecting to/from globs is"
bashism (T_CoProc id _ _) = bashism (T_CoProc id _ _) =
warnMsg id "coproc is" warnMsg id 3032 "coproc is"
bashism (T_Function id _ _ str _) | not (isVariableName str) = bashism (T_Function id _ _ str _) | not (isVariableName str) =
warnMsg id "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is" warnMsg id 3033 "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
bashism (T_DollarExpansion id [x]) | isOnlyRedirection x = bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
warnMsg id "$(<file) to read files is" warnMsg id 3034 "$(<file) to read files is"
bashism (T_Backticked id [x]) | isOnlyRedirection x = bashism (T_Backticked id [x]) | isOnlyRedirection x =
warnMsg id "`<file` to read files is" warnMsg id 3035 "`<file` to read files is"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "echo" && "-" `isPrefixOf` argString = | t `isCommand` "echo" && argString `matches` flagRegex =
unless ("--" `isPrefixOf` argString) $ -- echo "-----" if isBusyboxSh
if isDash then
unless (argString `matches` busyboxFlagRegex) $
warnMsg (getId arg) 3036 "echo flags besides -n and -e"
else if isDash
then then
when (argString /= "-n") $ when (argString /= "-n") $
warnMsg (getId arg) "echo flags besides -n" warnMsg (getId arg) 3036 "echo flags besides -n"
else else
warnMsg (getId arg) "echo flags are" warnMsg (getId arg) 3037 "echo flags are"
where argString = concat $ oversimplify arg where
argString = concat $ oversimplify arg
flagRegex = mkRegex "^-[eEsn]+$"
busyboxFlagRegex = mkRegex "^-[en]+$"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) = | getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
warnMsg (getId arg) "exec flags are" warnMsg (getId arg) 3038 "exec flags are"
bashism t@(T_SimpleCommand id _ _) bashism t@(T_SimpleCommand id _ _)
| t `isCommand` "let" = warnMsg id "'let' is" | t `isCommand` "let" = warnMsg id 3039 "'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 = foldr go []
where
go first rest = case getLiteralString first of
Just str -> (getId first, str) : rest
Nothing -> []
-- 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 3040 $ "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 3041 $ "set flag " <> ('-':letter:" is")
checkOptions rest
| beginsWithDoubleDash flag' = do
warnMsg fid 3042 $ "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)) = bashism t@(T_SimpleCommand id _ (cmd:rest)) =
let name = fromMaybe "" $ getCommandName t let name = fromMaybe "" $ getCommandName t
flags = getLeadingFlags t flags = getLeadingFlags t
in do in do
when (name == "local" && not isDash) $
-- This is so commonly accepted that we'll make it a special case
warnMsg id 3043 $ "'local' is"
when (name `elem` unsupportedCommands) $ when (name `elem` unsupportedCommands) $
warnMsg id $ "'" ++ name ++ "' is" warnMsg id 3044 $ "'" ++ name ++ "' is"
potentially $ do sequence_ $ do
allowed <- Map.lookup name allowedFlags allowed' <- Map.lookup name allowedFlags
(word, flag) <- listToMaybe $ allowed <- allowed'
filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags (word, flag) <- find
return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is" (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is"
when (name == "source") $ warnMsg id "'source' in place of '.' is" when (name == "source" && not isBusyboxSh) $
warnMsg id 3046 "'source' in place of '.' is"
when (name == "trap") $ when (name == "trap") $
let let
check token = potentially $ do check token = sequence_ $ do
str <- getLiteralString token str <- getLiteralString token
let upper = map toUpper str let upper = map toUpper str
return $ do return $ do
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $ when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
warnMsg (getId token) $ "trapping " ++ str ++ " is" warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is"
when ("SIG" `isPrefixOf` upper) $ when (not isBusyboxSh && "SIG" `isPrefixOf` upper) $
warnMsg (getId token) warnMsg (getId token) 3048
"prefixing signal names with 'SIG' is" "prefixing signal names with 'SIG' is"
when (not isDash && upper /= str) $ when (not isDash && upper /= str) $
warnMsg (getId token) warnMsg (getId token) 3049
"using lower/mixed case for signal names is" "using lower/mixed case for signal names is"
in in
mapM_ check (drop 1 rest) mapM_ check (drop 1 rest)
when (name == "printf") $ potentially $ do when (name == "printf") $ sequence_ $ do
format <- rest !!! 0 -- flags are covered by allowedFlags format <- rest !!! 0 -- flags are covered by allowedFlags
let literal = onlyLiteralString format let literal = onlyLiteralString format
guard $ "%q" `isInfixOf` literal guard $ "%q" `isInfixOf` literal
return $ warnMsg (getId format) "printf %q is" return $ warnMsg (getId format) 3050 "printf %q is"
when (name == "read" && all isFlag rest) $
warnMsg (getId cmd) 3061 "read without a variable is"
where where
unsupportedCommands = [ unsupportedCommands = [
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown", "let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend", "enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
"typeset" "typeset"
] ++ if not isDash then ["local"] else []
allowedFlags = Map.fromList [
("exec", []),
("export", ["-p"]),
("printf", []),
("read", if isDash then ["r", "p"] else ["r"]),
("ulimit", ["f"])
] ]
bashism t@(T_SourceCommand id src _) = allowedFlags = Map.fromList [
let name = fromMaybe "" $ getCommandName src ("cd", Just ["L", "P"]),
in do ("exec", Just []),
when (name == "source") $ warnMsg id "'source' in place of '.' is" ("export", Just ["p"]),
("hash", Just $ if isDash then ["r", "v"] else ["r"]),
("jobs", Just ["l", "p"]),
("printf", Just []),
("read", Just $ if isDash || isBusyboxSh then ["r", "p"] else ["r"]),
("readonly", Just ["p"]),
("trap", Just []),
("type", Just $ if isBusyboxSh then ["p"] else []),
("ulimit", if isDash then Nothing else Just ["f"]),
("umask", Just ["S"]),
("unset", Just ["f", "v"]),
("wait", Just [])
]
bashism t@(T_SourceCommand id src _)
| getCommandName src == Just "source" =
unless isBusyboxSh $
warnMsg id 3051 "'source' in place of '.' is"
bashism (TA_Expansion _ (T_Literal id str : _))
| str `matches` radix = warnMsg id 3052 "arithmetic base conversion is"
where
radix = mkRegex "^[0-9]+#"
bashism _ = return () bashism _ = return ()
varChars="_0-9a-zA-Z" varChars="_0-9a-zA-Z"
expansion = let re = mkRegex in [ advancedExpansions = let re = mkRegex in [
(re $ "^![" ++ varChars ++ "]", "indirect expansion is"), (re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"),
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"), (re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"),
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"), (re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"),
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"), (re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"),
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", "string indexing is"), (re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is")
(re $ "^([*@][%#]|#[@*])", "string operations on $@/$* are"), ]
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is") simpleExpansions = let re = mkRegex in [
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", 3057, "string indexing is"),
(re $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is")
] ]
bashVars = [ bashVars = [
-- This list deliberately excludes $BASH_VERSION as it's often used
-- for shell identification.
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS" "DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS",
"_", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
"BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND",
"BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE",
"BASH_SUBSHELL", "BASH_VERSINFO", "EPOCHREALTIME", "EPOCHSECONDS",
"FUNCNAME", "GROUPS", "MACHTYPE", "MAPFILE"
] ]
bashDynamicVars = [ "RANDOM", "SECONDS" ] bashDynamicVars = [ "RANDOM", "SECONDS" ]
dashVars = [ ] dashVars = [ "_" ]
isBashVariable var = isBashVariable var =
(var `elem` bashDynamicVars (var `elem` bashDynamicVars
|| var `elem` bashVars && not (isAssigned var)) || var `elem` bashVars && not (isAssigned var))
@ -317,31 +497,88 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
Assignment (_, _, name, _) -> name == var Assignment (_, _, name, _) -> name == var
_ -> False _ -> False
checkTestOp table op id = sequence_ $ do
(code, shells, msg) <- Map.lookup op table
guard . not $ shellType params `elem` shells
return $ warnMsg id code (msg op)
buildTestFlagMap list = Map.fromList $ concatMap (\(x,y) -> map (\c -> (c,y)) x) list
bashismBinaryTestFlags = buildTestFlagMap [
-- ([list of applicable flags],
-- (error code, exempt shells, message builder :: String -> String)),
--
-- Distinct error codes allow the wiki to give more helpful, targeted
-- information.
(["<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="],
(3012, [Dash, BusyboxSh], \op -> "lexicographical " ++ op ++ " is")),
(["=="],
(3014, [BusyboxSh], \op -> op ++ " in place of = is")),
(["=~"],
(3015, [], \op -> op ++ " regex matching is")),
([], (0,[],const ""))
]
bashismUnaryTestFlags = buildTestFlagMap [
(["-v"],
(3016, [], \op -> "test " ++ op ++ " (in place of [ -n \"${var+x}\" ]) is")),
(["-a"],
(3017, [], \op -> "unary " ++ op ++ " in place of -e is")),
(["-o"],
(3062, [], \op -> "test " ++ op ++ " to check options is")),
(["-R"],
(3063, [], \op -> "test " ++ op ++ " and namerefs in general are")),
(["-N"],
(3064, [], \op -> "test " ++ op ++ " is")),
(["-k"],
(3065, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")),
(["-G"],
(3066, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")),
(["-O"],
(3067, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")),
([], (0,[],const ""))
]
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')" 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_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 checkEchoSed = ForShell [Bash, Ksh] f
where 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]) = f (T_Pipeline id _ [a, b]) =
when (acmd == ["echo", "${VAR}"]) $ when (acmd == ["echo", "${VAR}"]) $
case bcmd of checkSed id bcmd
["sed", v] -> checkIn v
["sed", "-e", v] -> checkIn v
_ -> return ()
where where
-- 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
acmd = oversimplify a acmd = oversimplify a
bcmd = oversimplify b bcmd = oversimplify b
checkIn s =
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 = isJust $ do
[h:_,rest] <- matchRegex sedRe s
let delimiters = filter (== h) rest
guard $ length delimiters == 2
checkIn id s =
when (isSimpleSed s) $ when (isSimpleSed s) $
style id 2001 "See if you can use ${variable//search/replace} instead." style id 2001 "See if you can use ${variable//search/replace} instead."
f _ = return ()
prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}" prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}"
@ -365,11 +602,11 @@ checkBraceExpansionVars = ForShell [Bash] f
T_DollarBraced {} -> return "$" T_DollarBraced {} -> return "$"
T_DollarExpansion {} -> return "$" T_DollarExpansion {} -> return "$"
T_DollarArithmetic {} -> return "$" T_DollarArithmetic {} -> return "$"
otherwise -> return "-" _ -> return "-"
toString t = fromJust $ getLiteralStringExt literalExt t toString t = runIdentity $ getLiteralStringExt literalExt t
isEvaled t = do isEvaled t = do
cmd <- getClosestCommandM t cmd <- getClosestCommandM t
return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval" return $ maybe False (`isUnqualifiedCommand` "eval") cmd
prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3" prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3"
@ -384,13 +621,13 @@ checkMultiDimensionalArrays = ForShell [Bash] f
case token of case token of
T_Assignment _ _ name (first:second:_) _ -> about second T_Assignment _ _ name (first:second:_) _ -> about second
T_IndexedElement _ (first:second:_) _ -> about second T_IndexedElement _ (first:second:_) _ -> about second
T_DollarBraced {} -> T_DollarBraced _ _ l ->
when (isMultiDim token) $ about token when (isMultiDim l) $ about token
_ -> return () _ -> return ()
about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays." 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 re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
isMultiDim t = getBracedModifier (bracedString t) `matches` re isMultiDim l = getBracedModifier (concat $ oversimplify l) `matches` re
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '" prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
prop_checkPS11a = verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '" prop_checkPS11a = verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
@ -419,5 +656,46 @@ checkPS1Assignments = ForShell [Bash] f
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033" escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
prop_checkMultipleBangs1 = verify checkMultipleBangs "! ! true"
prop_checkMultipleBangs2 = verifyNot checkMultipleBangs "! true"
checkMultipleBangs = ForShell [Dash, BusyboxSh, Sh] f
where
f token = case token of
T_Banged id (T_Banged _ _) ->
err id 2325 "Multiple ! in front of pipelines are a bash/ksh extension. Use only 0 or 1."
_ -> return ()
prop_checkBangAfterPipe1 = verify checkBangAfterPipe "true | ! true"
prop_checkBangAfterPipe2 = verifyNot checkBangAfterPipe "true | ( ! true )"
prop_checkBangAfterPipe3 = verifyNot checkBangAfterPipe "! ! true | true"
checkBangAfterPipe = ForShell [Dash, BusyboxSh, Sh, Bash] f
where
f token = case token of
T_Pipeline _ _ cmds -> mapM_ check cmds
_ -> return ()
check token = case token of
T_Banged id _ ->
err id 2326 "! is not allowed in the middle of pipelines. Use command group as in cmd | { ! cmd; } if necessary."
_ -> return ()
prop_checkNegatedUnaryOps1 = verify checkNegatedUnaryOps "[ ! -o braceexpand ]"
prop_checkNegatedUnaryOps2 = verifyNot checkNegatedUnaryOps "[ -o braceexpand ]"
prop_checkNegatedUnaryOps3 = verifyNot checkNegatedUnaryOps "[[ ! -o braceexpand ]]"
prop_checkNegatedUnaryOps4 = verifyNot checkNegatedUnaryOps "! [ -o braceexpand ]"
prop_checkNegatedUnaryOps5 = verify checkNegatedUnaryOps "[ ! -a file ]"
checkNegatedUnaryOps = ForShell [Bash] f
where
f token = case token of
TC_Unary id SingleBracket "!" (TC_Unary _ _ op _) | op `elem` ["-a", "-o"] ->
err id 2332 $ msg op
_ -> return ()
msg "-o" = "[ ! -o opt ] is always true because -o becomes logical OR. Use [[ ]] or ! [ -o opt ]."
msg "-a" = "[ ! -a file ] is always true because -a becomes logical AND. Use -e instead."
msg _ = pleaseReport "unhandled negated unary message"
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View file

@ -2,33 +2,55 @@ module ShellCheck.Data where
import ShellCheck.Interface import ShellCheck.Interface
import Data.Version (showVersion) import Data.Version (showVersion)
import Paths_ShellCheck (version)
{-
If you are here because you saw an error about Paths_ShellCheck in this file,
simply comment out the import below and define the version as a constant string.
Instead of:
import Paths_ShellCheck (version)
shellcheckVersion = showVersion version shellcheckVersion = showVersion version
Use:
-- import Paths_ShellCheck (version)
shellcheckVersion = "kludge"
-}
import Paths_ShellCheck (version)
shellcheckVersion = showVersion version -- VERSIONSTRING
internalVariables = [ internalVariables = [
-- Generic -- Generic
"", "_", "rest", "REST", "", "_", "rest", "REST",
-- Bash -- Bash
"BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC", "BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
"BASH_ARGV", "BASH_CMDS", "BASH_COMMAND", "BASH_EXECUTION_STRING", "BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND",
"BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL", "BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_LOADABLES_PATH",
"BASH_VERSINFO", "BASH_VERSION", "COMP_CWORD", "COMP_KEY", "BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL", "BASH_VERSINFO",
"COMP_LINE", "COMP_POINT", "COMP_TYPE", "COMP_WORDBREAKS", "BASH_VERSION", "COMP_CWORD", "COMP_KEY", "COMP_LINE", "COMP_POINT",
"COMP_WORDS", "COPROC", "DIRSTACK", "EUID", "FUNCNAME", "GROUPS", "COMP_TYPE", "COMP_WORDBREAKS", "COMP_WORDS", "COPROC", "DIRSTACK",
"HISTCMD", "HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE", "EPOCHREALTIME", "EPOCHSECONDS", "EUID", "FUNCNAME", "GROUPS", "HISTCMD",
"OLDPWD", "OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD", "HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE", "OLDPWD",
"RANDOM", "READLINE_LINE", "READLINE_POINT", "REPLY", "SECONDS", "OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD", "RANDOM",
"SHELLOPTS", "SHLVL", "UID", "BASH_ENV", "BASH_XTRACEFD", "CDPATH", "READLINE_ARGUMENT", "READLINE_LINE", "READLINE_MARK", "READLINE_POINT",
"COLUMNS", "COMPREPLY", "EMACS", "ENV", "FCEDIT", "FIGNORE", "REPLY", "SECONDS", "SHELLOPTS", "SHLVL", "SRANDOM", "UID", "BASH_COMPAT",
"BASH_ENV", "BASH_XTRACEFD", "CDPATH", "CHILD_MAX", "COLUMNS",
"COMPREPLY", "EMACS", "ENV", "EXECIGNORE", "FCEDIT", "FIGNORE",
"FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE", "FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",
"HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS", "HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",
"IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE", "IGNOREEOF", "INPUTRC", "INSIDE_EMACS", "LANG", "LC_ALL", "LC_COLLATE",
"LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LINES", "MAIL", "LC_CTYPE", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME",
"MAILCHECK", "MAILPATH", "OPTERR", "PATH", "POSIXLY_CORRECT", "LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH",
"PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL", "POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1",
"TIMEFORMAT", "TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC", "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR",
"BASH_MONOSECONDS", "BASH_TRAPSIG", "GLOBSORT",
"auto_resume", "histchars",
-- Other -- Other
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY", "USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
@ -36,13 +58,37 @@ internalVariables = [
-- Ksh -- Ksh
, ".sh.version" , ".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"
-- Bats
,"stderr", "stderr_lines"
] ]
variablesWithoutSpaces = [ specialIntegerVariables = [
"$", "-", "?", "!", "#", "$", "?", "!", "#"
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO", ]
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES" specialVariablesWithoutSpaces = "-" : specialIntegerVariables
variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID",
"EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM",
"READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS",
"SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE",
"HISTSIZE", "LINES", "BASH_MONOSECONDS", "BASH_TRAPSIG"
-- shflags
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
]
specialVariables = specialVariablesWithoutSpaces ++ ["@", "*"]
unbracedVariables = specialVariables ++ [
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
] ]
arrayVariables = [ arrayVariables = [
@ -79,10 +125,10 @@ commonCommands = [
nonReadingCommands = [ nonReadingCommands = [
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown", "alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
"cp", "du", "echo", "export", "false", "fg", "fuser", "getconf", "cp", "du", "echo", "export", "fg", "fuser", "getconf",
"getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls", "getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir", "locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
"set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname" "set", "sleep", "touch", "trap", "ulimit", "unalias", "uname"
] ]
sampleWords = [ sampleWords = [
@ -98,6 +144,10 @@ binaryTestOps = [
"-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>=" "-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="
] ]
arithmeticBinaryTestOps = [
"-eq", "-ne", "-lt", "-le", "-gt", "-ge"
]
unaryTestOps = [ unaryTestOps = [
"!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p", "!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p",
"-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n", "-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n",
@ -109,9 +159,19 @@ shellForExecutable name =
case name of case name of
"sh" -> return Sh "sh" -> return Sh
"bash" -> return Bash "bash" -> return Bash
"bats" -> return Bash
"busybox" -> return BusyboxSh -- Used for directives and --shell=busybox
"busybox sh" -> return BusyboxSh
"busybox ash" -> return BusyboxSh
"dash" -> return Dash "dash" -> return Dash
"ash" -> return Dash -- There's also a warning for this. "ash" -> return Dash -- There's also a warning for this.
"ksh" -> return Ksh "ksh" -> return Ksh
"ksh88" -> return Ksh "ksh88" -> return Ksh
"ksh93" -> return Ksh "ksh93" -> return Ksh
otherwise -> Nothing "oksh" -> return Ksh
_ -> Nothing
flagsForRead = "sreu:n:N:i:p:a:t:"
flagsForMapfile = "d:n:O:s:u:C:c:t"
declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"]

313
src/ShellCheck/Debug.hs Normal file
View file

@ -0,0 +1,313 @@
{-
This file contains useful functions for debugging and developing ShellCheck.
To invoke them interactively, run:
cabal repl
At the ghci prompt, enter:
:load ShellCheck.Debug
You can now invoke the functions. Here are some examples:
shellcheckString "echo $1"
stringToAst "(( x+1 ))"
stringToCfg "if foo; then bar; else baz; fi"
writeFile "/tmp/test.dot" $ stringToCfgViz "while foo; do bar; done"
The latter file can be rendered to png with GraphViz:
dot -Tpng /tmp/test.dot > /tmp/test.png
To run all unit tests in a module:
ShellCheck.Parser.runTests
ShellCheck.Analytics.runTests
To run a specific test:
:load ShellCheck.Analytics
prop_checkUuoc3
If you make code changes, reload in seconds at any time with:
:r
===========================================================================
Crash course in printf debugging in Haskell:
import Debug.Trace
greet 0 = return ()
-- Print when a function is invoked
greet n | trace ("calling greet " ++ show n) False = undefined
greet n = do
putStrLn "Enter name"
name <- getLine
-- Print at some point in any monadic function
traceM $ "user entered " ++ name
putStrLn $ "Hello " ++ name
-- Print a value before passing it on
greet $ traceShowId (n - 1)
===========================================================================
If you want to invoke `ghci` directly, such as on `shellcheck.hs`, to
debug all of ShellCheck including I/O, you may see an error like this:
src/ShellCheck/Data.hs:5:1: error:
Could not load module Paths_ShellCheck
it is a hidden module in the package ShellCheck-0.8.0
This can easily be circumvented by running `./setgitversion` or manually
editing src/ShellCheck/Data.hs to replace the auto-deduced version number
with a constant string as indicated.
Afterwards, you can run the ShellCheck tool, as if from the shell, with:
$ ghci shellcheck.hs
ghci> runMain ["-x", "file.sh"]
-}
module ShellCheck.Debug () where
import ShellCheck.Analyzer
import ShellCheck.AST
import ShellCheck.CFG
import ShellCheck.Checker
import ShellCheck.CFGAnalysis as CF
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Prelude
import Control.Monad
import Control.Monad.Identity
import Control.Monad.RWS
import Control.Monad.Writer
import Data.Graph.Inductive.Graph as G
import Data.List
import Data.Maybe
import qualified Data.Map as M
import qualified Data.Set as S
-- Run all of ShellCheck (minus output formatters)
shellcheckString :: String -> CheckResult
shellcheckString scriptString =
runIdentity $ checkScript dummySystemInterface checkSpec
where
checkSpec :: CheckSpec
checkSpec = emptyCheckSpec {
csScript = scriptString
}
dummySystemInterface :: SystemInterface Identity
dummySystemInterface = mockedSystemInterface [
-- A tiny, fake filesystem for sourced files
("lib/mylib1.sh", "foo=$(cat $1 | wc -l)"),
("lib/mylib2.sh", "bar=42")
]
-- Parameters used when generating Control Flow Graphs
cfgParams :: CFGParameters
cfgParams = CFGParameters {
cfLastpipe = False,
cfPipefail = False
}
-- An example script to play with
exampleScript :: String
exampleScript = unlines [
"#!/bin/sh",
"count=0",
"for file in *",
"do",
" (( count++ ))",
"done",
"echo $count"
]
-- Parse the script string into ShellCheck's ParseResult
parseScriptString :: String -> ParseResult
parseScriptString scriptString =
runIdentity $ parseScript dummySystemInterface parseSpec
where
parseSpec :: ParseSpec
parseSpec = newParseSpec {
psFilename = "myscript",
psScript = scriptString
}
-- Parse the script string into an Abstract Syntax Tree
stringToAst :: String -> Token
stringToAst scriptString =
case maybeRoot of
Just root -> root
Nothing -> error $ "Script failed to parse: " ++ show parserWarnings
where
parseResult :: ParseResult
parseResult = parseScriptString scriptString
maybeRoot :: Maybe Token
maybeRoot = prRoot parseResult
parserWarnings :: [PositionedComment]
parserWarnings = prComments parseResult
astToCfgResult :: Token -> CFGResult
astToCfgResult = buildGraph cfgParams
astToDfa :: Token -> CFGAnalysis
astToDfa = analyzeControlFlow cfgParams
astToCfg :: Token -> CFGraph
astToCfg = cfGraph . astToCfgResult
stringToCfg :: String -> CFGraph
stringToCfg = astToCfg . stringToAst
stringToDfa :: String -> CFGAnalysis
stringToDfa = astToDfa . stringToAst
cfgToGraphViz :: CFGraph -> String
cfgToGraphViz = cfgToGraphVizWith show
stringToCfgViz :: String -> String
stringToCfgViz = cfgToGraphViz . stringToCfg
stringToDfaViz :: String -> String
stringToDfaViz = dfaToGraphViz . stringToDfa
-- Dump a Control Flow Graph as GraphViz with extended information
stringToDetailedCfgViz :: String -> String
stringToDetailedCfgViz scriptString = cfgToGraphVizWith nodeLabel graph
where
ast :: Token
ast = stringToAst scriptString
cfgResult :: CFGResult
cfgResult = astToCfgResult ast
graph :: CFGraph
graph = cfGraph cfgResult
idToToken :: M.Map Id Token
idToToken = M.fromList $ execWriter $ doAnalysis (\c -> tell [(getId c, c)]) ast
idToNode :: M.Map Id (Node, Node)
idToNode = cfIdToRange cfgResult
nodeToStartIds :: M.Map Node (S.Set Id)
nodeToStartIds =
M.fromListWith S.union $
map (\(id, (start, _)) -> (start, S.singleton id)) $
M.toList idToNode
nodeToEndIds :: M.Map Node (S.Set Id)
nodeToEndIds =
M.fromListWith S.union $
map (\(id, (_, end)) -> (end, S.singleton id)) $
M.toList idToNode
formatId :: Id -> String
formatId id = fromMaybe ("Unknown " ++ show id) $ do
(OuterToken _ token) <- M.lookup id idToToken
firstWord <- words (show token) !!! 0
-- Strip off "Inner_"
(_ : tokenName) <- return $ dropWhile (/= '_') firstWord
return $ tokenName ++ " " ++ show id
formatGroup :: S.Set Id -> String
formatGroup set = intercalate ", " $ map formatId $ S.toList set
nodeLabel (node, label) = unlines [
show node ++ ". " ++ show label,
"Begin: " ++ formatGroup (M.findWithDefault S.empty node nodeToStartIds),
"End: " ++ formatGroup (M.findWithDefault S.empty node nodeToEndIds)
]
-- Dump a Control Flow Graph with Data Flow Analysis as GraphViz
dfaToGraphViz :: CF.CFGAnalysis -> String
dfaToGraphViz analysis = cfgToGraphVizWith label $ CF.graph analysis
where
label (node, label) =
let
desc = show node ++ ". " ++ show label
in
fromMaybe ("No DFA available\n\n" ++ desc) $ do
(pre, post) <- M.lookup node $ CF.nodeToData analysis
return $ unlines [
"Precondition: " ++ show pre,
"",
desc,
"",
"Postcondition: " ++ show post
]
-- Dump an Control Flow Graph to GraphViz with a given node formatter
cfgToGraphVizWith :: (LNode CFNode -> String) -> CFGraph -> String
cfgToGraphVizWith nodeLabel graph = concat [
"digraph {\n",
concatMap dumpNode (labNodes graph),
concatMap dumpLink (labEdges graph),
tagVizEntries graph,
"}\n"
]
where
dumpNode l@(node, label) = show node ++ " [label=" ++ quoteViz (nodeLabel l) ++ "]\n"
dumpLink (from, to, typ) = show from ++ " -> " ++ show to ++ " [style=" ++ quoteViz (edgeStyle typ) ++ "]\n"
edgeStyle CFEFlow = "solid"
edgeStyle CFEExit = "bold"
edgeStyle CFEFalseFlow = "dotted"
quoteViz str = "\"" ++ escapeViz str ++ "\""
escapeViz [] = []
escapeViz (c:rest) =
case c of
'\"' -> '\\' : '\"' : escapeViz rest
'\n' -> '\\' : 'l' : escapeViz rest
'\\' -> '\\' : '\\' : escapeViz rest
_ -> c : escapeViz rest
-- Dump an Abstract Syntax Tree (or branch thereof) to GraphViz format
astToGraphViz :: Token -> String
astToGraphViz token = concat [
"digraph {\n",
formatTree token,
"}\n"
]
where
formatTree :: Token -> String
formatTree t = snd $ execRWS (doStackAnalysis push pop t) () []
push :: Token -> RWS () String [Int] ()
push (OuterToken (Id n) inner) = do
stack <- get
put (n : stack)
case stack of
[] -> return ()
(top:_) -> tell $ show top ++ " -> " ++ show n ++ "\n"
tell $ show n ++ " [label=" ++ quoteViz (show n ++ ": " ++ take 32 (show inner)) ++ "]\n"
pop :: Token -> RWS () String [Int] ()
pop _ = modify tail
-- For each entry point, set the rank so that they'll align in the graph
tagVizEntries :: CFGraph -> String
tagVizEntries graph = "{ rank=same " ++ rank ++ " }"
where
entries = mapMaybe find $ labNodes graph
find (node, CFEntryPoint name) = return (node, name)
find _ = Nothing
rank = unwords $ map (\(c, _) -> show c) entries

412
src/ShellCheck/Fixer.hs Normal file
View file

@ -0,0 +1,412 @@
{-
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 ShellCheck.Prelude
import Control.Monad
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 =
xEnd > yStart && 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 = (<>)
mconcat = foldl mappend mempty -- fold left to right since <> discards right on overlap
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
z = drop (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 $ pleaseReport "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 target `compare` pivot of
LT -> f sum target left
GT -> f (sum+cumulative) target right
EQ -> 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 key `compare` pivot of
LT -> PSBranch pivot (f left) right (sum + value)
GT -> PSBranch pivot left (f right) sum
EQ -> 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 [v | (k,v) <- kvs, k <= target]
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,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@ -24,8 +24,8 @@ import ShellCheck.Formatter.Format
import Data.Char import Data.Char
import Data.List import Data.List
import GHC.Exts
import System.IO import System.IO
import qualified Data.List.NonEmpty as NE
format :: IO Formatter format :: IO Formatter
format = return Formatter { format = return Formatter {
@ -45,12 +45,12 @@ outputResults cr sys =
else mapM_ outputGroup fileGroups else mapM_ outputGroup fileGroups
where where
comments = crComments cr comments = crComments cr
fileGroups = groupWith sourceFile comments fileGroups = NE.groupWith sourceFile comments
outputGroup group = do outputGroup group = do
let filename = sourceFile (head group) let filename = sourceFile (NE.head group)
result <- (siReadFile sys) filename result <- siReadFile sys (Just True) filename
let contents = either (const "") id result let contents = either (const "") id result
outputFile filename contents group outputFile filename contents (NE.toList group)
outputFile filename contents warnings = do outputFile filename contents warnings = do
let comments = makeNonVirtual warnings contents let comments = makeNonVirtual warnings contents

View file

@ -0,0 +1,254 @@
{-
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
format :: FormatterOptions -> IO Formatter
format options = do
foundIssues <- newIORef False
reportedIssues <- newIORef False
shouldColor <- shouldOutputColor (foColorOption options)
let color = if shouldColor then colorize else nocolor
return Formatter {
header = return (),
footer = checkFooter foundIssues reportedIssues color,
onFailure = reportFailure color,
onResult = reportResult foundIssues reportedIssues 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 foundIssues reportedIssues color = do
found <- readIORef foundIssues
output <- readIORef reportedIssues
when (found && not 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) -> (IORef Bool) -> ColorFunc -> CheckResult -> SystemInterface IO -> IO ()
reportResult foundIssues reportedIssues color result sys = do
let comments = crComments result
unless (null comments) $ writeIORef foundIssues True
let suggestedFixes = mapMaybe pcFix comments
let fixmap = buildFixMap suggestedFixes
mapM_ output $ M.toList fixmap
where
output (name, fix) = do
file <- siReadFile sys (Just True) name
case file of
Right contents -> do
putStrLn $ formatDoc color $ makeDiff name contents fix
writeIORef reportedIssues 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 = splitFixByFile $ mconcat fixes
perFile = groupByMap (posFile . repStartPos . head . fixReplacements) splitFixes
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,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@ -21,6 +21,14 @@ module ShellCheck.Formatter.Format where
import ShellCheck.Data import ShellCheck.Data
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Fixer
import Control.Monad
import Data.Array
import Data.List
import System.IO
import System.Info
import System.Environment
-- A formatter that carries along an arbitrary piece of data -- A formatter that carries along an arbitrary piece of data
data Formatter = Formatter { data Formatter = Formatter {
@ -50,21 +58,25 @@ severityText pc =
makeNonVirtual comments contents = makeNonVirtual comments contents =
map fix comments map fix comments
where where
ls = lines contents list = lines contents
fix c = c { arr = listArray (1, length list) list
pcStartPos = (pcStartPos c) { untabbedFix f = newFix {
posColumn = realignColumn lineNo colNo c fixReplacements = map (\r -> removeTabStops r arr) (fixReplacements f)
} }
, pcEndPos = (pcEndPos c) { fix c = (removeTabStops c arr) {
posColumn = realignColumn endLineNo endColNo c pcFix = fmap untabbedFix (pcFix c)
} }
}
realignColumn lineNo colNo c =
if lineNo c > 0 && lineNo c <= fromIntegral (length ls) shouldOutputColor :: ColorOption -> IO Bool
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c) shouldOutputColor colorOption =
else colNo c case colorOption of
real _ r v target | target <= v = r ColorAlways -> return True
real [] r v _ = r -- should never happen ColorNever -> return False
real ('\t':rest) r v target = ColorAuto -> do
real rest (r+1) (v + 8 - (v `mod` 8)) target isTerminal <- hIsTerminalDevice stdout
real (_:rest) r v target = real rest (r+1) (v+1) target term <- lookupEnv "TERM"
let windows = "mingw" `isPrefixOf` os
let dumbTerm = term `elem` [Just "dumb", Just "", Nothing]
let isUsableTty = isTerminal && not windows && not dumbTerm
return isUsableTty

View file

@ -1,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@ -23,8 +23,8 @@ import ShellCheck.Interface
import ShellCheck.Formatter.Format import ShellCheck.Formatter.Format
import Data.List import Data.List
import GHC.Exts
import System.IO import System.IO
import qualified Data.List.NonEmpty as NE
format :: IO Formatter format :: IO Formatter
format = return Formatter { format = return Formatter {
@ -39,13 +39,13 @@ outputError file error = hPutStrLn stderr $ file ++ ": " ++ error
outputAll cr sys = mapM_ f groups outputAll cr sys = mapM_ f groups
where where
comments = crComments cr comments = crComments cr
groups = groupWith sourceFile comments groups = NE.groupWith sourceFile comments
f :: [PositionedComment] -> IO () f :: NE.NonEmpty PositionedComment -> IO ()
f group = do f group = do
let filename = sourceFile (head group) let filename = sourceFile (NE.head group)
result <- (siReadFile sys) filename result <- siReadFile sys (Just True) filename
let contents = either (const "") id result let contents = either (const "") id result
outputResult filename contents group outputResult filename contents (NE.toList group)
outputResult filename contents warnings = do outputResult filename contents warnings = do
let comments = makeNonVirtual warnings contents let comments = makeNonVirtual warnings contents

View file

@ -0,0 +1,128 @@
{-# 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 Control.DeepSeq
import Data.Aeson
import Data.IORef
import Data.Monoid
import System.IO
import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.List.NonEmpty as NE
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 = NE.groupWith sourceFile comments
f :: NE.NonEmpty PositionedComment -> IO ()
f group = do
let filename = sourceFile (NE.head group)
result <- siReadFile sys (Just True) filename
let contents = either (const "") id result
let comments' = makeNonVirtual comments contents
deepseq comments' $ modifyIORef ref (\x -> comments' ++ x)
finish ref = do
list <- readIORef ref
BL.putStrLn $ encode $ Json1Output { comments = list }

View file

@ -0,0 +1,36 @@
{-
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
}

File diff suppressed because it is too large Load diff

51
src/ShellCheck/Prelude.hs Normal file
View file

@ -0,0 +1,51 @@
{-
Copyright 2022 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/>.
-}
-- Generic basic utility functions
module ShellCheck.Prelude where
import Data.Semigroup
-- Get element 0 or a default. Like `head` but safe.
headOrDefault _ (a:_) = a
headOrDefault def _ = def
-- Get the last element or a default. Like `last` but safe.
lastOrDefault def [] = def
lastOrDefault _ list = last list
--- Get element n of a list, or Nothing. Like `!!` but safe.
(!!!) list i =
case drop i list of
[] -> Nothing
(r:_) -> Just r
-- Like mconcat but for Semigroups
sconcat1 :: (Semigroup t) => [t] -> t
sconcat1 [x] = x
sconcat1 (x:xs) = x <> sconcat1 xs
sconcatOrDefault def [] = def
sconcatOrDefault _ list = sconcat1 list
-- For more actionable "impossible" errors
pleaseReport str = "ShellCheck internal error, please report: " ++ str

View file

@ -1,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@ -30,7 +30,7 @@ import Text.Regex.TDFA
-- Precompile the regex -- Precompile the regex
mkRegex :: String -> Regex mkRegex :: String -> Regex
mkRegex str = mkRegex str =
let make :: RegexMaker Regex CompOption ExecOption String => String -> Regex let make :: String -> Regex
make = makeRegex make = makeRegex
in in
make str make str

View file

@ -2,7 +2,7 @@
# For more information, see: https://docs.haskellstack.org/en/stable/yaml_configuration/ # For more information, see: https://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) # Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
resolver: lts-8.5 resolver: lts-18.15
# Local packages, usually specified by relative directory name # Local packages, usually specified by relative directory name
packages: packages:

View file

@ -29,6 +29,7 @@ detestify() {
state = 0; state = 0;
} }
/STRIP/ { next; }
/LANGUAGE TemplateHaskell/ { next; } /LANGUAGE TemplateHaskell/ { next; }
/^import.*Test\./ { next; } /^import.*Test\./ { next; }
@ -75,4 +76,3 @@ find . -name '.git' -prune -o -type f -name '*.hs' -print |
do do
modify "$file" detestify modify "$file" detestify
done done

View file

@ -11,21 +11,37 @@ command -v cabal ||
cabal update || cabal update ||
die "can't update" die "can't update"
cabal install --dependencies-only --enable-tests ||
if [ -e /etc/arch-release ]
then
# Arch has an unconventional packaging setup
flags=(--disable-library-vanilla --enable-shared --enable-executable-dynamic --ghc-options=-dynamic)
else
flags=()
fi
cabal install --dependencies-only --enable-tests "${flags[@]}" ||
cabal install --dependencies-only "${flags[@]}" ||
cabal install --dependencies-only --max-backjumps -1 "${flags[@]}" ||
die "can't install dependencies" die "can't install dependencies"
cabal configure --enable-tests || cabal configure --enable-tests "${flags[@]}" ||
die "configure failed" die "configure failed"
cabal build || cabal build ||
die "build failed" die "build failed"
cabal test || cabal test ||
die "test failed" die "test failed"
cabal haddock ||
die "haddock failed"
dist/build/shellcheck/shellcheck - << 'EOF' || die "execution failed" sc="$(find . -name shellcheck -type f -perm -111)"
[ -x "$sc" ] || die "Can't find executable"
"$sc" - << 'EOF' || die "execution failed"
#!/bin/sh #!/bin/sh
echo "Hello World" echo "Hello World"
EOF EOF
dist/build/shellcheck/shellcheck - << 'EOF' && die "negative execution failed" "$sc" - << 'EOF' && die "negative execution failed"
#!/bin/sh #!/bin/sh
echo $1 echo $1
EOF EOF

84
test/check_release Executable file
View file

@ -0,0 +1,84 @@
#!/usr/bin/env bash
# shellcheck disable=SC2257
failed=0
fail() {
echo "$(tput setaf 1)$*$(tput sgr0)"
failed=1
}
if git diff | grep -q ""
then
fail "There are uncommitted changes"
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
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
if [[ $(git log -1 --pretty=%B) != "Stable version "* ]]
then
fail "Expected git log message to be 'Stable version ...'"
fi
if [[ $(git log -1 --pretty=%B) != *"CHANGELOG"* ]]
then
fail "Expected git log message to contain CHANGELOG"
fi
i=1 j=1
cat << EOF
Manual Checklist
$((i++)). Make sure none of the automated checks above failed
$((i++)). Run \`build/build_builder build/*/\` to update all builder images.
$((j++)). \`build/run_builder dist-newstyle/sdist/ShellCheck-*.tar.gz build/*/\` to verify that they work.
$((j++)). \`for f in \$(cat build/*/tag); do docker push "\$f"; done\` to upload them.
$((i++)). Run test/distrotest to ensure that most distros can build OOTB.
$((i++)). Make sure GitHub Build currently passes: https://github.com/koalaman/shellcheck/actions
$((i++)). Make sure SnapCraft build currently works: https://build.snapcraft.io/user/koalaman
$((i++)). Format and read over the manual for bad formatting and outdated info.
$((i++)). Make sure the Hackage package builds locally.
Release Steps
$((j++)). \`cabal sdist\` to generate a Hackage package
$((j++)). \`git push --follow-tags\` to push commit
$((j++)). Wait for GitHub Actions to build.
$((j++)). Verify release:
a. Check that the new versions are uploaded: https://github.com/koalaman/shellcheck/tags
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

@ -16,10 +16,21 @@ and is still highly experimental.
Make sure you're plugged in and have screen/tmux in place, Make sure you're plugged in and have screen/tmux in place,
then re-run with $0 --run to continue. then re-run with $0 --run to continue.
Also note that dist*/ and .stack-work/ will be deleted.
EOF EOF
exit 0 exit 0
} }
echo "Deleting 'dist', 'dist-newstyle', and '.stack-work'..."
rm -rf dist dist-newstyle .stack-work
execs=$(find . -name shellcheck)
if [ -n "$execs" ]
then
die "Found unexpected executables. Remove and try again: $execs"
fi
log=$(mktemp) || die "Can't create temp file" log=$(mktemp) || die "Can't create temp file"
date >> "$log" || die "Can't write to log" date >> "$log" || die "Can't write to log"
@ -57,19 +68,18 @@ done << EOF
debian:stable apt-get update && apt-get install -y cabal-install debian:stable apt-get update && apt-get install -y cabal-install
debian:testing 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 ubuntu:latest apt-get update && apt-get install -y cabal-install
opensuse:latest zypper install -y cabal-install ghc
# Older Ubuntu versions we want to support
ubuntu:18.04 apt-get update && apt-get install -y cabal-install
ubuntu:17.10 apt-get update && apt-get install -y cabal-install
# Misc
haskell:latest true haskell:latest true
opensuse/leap:latest zypper install -y cabal-install ghc
fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils libstdc++-static gcc-c++
archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
# Known to currently fail # Ubuntu LTS
centos:latest yum install -y epel-release && yum install -y cabal-install ubuntu:24.04 apt-get update && apt-get install -y cabal-install
fedora:latest dnf install -y cabal-install ubuntu:22.04 apt-get update && apt-get install -y cabal-install
base/archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel ubuntu:20.04 apt-get update && apt-get install -y cabal-install
# Stack on Ubuntu LTS
ubuntu:24.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
EOF EOF
exit "$final" exit "$final"

View file

@ -2,23 +2,40 @@ module Main where
import Control.Monad import Control.Monad
import System.Exit import System.Exit
import qualified ShellCheck.Checker
import qualified ShellCheck.Analytics import qualified ShellCheck.Analytics
import qualified ShellCheck.AnalyzerLib import qualified ShellCheck.AnalyzerLib
import qualified ShellCheck.Parser import qualified ShellCheck.ASTLib
import qualified ShellCheck.CFG
import qualified ShellCheck.CFGAnalysis
import qualified ShellCheck.Checker
import qualified ShellCheck.Checks.Commands import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.ControlFlow
import qualified ShellCheck.Checks.Custom
import qualified ShellCheck.Checks.ShellSupport import qualified ShellCheck.Checks.ShellSupport
import qualified ShellCheck.Fixer
import qualified ShellCheck.Formatter.Diff
import qualified ShellCheck.Parser
main = do main = do
putStrLn "Running ShellCheck tests..." putStrLn "Running ShellCheck tests..."
results <- sequence [ failures <- filter (not . snd) <$> mapM sequenceA tests
ShellCheck.Checker.runTests, if null failures then exitSuccess else do
ShellCheck.Checks.Commands.runTests, putStrLn "Tests failed for the following module(s):"
ShellCheck.Checks.ShellSupport.runTests, mapM (putStrLn . ("- ShellCheck." ++) . fst) failures
ShellCheck.Analytics.runTests, exitFailure
ShellCheck.AnalyzerLib.runTests, where
ShellCheck.Parser.runTests tests =
[ ("Analytics" , ShellCheck.Analytics.runTests)
, ("AnalyzerLib" , ShellCheck.AnalyzerLib.runTests)
, ("ASTLib" , ShellCheck.ASTLib.runTests)
, ("CFG" , ShellCheck.CFG.runTests)
, ("CFGAnalysis" , ShellCheck.CFGAnalysis.runTests)
, ("Checker" , ShellCheck.Checker.runTests)
, ("Checks.Commands" , ShellCheck.Checks.Commands.runTests)
, ("Checks.ControlFlow" , ShellCheck.Checks.ControlFlow.runTests)
, ("Checks.Custom" , ShellCheck.Checks.Custom.runTests)
, ("Checks.ShellSupport", ShellCheck.Checks.ShellSupport.runTests)
, ("Fixer" , ShellCheck.Fixer.runTests)
, ("Formatter.Diff" , ShellCheck.Formatter.Diff.runTests)
, ("Parser" , ShellCheck.Parser.runTests)
] ]
if and results
then exitSuccess
else exitFailure

28
test/stacktest Executable file
View file

@ -0,0 +1,28 @@
#!/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 --allow-different-user || die "Failed to setup with default resolver"
stack build --test || die "Failed to build/test with default resolver"
# Nice to haves, but not necessary
for resolver in "${resolvers[@]}"
do
stack --resolver="$resolver" setup || die "Failed to setup $resolver. This probably doesn't matter."
stack --resolver="$resolver" build --test || die "Failed build/test with $resolver! This probably doesn't matter."
done
echo "Success"