Compare commits

..

No commits in common. "master" and "v0.7.1" have entirely different histories.

82 changed files with 2090 additions and 9444 deletions

73
.compile_binaries Executable file
View file

@ -0,0 +1,73 @@
#!/bin/bash
build_linux() {
# Linux Docker image
name="$DOCKER_BASE"
DOCKER_BUILDS="$DOCKER_BUILDS $name"
docker build -t "$name:current" .
docker run "$name:current" --version
printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
docker run -v "$PWD:/mnt" "$name:current" myscript
# Copy static executable from docker image
id=$(docker create "$name:current")
docker cp "$id:/bin/shellcheck" "shellcheck"
docker rm "$id"
ls -l shellcheck
./shellcheck myscript
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-x86_64";
done
}
build_aarch64() {
# Linux aarch64 static executable
docker run -v "$PWD:/mnt" koalaman/aarch64-builder 'buildsc'
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-aarch64"
done
}
build_armv6hf() {
# Linux armv6hf static executable
docker run -v "$PWD:/mnt" koalaman/armv6hf-builder -c 'compile-shellcheck'
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-armv6hf";
done
}
build_windows() {
# Windows .exe
docker run -v "$PWD:/appdata" koalaman/winghc cuib
for tag in $TAGS
do
cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe";
done
}
build_osx() {
# Darwin x86_64 executable
brew update
brew install cabal-install pandoc gnu-tar
sudo ln -sf /usr/local/bin/gsha512sum /usr/local/bin/sha512sum
sudo ln -sf /usr/local/bin/gtar /usr/local/bin/tar
export PATH="/usr/local/bin:$PATH"
cabal update
cabal install --dependencies-only
cabal build shellcheck
# Cabal 3 no longer has a predictable output path
path="$(find . -name 'shellcheck' -type f -perm +111)"
[[ -e "$path" ]]
for tag in $TAGS
do
cp "$path" "deploy/shellcheck-$tag.darwin-x86_64";
done
}

View file

@ -2,10 +2,10 @@
- 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"):
- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086) - [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
- [ ] I tried on https://www.shellcheck.net/ and verified that this is still a problem on the latest commit - [ ] I tried on 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
- [ ] https://www.shellcheck.net/ (i.e. the latest commit) currently gives no useful warnings about this - [ ] 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

View file

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

View file

@ -1,159 +0,0 @@
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 )

View file

@ -2,10 +2,39 @@
set -x set -x
shopt -s extglob shopt -s extglob
if [[ "$TRAVIS_SECURE_ENV_VARS" != "true" ]]
then
echo >&2 "Missing TRAVIS_SECURE_ENV_VARS. Skipping GitHub deployment."
exit 0
fi
install_deps() {
version="2.7.0" # 2.14.1 fails to overwrite duplicates
case "$(uname)" in
Linux)
sudo apt-get update
sudo apt-get install curl
curl -L "https://github.com/github/hub/releases/download/v$version/hub-linux-amd64-$version.tgz" | tar xvz --strip-components=1 "hub-linux-amd64-$version/bin/hub"
;;
Darwin)
curl -L "https://github.com/github/hub/releases/download/v$version/hub-darwin-amd64-$version.tgz" | tar xvz --strip-components=1 "hub-darwin-amd64-$version/bin/hub"
;;
*)
echo "Unknown: $(uname)"
exit 1
;;
esac
hub_path="$PWD/bin/hub"
hub() {
"$hub_path" "$@"
}
}
install_deps
export EDITOR="touch" export EDITOR="touch"
# Sanity check # Sanity check
gh --version || exit 1
hub release show latest || exit 1 hub release show latest || exit 1
for tag in $TAGS for tag in $TAGS
@ -21,8 +50,8 @@ do
for file in deploy/* for file in deploy/*
do do
[[ $file == *.@(xz|gz|zip) ]] || continue [[ $file == *.@(xz|gz|zip) ]] || continue
[[ $file == *"$tag"* ]] || continue files+=(-a "$file")
files+=("$file")
done done
gh release upload "$tag" "${files[@]}" --clobber || exit 1 hub release edit "${files[@]}" "$tag" || exit 1
done done

1
.gitignore vendored
View file

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

View file

@ -1,12 +1,36 @@
#!/bin/bash #!/bin/bash
# This script builds and deploys multi-architecture docker images from the # This script builds and deploys multi-architecture docker images from the
# binaries previously built and deployed to GitHub. # binaries previously built and deployed to GCS by the Travis pipeline.
if [[ "$TRAVIS_SECURE_ENV_VARS" != "true" ]]
then
echo >&2 "Missing TRAVIS_SECURE_ENV_VARS. Skipping Docker builds."
exit 0
fi
function multi_arch_docker::install_docker_buildx() { function multi_arch_docker::install_docker_buildx() {
# Install up-to-date version of docker, with buildx support.
local -r docker_apt_repo='https://download.docker.com/linux/ubuntu'
curl -fsSL "${docker_apt_repo}/gpg" | sudo apt-key add -
local -r os="$(lsb_release -cs)"
sudo add-apt-repository "deb [arch=amd64] $docker_apt_repo $os stable"
sudo apt-get update
sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce
# Enable docker daemon experimental support (for 'pull --platform').
local -r config='/etc/docker/daemon.json'
if [[ -e "$config" ]]; then
sudo sed -i -e 's/{/{ "experimental": true, /' "$config"
else
echo '{ "experimental": true }' | sudo tee "$config"
fi
sudo systemctl restart docker
# Install QEMU multi-architecture support for docker buildx. # Install QEMU multi-architecture support for docker buildx.
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
# Instantiate docker buildx builder with multi-architecture support. # Instantiate docker buildx builder with multi-architecture support.
export DOCKER_CLI_EXPERIMENTAL=enabled
docker buildx create --name mybuilder docker buildx create --name mybuilder
docker buildx use mybuilder docker buildx use mybuilder
# Start up buildx and verify that all is OK. # Start up buildx and verify that all is OK.
@ -80,10 +104,10 @@ function multi_arch_docker::main() {
export DOCKER_PLATFORMS='linux/amd64' export DOCKER_PLATFORMS='linux/amd64'
DOCKER_PLATFORMS+=' linux/arm64' DOCKER_PLATFORMS+=' linux/arm64'
DOCKER_PLATFORMS+=' linux/arm/v6' DOCKER_PLATFORMS+=' linux/arm/v6'
DOCKER_PLATFORMS+=' linux/riscv64'
multi_arch_docker::install_docker_buildx multi_arch_docker::install_docker_buildx
multi_arch_docker::login_to_docker_hub multi_arch_docker::login_to_docker_hub
multi_arch_docker::build_and_push_all multi_arch_docker::build_and_push_all
set +x
multi_arch_docker::test_all multi_arch_docker::test_all
} }

View file

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

14
.snapsquid.conf Normal file
View file

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

64
.travis.yml Normal file
View file

@ -0,0 +1,64 @@
language: shell
os: linux
services:
- docker
jobs:
include:
- stage: Build
# This must weirdly not have a dash, otherwise an empty job is created
env: BUILD=linux
- env: BUILD=windows
- env: BUILD=armv6hf
- env: BUILD=aarch64
- env: BUILD=osx
os: osx
- stage: Deploy docker image
# Deploy only for pushes to master branch, not other branches, not PRs.
if: type = push
script:
- source ./.multi_arch_docker
- set -ex; multi_arch_docker::main; set +x
# This is in global context and runs for every stage that doesn't override it.
before_install: |
DOCKER_BASE="$DOCKER_USERNAME/shellcheck"
DOCKER_BUILDS=""
export TAGS=""
test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
echo "Tags are $TAGS"
# This is in global context and runs for every stage that doesn't override it.
script:
- mkdir -p deploy
- source ./.compile_binaries
- ./striptests
- set -ex; build_"$BUILD"; set +x;
- ./.prepare_deploy
- ./.github_deploy
# This is in global context and runs for every stage that doesn't override it.
after_failure: |
id
pwd
df -h
find . -name '*.log' -type f -exec grep "" /dev/null {} +
find . -ls
# This is in global context and runs for every stage that doesn't override it.
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-private
local_dir: deploy
on:
repo: koalaman/shellcheck
condition: $TRAVIS_BUILD_STAGE_NAME = Build
all_branches: true

View file

@ -1,135 +1,3 @@
## 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 ## v0.7.1 - 2020-04-04
### Fixed ### Fixed
- `-f diff` no longer claims that it found more issues when it didn't - `-f diff` no longer claims that it found more issues when it didn't
@ -270,7 +138,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 deprecated egrep/fgrep - SC2196/SC2197: Warn about deprected 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
@ -287,7 +155,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 delimiters with esoteric quoting like `foo""` are now handled - Here doc delimters 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

29
Dockerfile Normal file
View file

@ -0,0 +1,29 @@
# Build-only image
FROM ubuntu:18.04 AS build
USER root
WORKDIR /opt/shellCheck
# Install OS deps
RUN apt-get update && apt-get install -y ghc cabal-install
# Install Haskell deps
# (This is a separate copy/run so that source changes don't require rebuilding)
COPY ShellCheck.cabal ./
RUN cabal update && cabal install --dependencies-only --ghc-options="-optlo-Os -split-sections"
# Copy source and build it
COPY LICENSE 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 ShellCheck image
FROM scratch
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
WORKDIR /mnt
COPY --from=build /out /
ENTRYPOINT ["/bin/shellcheck"]

12
LICENSE
View file

@ -1,3 +1,13 @@
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
@ -671,4 +681,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/licenses/why-not-lgpl.html>. <https://www.gnu.org/philosophy/why-not-lgpl.html>.

View file

@ -1,5 +1,4 @@
[![Build Status](https://github.com/koalaman/shellcheck/actions/workflows/build.yml/badge.svg)](https://github.com/koalaman/shellcheck/actions/workflows/build.yml) [![Build Status](https://travis-ci.org/koalaman/shellcheck.svg?branch=master)](https://travis-ci.org/koalaman/shellcheck)
# ShellCheck - A shell script static analysis tool # ShellCheck - A shell script static analysis tool
@ -77,7 +76,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).
* Pulsar Edit (former Atom), through [linter-shellcheck-pulsar](https://github.com/pulsar-cooperative/linter-shellcheck-pulsar). * Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
* VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck). * VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck).
@ -110,11 +109,11 @@ Services and platforms that have ShellCheck pre-installed and ready to use:
* [Codacy](https://www.codacy.com/) * [Codacy](https://www.codacy.com/)
* [Code Climate](https://codeclimate.com/) * [Code Climate](https://codeclimate.com/)
* [Code Factor](https://www.codefactor.io/) * [Code Factor](https://www.codefactor.io/)
* [Codety](https://www.codety.io/) via the [Codety Scanner](https://github.com/codetyio/codety-scanner) * [Github](https://github.com/features/actions)(only Linux)
* [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck)
* [Github](https://github.com/features/actions) (only Linux) Services and platforms with third party plugins:
* [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/) * [SonarQube](https://www.sonarqube.org/) through [sonar-shellcheck-plugin](https://github.com/emerald-squad/sonar-shellcheck-plugin)
Most other services, including [GitLab](https://about.gitlab.com/), let you install Most other services, including [GitLab](https://about.gitlab.com/), let you install
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)), ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
@ -143,13 +142,13 @@ On systems with Stack (installs to `~/.local/bin`):
On Debian based distros: On Debian based distros:
sudo apt install shellcheck apt-get install shellcheck
On Arch Linux based distros: On Arch Linux based distros:
pacman -S shellcheck pacman -S shellcheck
or get the dependency free [shellcheck-bin](https://aur.archlinux.org/packages/shellcheck-bin/) from the AUR. or get the dependency free [shellcheck-static](https://aur.archlinux.org/packages/shellcheck-static/) from the AUR.
On Gentoo based distros: On Gentoo based distros:
@ -157,8 +156,8 @@ On Gentoo based distros:
On EPEL based distros: On EPEL based distros:
sudo yum -y install epel-release yum -y install epel-release
sudo yum install ShellCheck yum install ShellCheck
On Fedora based distros: On Fedora based distros:
@ -168,14 +167,10 @@ On FreeBSD:
pkg install hs-ShellCheck pkg install hs-ShellCheck
On macOS (OS X) with Homebrew: On OS X with homebrew:
brew install shellcheck brew install shellcheck
Or with MacPorts:
sudo port install shellcheck
On OpenBSD: On OpenBSD:
pkg_add shellcheck pkg_add shellcheck
@ -196,22 +191,12 @@ On Windows (via [chocolatey](https://chocolatey.org/packages/shellcheck)):
C:\> choco install shellcheck 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)): Or Windows (via [scoop](http://scoop.sh)):
```cmd ```cmd
C:\> scoop install shellcheck 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:
snap install --channel=edge shellcheck snap install --channel=edge shellcheck
@ -230,26 +215,17 @@ Using the [nix package manager](https://nixos.org/nix):
nix-env -iA nixpkgs.shellcheck 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://github.com/koalaman/shellcheck/releases/download/stable/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://github.com/koalaman/shellcheck/releases/download/stable/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)
* [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked) * [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)
* [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) * [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip)
or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases 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). (including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds).
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: Distro packages already come with a `man` page. If you are building from source, it can be installed with:
```console ```console
@ -257,19 +233,6 @@ pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1
sudo mv shellcheck.1 /usr/share/man/man1 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
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.
@ -301,7 +264,7 @@ 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 cabal-install $ brew install cabal-install
@ -317,6 +280,10 @@ 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`):
@ -372,7 +339,6 @@ 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
@ -391,7 +357,6 @@ 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
@ -463,8 +428,6 @@ 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
@ -489,7 +452,6 @@ 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
@ -510,15 +472,10 @@ 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
``` ```
@ -562,3 +519,4 @@ Happy ShellChecking!
* 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,5 +1,5 @@
Name: ShellCheck Name: ShellCheck
Version: 0.10.0 Version: 0.7.1
Synopsis: Shell script analysis tool Synopsis: Shell script analysis tool
License: GPL-3 License: GPL-3
License-file: LICENSE License-file: LICENSE
@ -8,7 +8,7 @@ Author: Vidar Holen
Maintainer: vidar@vidarholen.net Maintainer: vidar@vidarholen.net
Homepage: https://www.shellcheck.net/ Homepage: https://www.shellcheck.net/
Build-Type: Simple Build-Type: Simple
Cabal-Version: 1.18 Cabal-Version: >= 1.8
Bug-reports: https://github.com/koalaman/shellcheck/issues Bug-reports: https://github.com/koalaman/shellcheck/issues
Description: Description:
The goals of ShellCheck are: The goals of ShellCheck are:
@ -22,11 +22,9 @@ Description:
* 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.
Extra-Doc-Files:
README.md
CHANGELOG.md
Extra-Source-Files: Extra-Source-Files:
-- documentation -- documentation
README.md
shellcheck.1.md shellcheck.1.md
-- A script to build the man page using pandoc -- A script to build the man page using pandoc
manpage manpage
@ -45,26 +43,19 @@ library
build-depends: build-depends:
semigroups semigroups
build-depends: build-depends:
-- The lower bounds are based on GHC 7.10.3 aeson,
-- The upper bounds are based on GHC 9.8.1 array,
aeson >= 1.4.0 && < 2.3, base >= 4.8.0.0 && < 5,
array >= 0.5.1 && < 0.6, bytestring,
base >= 4.8.0.0 && < 5, containers >= 0.5,
bytestring >= 0.10.6 && < 0.13, deepseq >= 1.4.0.0,
containers >= 0.5.6 && < 0.8, Diff >= 0.2.0,
deepseq >= 1.4.1 && < 1.6, directory >= 1.2.3.0,
Diff >= 0.4.0 && < 1.1, mtl >= 2.2.1,
fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9), filepath,
filepath >= 1.4.0 && < 1.6, parsec,
mtl >= 2.2.2 && < 2.4, regex-tdfa,
parsec >= 3.1.14 && < 3.2, QuickCheck >= 2.7.4,
QuickCheck >= 2.14.2 && < 2.16,
regex-tdfa >= 1.2.0 && < 1.4,
transformers >= 0.4.2 && < 0.7,
-- getXdgDirectory from 1.2.3.0
directory >= 1.2.3 && < 1.4,
-- When cabal supports it, move this to setup-depends: -- When cabal supports it, move this to setup-depends:
process process
exposed-modules: exposed-modules:
@ -73,15 +64,11 @@ library
ShellCheck.Analytics ShellCheck.Analytics
ShellCheck.Analyzer ShellCheck.Analyzer
ShellCheck.AnalyzerLib ShellCheck.AnalyzerLib
ShellCheck.CFG
ShellCheck.CFGAnalysis
ShellCheck.Checker ShellCheck.Checker
ShellCheck.Checks.Commands ShellCheck.Checks.Commands
ShellCheck.Checks.ControlFlow
ShellCheck.Checks.Custom ShellCheck.Checks.Custom
ShellCheck.Checks.ShellSupport ShellCheck.Checks.ShellSupport
ShellCheck.Data ShellCheck.Data
ShellCheck.Debug
ShellCheck.Fixer ShellCheck.Fixer
ShellCheck.Formatter.Format ShellCheck.Formatter.Format
ShellCheck.Formatter.CheckStyle ShellCheck.Formatter.CheckStyle
@ -93,11 +80,9 @@ library
ShellCheck.Formatter.Quiet ShellCheck.Formatter.Quiet
ShellCheck.Interface ShellCheck.Interface
ShellCheck.Parser ShellCheck.Parser
ShellCheck.Prelude
ShellCheck.Regex ShellCheck.Regex
other-modules: other-modules:
Paths_ShellCheck Paths_ShellCheck
default-language: Haskell98
executable shellcheck executable shellcheck
if impl(ghc < 8.0) if impl(ghc < 8.0)
@ -106,21 +91,18 @@ executable shellcheck
build-depends: build-depends:
aeson, aeson,
array, array,
base, base >= 4 && < 5,
bytestring, bytestring,
containers, containers,
deepseq, deepseq >= 1.4.0.0,
Diff, Diff >= 0.2.0,
directory, directory >= 1.2.3.0,
fgl, mtl >= 2.2.1,
mtl,
filepath, filepath,
parsec, parsec >= 3.0,
QuickCheck, QuickCheck >= 2.7.4,
regex-tdfa, regex-tdfa,
transformers,
ShellCheck ShellCheck
default-language: Haskell98
main-is: shellcheck.hs main-is: shellcheck.hs
test-suite test-shellcheck test-suite test-shellcheck
@ -128,19 +110,17 @@ test-suite test-shellcheck
build-depends: build-depends:
aeson, aeson,
array, array,
base, base >= 4 && < 5,
bytestring, bytestring,
containers, containers,
deepseq, deepseq >= 1.4.0.0,
Diff, Diff >= 0.2.0,
directory, directory >= 1.2.3.0,
fgl, mtl >= 2.2.1,
filepath, filepath,
mtl,
parsec, parsec,
QuickCheck, QuickCheck >= 2.7.4,
regex-tdfa, regex-tdfa,
transformers,
ShellCheck ShellCheck
default-language: Haskell98
main-is: test/shellcheck.hs main-is: test/shellcheck.hs

View file

@ -1,17 +0,0 @@
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.

View file

@ -1,12 +0,0 @@
#!/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

@ -1,40 +0,0 @@
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"]

View file

@ -1,16 +0,0 @@
#!/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"

View file

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

View file

@ -1,33 +0,0 @@
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"]

View file

@ -1,13 +0,0 @@
#!/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"

View file

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

View file

@ -1,40 +0,0 @@
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"]

View file

@ -1,14 +0,0 @@
#!/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"

View file

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

View file

@ -1,42 +0,0 @@
# 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"]

View file

@ -1,17 +0,0 @@
#!/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

@ -1,93 +0,0 @@
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

@ -1,48 +0,0 @@
#!/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 "$@"
}
"$@"

View file

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

View file

@ -1,46 +0,0 @@
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"]

View file

@ -1,21 +0,0 @@
#!/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

@ -1,93 +0,0 @@
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

View file

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

View file

@ -1,24 +0,0 @@
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"]

View file

@ -1,15 +0,0 @@
#!/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"

View file

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

View file

@ -1,30 +0,0 @@
#!/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

@ -1,27 +0,0 @@
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"]

View file

@ -1,18 +0,0 @@
#!/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"

View file

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 244 KiB

View file

@ -6,7 +6,7 @@ then
exit 1 exit 1
fi fi
for i in 1 2 3 for i in 1 2
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))"

View file

@ -2,12 +2,4 @@
# 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.
path=$(find . -type f -path './dist*/Paths_ShellCheck.hs' | sort | head -n 1) runghc -isrc -idist/build/autogen shellcheck.hs "$@"
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,17 +3,8 @@
# 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 'main' | ghci -isrc -i"$path" test/shellcheck.hs 2>&1 | tee /dev/stderr) var=$(echo 'main' | ghci test/shellcheck.hs 2>&1 | tee /dev/stderr)
if [[ $var == *ExitSuccess* ]] if [[ $var == *ExitSuccess* ]]
then then
exit 0 exit 0

View file

@ -1,11 +0,0 @@
#!/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

@ -56,13 +56,6 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
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
@ -78,11 +71,6 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
: Don't try to look for .shellcheckrc configuration files. : 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*...] **-o**\ *NAME1*[,*NAME2*...],\ **--enable=***NAME1*[,*NAME2*...]
: Enable optional checks. The special name *all* enables all of them. : Enable optional checks. The special name *all* enables all of them.
@ -97,8 +85,7 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
**-s**\ *shell*,\ **--shell=***shell* **-s**\ *shell*,\ **--shell=***shell*
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash*, *ksh*, : Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
and *busybox*.
The default is to deduce the shell from the file's `shell` directive, 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 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. POSIX `sh` (not the system's), and will warn of portability issues.
@ -125,9 +112,6 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
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...** **FILES...**
: One or more script files to check, or "-" for standard input. : One or more script files to check, or "-" for standard input.
@ -248,28 +232,12 @@ 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. A range can be like a function definition, subshell block or loop.
be specified with a dash, e.g. `disable=SC3000-SC4000` to exclude 3xxx.
All warnings can be disabled with `disable=all`.
**enable** **enable**
: Enable an optional check by name, as listed with **--list-optional**. : Enable an optional check by name, as listed with **--list-optional**.
Only file-wide `enable` directives are considered. 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
@ -286,7 +254,7 @@ Valid keys are:
**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=SC2039'. as a more targeted alternative to 'disable=2039'.
# RC FILES # RC FILES
@ -301,12 +269,6 @@ Here is an example `.shellcheckrc`:
source-path=SCRIPTDIR source-path=SCRIPTDIR
source-path=/mnt/chroot 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 # Turn on warnings for unquoted variables with safe values
enable=quote-safe-variables enable=quote-safe-variables
@ -317,7 +279,7 @@ Here is an example `.shellcheckrc`:
disable=SC2236 disable=SC2236
If no `.shellcheckrc` is found in any of the parent directories, ShellCheck If no `.shellcheckrc` is found in any of the parent directories, ShellCheck
will look in `~/.shellcheckrc` followed by the `$XDG_CONFIG_HOME` will look in `~/.shellcheckrc` followed by the XDG config directory
(usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on (usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on
Windows. Only the first file found will be used. Windows. Only the first file found will be used.
@ -357,32 +319,10 @@ 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`.
# KNOWN INCOMPATIBILITIES # AUTHORS
(If nothing in this section makes sense, you are unlikely to be affected by it) ShellCheck is developed and maintained by Vidar Holen, with assistance from a
long list of wonderful contributors.
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
@ -390,17 +330,12 @@ 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-2024, Vidar Holen and contributors. Copyright 2012-2019, 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) dash(1) ksh(1) sh(1) bash(1)

View file

@ -34,8 +34,6 @@ 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
@ -76,8 +74,7 @@ data Options = Options {
externalSources :: Bool, externalSources :: Bool,
sourcePaths :: [FilePath], sourcePaths :: [FilePath],
formatterOptions :: FormatterOptions, formatterOptions :: FormatterOptions,
minSeverity :: Severity, minSeverity :: Severity
rcfile :: Maybe FilePath
} }
defaultOptions = Options { defaultOptions = Options {
@ -87,8 +84,7 @@ defaultOptions = Options {
formatterOptions = newFormatterOptions { formatterOptions = newFormatterOptions {
foColorOption = ColorAuto foColorOption = ColorAuto
}, },
minSeverity = StyleC, minSeverity = StyleC
rcfile = Nothing
} }
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..." usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
@ -102,8 +98,6 @@ options = [
(ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings", (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 ++ ")",
@ -111,9 +105,6 @@ options = [
(NoArg $ Flag "list-optional" "true") "List checks disabled by default", (NoArg $ Flag "list-optional" "true") "List checks disabled by default",
Option "" ["norc"] Option "" ["norc"]
(NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc files", (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"] Option "o" ["enable"]
(ReqArg (Flag "enable") "check1,check2..") (ReqArg (Flag "enable") "check1,check2..")
"List of optional checks to enable (or 'all')", "List of optional checks to enable (or 'all')",
@ -122,7 +113,7 @@ options = [
"Specify path when looking for sourced files (\"SCRIPTDIR\" for script's dir)", "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, busybox)", "Specify dialect (sh, bash, dash, ksh)",
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)",
@ -234,7 +225,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
@ -243,7 +234,7 @@ runFormatter sys format options files = do
process :: FilePath -> IO Status process :: FilePath -> IO Status
process filename = do process filename = do
input <- siReadFile sys Nothing filename input <- siReadFile sys filename
either (reportFailure filename) check input either (reportFailure filename) check input
where where
check contents = do check contents = do
@ -259,9 +250,9 @@ runFormatter sys format options files = do
else SomeProblems else SomeProblems
parseEnum name value list = parseEnum name value list =
case lookup value list of case filter ((== value) . fst) list of
Just value -> return value [(name, value)] -> return value
Nothing -> do [] -> 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
@ -374,11 +365,6 @@ parseOption flag options =
} }
} }
Flag "rcfile" str -> do
return options {
rcfile = Just str
}
Flag "enable" value -> Flag "enable" value ->
let cs = checkSpec options in return options { let cs = checkSpec options in return options {
checkSpec = cs { checkSpec = cs {
@ -386,14 +372,6 @@ parseOption flag options =
} }
} }
Flag "extended-analysis" str -> do
value <- parseBool str
return options {
checkSpec = (checkSpec options) {
csExtendedAnalysis = Just value
}
}
-- This flag is handled specially in 'process' -- This flag is handled specially in 'process'
Flag "format" _ -> return options Flag "format" _ -> return options
@ -411,20 +389,11 @@ 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
configCache <- newIORef ("", Nothing) configCache <- newIORef ("", Nothing)
return (newSystemInterface :: SystemInterface IO) { return SystemInterface {
siReadFile = get cache inputs, siReadFile = get cache inputs,
siFindSource = findSourceFile inputs (sourcePaths options), siFindSource = findSourceFile inputs (sourcePaths options),
siGetConfig = getConfig configCache siGetConfig = getConfig configCache
@ -433,14 +402,14 @@ ioInterface options files = do
emptyCache :: Map.Map FilePath String emptyCache :: Map.Map FilePath String
emptyCache = Map.empty emptyCache = Map.empty
get cache inputs rcSuggestsExternal file = do get cache inputs 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 rcSuggestsExternal file Nothing -> fetch cache inputs file
fetch cache inputs rcSuggestsExternal file = do fetch cache inputs file = do
ok <- allowable rcSuggestsExternal inputs file ok <- allowable inputs file
if ok if ok
then (do then (do
(contents, shouldCache) <- inputFile file (contents, shouldCache) <- inputFile file
@ -448,16 +417,13 @@ 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 else return $ Left (file ++ " was not specified as input (see shellcheck -x).")
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).")
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 rcSuggestsExternal inputs x = allowable inputs x =
if fromMaybe (externalSources options) rcSuggestsExternal if externalSources options
then return True then return True
else do else do
path <- normalize x path <- normalize x
@ -469,33 +435,18 @@ 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 -- Returns the name and contents of .shellcheckrc for the given file
getConfig cache filename = getConfig cache filename = do
case rcfile options of path <- normalize filename
Just file -> do let dir = takeDirectory path
-- We have a specified rcfile. Ignore normal rcfile resolution. (previousPath, result) <- readIORef cache
(path, result) <- readIORef cache if dir == previousPath
if path == "/" then return result
then return result else do
else do paths <- getConfigPaths dir
result <- readConfig file result <- findConfig paths
when (isNothing result) $ writeIORef cache (dir, result)
hPutStrLn stderr $ "Warning: unable to read --rcfile " ++ file return result
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 = findConfig paths =
case paths of case paths of
@ -533,7 +484,7 @@ ioInterface options files = do
where where
handler :: FilePath -> IOException -> IO (String, Bool) handler :: FilePath -> IOException -> IO (String, Bool)
handler file err = do handler file err = do
hPutStrLn stderr $ file ++ ": " ++ show err putStrLn $ file ++ ": " ++ show err
return ("", True) return ("", True)
andM a b arg = do andM a b arg = do
@ -546,7 +497,7 @@ ioInterface options files = do
b <- p x b <- p x
if b then pure (Just x) else acc if b then pure (Just x) else acc
findSourceFile inputs sourcePathFlag currentScript rcSuggestsExternal sourcePathAnnotation original = findSourceFile inputs sourcePathFlag currentScript sourcePathAnnotation original =
if isAbsolute original if isAbsolute original
then then
let (_, relative) = splitDrive original let (_, relative) = splitDrive original
@ -555,8 +506,8 @@ ioInterface options files = do
find original original find original original
where where
find filename deflt = do find filename deflt = do
sources <- findM ((allowable rcSuggestsExternal inputs) `andM` doesFileExist) $ sources <- findM ((allowable inputs) `andM` doesFileExist) $
(adjustPath filename):(map ((</> filename) . adjustPath) $ sourcePathFlag ++ sourcePathAnnotation) (adjustPath filename):(map (</> filename) $ map adjustPath $ sourcePathFlag ++ sourcePathAnnotation)
case sources of case sources of
Nothing -> return deflt Nothing -> return deflt
Just first -> return first Just first -> return first

View file

@ -23,7 +23,7 @@ description: |
# snap connect shellcheck:removable-media # snap connect shellcheck:removable-media
version: git version: git
base: core20 base: core18
grade: stable grade: stable
confinement: strict confinement: strict
@ -31,8 +31,6 @@ 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:
@ -40,16 +38,16 @@ parts:
source: . source: .
build-packages: build-packages:
- cabal-install - cabal-install
stage-packages: - squid
- libatomic1
override-build: | override-build: |
# Give ourselves enough memory to build # See comments in .snapsquid.conf
dd if=/dev/zero of=/tmp/swap bs=1M count=2000 [ "$http_proxy" ] && {
mkswap /tmp/swap squid3 -f .snapsquid.conf
swapon /tmp/swap export http_proxy="http://localhost:8888"
sleep 3
}
cabal sandbox init cabal sandbox init
cabal update cabal update || cat /var/log/squid/*
cabal install -j cabal install -j
install -d $SNAPCRAFT_PART_INSTALL/usr/bin install -d $SNAPCRAFT_PART_INSTALL/usr/bin

View file

@ -45,7 +45,6 @@ data InnerToken t =
| Inner_TA_Variable String [t] | Inner_TA_Variable String [t]
| Inner_TA_Expansion [t] | Inner_TA_Expansion [t]
| Inner_TA_Sequence [t] | Inner_TA_Sequence [t]
| Inner_TA_Parenthesis t
| Inner_TA_Trinary t t t | Inner_TA_Trinary t t t
| Inner_TA_Unary String t | Inner_TA_Unary String t
| Inner_TC_And ConditionType String t t | Inner_TC_And ConditionType String t t
@ -138,21 +137,19 @@ data InnerToken t =
| Inner_T_WhileExpression [t] [t] | Inner_T_WhileExpression [t] [t]
| Inner_T_Annotation [Annotation] t | Inner_T_Annotation [Annotation] t
| Inner_T_Pipe String | Inner_T_Pipe String
| Inner_T_CoProc (Maybe Token) t | Inner_T_CoProc (Maybe String) t
| Inner_T_CoProcBody t | Inner_T_CoProcBody t
| Inner_T_Include t | Inner_T_Include t
| Inner_T_SourceCommand t t | Inner_T_SourceCommand t t
| Inner_T_BatsTest String t | Inner_T_BatsTest t t
deriving (Show, Eq, Functor, Foldable, Traversable) deriving (Show, Eq, Functor, Foldable, Traversable)
data Annotation = data Annotation =
DisableComment Integer Integer -- [from, to) DisableComment Integer
| EnableComment String | EnableComment String
| SourceOverride String | SourceOverride String
| ShellOverride String | ShellOverride String
| SourcePath String | SourcePath String
| ExternalSources Bool
| ExtendedAnalysis Bool
deriving (Show, Eq) deriving (Show, Eq)
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq) data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
@ -206,7 +203,6 @@ pattern T_Annotation id anns t = OuterToken id (Inner_T_Annotation anns t)
pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c) pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c)
pattern T_Array id t = OuterToken id (Inner_T_Array t) pattern T_Array id t = OuterToken id (Inner_T_Array t)
pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l) pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)
pattern TA_Parenthesis id t = OuterToken id (Inner_TA_Parenthesis t)
pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value) pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value)
pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3) pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3)
pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1) pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1)
@ -259,7 +255,7 @@ pattern T_Subshell id l = OuterToken id (Inner_T_Subshell l)
pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l) pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l)
pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l) pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l)
{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, TA_Parenthesis, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-} {-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-}
instance Eq Token where instance Eq Token where
OuterToken _ a == OuterToken _ b = a == b OuterToken _ a == OuterToken _ b = a == b

View file

@ -1,5 +1,5 @@
{- {-
Copyright 2012-2021 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,12 +17,9 @@
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
@ -31,13 +28,6 @@ import Data.Functor
import Data.Functor.Identity 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
@ -61,28 +51,10 @@ willSplit x =
T_NormalWord _ l -> any willSplit l T_NormalWord _ l -> any willSplit l
_ -> False _ -> False
isGlob t = case t of isGlob T_Extglob {} = True
T_Extglob {} -> True isGlob T_Glob {} = True
T_Glob {} -> True isGlob (T_NormalWord _ l) = any isGlob l
T_NormalWord _ l -> any isGlob l || hasSplitRange l isGlob _ = False
_ -> 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 =
@ -140,7 +112,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 $ pleaseReport "getFlags on non-command" getFlagsUntil _ _ = error "Internal shellcheck error, please report! (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)]
@ -158,146 +130,30 @@ isFlag token =
_ -> False _ -> False
-- Is this token a flag where the - is unquoted? -- Is this token a flag where the - is unquoted?
isUnquotedFlag token = isUnquotedFlag token = fromMaybe False $ do
case getLeadingUnquotedString token of str <- getLeadingUnquotedString token
Just ('-':_) -> True return $ "-" `isPrefixOf` str
_ -> 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_DollarBraced _ _ l) = isArrayExpansion t@(T_DollarBraced _ _ _) =
let string = concat $ oversimplify l in let string = bracedString t 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 False t mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
where where
f quoted (T_DollarBraced _ _ l) = f t@(T_DollarBraced _ _ _) =
let string = concat $ oversimplify l in let string = bracedString t in
not quoted || "!" `isPrefixOf` string "!" `isPrefixOf` string
f quoted (T_DoubleQuoted _ parts) = any (f True) parts f (T_DoubleQuoted _ parts) = any f parts
f quoted (T_NormalWord _ parts) = any (f quoted) parts f (T_NormalWord _ parts) = any f 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
@ -305,6 +161,7 @@ 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
@ -336,12 +193,6 @@ 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
@ -360,11 +211,8 @@ 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) : rest) -> return $ s ++ from rest T_NormalWord _ ((T_Literal _ s) : _) -> return s
_ -> 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
@ -372,21 +220,6 @@ 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 :: Monad m => (Token -> m String) -> Token -> m String getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String
@ -419,15 +252,14 @@ getLiteralStringExt more = g
'\\' -> '\\' : rest '\\' -> '\\' : rest
'x' -> 'x' ->
case cs of case cs of
(x:y:more) | isHexDigit x && isHexDigit y -> (x:y:more) ->
chr (16*(digitToInt x) + (digitToInt y)) : decodeEscapes more if isHexDigit x && isHexDigit y
(x:more) | isHexDigit x -> then chr (16*(digitToInt x) + (digitToInt y)) : rest
chr (digitToInt x) : decodeEscapes more else '\\':c:rest
more -> '\\' : 'x' : decodeEscapes more
_ | isOctDigit c -> _ | isOctDigit c ->
let (digits, more) = spanMax isOctDigit 3 (c:cs) let digits = take 3 $ takeWhile isOctDigit (c:cs)
num = (parseOct digits) `mod` 256 num = parseOct digits
in (chr num) : decodeEscapes more in (if num < 256 then chr num else '?') : rest
_ -> '\\' : c : rest _ -> '\\' : c : rest
where where
rest = decodeEscapes cs rest = decodeEscapes cs
@ -435,54 +267,12 @@ 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
@ -511,7 +301,7 @@ 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 False getCommandName = fst . getCommandNameAndToken
-- Maybe get the name+arguments of a command. -- Maybe get the name+arguments of a command.
getCommandArgv t = do getCommandArgv t = do
@ -521,38 +311,20 @@ getCommandArgv t = do
-- 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 False getCommandTokenOrThis = snd . getCommandNameAndToken
-- Given a command, get the string and token that represents the command name. getCommandNameAndToken :: Token -> (Maybe String, Token)
-- If direct, return the actual command (e.g. exec in 'exec ls') getCommandNameAndToken t = fromMaybe (Nothing, t) $ do
-- If not, return the logical command (e.g. 'ls' in 'exec ls') (T_SimpleCommand _ _ (w:rest)) <- getCommand t
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
return $ fromMaybe (Just s, w) $ do if "busybox" `isSuffixOf` s || "builtin" == s
guard $ not direct then
actual <- getEffectiveCommandToken s cmd rest case rest of
return (getLiteralString actual, actual) (applet:_) -> return (getLiteralString applet, applet)
where _ -> return (Just s, w)
getEffectiveCommandToken str cmd args = else
let return (Just s, w)
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"
@ -569,8 +341,8 @@ 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 =
case t of case t of
@ -628,10 +400,10 @@ getAssociativeArrays t =
f t@T_SimpleCommand {} = sequence_ $ do f t@T_SimpleCommand {} = sequence_ $ do
name <- getCommandName t name <- getCommandName t
let assocNames = ["declare","local","typeset"] let assocNames = ["declare","local","typeset"]
guard $ name `elem` assocNames guard $ elem name assocNames
let flags = getAllFlags t let flags = getAllFlags t
guard $ "A" `elem` map snd flags guard $ elem "A" $ map snd flags
let args = [arg | (arg, "") <- flags] let args = map fst . filter ((==) "" . snd) $ flags
let names = mapMaybe (getLiteralStringExt nameAssignments) args let names = mapMaybe (getLiteralStringExt nameAssignments) args
return $ tell names return $ tell names
f _ = return () f _ = return ()
@ -649,36 +421,38 @@ 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 -> [PseudoGlob] wordToPseudoGlob :: Token -> Maybe [PseudoGlob]
wordToPseudoGlob = fromMaybe [PGMany] . wordToPseudoGlob' False wordToPseudoGlob word =
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
where
f x = case x of
T_Literal _ s -> return $ map PGChar s
T_SingleQuoted _ s -> return $ map PGChar s
T_DollarBraced {} -> return [PGMany]
T_DollarExpansion {} -> return [PGMany]
T_Backticked {} -> return [PGMany]
T_Glob _ "?" -> return [PGAny]
T_Glob _ ('[':_) -> return [PGAny]
T_Glob {} -> return [PGMany]
T_Extglob {} -> return [PGMany]
_ -> return [PGMany]
-- Turn a word into a PG pattern, but only if we can preserve -- 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 = wordToPseudoGlob' True wordToExactPseudoGlob word =
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]
T_Glob _ ('[':_) | not exact -> return [PGAny] _ -> fail "Unknown token type"
_ -> 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
@ -728,7 +502,8 @@ pseudoGlobIsSuperSetof = matchable
matchable (PGMany : rest) [] = matchable rest [] matchable (PGMany : rest) [] = matchable rest []
matchable _ _ = False matchable _ _ = False
wordsCanBeEqual x y = pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y) wordsCanBeEqual x y = fromMaybe True $
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,})?
@ -742,11 +517,6 @@ isCommandSubstitution t = case t of
T_Backticked {} -> True T_Backticked {} -> 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? -- Is this a T_Annotation that ignores a specific code?
isAnnotationIgnoringCode code t = isAnnotationIgnoringCode code t =
@ -754,173 +524,5 @@ isAnnotationIgnoringCode code t =
T_Annotation _ anns _ -> any hasNum anns T_Annotation _ anns _ -> any hasNum anns
_ -> False _ -> False
where where
hasNum (DisableComment from to) = code >= from && code < to hasNum (DisableComment ts) = code == ts
hasNum _ = False 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

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
{- {-
Copyright 2012-2022 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
@ -25,7 +25,6 @@ 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.Custom
import qualified ShellCheck.Checks.ShellSupport import qualified ShellCheck.Checks.ShellSupport
@ -35,21 +34,19 @@ analyzeScript :: AnalysisSpec -> AnalysisResult
analyzeScript spec = newAnalysisResult { analyzeScript spec = newAnalysisResult {
arComments = arComments =
filterByAnnotation spec params . nub $ filterByAnnotation spec params . nub $
runChecker params (checkers spec params) runAnalytics spec
++ runChecker params (checkers spec params)
} }
where where
params = makeParameters spec params = makeParameters spec
checkers spec params = mconcat $ map ($ params) [ checkers spec params = mconcat $ map ($ params) [
ShellCheck.Analytics.checker spec,
ShellCheck.Checks.Commands.checker spec, ShellCheck.Checks.Commands.checker spec,
ShellCheck.Checks.ControlFlow.checker spec,
ShellCheck.Checks.Custom.checker, ShellCheck.Checks.Custom.checker,
ShellCheck.Checks.ShellSupport.checker ShellCheck.Checks.ShellSupport.checker
] ]
optionalChecks = mconcat $ [ optionalChecks = mconcat $ [
ShellCheck.Analytics.optionalChecks, ShellCheck.Analytics.optionalChecks,
ShellCheck.Checks.Commands.optionalChecks, ShellCheck.Checks.Commands.optionalChecks
ShellCheck.Checks.ControlFlow.optionalChecks
] ]

View file

@ -1,5 +1,5 @@
{- {-
Copyright 2012-2022 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,16 +23,13 @@ module ShellCheck.AnalyzerLib where
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.ASTLib import ShellCheck.ASTLib
import qualified ShellCheck.CFGAnalysis as CF
import ShellCheck.Data import ShellCheck.Data
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Parser import ShellCheck.Parser
import ShellCheck.Prelude
import ShellCheck.Regex import ShellCheck.Regex
import Control.Arrow (first) import Control.Arrow (first)
import Control.DeepSeq import Control.DeepSeq
import Control.Monad
import Control.Monad.Identity import Control.Monad.Identity
import Control.Monad.RWS import Control.Monad.RWS
import Control.Monad.State import Control.Monad.State
@ -41,7 +38,6 @@ import Data.Char
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import Data.Semigroup import Data.Semigroup
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map import qualified Data.Map as Map
import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.All (forAllProperties)
@ -83,18 +79,10 @@ composeAnalyzers f g x = f x >> g x
data Parameters = Parameters { data Parameters = Parameters {
-- Whether this script has the 'lastpipe' option set/default. -- Whether this script has the 'lastpipe' option set/default.
hasLastpipe :: Bool, hasLastpipe :: Bool,
-- Whether this script has the 'inherit_errexit' option set/default.
hasInheritErrexit :: Bool,
-- Whether this script has 'set -e' anywhere. -- Whether this script has 'set -e' anywhere.
hasSetE :: Bool, hasSetE :: Bool,
-- Whether this script has 'set -o pipefail' anywhere.
hasPipefail :: Bool,
-- Whether this script has 'shopt -s execfail' anywhere.
hasExecfail :: Bool,
-- A linear (bad) analysis of data flow -- A linear (bad) analysis of data flow
variableFlow :: [StackData], variableFlow :: [StackData],
-- A map from Id to Token
idMap :: Map.Map Id Token,
-- A map from Id to parent Token -- A map from Id to parent Token
parentMap :: Map.Map Id Token, parentMap :: Map.Map Id Token,
-- The shell type, such as Bash or Ksh -- The shell type, such as Bash or Ksh
@ -104,9 +92,7 @@ data Parameters = Parameters {
-- The root node of the AST -- The root node of the AST
rootNode :: Token, rootNode :: Token,
-- map from token id to start and end position -- map from token id to start and end position
tokenPositions :: Map.Map Id (Position, Position), tokenPositions :: Map.Map Id (Position, Position)
-- Result from Control Flow Graph analysis (including data flow analysis)
cfgAnalysis :: Maybe CF.CFGAnalysis
} deriving (Show) } deriving (Show)
-- TODO: Cache results of common AST ops here -- TODO: Cache results of common AST ops here
@ -156,7 +142,7 @@ producesComments c s = do
prRoot pr prRoot pr
let spec = defaultSpec pr let spec = defaultSpec pr
let params = makeParameters spec let params = makeParameters spec
return . not . null $ filterByAnnotation spec params $ runChecker params c return . not . null $ runChecker params c
makeComment :: Severity -> Id -> Code -> String -> TokenComment makeComment :: Severity -> Id -> Code -> String -> TokenComment
makeComment severity id code note = makeComment severity id code note =
@ -177,12 +163,8 @@ err id code str = addComment $ makeComment ErrorC id code str
info id code str = addComment $ makeComment InfoC id code str info id code str = addComment $ makeComment InfoC id code str
style id code str = addComment $ makeComment StyleC id code str style id code str = addComment $ makeComment StyleC id code str
errWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
errWithFix = addCommentWithFix ErrorC
warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m () warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
warnWithFix = addCommentWithFix WarningC warnWithFix = addCommentWithFix WarningC
infoWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
infoWithFix = addCommentWithFix InfoC
styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m () styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
styleWithFix = addCommentWithFix StyleC styleWithFix = addCommentWithFix StyleC
@ -194,58 +176,28 @@ makeCommentWithFix :: Severity -> Id -> Code -> String -> Fix -> TokenComment
makeCommentWithFix severity id code str fix = makeCommentWithFix severity id code str fix =
let comment = makeComment severity id code str let comment = makeComment severity id code str
withFix = comment { withFix = comment {
-- If fix is empty, pretend it wasn't there. tcFix = Just fix
tcFix = if null (fixReplacements fix) then Nothing else Just fix
} }
in force withFix in withFix `deepseq` withFix
-- makeParameters :: CheckSpec -> Parameters makeParameters spec =
makeParameters spec = params let params = Parameters {
where
extendedAnalysis = fromMaybe True $ msum [asExtendedAnalysis spec, getExtendedAnalysisDirective root]
params = Parameters {
rootNode = root, rootNode = root,
shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec, shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec,
hasSetE = containsSetE root, hasSetE = containsSetE root,
hasLastpipe = hasLastpipe =
case shellType params of case shellType params of
Bash -> isOptionSet "lastpipe" root Bash -> containsLastpipe root
Dash -> False Dash -> False
BusyboxSh -> False
Sh -> False Sh -> False
Ksh -> True, Ksh -> True,
hasInheritErrexit =
case shellType params of
Bash -> isOptionSet "inherit_errexit" root
Dash -> True
BusyboxSh -> True
Sh -> True
Ksh -> False,
hasPipefail =
case shellType params of
Bash -> isOptionSet "pipefail" root
Dash -> True
BusyboxSh -> isOptionSet "pipefail" root
Sh -> True
Ksh -> isOptionSet "pipefail" root,
hasExecfail =
case shellType params of
Bash -> isOptionSet "execfail" root
_ -> False,
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec), shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
idMap = getTokenMap root,
parentMap = getParentTree root, parentMap = getParentTree root,
variableFlow = getVariableFlow params root, variableFlow = getVariableFlow params root,
tokenPositions = asTokenPositions spec, tokenPositions = asTokenPositions spec
cfgAnalysis = do } in params
guard extendedAnalysis where root = asScript spec
return $ CF.analyzeControlFlow cfParams root
}
cfParams = CF.CFGParameters {
CF.cfLastpipe = hasLastpipe params,
CF.cfPipefail = hasPipefail params
}
root = asScript spec
-- Does this script mention 'set -e' anywhere? -- Does this script mention 'set -e' anywhere?
@ -262,30 +214,18 @@ containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
_ -> False _ -> False
re = mkRegex "[[:space:]]-[^-]*e" re = mkRegex "[[:space:]]-[^-]*e"
-- Does this script mention 'shopt -s lastpipe' anywhere?
containsSetOption opt root = isNothing $ doAnalysis (guard . not . isPipefail) root -- Also used as a hack.
where containsLastpipe root =
isPipefail t =
case t of
T_SimpleCommand {} ->
t `isUnqualifiedCommand` "set" &&
(opt `elem` oversimplify t ||
"o" `elem` map snd (getAllFlags t))
_ -> False
containsShopt shopt root =
isNothing $ doAnalysis (guard . not . isShoptLastPipe) root isNothing $ doAnalysis (guard . not . isShoptLastPipe) root
where where
isShoptLastPipe t = isShoptLastPipe t =
case t of case t of
T_SimpleCommand {} -> T_SimpleCommand {} ->
t `isUnqualifiedCommand` "shopt" && t `isUnqualifiedCommand` "shopt" &&
(shopt `elem` oversimplify t) ("lastpipe" `elem` oversimplify t)
_ -> False _ -> False
-- Does this script mention 'shopt -s $opt' or 'set -o $opt' anywhere?
isOptionSet opt root = containsShopt opt root || containsSetOption opt root
prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh
prop_determineShell1 = determineShellTest "#!/usr/bin/env ksh" == Ksh prop_determineShell1 = determineShellTest "#!/usr/bin/env ksh" == Ksh
@ -296,10 +236,6 @@ prop_determineShell5 = determineShellTest "#shellcheck shell=sh\nfoo" == Sh
prop_determineShell6 = determineShellTest "#! /bin/sh" == Sh prop_determineShell6 = determineShellTest "#! /bin/sh" == Sh
prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh
prop_determineShell9 = determineShellTest "#!/bin/env -S dash -x" == Dash
prop_determineShell10 = determineShellTest "#!/bin/env --split-string= dash -x" == Dash
prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == BusyboxSh -- busybox sh is a specific shell, not posix sh
prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == BusyboxSh
determineShellTest = determineShellTest' Nothing determineShellTest = determineShellTest' Nothing
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
@ -313,11 +249,22 @@ determineShell fallbackShell t = fromMaybe Bash $
headOrDefault (fromShebang s) [s | ShellOverride s <- annotations] headOrDefault (fromShebang s) [s | ShellOverride s <- annotations]
fromShebang (T_Script _ (T_Literal _ s) _) = executableFromShebang s fromShebang (T_Script _ (T_Literal _ s) _) = executableFromShebang s
-- Given a string like "/bin/bash" or "/usr/bin/env dash",
-- return the shell basename like "bash" or "dash"
executableFromShebang :: String -> String
executableFromShebang = shellFor
where
shellFor s | "/env " `isInfixOf` s = headOrDefault "" (drop 1 $ words s)
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
-- Given a root node, make a map from Id to parent Token. -- Given a root node, make a map from Id to parent Token.
-- This is used to populate parentMap in Parameters -- This is used to populate parentMap in Parameters
getParentTree :: Token -> Map.Map Id Token getParentTree :: Token -> Map.Map Id Token
getParentTree t = getParentTree t =
snd $ execState (doStackAnalysis pre post t) ([], Map.empty) snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
where where
pre t = modify (first ((:) t)) pre t = modify (first ((:) t))
post t = do post t = do
@ -345,16 +292,16 @@ isStrictlyQuoteFree = isQuoteFreeNode True
isQuoteFree = isQuoteFreeNode False isQuoteFree = isQuoteFreeNode False
isQuoteFreeNode strict shell tree t = isQuoteFreeNode strict tree t =
isQuoteFreeElement t || (isQuoteFreeElement t == Just True) ||
(fromMaybe False $ msum $ map isQuoteFreeContext $ NE.tail $ getPath tree t) headOrDefault False (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t))
where where
-- Is this node self-quoting in itself? -- Is this node self-quoting in itself?
isQuoteFreeElement t = isQuoteFreeElement t =
case t of case t of
T_Assignment id _ _ _ _ -> assignmentIsQuoting id T_Assignment {} -> return True
T_FdRedirect {} -> True T_FdRedirect {} -> return True
_ -> False _ -> Nothing
-- Are any subnodes inherently self-quoting? -- Are any subnodes inherently self-quoting?
isQuoteFreeContext t = isQuoteFreeContext t =
@ -364,7 +311,7 @@ isQuoteFreeNode strict shell tree t =
TC_Binary _ DoubleBracket _ _ _ -> return True TC_Binary _ DoubleBracket _ _ _ -> return True
TA_Sequence {} -> return True TA_Sequence {} -> return True
T_Arithmetic {} -> return True T_Arithmetic {} -> return True
T_Assignment id _ _ _ _ -> return $ assignmentIsQuoting id T_Assignment {} -> return True
T_Redirecting {} -> return False T_Redirecting {} -> return False
T_DoubleQuoted _ _ -> return True T_DoubleQuoted _ _ -> return True
T_DollarDoubleQuoted _ _ -> return True T_DollarDoubleQuoted _ _ -> return True
@ -376,18 +323,6 @@ isQuoteFreeNode strict shell tree t =
T_SelectIn {} -> return (not strict) T_SelectIn {} -> return (not strict)
_ -> Nothing _ -> Nothing
-- Check whether this assignment is self-quoting due to being a recognized
-- assignment passed to a Declaration Utility. This will soon be required
-- by POSIX: https://austingroupbugs.net/view.php?id=351
assignmentIsQuoting id = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand id)
shellParsesParamsAsAssignments = shell /= Sh
-- Is this assignment a parameter to a command like export/typeset/etc?
isAssignmentParamToCommand id =
case Map.lookup id tree of
Just (T_SimpleCommand _ _ (_:args)) -> id `elem` (map getId args)
_ -> False
-- Check if a token is a parameter to a certain command by name: -- Check if a token is a parameter to a certain command by name:
-- Example: isParamTo (parentMap params) "sed" t -- Example: isParamTo (parentMap params) "sed" t
isParamTo :: Map.Map Id Token -> String -> Token -> Bool isParamTo :: Map.Map Id Token -> String -> Token -> Bool
@ -409,7 +344,7 @@ isParamTo tree cmd =
-- Get the parent command (T_Redirecting) of a Token, if any. -- Get the parent command (T_Redirecting) of a Token, if any.
getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token
getClosestCommand tree t = getClosestCommand tree t =
findFirst findCommand $ NE.toList $ getPath tree t findFirst findCommand $ getPath tree t
where where
findCommand t = findCommand t =
case t of case t of
@ -419,43 +354,48 @@ getClosestCommand tree t =
-- Like above, if koala_man knew Haskell when starting this project. -- Like above, if koala_man knew Haskell when starting this project.
getClosestCommandM t = do getClosestCommandM t = do
params <- ask tree <- asks parentMap
return $ getClosestCommand (parentMap params) t return $ getClosestCommand tree t
-- Is the token used as a command name (the first word in a T_SimpleCommand)? -- Is the token used as a command name (the first word in a T_SimpleCommand)?
usedAsCommandName tree token = go (getId token) (NE.tail $ getPath tree token) usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
where where
go currentId (T_NormalWord id [word]:rest) go currentId (T_NormalWord id [word]:rest)
| currentId == getId word = go id rest | currentId == getId word = go id rest
go currentId (T_DoubleQuoted id [word]:rest) go currentId (T_DoubleQuoted id [word]:rest)
| currentId == getId word = go id rest | currentId == getId word = go id rest
go currentId (t@(T_SimpleCommand _ _ (word:_)):_) = go currentId (T_SimpleCommand _ _ (word:_):_)
getId word == currentId || getId (getCommandTokenOrThis t) == currentId | currentId == getId word = True
go _ _ = False go _ _ = False
-- A list of the element and all its parents up to the root node.
getPath tree t = t :
case Map.lookup (getId t) tree of
Nothing -> []
Just parent -> getPath tree parent
-- Version of the above taking the map from the current context -- Version of the above taking the map from the current context
-- Todo: give this the name "getPath" -- Todo: give this the name "getPath"
getPathM t = do getPathM t = do
params <- ask map <- asks parentMap
return $ getPath (parentMap params) t return $ getPath map t
isParentOf tree parent child = isParentOf tree parent child =
any (\t -> parentId == getId t) (getPath tree child) elem (getId parent) . map getId $ getPath tree child
where
parentId = getId parent
parents params = getPath (parentMap params) parents params = getPath (parentMap params)
-- Find the first match in a list where the predicate is Just True. -- Find the first match in a list where the predicate is Just True.
-- Stops if it's Just False and ignores Nothing. -- Stops if it's Just False and ignores Nothing.
findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a
findFirst p = foldr go Nothing findFirst p l =
where case l of
go x acc = [] -> Nothing
case p x of (x:xs) ->
Just True -> return x case p x of
Just False -> Nothing Just True -> return x
Nothing -> acc Just False -> Nothing
Nothing -> findFirst p xs
-- Check whether a word is entirely output from a single command -- Check whether a word is entirely output from a single command
tokenIsJustCommandOutput t = case t of tokenIsJustCommandOutput t = case t of
@ -470,7 +410,8 @@ tokenIsJustCommandOutput t = case t of
-- TODO: Replace this with a proper Control Flow Graph -- TODO: Replace this with a proper Control Flow Graph
getVariableFlow params t = getVariableFlow params t =
reverse $ execState (doStackAnalysis startScope endScope t) [] let (_, stack) = runState (doStackAnalysis startScope endScope t) []
in reverse stack
where where
startScope t = startScope t =
let scopeType = leadType params t let scopeType = leadType params t
@ -521,82 +462,83 @@ leadType params t =
causesSubshell = do causesSubshell = do
(T_Pipeline _ _ list) <- parentPipeline (T_Pipeline _ _ list) <- parentPipeline
return $ case list of if length list <= 1
_:_:_ -> not (hasLastpipe params) || getId (last list) /= getId t then return False
_ -> False else if not $ hasLastpipe params
then return True
else return . not $ (getId . head $ reverse list) == getId t
getModifiedVariables t = getModifiedVariables t =
case t of case t of
T_SimpleCommand _ vars [] -> T_SimpleCommand _ vars [] ->
[(x, x, name, dataTypeFrom DataString w) | x@(T_Assignment id _ name _ w) <- vars] concatMap (\x -> case x of
T_SimpleCommand {} -> T_Assignment id _ name _ w ->
getModifiedVariableCommand t [(x, x, name, dataTypeFrom DataString w)]
_ -> []
) vars
c@T_SimpleCommand {} ->
getModifiedVariableCommand c
TA_Unary _ op v@(TA_Variable _ name _) | "--" `isInfixOf` op || "++" `isInfixOf` op -> TA_Unary _ "++|" v@(TA_Variable _ name _) ->
[(t, v, name, DataString SourceInteger)] [(t, v, name, DataString $ SourceFrom [v])]
TA_Assignment _ op (TA_Variable _ name _) rhs -> do TA_Unary _ "|++" v@(TA_Variable _ name _) ->
[(t, v, name, DataString $ SourceFrom [v])]
TA_Assignment _ op (TA_Variable _ name _) rhs -> maybeToList $ do
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="] guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
return (t, t, name, DataString SourceInteger) return (t, t, name, DataString $ SourceFrom [rhs])
T_BatsTest {} -> [ T_BatsTest {} -> [
(t, t, "lines", DataArray SourceExternal), (t, t, "lines", DataArray SourceExternal),
(t, t, "status", DataString SourceInteger), (t, t, "status", DataString SourceInteger),
(t, t, "output", DataString SourceExternal), (t, t, "output", DataString SourceExternal)
(t, t, "stderr", DataString SourceExternal),
(t, t, "stderr_lines", DataArray SourceExternal)
] ]
-- Count [[ -v foo ]] as an "assignment". -- Count [[ -v foo ]] as an "assignment".
-- This is to prevent [ -v foo ] being unassigned or unused. -- This is to prevent [ -v foo ] being unassigned or unused.
TC_Unary id _ "-v" token -> maybeToList $ do TC_Unary id _ "-v" token -> maybeToList $ do
str <- getVariableForTestDashV token str <- fmap (takeWhile (/= '[')) $ -- Quoted index
flip getLiteralStringExt token $ \x ->
case x of
T_Glob _ s -> return s -- Unquoted index
_ -> Nothing
guard . not . null $ str
return (t, token, str, DataString SourceChecked) return (t, token, str, DataString SourceChecked)
TC_Unary _ _ "-n" token -> markAsChecked t token
TC_Unary _ _ "-z" token -> markAsChecked t token
TC_Nullary _ _ token -> markAsChecked t token
T_DollarBraced _ _ l -> maybeToList $ do T_DollarBraced _ _ l -> maybeToList $ do
let string = concat $ oversimplify l let string = bracedString t
let modifier = getBracedModifier string let modifier = getBracedModifier string
guard $ any (`isPrefixOf` modifier) ["=", ":="] guard $ any (`isPrefixOf` modifier) ["=", ":="]
return (t, t, getBracedReference string, DataString $ SourceFrom [l]) return (t, t, getBracedReference string, DataString $ SourceFrom [l])
T_FdRedirect _ ('{':var) op -> -- {foo}>&2 modifies foo t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op] [(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
T_CoProc _ Nothing _ -> t@(T_CoProc _ name _) ->
[(t, t, "COPROC", DataArray SourceInteger)] [(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
T_CoProc _ (Just token) _ -> do
name <- maybeToList $ getLiteralString token
[(t, t, name, DataArray SourceInteger)]
--Points to 'for' rather than variable --Points to 'for' rather than variable
T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)] T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)]
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)] T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)] T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
_ -> [] _ -> []
where
markAsChecked place token = mapMaybe (f place) $ getWordParts token isClosingFileOp op =
f place t = case t of case op of
T_DollarBraced _ _ l -> T_IoDuplicate _ (T_GREATAND _) "-" -> True
let str = getBracedReference $ concat $ oversimplify l in do T_IoDuplicate _ (T_LESSAND _) "-" -> True
guard $ isVariableName str _ -> False
return (place, t, str, DataString SourceChecked)
_ -> Nothing
-- Consider 'export/declare -x' a reference, since it makes the var available -- Consider 'export/declare -x' a reference, since it makes the var available
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) = getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
case x of case x of
"declare" -> forDeclare
"typeset" -> forDeclare
"export" -> if "f" `elem` flags "export" -> if "f" `elem` flags
then [] then []
else concatMap getReference rest else concatMap getReference rest
"local" -> if "x" `elem` flags "declare" -> if
any (`elem` flags) ["x", "p"] &&
(not $ any (`elem` flags) ["f", "F"])
then concatMap getReference rest then concatMap getReference rest
else [] else []
"trap" -> "trap" ->
@ -606,13 +548,6 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
"alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token] "alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token]
_ -> [] _ -> []
where where
forDeclare =
if
any (`elem` flags) ["x", "p"] &&
(not $ any (`elem` flags) ["f", "F"])
then concatMap getReference rest
else []
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)] getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)] getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
getReference _ = [] getReference _ = []
@ -634,14 +569,10 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
"builtin" -> "builtin" ->
getModifiedVariableCommand $ T_SimpleCommand id cmdPrefix rest getModifiedVariableCommand $ T_SimpleCommand id cmdPrefix rest
"read" -> "read" ->
let fallback = catMaybes $ takeWhile isJust (reverse $ map getLiteral rest) let params = map getLiteral rest
in fromMaybe fallback $ do readArrayVars = getReadArrayVariables rest
parsed <- getGnuOpts flagsForRead rest in
case lookup "a" parsed of catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params
Just (_, var) -> (:[]) <$> getLiteralArray var
Nothing -> return $ catMaybes $
map (getLiteral . snd . snd) $ filter (null . fst) parsed
"getopts" -> "getopts" ->
case rest of case rest of
opts:var:_ -> maybeToList $ getLiteral var opts:var:_ -> maybeToList $ getLiteral var
@ -652,8 +583,8 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
"export" -> "export" ->
if "f" `elem` flags then [] else concatMap getModifierParamString rest if "f" `elem` flags then [] else concatMap getModifierParamString rest
"declare" -> forDeclare "declare" -> if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
"typeset" -> forDeclare "typeset" -> declaredVars
"local" -> concatMap getModifierParamString rest "local" -> concatMap getModifierParamString rest
"readonly" -> "readonly" ->
@ -665,7 +596,6 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
return (base, base, "@", DataString $ SourceFrom params) return (base, base, "@", DataString $ SourceFrom params)
"printf" -> maybeToList $ getPrintfVariable rest "printf" -> maybeToList $ getPrintfVariable rest
"wait" -> maybeToList $ getWaitVariable rest
"mapfile" -> maybeToList $ getMapfileArray base rest "mapfile" -> maybeToList $ getMapfileArray base rest
"readarray" -> maybeToList $ getMapfileArray base rest "readarray" -> maybeToList $ getMapfileArray base rest
@ -685,8 +615,6 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]] T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
stripEqualsFrom t = t stripEqualsFrom t = t
forDeclare = if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
declaredVars = concatMap (getModifierParam defaultType) rest declaredVars = concatMap (getModifierParam defaultType) rest
where where
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
@ -725,39 +653,36 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
_ -> return (t:fromMaybe [] (getSetParams rest)) _ -> return (t:fromMaybe [] (getSetParams rest))
getSetParams [] = Nothing getSetParams [] = Nothing
getPrintfVariable list = getFlagAssignedVariable "v" (SourceFrom list) $ getBsdOpts "v:" list getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
getWaitVariable list = getFlagAssignedVariable "p" SourceInteger $ return $ getGenericOpts list where
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, varName, varType $ SourceFrom list)
getFlagAssignedVariable str dataSource maybeFlags = do where
flags <- maybeFlags (varName, varType) = case elemIndex '[' var of
(_, (flag, value)) <- find ((== str) . fst) flags Just i -> (take i var, DataArray)
variableName <- getLiteralStringExt (const $ return "!") value Nothing -> (var, DataString)
let (baseName, index) = span (/= '[') variableName f (_:rest) = f rest
return (base, value, baseName, (if null index then DataString else DataArray) dataSource) f [] = fail "not found"
-- mapfile has some curious syntax allowing flags plus 0..n variable names -- mapfile has some curious syntax allowing flags plus 0..n variable names
-- where only the first non-option one is used if any. -- where only the first non-option one is used if any. Here we cheat and
getMapfileArray base rest = parseArgs `mplus` fallback -- just get the last one, if it's a variable name.
where getMapfileArray base arguments = do
parseArgs :: Maybe (Token, Token, String, DataType) lastArg <- listToMaybe (reverse arguments)
parseArgs = do name <- getLiteralString lastArg
args <- getGnuOpts "d:n:O:s:u:C:c:t" rest guard $ isVariableName name
case [y | ("",(_,y)) <- args] of return (base, lastArg, name, DataArray SourceExternal)
[] ->
return (base, base, "MAPFILE", DataArray SourceExternal) -- get all the array variables used in read, e.g. read -a arr
first:_ -> do getReadArrayVariables args =
name <- getLiteralString first map (getLiteralArray . snd)
guard $ isVariableName name (filter (isArrayFlag . fst) (zip args (tail args)))
return (base, first, name, DataArray SourceExternal)
-- If arg parsing fails (due to bad or new flags), get the last variable name isArrayFlag x = fromMaybe False $ do
fallback :: Maybe (Token, Token, String, DataType) str <- getLiteralString x
fallback = do return $ case str of
(name, token) <- listToMaybe . mapMaybe f $ reverse rest '-':'-':_ -> False
return (base, token, name, DataArray SourceExternal) '-':str -> 'a' `elem` str
f arg = do _ -> False
name <- getLiteralString arg
guard $ isVariableName name
return (name, arg)
-- get the FLAGS_ variable created by a shflags DEFINE_ call -- get the FLAGS_ variable created by a shflags DEFINE_ call
getFlagVariable (n:v:_) = do getFlagVariable (n:v:_) = do
@ -767,23 +692,28 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
getModifiedVariableCommand _ = [] getModifiedVariableCommand _ = []
-- Given a NormalWord like foo or foo[$bar], get foo. getIndexReferences s = fromMaybe [] $ do
-- Primarily used to get references for [[ -v foo[bar] ]] match <- matchRegex re s
getVariableForTestDashV :: Token -> Maybe String index <- match !!! 0
getVariableForTestDashV t = do return $ matchAllStrings variableNameRegex index
str <- takeWhile ('[' /=) <$> getLiteralStringExt toStr t
guard $ isVariableName str
return str
where where
-- foo[bar] gets parsed with [bar] as a glob, so undo that re = mkRegex "(\\[.*\\])"
toStr (T_Glob _ s) = return s
-- Turn foo[$x] into foo[\0] so that we can get the constant array name prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
-- in a non-constant expression (while filtering out foo$x[$y]) prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
toStr _ = return "\0" prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
getOffsetReferences mods = fromMaybe [] $ do
-- if mods start with [, then drop until ]
match <- matchRegex re mods
offsets <- match !!! 1
return $ matchAllStrings variableNameRegex offsets
where
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
getReferencedVariables parents t = getReferencedVariables parents t =
case t of case t of
T_DollarBraced id _ l -> let str = concat $ oversimplify l in T_DollarBraced id _ l -> let str = bracedString t in
(t, t, getBracedReference str) : (t, t, getBracedReference str) :
map (\x -> (l, l, x)) ( map (\x -> (l, l, x)) (
getIndexReferences str getIndexReferences str
@ -798,7 +728,7 @@ getReferencedVariables parents t =
TC_Unary id _ "-v" token -> getIfReference t token TC_Unary id _ "-v" token -> getIfReference t token
TC_Unary id _ "-R" token -> getIfReference t token TC_Unary id _ "-R" token -> getIfReference t token
TC_Binary id DoubleBracket op lhs rhs -> TC_Binary id DoubleBracket op lhs rhs ->
if isDereferencingBinaryOp op if isDereferencing op
then concatMap (getIfReference t) [lhs, rhs] then concatMap (getIfReference t) [lhs, rhs]
else [] else []
@ -808,7 +738,7 @@ getReferencedVariables parents t =
(t, t, "output") (t, t, "output")
] ]
T_FdRedirect _ ('{':var) op -> -- {foo}>&- references and closes foo t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op] [(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
x -> getReferencedVariableCommand x x -> getReferencedVariableCommand x
where where
@ -825,17 +755,18 @@ getReferencedVariables parents t =
literalizer t = case t of literalizer t = case t of
T_Glob _ s -> return s -- Also when parsed as globs T_Glob _ s -> return s -- Also when parsed as globs
_ -> [] _ -> Nothing
getIfReference context token = maybeToList $ do getIfReference context token = maybeToList $ do
str <- getVariableForTestDashV token str@(h:_) <- getLiteralStringExt literalizer token
when (isDigit h) $ fail "is a number"
return (context, token, getBracedReference str) return (context, token, getBracedReference str)
isArithmeticAssignment t = case getPath parents t of isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
this NE.:| TA_Assignment _ "=" lhs _ :_ -> lhs == t
_ -> False
isDereferencingBinaryOp = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"]) isArithmeticAssignment t = case getPath parents t of
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
_ -> False
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v] dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
@ -859,6 +790,16 @@ isConfusedGlobRegex ('*':_) = True
isConfusedGlobRegex [x,'*'] | x `notElem` "\\." = True isConfusedGlobRegex [x,'*'] | x `notElem` "\\." = True
isConfusedGlobRegex _ = False isConfusedGlobRegex _ = False
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
isVariableChar x = isVariableStartChar x || isDigit x
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
prop_isVariableName1 = isVariableName "_fo123"
prop_isVariableName2 = not $ isVariableName "4"
prop_isVariableName3 = not $ isVariableName "test: "
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
isVariableName _ = False
getVariablesFromLiteralToken token = getVariablesFromLiteralToken token =
getVariablesFromLiteral (getLiteralStringDef " " token) getVariablesFromLiteral (getLiteralStringDef " " token)
@ -867,15 +808,77 @@ getVariablesFromLiteralToken token =
prop_getVariablesFromLiteral1 = prop_getVariablesFromLiteral1 =
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"] getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
getVariablesFromLiteral string = getVariablesFromLiteral string =
map head $ matchAllSubgroups variableRegex string map (!! 0) $ matchAllSubgroups variableRegex string
where where
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)" variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
-- Get the variable name from an expansion like ${var:-foo}
prop_getBracedReference1 = getBracedReference "foo" == "foo"
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
prop_getBracedReference3 = getBracedReference "#" == "#"
prop_getBracedReference4 = getBracedReference "##" == "#"
prop_getBracedReference5 = getBracedReference "#!" == "!"
prop_getBracedReference6 = getBracedReference "!#" == "#"
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
prop_getBracedReference11= getBracedReference "!os*" == ""
prop_getBracedReference12= getBracedReference "!os?bar**" == ""
prop_getBracedReference13= getBracedReference "foo[bar]" == "foo"
getBracedReference s = fromMaybe s $
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
where
noPrefix = dropPrefix s
dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest
dropPrefix "" = ""
takeName s = do
let name = takeWhile isVariableChar s
guard . not $ null name
return name
getSpecial (c:_) =
if c `elem` "*@#?-$!" then return [c] else fail "not special"
getSpecial _ = fail "empty"
nameExpansion ('!':rest) = do -- e.g. ${!foo*bar*}
let suffix = dropWhile isVariableChar rest
guard $ suffix /= rest -- e.g. ${!@}
first <- suffix !!! 0
guard $ first `elem` "*?"
return ""
nameExpansion _ = Nothing
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
getBracedModifier s = 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]
-- Useful generic functions.
-- Get element 0 or a default. Like `head` but safe.
headOrDefault _ (a:_) = a
headOrDefault def _ = def
--- Get element n of a list, or Nothing. Like `!!` but safe.
(!!!) list i =
case drop i list of
[] -> Nothing
(r:_) -> Just r
-- Run a command if the shell is in the given list -- Run a command if the shell is in the given list
whenShell l c = do whenShell l c = do
params <- ask shell <- asks shellType
when (shellType params `elem` l ) c when (shell `elem` l ) c
filterByAnnotation asSpec params = filterByAnnotation asSpec params =
@ -904,41 +907,54 @@ isCountingReference _ = False
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"} -- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
isQuotedAlternativeReference t = isQuotedAlternativeReference t =
case t of case t of
T_DollarBraced _ _ l -> T_DollarBraced _ _ _ ->
getBracedModifier (concat $ oversimplify l) `matches` re getBracedModifier (bracedString t) `matches` re
_ -> False _ -> False
where where
re = mkRegex "(^|\\]):?\\+" re = mkRegex "(^|\\]):?\\+"
supportsArrays Bash = True -- getGnuOpts "erd:u:" will parse a SimpleCommand like
supportsArrays Ksh = True -- read -re -d : -u 3 bar
supportsArrays _ = False -- into
-- Just [("r", -re), ("e", -re), ("d", :), ("u", 3), ("", bar)]
isTrueAssignmentSource c = -- where flags with arguments map to arguments, while others map to themselves.
case c of -- Any unrecognized flag will result in Nothing.
DataString SourceChecked -> False getGnuOpts str t = getOpts str $ getAllFlags t
DataString SourceDeclaration -> False getBsdOpts str t = getOpts str $ getLeadingFlags t
DataArray SourceChecked -> False getOpts :: String -> [(Token, String)] -> Maybe [(String, Token)]
DataArray SourceDeclaration -> False getOpts string flags = process flags
_ -> True
modifiesVariable params token name =
or $ map check flow
where where
flow = getVariableFlow params token flagList (c:':':rest) = ([c], True) : flagList rest
check t = flagList (c:rest) = ([c], False) : flagList rest
case t of flagList [] = []
Assignment (_, _, n, source) -> isTrueAssignmentSource source && n == name flagMap = Map.fromList $ ("", False) : flagList string
_ -> False
isTestCommand t = process [] = return []
case t of process [(token, flag)] = do
T_Condition {} -> True takesArg <- Map.lookup flag flagMap
T_SimpleCommand {} -> t `isCommand` "test" guard $ not takesArg
T_Redirecting _ _ t -> isTestCommand t return [(flag, token)]
T_Annotation _ _ t -> isTestCommand t process ((token1, flag1):rest2@((token2, flag2):rest)) = do
T_Pipeline _ _ [t] -> isTestCommand t takesArg <- Map.lookup flag1 flagMap
_ -> False if takesArg
then do
guard $ null flag2
more <- process rest
return $ (flag1, token2) : more
else do
more <- process rest2
return $ (flag1, token1) : more
supportsArrays shell = shell == Bash || shell == Ksh
-- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh)
isBashLike :: Parameters -> Bool
isBashLike params =
case shellType params of
Bash -> True
Ksh -> True
Dash -> False
Sh -> False
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
{- {-
Copyright 2012-2022 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
@ -20,12 +20,10 @@
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
import ShellCheck.Analyzer
import ShellCheck.ASTLib
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Parser import ShellCheck.Parser
import ShellCheck.Analyzer
import Debug.Trace -- DO NOT SUBMIT
import Data.Either import Data.Either
import Data.Functor import Data.Functor
import Data.List import Data.List
@ -87,8 +85,7 @@ checkScript sys spec = do
asCheckSourced = csCheckSourced spec, asCheckSourced = csCheckSourced spec,
asExecutionMode = Executed, asExecutionMode = Executed,
asTokenPositions = tokenPositions, asTokenPositions = tokenPositions,
asExtendedAnalysis = csExtendedAnalysis spec, asOptionalChecks = csOptionalChecks spec
asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec
} where as = newAnalysisSpec root } where as = newAnalysisSpec root
let analysisMessages = let analysisMessages =
maybe [] maybe []
@ -159,11 +156,6 @@ checkWithIncludesAndSourcePath includes mapper = getErrors
siFindSource = mapper siFindSource = mapper
} }
checkWithRcIncludesAndSourcePath rc includes mapper = getErrors
(mockRcFile rc $ mockedSystemInterface includes) {
siFindSource = mapper
}
prop_findsParseIssue = check "echo \"$12\"" == [1037] prop_findsParseIssue = check "echo \"$12\"" == [1037]
prop_commentDisablesParseIssue1 = prop_commentDisablesParseIssue1 =
@ -237,18 +229,9 @@ prop_cantSourceDynamic =
prop_cantSourceDynamic2 = prop_cantSourceDynamic2 =
[1090] == checkWithIncludes [("lib", "")] "source ~/foo" [1090] == checkWithIncludes [("lib", "")] "source ~/foo"
prop_canStripPrefixAndSource =
null $ checkWithIncludes [("./lib", "")] "source \"$MYDIR/lib\""
prop_canStripPrefixAndSource2 =
null $ checkWithIncludes [("./utils.sh", "")] "source \"$(dirname \"${BASH_SOURCE[0]}\")/utils.sh\""
prop_canSourceDynamicWhenRedirected = prop_canSourceDynamicWhenRedirected =
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\"" null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
prop_canRedirectWithSpaces =
null $ checkWithIncludes [("my file", "")] "#shellcheck source=\"my file\"\n. \"$1\""
prop_recursiveAnalysis = prop_recursiveAnalysis =
[2086] == checkRecursive [("lib", "echo $1")] "source lib" [2086] == checkRecursive [("lib", "echo $1")] "source lib"
@ -287,7 +270,7 @@ prop_filewideAnnotation8 = null $
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1" check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source' prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
3046 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh" 2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
prop_spinBug1413 = null $ check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n" prop_spinBug1413 = null $ check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n"
@ -312,20 +295,6 @@ prop_canDisableShebangWarning = null $ result
csScript = "#shellcheck disable=SC2148\nfoo" csScript = "#shellcheck disable=SC2148\nfoo"
} }
prop_canDisableAllWarnings = result == [2086]
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.sh",
csScript = "#!/bin/sh\necho $1\n#shellcheck disable=all\necho `echo $1`"
}
prop_canDisableParseErrors = null $ result
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.sh",
csScript = "#shellcheck disable=SC1073,SC1072,SC2148\n()"
}
prop_shExtensionDoesntMatter = result == [2148] prop_shExtensionDoesntMatter = result == [2148]
where where
result = checkWithSpec [] emptyCheckSpec { result = checkWithSpec [] emptyCheckSpec {
@ -402,7 +371,7 @@ prop_canEnableOptionalsWithRc = result == [2244]
prop_sourcePathRedirectsName = result == [2086] prop_sourcePathRedirectsName = result == [2086]
where where
f "dir/myscript" _ _ "lib" = return "foo/lib" f "dir/myscript" _ "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec { result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource lib", csScript = "#!/bin/bash\nsource lib",
csFilename = "dir/myscript", csFilename = "dir/myscript",
@ -411,154 +380,22 @@ prop_sourcePathRedirectsName = result == [2086]
prop_sourcePathAddsAnnotation = result == [2086] prop_sourcePathAddsAnnotation = result == [2086]
where where
f "dir/myscript" _ ["mypath"] "lib" = return "foo/lib" f "dir/myscript" ["mypath"] "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec { result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source-path=mypath\nsource lib", csScript = "#!/bin/bash\n# shellcheck source-path=mypath\nsource lib",
csFilename = "dir/myscript", csFilename = "dir/myscript",
csCheckSourced = True csCheckSourced = True
} }
prop_sourcePathWorksWithSpaces = result == [2086]
where
f "dir/myscript" _ ["my path"] "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source-path='my path'\nsource lib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_sourcePathRedirectsDirective = result == [2086] prop_sourcePathRedirectsDirective = result == [2086]
where where
f "dir/myscript" _ _ "lib" = return "foo/lib" f "dir/myscript" _ "lib" = return "foo/lib"
f _ _ _ _ = return "/dev/null" f _ _ _ = return "/dev/null"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec { result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source=lib\nsource kittens", csScript = "#!/bin/bash\n# shellcheck source=lib\nsource kittens",
csFilename = "dir/myscript", csFilename = "dir/myscript",
csCheckSourced = True csCheckSourced = True
} }
prop_rcCanAllowExternalSources = result == [2086]
where
f "dir/myscript" (Just True) _ "mylib" = return "resolved/mylib"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "external-sources=true" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource mylib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_rcCanDenyExternalSources = result == [2086]
where
f "dir/myscript" (Just False) _ "mylib" = return "resolved/mylib"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "external-sources=false" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource mylib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_rcCanLeaveExternalSourcesUnspecified = result == [2086]
where
f "dir/myscript" Nothing _ "mylib" = return "resolved/mylib"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource mylib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_fileCanDisableExternalSources = result == [2006, 2086]
where
f "dir/myscript" (Just True) _ "withExternal" = return "withExternal"
f "dir/myscript" (Just False) _ "withoutExternal" = return "withoutExternal"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "external-sources=true" [("withExternal", "echo $1"), ("withoutExternal", "_=`foo`")] f emptyCheckSpec {
csScript = "#!/bin/bash\ntrue\nsource withExternal\n# shellcheck external-sources=false\nsource withoutExternal",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_fileCannotEnableExternalSources = result == [1144]
where
f "dir/myscript" Nothing _ "foo" = return "foo"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "" [("foo", "true")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck external-sources=true\nsource foo",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_fileCannotEnableExternalSources2 = result == [1144]
where
f "dir/myscript" (Just False) _ "foo" = return "foo"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "external-sources=false" [("foo", "true")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck external-sources=true\nsource foo",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_rcCanSuppressEarlyProblems1 = null result
where
result = checkWithRc "disable=1071" emptyCheckSpec {
csScript = "#!/bin/zsh\necho $1"
}
prop_rcCanSuppressEarlyProblems2 = null result
where
result = checkWithRc "disable=1104" emptyCheckSpec {
csScript = "!/bin/bash\necho 'hello world'"
}
prop_sourceWithHereDocWorks = null result
where
result = checkWithIncludes [("bar", "true\n")] "source bar << eof\nlol\neof"
prop_hereDocsAreParsedWithoutTrailingLinefeed = 1044 `elem` result
where
result = check "cat << eof"
prop_hereDocsWillHaveParsedIndices = null result
where
result = check "#!/bin/bash\nmy_array=(a b)\ncat <<EOF >> ./test\n $(( 1 + my_array[1] ))\nEOF"
prop_rcCanSuppressDfa = null result
where
result = checkWithRc "extended-analysis=false" emptyCheckSpec {
csScript = "#!/bin/sh\nexit; foo;"
}
prop_fileCanSuppressDfa = null $ traceShowId result
where
result = checkWithRc "" emptyCheckSpec {
csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;"
}
prop_fileWinsWhenSuppressingDfa1 = null result
where
result = checkWithRc "extended-analysis=true" emptyCheckSpec {
csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;"
}
prop_fileWinsWhenSuppressingDfa2 = result == [2317]
where
result = checkWithRc "extended-analysis=false" emptyCheckSpec {
csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;"
}
prop_flagWinsWhenSuppressingDfa1 = result == [2317]
where
result = checkWithRc "extended-analysis=false" emptyCheckSpec {
csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;",
csExtendedAnalysis = Just True
}
prop_flagWinsWhenSuppressingDfa2 = null result
where
result = checkWithRc "extended-analysis=true" emptyCheckSpec {
csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;",
csExtendedAnalysis = Just False
}
return [] return []
runTests = $quickCheckAll runTests = $quickCheckAll

View file

@ -1,5 +1,5 @@
{- {-
Copyright 2012-2022 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
@ -19,8 +19,6 @@
-} -}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE PatternGuards #-}
-- This module contains checks that examine specific commands by name. -- This module contains checks that examine specific commands by name.
module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where
@ -28,29 +26,21 @@ module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Co
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.ASTLib import ShellCheck.ASTLib
import ShellCheck.AnalyzerLib import ShellCheck.AnalyzerLib
import ShellCheck.CFG
import qualified ShellCheck.CFGAnalysis as CF
import ShellCheck.Data import ShellCheck.Data
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Parser import ShellCheck.Parser
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.Functor.Identity
import qualified Data.Graph.Inductive.Graph as G
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import qualified Data.List.NonEmpty as NE import qualified Data.Map.Strict as Map
import qualified Data.Map.Strict as M
import qualified Data.Set as S
import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess) import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
import Debug.Trace -- STRIP
data CommandName = Exactly String | Basename String data CommandName = Exactly String | Basename String
deriving (Eq, Ord) deriving (Eq, Ord)
@ -62,11 +52,13 @@ verify :: CommandCheck -> String -> Bool
verify f s = producesComments (getChecker [f]) s == Just True verify f s = producesComments (getChecker [f]) s == Just True
verifyNot f s = producesComments (getChecker [f]) s == Just False verifyNot f s = producesComments (getChecker [f]) s == Just False
arguments (T_SimpleCommand _ _ (cmd:args)) = args
commandChecks :: [CommandCheck] commandChecks :: [CommandCheck]
commandChecks = [ commandChecks = [
checkTr checkTr
,checkFindNameGlob ,checkFindNameGlob
,checkExpr ,checkNeedlessExpr
,checkGrepRe ,checkGrepRe
,checkTrapQuotes ,checkTrapQuotes
,checkReturn ,checkReturn
@ -103,15 +95,7 @@ commandChecks = [
,checkSudoArgs ,checkSudoArgs
,checkSourceArgs ,checkSourceArgs
,checkChmodDashr ,checkChmodDashr
,checkXargsDashi
,checkUnquotedEchoSpaces
,checkEvalArray
] ]
++ map checkArgComparison ("alias" : declaringCommands)
++ map checkMaskedReturns declaringCommands
++ map checkMultipleDeclaring declaringCommands
++ map checkBackreferencingDeclaration declaringCommands
optionalChecks = map fst optionalCommandChecks optionalChecks = map fst optionalCommandChecks
optionalCommandChecks :: [(CheckDescription, CommandCheck)] optionalCommandChecks :: [(CheckDescription, CommandCheck)]
@ -123,7 +107,7 @@ optionalCommandChecks = [
cdNegative = "command -v javac" cdNegative = "command -v javac"
}, checkWhich) }, checkWhich)
] ]
optionalCheckMap = M.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks optionalCheckMap = Map.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks
prop_verifyOptionalExamples = all check optionalCommandChecks prop_verifyOptionalExamples = all check optionalCommandChecks
where where
@ -131,67 +115,27 @@ prop_verifyOptionalExamples = all check optionalCommandChecks
verify check (cdPositive desc) verify check (cdPositive desc)
&& verifyNot check (cdNegative desc) && verifyNot check (cdNegative desc)
-- Run a check against the getopt parser. If it fails, the lists are empty. buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
checkGetOpts str flags args f = buildCommandMap = foldl' addCheck Map.empty
flags == actualFlags && args == actualArgs
where
toTokens = map (T_Literal (Id 0)) . words
opts = fromMaybe [] $ f (toTokens str)
actualFlags = filter (not . null) $ map fst opts
actualArgs = [onlyLiteralString x | ("", (_, x)) <- opts]
-- Short options
prop_checkGetOptsS1 = checkGetOpts "-f x" ["f"] [] $ getOpts (True, True) "f:" []
prop_checkGetOptsS2 = checkGetOpts "-fx" ["f"] [] $ getOpts (True, True) "f:" []
prop_checkGetOptsS3 = checkGetOpts "-f -x" ["f", "x"] [] $ getOpts (True, True) "fx" []
prop_checkGetOptsS4 = checkGetOpts "-f -x" ["f"] [] $ getOpts (True, True) "f:" []
prop_checkGetOptsS5 = checkGetOpts "-fx" [] [] $ getOpts (True, True) "fx:" []
prop_checkGenericOptsS1 = checkGetOpts "-f x" ["f"] [] $ return . getGenericOpts
prop_checkGenericOptsS2 = checkGetOpts "-abc x" ["a", "b", "c"] [] $ return . getGenericOpts
prop_checkGenericOptsS3 = checkGetOpts "-abc -x" ["a", "b", "c", "x"] [] $ return . getGenericOpts
prop_checkGenericOptsS4 = checkGetOpts "-x" ["x"] [] $ return . getGenericOpts
-- Long options
prop_checkGetOptsL1 = checkGetOpts "--foo=bar baz" ["foo"] ["baz"] $ getOpts (True, False) "" [("foo", True)]
prop_checkGetOptsL2 = checkGetOpts "--foo bar baz" ["foo"] ["baz"] $ getOpts (True, False) "" [("foo", True)]
prop_checkGetOptsL3 = checkGetOpts "--foo baz" ["foo"] ["baz"] $ getOpts (True, True) "" []
prop_checkGetOptsL4 = checkGetOpts "--foo baz" [] [] $ getOpts (True, False) "" []
prop_checkGenericOptsL1 = checkGetOpts "--foo=bar" ["foo"] [] $ return . getGenericOpts
prop_checkGenericOptsL2 = checkGetOpts "--foo bar" ["foo"] ["bar"] $ return . getGenericOpts
prop_checkGenericOptsL3 = checkGetOpts "-x --foo" ["x", "foo"] [] $ return . getGenericOpts
-- Know when to terminate
prop_checkGetOptsT1 = checkGetOpts "-a x -b" ["a", "b"] ["x"] $ getOpts (True, True) "ab" []
prop_checkGetOptsT2 = checkGetOpts "-a x -b" ["a"] ["x","-b"] $ getOpts (False, True) "ab" []
prop_checkGetOptsT3 = checkGetOpts "-a -- -b" ["a"] ["-b"] $ getOpts (True, True) "ab" []
prop_checkGetOptsT4 = checkGetOpts "-a -- -b" ["a", "b"] [] $ getOpts (True, True) "a:b" []
prop_checkGenericOptsT1 = checkGetOpts "-x -- -y" ["x"] ["-y"] $ return . getGenericOpts
prop_checkGenericOptsT2 = checkGetOpts "-xy --" ["x", "y"] [] $ return . getGenericOpts
buildCommandMap :: [CommandCheck] -> M.Map CommandName (Token -> Analysis)
buildCommandMap = foldl' addCheck M.empty
where where
addCheck map (CommandCheck name function) = addCheck map (CommandCheck name function) =
M.insertWith composeAnalyzers name function map Map.insertWith composeAnalyzers name function map
checkCommand :: M.Map CommandName (Token -> Analysis) -> Token -> Analysis checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do
name <- getLiteralString cmd name <- getLiteralString cmd
return $ return $
if | '/' `elem` name -> if '/' `elem` name
M.findWithDefault nullCheck (Basename $ basename name) map t then
| name == "builtin", (h:_) <- rest -> Map.findWithDefault nullCheck (Basename $ basename name) map t
let t' = T_SimpleCommand id cmdPrefix rest else if name == "builtin" && not (null rest) then
selectedBuiltin = onlyLiteralString h let t' = T_SimpleCommand id cmdPrefix rest
in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t' selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest
| otherwise -> do in Map.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
M.findWithDefault nullCheck (Exactly name) map t else do
M.findWithDefault nullCheck (Basename name) map t Map.findWithDefault nullCheck (Exactly name) map t
Map.findWithDefault nullCheck (Basename name) map t
where where
basename = reverse . takeWhile (/= '/') . reverse basename = reverse . takeWhile (/= '/') . reverse
@ -213,22 +157,22 @@ checker spec params = getChecker $ commandChecks ++ optionals
optionals = optionals =
if "all" `elem` keys if "all" `elem` keys
then map snd optionalCommandChecks then map snd optionalCommandChecks
else mapMaybe (\x -> M.lookup x optionalCheckMap) keys else mapMaybe (\x -> Map.lookup x optionalCheckMap) keys
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]" prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'" prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
prop_checkTr2a = verify checkTr "tr '[a-z]' '[A-Z]'" prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'"
prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'" prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'"
prop_checkTr3a = verifyNot checkTr "tr -d '[:upper:]'" prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'"
prop_checkTr3b = verifyNot checkTr "tr -d '|/_[:upper:]'" prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'"
prop_checkTr4 = verifyNot checkTr "ls [a-z]" prop_checkTr4 = verifyNot checkTr "ls [a-z]"
prop_checkTr5 = verify checkTr "tr foo bar" prop_checkTr5 = verify checkTr "tr foo bar"
prop_checkTr6 = verify checkTr "tr 'hello' 'world'" prop_checkTr6 = verify checkTr "tr 'hello' 'world'"
prop_checkTr8 = verifyNot checkTr "tr aeiou _____" prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
prop_checkTr9 = verifyNot checkTr "a-z n-za-m" prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
prop_checkTr10 = verifyNot checkTr "tr --squeeze-repeats rl lr" prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
prop_checkTr11 = verifyNot checkTr "tr abc '[d*]'" prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
prop_checkTr12 = verifyNot checkTr "tr '[=e=]' 'e'" prop_checkTr12= verifyNot checkTr "tr '[=e=]' 'e'"
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments) checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
where where
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme? f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
@ -255,81 +199,27 @@ prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
acceptsGlob s = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ] acceptsGlob s = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
f [] = return () f [] = return ()
f (x:xs) = foldr g (const $ return ()) xs x f (x:xs) = g x xs
g b acc a = do g _ [] = return ()
g a (b:r) = do
forM_ (getLiteralString a) $ \s -> when (acceptsGlob s && isGlob b) $ forM_ (getLiteralString a) $ \s -> when (acceptsGlob s && isGlob b) $
warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it." warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."
acc b g b r
prop_checkExpr = verify checkExpr "foo=$(expr 3 + 2)" prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
prop_checkExpr2 = verify checkExpr "foo=`echo \\`expr 3 + 2\\``" prop_checkNeedlessExpr2 = verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``"
prop_checkExpr3 = verifyNot checkExpr "foo=$(expr foo : regex)" prop_checkNeedlessExpr3 = verifyNot checkNeedlessExpr "foo=$(expr foo : regex)"
prop_checkExpr4 = verifyNot checkExpr "foo=$(expr foo \\< regex)" prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)"
prop_checkExpr5 = verify checkExpr "# shellcheck disable=SC2003\nexpr match foo bar" checkNeedlessExpr = CommandCheck (Basename "expr") f where
prop_checkExpr6 = verify checkExpr "# shellcheck disable=SC2003\nexpr foo : fo*" f t =
prop_checkExpr7 = verify checkExpr "# shellcheck disable=SC2003\nexpr 5 -3"
prop_checkExpr8 = verifyNot checkExpr "# shellcheck disable=SC2003\nexpr \"$@\""
prop_checkExpr9 = verifyNot checkExpr "# shellcheck disable=SC2003\nexpr 5 $rest"
prop_checkExpr10 = verify checkExpr "# shellcheck disable=SC2003\nexpr length \"$var\""
prop_checkExpr11 = verify checkExpr "# shellcheck disable=SC2003\nexpr foo > bar"
prop_checkExpr12 = verify checkExpr "# shellcheck disable=SC2003\nexpr 1 | 2"
prop_checkExpr13 = verify checkExpr "# shellcheck disable=SC2003\nexpr 1 * 2"
prop_checkExpr14 = verify checkExpr "# shellcheck disable=SC2003\nexpr \"$x\" >= \"$y\""
checkExpr = CommandCheck (Basename "expr") f where
f t = do
when (all (`notElem` exceptions) (words $ arguments t)) $ when (all (`notElem` exceptions) (words $ arguments t)) $
style (getId $ getCommandTokenOrThis t) 2003 style (getId $ getCommandTokenOrThis t) 2003
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]." "expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
case arguments t of
[lhs, op, rhs] -> do
checkOp lhs
case getWordParts op of
[T_Glob _ "*"] ->
err (getId op) 2304
"* must be escaped to multiply: \\*. Modern $((x * y)) avoids this issue."
[T_Literal _ ":"] | isGlob rhs ->
warn (getId rhs) 2305
"Quote regex argument to expr to avoid it expanding as a glob."
_ -> return ()
[single] | not (willSplit single) ->
warn (getId single) 2307
"'expr' expects 3+ arguments but sees 1. Make sure each operator/operand is a separate argument, and escape <>&|."
[first, second] |
onlyLiteralString first /= "length"
&& not (willSplit first || willSplit second) -> do
checkOp first
warn (getId t) 2307
"'expr' expects 3+ arguments, but sees 2. Make sure each operator/operand is a separate argument, and escape <>&|."
(first:rest) -> do
checkOp first
forM_ rest $ \t ->
-- We already find 95%+ of multiplication and regex earlier, so don't bother classifying this further.
when (isGlob t) $ warn (getId t) 2306 "Escape glob characters in arguments to expr to avoid pathname expansion."
_ -> return ()
-- These operators are hard to replicate in POSIX -- These operators are hard to replicate in POSIX
exceptions = [ ":", "<", ">", "<=", ">=", exceptions = [ ":", "<", ">", "<=", ">=" ]
-- We can offer better suggestions for these
"match", "length", "substr", "index"]
words = mapMaybe getLiteralString words = mapMaybe getLiteralString
checkOp side =
case getLiteralString side of
Just "match" -> msg "'expr match' has unspecified results. Prefer 'expr str : regex'."
Just "length" -> msg "'expr length' has unspecified results. Prefer ${#var}."
Just "substr" -> msg "'expr substr' has unspecified results. Prefer 'cut' or ${var#???}."
Just "index" -> msg "'expr index' has unspecified results. Prefer x=${var%%[chars]*}; $((${#x}+1))."
_ -> return ()
where
msg = info (getId side) 2308
prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3" prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3"
prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3" prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3"
@ -340,20 +230,20 @@ prop_checkGrepRe6 = verifyNot checkGrepRe "grep foo \\*.mp3"
prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file" prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file"
prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg" prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg"
prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file" prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"
prop_checkGrepRe10 = verifyNot checkGrepRe "grep '^aa*' file" prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file"
prop_checkGrepRe11 = verifyNot checkGrepRe "grep --include=*.png foo" prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo"
prop_checkGrepRe12 = verifyNot checkGrepRe "grep -F 'Foo*' file" prop_checkGrepRe12= verifyNot checkGrepRe "grep -F 'Foo*' file"
prop_checkGrepRe13 = verifyNot checkGrepRe "grep -- -foo bar*" prop_checkGrepRe13= verifyNot checkGrepRe "grep -- -foo bar*"
prop_checkGrepRe14 = verifyNot checkGrepRe "grep -e -foo bar*" prop_checkGrepRe14= verifyNot checkGrepRe "grep -e -foo bar*"
prop_checkGrepRe15 = verifyNot checkGrepRe "grep --regex -foo bar*" prop_checkGrepRe15= verifyNot checkGrepRe "grep --regex -foo bar*"
prop_checkGrepRe16 = verifyNot checkGrepRe "grep --include 'Foo*' file" prop_checkGrepRe16= verifyNot checkGrepRe "grep --include 'Foo*' file"
prop_checkGrepRe17 = verifyNot checkGrepRe "grep --exclude 'Foo*' file" prop_checkGrepRe17= verifyNot checkGrepRe "grep --exclude 'Foo*' file"
prop_checkGrepRe18 = verifyNot checkGrepRe "grep --exclude-dir 'Foo*' file" prop_checkGrepRe18= verifyNot checkGrepRe "grep --exclude-dir 'Foo*' file"
prop_checkGrepRe19 = verify checkGrepRe "grep -- 'Foo*' file" prop_checkGrepRe19= verify checkGrepRe "grep -- 'Foo*' file"
prop_checkGrepRe20 = verifyNot checkGrepRe "grep --fixed-strings 'Foo*' file" prop_checkGrepRe20= verifyNot checkGrepRe "grep --fixed-strings 'Foo*' file"
prop_checkGrepRe21 = verifyNot checkGrepRe "grep -o 'x*' file" prop_checkGrepRe21= verifyNot checkGrepRe "grep -o 'x*' file"
prop_checkGrepRe22 = verifyNot checkGrepRe "grep --only-matching 'x*' file" prop_checkGrepRe22= verifyNot checkGrepRe "grep --only-matching 'x*' file"
prop_checkGrepRe23 = verifyNot checkGrepRe "grep '.*' file" prop_checkGrepRe23= verifyNot checkGrepRe "grep '.*' file"
checkGrepRe = CommandCheck (Basename "grep") check where checkGrepRe = CommandCheck (Basename "grep") check where
check cmd = f cmd (arguments cmd) check cmd = f cmd (arguments cmd)
@ -393,15 +283,21 @@ checkGrepRe = CommandCheck (Basename "grep") check where
candidates = candidates =
sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords
getSuspiciousRegexWildcard str = case matchRegex suspicious str of getSuspiciousRegexWildcard str =
Just [[c]] | not (str `matches` contra) -> Just c if not $ str `matches` contra
_ -> fail "looks good" then do
suspicious = mkRegex "([A-Za-z1-9])\\*" match <- matchRegex suspicious str
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]" str <- match !!! 0
str !!! 0
else
fail "looks good"
where
suspicious = mkRegex "([A-Za-z1-9])\\*"
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT" prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
prop_checkTrapQuotes1a = verify checkTrapQuotes "trap \"echo `ls`\" INT" prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT"
prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT" prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT"
prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG" prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
@ -482,16 +378,9 @@ prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\n
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\"" prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol" prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'" prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
prop_checkUnusedEchoEscapes6 = verify checkUnusedEchoEscapes "echo '\\506'"
prop_checkUnusedEchoEscapes7 = verify checkUnusedEchoEscapes "echo '\\5a'"
prop_checkUnusedEchoEscapes8 = verifyNot checkUnusedEchoEscapes "echo '\\8a'"
prop_checkUnusedEchoEscapes9 = verifyNot checkUnusedEchoEscapes "echo '\\d5a'"
prop_checkUnusedEchoEscapes10 = verify checkUnusedEchoEscapes "echo '\\x4a'"
prop_checkUnusedEchoEscapes11 = verify checkUnusedEchoEscapes "echo '\\xat'"
prop_checkUnusedEchoEscapes12 = verifyNot checkUnusedEchoEscapes "echo '\\xth'"
checkUnusedEchoEscapes = CommandCheck (Basename "echo") f checkUnusedEchoEscapes = CommandCheck (Basename "echo") f
where where
hasEscapes = mkRegex "\\\\([rntabefv\\']|[0-7]{1,3}|x([0-9]|[A-F]|[a-f]){1,2})" hasEscapes = mkRegex "\\\\[rnt]"
f cmd = f cmd =
whenShell [Sh, Bash, Ksh] $ whenShell [Sh, Bash, Ksh] $
unless (cmd `hasFlag` "e") $ unless (cmd `hasFlag` "e") $
@ -573,8 +462,8 @@ checkMkdirDashPM = CommandCheck (Basename "mkdir") check
where where
check t = sequence_ $ do check t = sequence_ $ do
let flags = getAllFlags t let flags = getAllFlags t
dashP <- find (\(_,f) -> f == "p" || f == "parents") flags dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
dashM <- find (\(_,f) -> f == "m" || f == "mode") flags dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
-- mkdir -pm 0700 dir is fine, so is ../dir, but dir/subdir is not. -- mkdir -pm 0700 dir is fine, so is ../dir, but dir/subdir is not.
guard $ any couldHaveSubdirs (drop 1 $ arguments t) guard $ any couldHaveSubdirs (drop 1 $ arguments t)
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory." return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
@ -594,7 +483,7 @@ prop_checkNonportableSignals7 = verifyNot checkNonportableSignals "trap 'stop' i
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments) checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
where where
f args = case args of f args = case args of
first:rest | not $ isFlag first -> mapM_ check rest first:rest -> unless (isFlag first) $ mapM_ check rest
_ -> return () _ -> return ()
check param = sequence_ $ do check param = sequence_ $ do
@ -631,9 +520,9 @@ checkInteractiveSu = CommandCheck (Basename "su") f
info (getId cmd) 2117 info (getId cmd) 2117
"To run commands as another user, use su -c or sudo." "To run commands as another user, use su -c or sudo."
undirected (T_Pipeline _ _ (_:_:_)) = False undirected (T_Pipeline _ _ l) = length l <= 1
-- This should really just be modifications to stdin, but meh -- This should really just be modifications to stdin, but meh
undirected (T_Redirecting _ (_:_) _) = False undirected (T_Redirecting _ list _) = null list
undirected _ = True undirected _ = True
@ -650,8 +539,9 @@ checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
([], hostport:r@(_:_)) -> checkArg $ last r ([], hostport:r@(_:_)) -> checkArg $ last r
_ -> return () _ -> return ()
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) = checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
forM_ (find (not . isConstant) parts) $ case filter (not . isConstant) parts of
\x -> info (getId x) 2029 [] -> return ()
(x:_) -> info (getId x) 2029
"Note that, unescaped, this expands on the client side." "Note that, unescaped, this expands on the client side."
checkArg _ = return () checkArg _ = return ()
@ -665,20 +555,18 @@ prop_checkPrintfVar6 = verify checkPrintfVar "printf foo bar baz"
prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz" prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz"
prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\"" prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\""
prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png" prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
prop_checkPrintfVar10 = verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz" prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
prop_checkPrintfVar11 = verifyNot checkPrintfVar "printf '%(%s%s)T' -1" prop_checkPrintfVar11= verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
prop_checkPrintfVar12 = verify checkPrintfVar "printf '%s %s\\n' 1 2 3" prop_checkPrintfVar12= verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
prop_checkPrintfVar13 = verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4" prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
prop_checkPrintfVar14 = verify checkPrintfVar "printf '%*s\\n' 1" prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1"
prop_checkPrintfVar15 = verifyNot checkPrintfVar "printf '%*s\\n' 1 2" prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
prop_checkPrintfVar16 = verifyNot checkPrintfVar "printf $'string'" prop_checkPrintfVar16= verifyNot checkPrintfVar "printf $'string'"
prop_checkPrintfVar17 = verify checkPrintfVar "printf '%-*s\\n' 1" prop_checkPrintfVar17= verify checkPrintfVar "printf '%-*s\\n' 1"
prop_checkPrintfVar18 = verifyNot checkPrintfVar "printf '%-*s\\n' 1 2" prop_checkPrintfVar18= verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
prop_checkPrintfVar19 = verifyNot checkPrintfVar "printf '%(%s)T'" prop_checkPrintfVar19= verifyNot checkPrintfVar "printf '%(%s)T'"
prop_checkPrintfVar20 = verifyNot checkPrintfVar "printf '%d %(%s)T' 42" prop_checkPrintfVar20= verifyNot checkPrintfVar "printf '%d %(%s)T' 42"
prop_checkPrintfVar21 = verify checkPrintfVar "printf '%d %(%s)T'" prop_checkPrintfVar21= verify checkPrintfVar "printf '%d %(%s)T'"
prop_checkPrintfVar22 = verify checkPrintfVar "printf '%s\n%s' foo"
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
@ -691,24 +579,23 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
let formats = getPrintfFormats string let formats = getPrintfFormats string
let formatCount = length formats let formatCount = length formats
let argCount = length more let argCount = length more
let pluraliseIfMany word n = if n > 1 then word ++ "s" else word
return $ if return $
| argCount == 0 && formatCount == 0 -> case () of
return () -- This is fine () | argCount == 0 && formatCount == 0 ->
| formatCount == 0 && argCount > 0 -> return () -- This is fine
err (getId format) 2182 () | formatCount == 0 && argCount > 0 ->
"This printf format string has no variables. Other arguments are ignored." err (getId format) 2182
| any mayBecomeMultipleArgs more -> "This printf format string has no variables. Other arguments are ignored."
return () -- We don't know so trust the user () | any mayBecomeMultipleArgs more ->
| argCount < formatCount && onlyTrailingTs formats argCount -> return () -- We don't know so trust the user
return () -- Allow trailing %()Ts since they use the current time () | argCount < formatCount && onlyTrailingTs formats argCount ->
| argCount > 0 && argCount `mod` formatCount == 0 -> return () -- Allow trailing %()Ts since they use the current time
return () -- Great: a suitable number of arguments () | argCount > 0 && argCount `mod` formatCount == 0 ->
| otherwise -> return () -- Great: a suitable number of arguments
warn (getId format) 2183 $ () ->
"This format string has " ++ show formatCount ++ " " ++ pluraliseIfMany "variable" formatCount ++ warn (getId format) 2183 $
", but is passed " ++ show argCount ++ pluraliseIfMany " argument" argCount ++ "." "This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments."
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $ unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
info (getId format) 2059 info (getId format) 2059
@ -723,8 +610,6 @@ prop_checkGetPrintfFormats2 = getPrintfFormats "%0*s" == "*s"
prop_checkGetPrintfFormats3 = getPrintfFormats "%(%s)T" == "T" prop_checkGetPrintfFormats3 = getPrintfFormats "%(%s)T" == "T"
prop_checkGetPrintfFormats4 = getPrintfFormats "%d%%%(%s)T" == "dT" prop_checkGetPrintfFormats4 = getPrintfFormats "%d%%%(%s)T" == "dT"
prop_checkGetPrintfFormats5 = getPrintfFormats "%bPassed: %d, %bFailed: %d%b, Skipped: %d, %bErrored: %d%b\\n" == "bdbdbdbdb" prop_checkGetPrintfFormats5 = getPrintfFormats "%bPassed: %d, %bFailed: %d%b, Skipped: %d, %bErrored: %d%b\\n" == "bdbdbdbdb"
prop_checkGetPrintfFormats6 = getPrintfFormats "%s%s" == "ss"
prop_checkGetPrintfFormats7 = getPrintfFormats "%s\n%s" == "ss"
getPrintfFormats = getFormats getPrintfFormats = getFormats
where where
-- Get the arguments in the string as a string of type characters, -- Get the arguments in the string as a string of type characters,
@ -743,18 +628,18 @@ getPrintfFormats = getFormats
regexBasedGetFormats rest = regexBasedGetFormats rest =
case matchRegex re rest of case matchRegex re rest of
Just [width, precision, typ, rest, _] -> Just [width, precision, typ, rest] ->
(if width == "*" then "*" else "") ++ (if width == "*" then "*" else "") ++
(if precision == "*" then "*" else "") ++ (if precision == "*" then "*" else "") ++
typ ++ getFormats rest typ ++ getFormats rest
Nothing -> take 1 rest ++ getFormats rest Nothing -> take 1 rest ++ getFormats rest
where where
-- constructed based on specifications in "man printf" -- constructed based on specifications in "man printf"
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)([diouxXfFeEgGaAcsbq])((\n|.)*)" re = mkRegex "#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)([diouxXfFeEgGaAcsbq])(.*)"
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \______ / -- \____ _____/\___ ____/ \____ ____/\_________ _________/ \ /
-- V V V V V -- V V V V V
-- flags field width precision format character rest -- flags field width precision format character rest
-- field width and precision can be specified with an '*' instead of a digit, -- field width and precision can be specified with a '*' instead of a digit,
-- in which case printf will accept one more argument for each '*' used -- in which case printf will accept one more argument for each '*' used
@ -778,12 +663,17 @@ prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'"
prop_checkSetAssignment6 = verifyNot checkSetAssignment "set" prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments) checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
where where
f (var:rest) f (var:value:rest) =
| (not (null rest) && isVariableName str) || isAssignment str = let str = literal var in
warn (getId var) 2121 "To assign a variable, use just 'var=value', no 'set ..'." when (isVariableName str || isAssignment str) $
where str = literal var msg (getId var)
f (var:_) =
when (isAssignment $ literal var) $
msg (getId var)
f _ = return () f _ = return ()
msg id = warn id 2121 "To assign a variable, use just 'var=value', no 'set ..'."
isAssignment str = '=' `elem` str isAssignment str = '=' `elem` str
literal (T_NormalWord _ l) = concatMap literal l literal (T_NormalWord _ l) = concatMap literal l
literal (T_Literal _ str) = str literal (T_Literal _ str) = str
@ -797,7 +687,8 @@ prop_checkExportedExpansions4 = verifyNot checkExportedExpansions "export ${foo?
checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments) checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
where where
check t = sequence_ $ do check t = sequence_ $ do
name <- getSingleUnmodifiedBracedString t var <- getSingleUnmodifiedVariable t
let name = bracedString var
return . warn (getId t) 2163 $ return . warn (getId t) 2163 $
"This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet." "This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
@ -809,43 +700,30 @@ prop_checkReadExpansions5 = verify checkReadExpansions "read \"$var\""
prop_checkReadExpansions6 = verify checkReadExpansions "read -a $var" prop_checkReadExpansions6 = verify checkReadExpansions "read -a $var"
prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1" prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1"
prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}" prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}"
prop_checkReadExpansions9 = verify checkReadExpansions "read arr[val]"
checkReadExpansions = CommandCheck (Exactly "read") check checkReadExpansions = CommandCheck (Exactly "read") check
where where
options = getGnuOpts flagsForRead options = getGnuOpts flagsForRead
getVars cmd = fromMaybe [] $ do getVars cmd = fromMaybe [] $ do
opts <- options $ arguments cmd opts <- options cmd
return [y | (x,(_, y)) <- opts, null x || x == "a"] return [y | (x,y) <- opts, null x || x == "a"]
check cmd = do check cmd = mapM_ warning $ getVars cmd
mapM_ dollarWarning $ getVars cmd warning t = sequence_ $ do
mapM_ arrayWarning $ arguments cmd var <- getSingleUnmodifiedVariable t
let name = bracedString var
dollarWarning t = sequence_ $ do
name <- getSingleUnmodifiedBracedString t
guard $ isVariableName name -- e.g. not $1 guard $ isVariableName name -- e.g. not $1
return . warn (getId t) 2229 $ return . warn (getId t) 2229 $
"This does not read '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet." "This does not read '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
arrayWarning word =
when (any isUnquotedBracket $ getWordParts word) $
warn (getId word) 2313 $
"Quote array indices to avoid them expanding as globs."
isUnquotedBracket t =
case t of
T_Glob _ ('[':_) -> True
_ -> False
-- Return the single variable expansion that makes up this word, if any. -- Return the single variable expansion that makes up this word, if any.
-- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing -- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing
getSingleUnmodifiedBracedString :: Token -> Maybe String getSingleUnmodifiedVariable :: Token -> Maybe Token
getSingleUnmodifiedBracedString word = getSingleUnmodifiedVariable word =
case getWordParts word of case getWordParts word of
[T_DollarBraced _ _ l] -> [t@(T_DollarBraced {})] ->
let contents = concat $ oversimplify l let contents = bracedString t
name = getBracedReference contents name = getBracedReference contents
in guard (contents == name) >> return contents in guard (contents == name) >> return t
_ -> Nothing _ -> Nothing
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'" prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
@ -876,9 +754,6 @@ checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]" prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]"
prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo" prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo"
prop_checkUnsetGlobs3 = verify checkUnsetGlobs "unset foo[$i]"
prop_checkUnsetGlobs4 = verify checkUnsetGlobs "unset foo[x${i}y]"
prop_checkUnsetGlobs5 = verifyNot checkUnsetGlobs "unset foo]["
checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments) checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)
where where
check arg = check arg =
@ -931,7 +806,7 @@ prop_checkTimedCommand2 = verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar
prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1" prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1"
checkTimedCommand = CommandCheck (Exactly "time") f where checkTimedCommand = CommandCheck (Exactly "time") f where
f (T_SimpleCommand _ _ (c:args@(_:_))) = f (T_SimpleCommand _ _ (c:args@(_:_))) =
whenShell [Sh, Dash, BusyboxSh] $ do whenShell [Sh, Dash] $ do
let cmd = last args -- "time" is parsed with a command as argument let cmd = last args -- "time" is parsed with a command as argument
when (isPiped cmd) $ when (isPiped cmd) $
warn (getId c) 2176 "'time' is undefined for pipelines. time single stage or bash -c instead." warn (getId c) 2176 "'time' is undefined for pipelines. time single stage or bash -c instead."
@ -955,27 +830,11 @@ checkTimedCommand = CommandCheck (Exactly "time") f where
prop_checkLocalScope1 = verify checkLocalScope "local foo=3" prop_checkLocalScope1 = verify checkLocalScope "local foo=3"
prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }" prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }"
checkLocalScope = CommandCheck (Exactly "local") $ \t -> checkLocalScope = CommandCheck (Exactly "local") $ \t ->
whenShell [Bash, Dash, BusyboxSh] $ do -- Ksh allows it, Sh doesn't support local whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local
path <- getPathM t path <- getPathM t
unless (any isFunctionLike path) $ unless (any isFunctionLike path) $
err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions." err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
prop_checkMultipleDeclaring1 = verify (checkMultipleDeclaring "local") "q() { local readonly var=1; }"
prop_checkMultipleDeclaring2 = verifyNot (checkMultipleDeclaring "local") "q() { local var=1; }"
prop_checkMultipleDeclaring3 = verify (checkMultipleDeclaring "readonly") "readonly local foo=5"
prop_checkMultipleDeclaring4 = verify (checkMultipleDeclaring "export") "export readonly foo=5"
prop_checkMultipleDeclaring5 = verifyNot (checkMultipleDeclaring "local") "f() { local -r foo=5; }"
prop_checkMultipleDeclaring6 = verifyNot (checkMultipleDeclaring "declare") "declare -rx foo=5"
prop_checkMultipleDeclaring7 = verifyNot (checkMultipleDeclaring "readonly") "readonly 'local' foo=5"
checkMultipleDeclaring cmd = CommandCheck (Exactly cmd) (mapM_ check . arguments)
where
check t = sequence_ $ do
lit <- getUnquotedLiteral t
guard $ lit `elem` declaringCommands
return $ err (getId $ getCommandTokenOrThis t) 2316 $
"This applies " ++ cmd ++ " to the variable named " ++ lit ++
", which is probably not what you want. Use a separate command or the appropriate `declare` options instead."
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)" prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)" prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $ checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
@ -994,56 +853,42 @@ prop_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x
prop_checkWhileGetoptsCase3 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; b) bar;; *) :;esac; done" prop_checkWhileGetoptsCase3 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; b) bar;; *) :;esac; done"
prop_checkWhileGetoptsCase4 = verifyNot checkWhileGetoptsCase "while getopts 'a:123' x; do case $x in a) foo;; [0-9]) bar;; esac; done" prop_checkWhileGetoptsCase4 = verifyNot checkWhileGetoptsCase "while getopts 'a:123' x; do case $x in a) foo;; [0-9]) bar;; esac; done"
prop_checkWhileGetoptsCase5 = verifyNot checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; \\?) bar;; *) baz;; esac; done" prop_checkWhileGetoptsCase5 = verifyNot checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; \\?) bar;; *) baz;; esac; done"
prop_checkWhileGetoptsCase6 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $y in a) foo;; esac; done"
prop_checkWhileGetoptsCase7 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case x$x in xa) foo;; xb) foo;; esac; done"
prop_checkWhileGetoptsCase8 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do x=a; case $x in a) foo;; esac; done"
checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
where where
f :: Token -> Analysis f :: Token -> Analysis
f t@(T_SimpleCommand _ _ (cmd:arg1:name:_)) = do f t@(T_SimpleCommand _ _ (cmd:arg1:_)) = do
path <- getPathM t path <- getPathM t
params <- ask
sequence_ $ do sequence_ $ do
options <- getLiteralString arg1 options <- getLiteralString arg1
getoptsVar <- getLiteralString name (T_WhileExpression _ _ body) <- findFirst whileLoop path
(T_WhileExpression _ _ body) <- findFirst whileLoop (NE.toList path) caseCmd <- mapMaybe findCase body !!! 0
T_CaseExpression id var list <- mapMaybe findCase body !!! 0 return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) caseCmd
-- Make sure getopts name and case variable matches
[T_DollarBraced _ _ bracedWord] <- return $ getWordParts var
[T_Literal _ caseVar] <- return $ getWordParts bracedWord
guard $ caseVar == getoptsVar
-- Make sure the variable isn't modified
guard . not $ modifiesVariable params (T_BraceGroup (Id 0) body) getoptsVar
return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) id list
f _ = return () f _ = return ()
check :: Id -> [String] -> Id -> [(CaseType, [Token], [Token])] -> Analysis check :: Id -> [String] -> Token -> Analysis
check optId opts id list = do check optId opts (T_CaseExpression id _ list) = do
unless (Nothing `M.member` handledMap) $ do unless (Nothing `Map.member` handledMap) $ do
mapM_ (warnUnhandled optId id) $ catMaybes $ M.keys notHandled mapM_ (warnUnhandled optId id) $ catMaybes $ Map.keys notHandled
unless (any (`M.member` handledMap) [Just "*",Just "?"]) $ unless (any (`Map.member` handledMap) [Just "*",Just "?"]) $
warn id 2220 "Invalid flags are not handled. Add a *) case." warn id 2220 "Invalid flags are not handled. Add a *) case."
mapM_ warnRedundant $ M.toList notRequested mapM_ warnRedundant $ Map.toList notRequested
where where
handledMap = M.fromList (concatMap getHandledStrings list) handledMap = Map.fromList (concatMap getHandledStrings list)
requestedMap = M.fromList $ map (\x -> (Just x, ())) opts requestedMap = Map.fromList $ map (\x -> (Just x, ())) opts
notHandled = M.difference requestedMap handledMap notHandled = Map.difference requestedMap handledMap
notRequested = M.difference handledMap requestedMap notRequested = Map.difference handledMap requestedMap
warnUnhandled optId caseId str = warnUnhandled optId caseId str =
warn caseId 2213 $ "getopts specified -" ++ (e4m str) ++ ", but it's not handled by this 'case'." warn caseId 2213 $ "getopts specified -" ++ str ++ ", but it's not handled by this 'case'."
warnRedundant (Just str, expr) warnRedundant (key, expr) = sequence_ $ do
| str `notElem` ["*", ":", "?"] = str <- key
warn (getId expr) 2214 "This case is not specified by getopts." guard $ str `notElem` ["*", ":", "?"]
warnRedundant _ = return () return $ warn (getId expr) 2214 "This case is not specified by getopts."
getHandledStrings (_, globs, _) = getHandledStrings (_, globs, _) =
map (\x -> (literal x, x)) globs map (\x -> (literal x, x)) globs
@ -1054,7 +899,7 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
fromGlob t = fromGlob t =
case t of case t of
T_Glob _ ['[', c, ']'] -> return [c] T_Glob _ ('[':c:']':[]) -> return [c]
T_Glob _ "*" -> return "*" T_Glob _ "*" -> return "*"
T_Glob _ "?" -> return "?" T_Glob _ "?" -> return "?"
_ -> Nothing _ -> Nothing
@ -1079,17 +924,17 @@ prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*
prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*" prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"
prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*" prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*"
prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home" prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home"
prop_checkCatastrophicRm10 = verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}" prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
prop_checkCatastrophicRm11 = verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec" prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
prop_checkCatastrophicRm12 = verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec" prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"
prop_checkCatastrophicRm13 = verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec" prop_checkCatastrophicRm13= verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"
prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg" prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"
prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*" prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"
checkCatastrophicRm = CommandCheck (Basename "rm") $ \t -> checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
when (isRecursive t) $ when (isRecursive t) $
mapM_ (mapM_ checkWord . braceExpand) $ arguments t mapM_ (mapM_ checkWord . braceExpand) $ arguments t
where where
isRecursive = any ((`elem` ["r", "R", "recursive"]) . snd) . getAllFlags isRecursive = any (`elem` ["r", "R", "recursive"]) . map snd . getAllFlags
checkWord token = checkWord token =
case getLiteralString token of case getLiteralString token of
@ -1121,9 +966,9 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
f _ = return "" f _ = return ""
stripTrailing c = reverse . dropWhile (== c) . reverse stripTrailing c = reverse . dropWhile (== c) . reverse
skipRepeating c = foldr go [] skipRepeating c (a:b:rest) | a == b && b == c = skipRepeating c (b:rest)
where skipRepeating c (a:r) = a:skipRepeating c r
go a r = a : case r of b:rest | b == c && a == b -> rest; _ -> r skipRepeating _ [] = []
paths = [ paths = [
"", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local", "", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",
@ -1194,7 +1039,7 @@ checkFindRedirections = CommandCheck (Basename "find") f
prop_checkWhich = verify checkWhich "which '.+'" prop_checkWhich = verify checkWhich "which '.+'"
checkWhich = CommandCheck (Basename "which") $ checkWhich = CommandCheck (Basename "which") $
\t -> info (getId $ getCommandTokenOrThis t) 2230 "'which' is non-standard. Use builtin 'command -v' instead." \t -> info (getId $ getCommandTokenOrThis t) 2230 "which is non-standard. Use builtin 'command -v' instead."
prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file" prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input" prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
@ -1236,8 +1081,9 @@ prop_checkSudoArgs7 = verifyNot checkSudoArgs "sudo docker export foo"
checkSudoArgs = CommandCheck (Basename "sudo") f checkSudoArgs = CommandCheck (Basename "sudo") f
where where
f t = sequence_ $ do f t = sequence_ $ do
opts <- parseOpts $ arguments t opts <- parseOpts t
(_,(commandArg, _)) <- find (null . fst) opts let nonFlags = [x | ("",x) <- opts]
commandArg <- nonFlags !!! 0
command <- getLiteralString commandArg command <- getLiteralString commandArg
guard $ command `elem` builtins guard $ command `elem` builtins
return $ warn (getId t) 2232 $ "Can't use sudo with builtins like " ++ command ++ ". Did you want sudo sh -c .. instead?" return $ warn (getId t) 2232 $ "Can't use sudo with builtins like " ++ command ++ ". Did you want sudo sh -c .. instead?"
@ -1267,206 +1113,5 @@ checkChmodDashr = CommandCheck (Basename "chmod") f
guard $ flag == "-r" guard $ flag == "-r"
return $ warn (getId t) 2253 "Use -R to recurse, or explicitly a-r to remove read permissions." return $ warn (getId t) 2253 "Use -R to recurse, or explicitly a-r to remove read permissions."
prop_checkXargsDashi1 = verify checkXargsDashi "xargs -i{} echo {}"
prop_checkXargsDashi2 = verifyNot checkXargsDashi "xargs -I{} echo {}"
prop_checkXargsDashi3 = verifyNot checkXargsDashi "xargs sed -i -e foo"
prop_checkXargsDashi4 = verify checkXargsDashi "xargs -e sed -i foo"
prop_checkXargsDashi5 = verifyNot checkXargsDashi "xargs -x sed -i foo"
checkXargsDashi = CommandCheck (Basename "xargs") f
where
f t = sequence_ $ do
opts <- parseOpts $ arguments t
(option, value) <- lookup "i" opts
return $ info (getId option) 2267 "GNU xargs -i is deprecated in favor of -I{}"
parseOpts = getBsdOpts "0oprtxadR:S:J:L:l:n:P:s:e:E:i:I:"
prop_checkArgComparison1 = verify (checkArgComparison "declare") "declare a = b"
prop_checkArgComparison2 = verify (checkArgComparison "declare") "declare a =b"
prop_checkArgComparison3 = verifyNot (checkArgComparison "declare") "declare a=b"
prop_checkArgComparison4 = verify (checkArgComparison "export") "export a +=b"
prop_checkArgComparison7 = verifyNot (checkArgComparison "declare") "declare -a +i foo"
prop_checkArgComparison8 = verify (checkArgComparison "let") "let x = 0"
prop_checkArgComparison9 = verify (checkArgComparison "alias") "alias x =0"
-- This mirrors checkSecondArgIsComparison but for arguments to local/readonly/declare/export
checkArgComparison cmd = CommandCheck (Exactly cmd) wordsWithEqual
where
wordsWithEqual t = mapM_ check $ arguments t
check arg = do
sequence_ $ do
str <- getLeadingUnquotedString arg
case str of
'=':_ ->
return $ err (headId arg) 2290 $
"Remove spaces around = to assign."
'+':'=':_ ->
return $ err (headId arg) 2290 $
"Remove spaces around += to append."
_ -> Nothing
-- 'let' is parsed as a sequence of arithmetic expansions,
-- so we want the additional warning for "x="
when (cmd == "let") $ sequence_ $ do
token <- getTrailingUnquotedLiteral arg
str <- getLiteralString token
guard $ "=" `isSuffixOf` str
return $ err (getId token) 2290 $
"Remove spaces around = to assign."
headId t =
case t of
T_NormalWord _ (x:_) -> getId x
_ -> getId t
prop_checkMaskedReturns1 = verify (checkMaskedReturns "local") "f() { local a=$(false); }"
prop_checkMaskedReturns2 = verify (checkMaskedReturns "declare") "declare a=$(false)"
prop_checkMaskedReturns3 = verify (checkMaskedReturns "declare") "declare a=\"`false`\""
prop_checkMaskedReturns4 = verify (checkMaskedReturns "readonly") "readonly a=$(false)"
prop_checkMaskedReturns5 = verify (checkMaskedReturns "readonly") "readonly a=\"`false`\""
prop_checkMaskedReturns6 = verifyNot (checkMaskedReturns "declare") "declare a; a=$(false)"
prop_checkMaskedReturns7 = verifyNot (checkMaskedReturns "local") "f() { local -r a=$(false); }"
prop_checkMaskedReturns8 = verifyNot (checkMaskedReturns "readonly") "a=$(false); readonly a"
prop_checkMaskedReturns9 = verify (checkMaskedReturns "typeset") "#!/bin/ksh\n f() { typeset -r x=$(false); }"
prop_checkMaskedReturns10 = verifyNot (checkMaskedReturns "typeset") "#!/bin/ksh\n function f { typeset -r x=$(false); }"
prop_checkMaskedReturns11 = verifyNot (checkMaskedReturns "typeset") "#!/bin/bash\n f() { typeset -r x=$(false); }"
prop_checkMaskedReturns12 = verify (checkMaskedReturns "typeset") "typeset -r x=$(false);"
prop_checkMaskedReturns13 = verify (checkMaskedReturns "typeset") "f() { typeset -g x=$(false); }"
prop_checkMaskedReturns14 = verify (checkMaskedReturns "declare") "declare x=${ false; }"
prop_checkMaskedReturns15 = verify (checkMaskedReturns "declare") "f() { declare x=$(false); }"
checkMaskedReturns str = CommandCheck (Exactly str) checkCmd
where
checkCmd t = do
path <- getPathM t
shell <- asks shellType
sequence_ $ do
name <- getCommandName t
let flags = map snd (getAllFlags t)
let hasDashR = "r" `elem` flags
let hasDashG = "g" `elem` flags
let isInScopedFunction = any (isScopedFunction shell) path
let isLocal = not hasDashG && isLocalInFunction name && isInScopedFunction
let isReadOnly = name == "readonly" || hasDashR
-- Don't warn about local variables that are declared readonly,
-- because the workaround `local x; x=$(false); local -r x;` is annoying
guard . not $ isLocal && isReadOnly
return $ mapM_ checkArgs $ arguments t
checkArgs (T_Assignment id _ _ _ word) | any hasReturn $ getWordParts word =
warn id 2155 "Declare and assign separately to avoid masking return values."
checkArgs _ = return ()
isLocalInFunction = (`elem` ["local", "declare", "typeset"])
isScopedFunction shell t =
case t of
T_BatsTest {} -> True
-- In ksh, only functions declared with 'function' have their own scope
T_Function _ (FunctionKeyword hasFunction) _ _ _ -> shell /= Ksh || hasFunction
_ -> False
hasReturn t = case t of
T_Backticked {} -> True
T_DollarExpansion {} -> True
T_DollarBraceCommandExpansion {} -> True
_ -> False
prop_checkUnquotedEchoSpaces1 = verify checkUnquotedEchoSpaces "echo foo bar"
prop_checkUnquotedEchoSpaces2 = verifyNot checkUnquotedEchoSpaces "echo foo"
prop_checkUnquotedEchoSpaces3 = verifyNot checkUnquotedEchoSpaces "echo foo bar"
prop_checkUnquotedEchoSpaces4 = verifyNot checkUnquotedEchoSpaces "echo 'foo bar'"
prop_checkUnquotedEchoSpaces5 = verifyNot checkUnquotedEchoSpaces "echo a > myfile.txt b"
prop_checkUnquotedEchoSpaces6 = verifyNot checkUnquotedEchoSpaces " echo foo\\\n bar"
checkUnquotedEchoSpaces = CommandCheck (Basename "echo") check
where
check t = do
let args = arguments t
m <- asks tokenPositions
redir <- getClosestCommandM t
sequence_ $ do
let positions = mapMaybe (\c -> M.lookup (getId c) m) args
let pairs = zip positions (drop 1 positions)
(T_Redirecting _ redirTokens _) <- redir
let redirPositions = mapMaybe (\c -> fst <$> M.lookup (getId c) m) redirTokens
guard $ any (hasSpacesBetween redirPositions) pairs
return $ info (getId t) 2291 "Quote repeated spaces to avoid them collapsing into one."
hasSpacesBetween redirs ((a,b), (c,d)) =
posLine a == posLine d
&& ((posColumn c) - (posColumn b)) >= 4
&& not (any (\x -> b < x && x < c) redirs)
prop_checkEvalArray1 = verify checkEvalArray "eval $@"
prop_checkEvalArray2 = verify checkEvalArray "eval \"${args[@]}\""
prop_checkEvalArray3 = verify checkEvalArray "eval \"${args[@]@Q}\""
prop_checkEvalArray4 = verifyNot checkEvalArray "eval \"${args[*]@Q}\""
prop_checkEvalArray5 = verifyNot checkEvalArray "eval \"$*\""
checkEvalArray = CommandCheck (Exactly "eval") (mapM_ check . concatMap getWordParts . arguments)
where
check t =
when (isArrayExpansion t) $
if isEscaped t
then style (getId t) 2293 "When eval'ing @Q-quoted words, use * rather than @ as the index."
else warn (getId t) 2294 "eval negates the benefit of arrays. Drop eval to preserve whitespace/symbols (or eval as string)."
isEscaped q =
case q of
-- Match ${arr[@]@Q} and ${@@Q} and such
T_DollarBraced _ _ l -> 'Q' `elem` getBracedModifier (concat $ oversimplify l)
_ -> False
prop_checkBackreferencingDeclaration1 = verify (checkBackreferencingDeclaration "declare") "declare x=1 y=foo$x"
prop_checkBackreferencingDeclaration2 = verify (checkBackreferencingDeclaration "readonly") "readonly x=1 y=$((1+x))"
prop_checkBackreferencingDeclaration3 = verify (checkBackreferencingDeclaration "local") "local x=1 y=$(echo $x)"
prop_checkBackreferencingDeclaration4 = verify (checkBackreferencingDeclaration "local") "local x=1 y[$x]=z"
prop_checkBackreferencingDeclaration5 = verify (checkBackreferencingDeclaration "declare") "declare x=var $x=1"
prop_checkBackreferencingDeclaration6 = verify (checkBackreferencingDeclaration "declare") "declare x=var $x=1"
prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration "declare") "declare x=var $k=$x"
checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check
where
check t = do
maybeCfga <- asks cfgAnalysis
mapM_ (\cfga -> foldM_ (perArg cfga) M.empty $ arguments t) maybeCfga
perArg cfga leftArgs t =
case t of
T_Assignment id _ name idx t -> do
warnIfBackreferencing cfga leftArgs $ t:idx
return $ M.insert name id leftArgs
t -> do
warnIfBackreferencing cfga leftArgs [t]
return leftArgs
warnIfBackreferencing cfga backrefs l = do
references <- findReferences cfga l
let reused = M.intersection backrefs references
mapM msg $ M.toList reused
msg (name, id) = warn id 2318 $ "This assignment is used again in this '" ++ cmd ++ "', but won't have taken effect. Use two '" ++ cmd ++ "'s."
findReferences cfga list = do
let graph = CF.graph cfga
let nodesMap = CF.tokenToNodes cfga
let nodes = S.unions $ map (\id -> M.findWithDefault S.empty id nodesMap) $ map getId $ list
let labels = mapMaybe (G.lab graph) $ S.toList nodes
let references = M.fromList $ concatMap refFromLabel labels
return references
refFromLabel lab =
case lab of
CFApplyEffects effects -> mapMaybe refFromEffect effects
_ -> []
refFromEffect e =
case e of
IdTagged id (CFReadVariable name) -> return (name, id)
_ -> Nothing
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View file

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

@ -1,5 +1,5 @@
{- {-
Copyright 2012-2020 Vidar Holen Copyright 2012-2016 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@ -19,14 +19,12 @@
-} -}
{-# 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
@ -61,9 +59,6 @@ checks = [
,checkBraceExpansionVars ,checkBraceExpansionVars
,checkMultiDimensionalArrays ,checkMultiDimensionalArrays
,checkPS1Assignments ,checkPS1Assignments
,checkMultipleBangs
,checkBangAfterPipe
,checkNegatedUnaryOps
] ]
testChecker (ForShell _ t) = testChecker (ForShell _ t) =
@ -77,79 +72,74 @@ 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, BusyboxSh, Bash] f checkForDecimals = ForShell [Sh, Dash, Bash] f
where where
f t@(TA_Expansion id _) = sequence_ $ do f t@(TA_Expansion id _) = sequence_ $ do
first:rest <- getLiteralString t str <- getLiteralString t
guard $ isDigit first && '.' `elem` rest first <- str !!! 0
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 = verifyNot checkBashisms "[ foo -nt bar ]" prop_checkBashisms2 = verify 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*}"
prop_checkBashisms10 = verify checkBashisms "echo ${var:4:12}" prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}"
prop_checkBashisms11 = verifyNot checkBashisms "echo ${var:-4}" prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}"
prop_checkBashisms12 = verify checkBashisms "echo ${var//foo/bar}" prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}"
prop_checkBashisms13 = verify checkBashisms "exec -c env" prop_checkBashisms13= verify checkBashisms "exec -c env"
prop_checkBashisms14 = verify checkBashisms "echo -n \"Foo: \"" prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \""
prop_checkBashisms15 = verify checkBashisms "let n++" prop_checkBashisms15= verify checkBashisms "let n++"
prop_checkBashisms16 = verify checkBashisms "echo $RANDOM" prop_checkBashisms16= verify checkBashisms "echo $RANDOM"
prop_checkBashisms17 = verify checkBashisms "echo $((RANDOM%6+1))" prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))"
prop_checkBashisms18 = verify checkBashisms "foo &> /dev/null" 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" prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123"
prop_checkBashisms25 = verify checkBashisms "cat < /dev/tcp/host/123" prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM"
prop_checkBashisms26 = verify checkBashisms "trap mything ERR SIGTERM" prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*"
prop_checkBashisms27 = verify checkBashisms "echo *[^0-9]*" prop_checkBashisms28= verify checkBashisms "exec {n}>&2"
prop_checkBashisms28 = verify checkBashisms "exec {n}>&2" prop_checkBashisms29= verify checkBashisms "echo ${!var}"
prop_checkBashisms29 = verify checkBashisms "echo ${!var}" prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\""
prop_checkBashisms30 = verify checkBashisms "printf -v '%s' \"$1\"" prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\""
prop_checkBashisms31 = verify checkBashisms "printf '%q' \"$1\"" prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
prop_checkBashisms32 = verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]" prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo"
prop_checkBashisms33 = verify checkBashisms "#!/bin/sh\necho -n foo" prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo"
prop_checkBashisms34 = verifyNot checkBashisms "#!/bin/dash\necho -n foo" prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo"
prop_checkBashisms35 = verifyNot checkBashisms "#!/bin/dash\nlocal foo" prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
prop_checkBashisms36 = verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar" prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
prop_checkBashisms37 = verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME" prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM"
prop_checkBashisms38 = verify checkBashisms "RANDOM=9; echo $RANDOM" prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }"
prop_checkBashisms39 = verify checkBashisms "foo-bar() { true; }" prop_checkBashisms40= verify checkBashisms "echo $(<file)"
prop_checkBashisms40 = verify checkBashisms "echo $(<file)" prop_checkBashisms41= verify checkBashisms "echo `<file`"
prop_checkBashisms41 = verify checkBashisms "echo `<file`" prop_checkBashisms42= verify checkBashisms "trap foo int"
prop_checkBashisms42 = verify checkBashisms "trap foo int" prop_checkBashisms43= verify checkBashisms "trap foo sigint"
prop_checkBashisms43 = verify checkBashisms "trap foo sigint" prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
prop_checkBashisms44 = verifyNot checkBashisms "#!/bin/dash\ntrap foo int" prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
prop_checkBashisms45 = verifyNot checkBashisms "#!/bin/dash\ntrap foo INT" prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
prop_checkBashisms46 = verify checkBashisms "#!/bin/dash\ntrap foo SIGINT" prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
prop_checkBashisms47 = verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null" prop_checkBashisms48= verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
prop_checkBashisms48 = verifyNot checkBashisms "#!/bin/sh\necho $LINENO" prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
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_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
prop_checkBashisms52b = verifyNot checkBashisms "#!/bin/sh\ncmd >& $var" prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"
prop_checkBashisms52c = verify checkBashisms "#!/bin/sh\ncmd >& $dir/$var" prop_checkBashisms55= verify checkBashisms "#!/bin/sh\necho ${@%foo}"
prop_checkBashisms53 = verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n" prop_checkBashisms56= verifyNot checkBashisms "#!/bin/sh\necho ${##}"
prop_checkBashisms54 = verify checkBashisms "#!/bin/sh\nfoo+=bar" prop_checkBashisms57= verifyNot checkBashisms "#!/bin/dash\nulimit -c 0"
prop_checkBashisms55 = verify checkBashisms "#!/bin/sh\necho ${@%foo}" prop_checkBashisms58= verify checkBashisms "#!/bin/sh\nulimit -c 0"
prop_checkBashisms56 = verifyNot checkBashisms "#!/bin/sh\necho ${##}"
prop_checkBashisms57 = verifyNot checkBashisms "#!/bin/dash\nulimit -c 0"
prop_checkBashisms58 = verify checkBashisms "#!/bin/sh\nulimit -c 0"
prop_checkBashisms59 = verify checkBashisms "#!/bin/sh\njobs -s" prop_checkBashisms59 = verify checkBashisms "#!/bin/sh\njobs -s"
prop_checkBashisms60 = verifyNot checkBashisms "#!/bin/sh\njobs -p" prop_checkBashisms60 = verifyNot checkBashisms "#!/bin/sh\njobs -p"
prop_checkBashisms61 = verifyNot checkBashisms "#!/bin/sh\njobs -lp" prop_checkBashisms61 = verifyNot checkBashisms "#!/bin/sh\njobs -lp"
@ -188,153 +178,110 @@ prop_checkBashisms93 = verify checkBashisms "#!/bin/sh\necho $(( 10#$(date +%m)
prop_checkBashisms94 = verify checkBashisms "#!/bin/sh\n[ -v var ]" prop_checkBashisms94 = verify checkBashisms "#!/bin/sh\n[ -v var ]"
prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_" prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_"
prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_" prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}" checkBashisms = ForShell [Sh, Dash] $ \t -> do
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
isBusyboxSh = shellType params == BusyboxSh isDash = shellType params == Dash
isDash = shellType params == Dash || isBusyboxSh warnMsg id s =
warnMsg id code s =
if isDash if isDash
then err id code $ "In dash, " ++ s ++ " not supported." then warn id 2169 $ "In dash, " ++ s ++ " not supported."
else warn id code $ "In POSIX sh, " ++ s ++ " undefined." else warn id 2039 $ "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` [ "-ot", "-nt", "-ef" ] =
unless isDash $ warnMsg id $ op ++ " is"
bashism (TC_Binary id SingleBracket "==" _ _) =
warnMsg id "== in place of = is"
bashism (TC_Binary id SingleBracket "=~" _ _) =
warnMsg id "=~ regex matching is"
bashism (TC_Unary id SingleBracket "-v" _) =
warnMsg id "unary -v (in place of [ -n \"${var+x}\" ]) is"
bashism (TC_Unary id _ "-a" _) =
warnMsg id "unary -a in place of -e is"
bashism (TA_Unary id op _) bashism (TA_Unary id op _)
| op `elem` [ "|++", "|--", "++|", "--|"] = | op `elem` [ "|++", "|--", "++|", "--|"] =
warnMsg id 3018 $ filter (/= '|') op ++ " is" warnMsg id $ filter (/= '|') op ++ " is"
bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are" bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are"
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is"
unless isBusyboxSh $ warnMsg id 3020 "&> is" bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id ">& is"
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) file)) = bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are"
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 3023 "FDs outside 0-9 are" | all isDigit num && length num > 1 = warnMsg id "FDs outside 0-9 are"
bashism (T_Assignment id Append _ _ _) = bashism (T_Assignment id Append _ _ _) =
warnMsg id 3024 "+= is" warnMsg id "+= is"
bashism (T_IoFile id _ word) | isNetworked = bashism (T_IoFile id _ word) | isNetworked =
warnMsg id 3025 "/dev/{tcp,udp} is" warnMsg id "/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 3026 "^ in place of ! in glob bracket expressions is" warnMsg id "^ 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 3028 $ str ++ " is" warnMsg id $ str ++ " is"
bashism t@(T_DollarBraced id _ token) = do bashism t@(T_DollarBraced id _ token) = do
unless isBusyboxSh $ mapM_ check simpleExpansions mapM_ check expansion
mapM_ check advancedExpansions
when (isBashVariable var) $ when (isBashVariable var) $
warnMsg id 3028 $ var ++ " is" warnMsg id $ var ++ " is"
where where
str = concat $ oversimplify token str = bracedString t
var = getBracedReference str var = getBracedReference str
check (regex, code, feature) = check (regex, feature) =
when (isJust $ matchRegex regex str) $ warnMsg id code feature when (isJust $ matchRegex regex str) $ warnMsg id feature
bashism t@(T_Pipe id "|&") = bashism t@(T_Pipe id "|&") =
warnMsg id 3029 "|& in place of 2>&1 | is" warnMsg id "|& in place of 2>&1 | is"
bashism (T_Array id _) = bashism (T_Array id _) =
warnMsg id 3030 "arrays are" warnMsg id "arrays are"
bashism (T_IoFile id _ t) | isGlob t = bashism (T_IoFile id _ t) | isGlob t =
warnMsg id 3031 "redirecting to/from globs is" warnMsg id "redirecting to/from globs is"
bashism (T_CoProc id _ _) = bashism (T_CoProc id _ _) =
warnMsg id 3032 "coproc is" warnMsg id "coproc is"
bashism (T_Function id _ _ str _) | not (isVariableName str) = bashism (T_Function id _ _ str _) | not (isVariableName str) =
warnMsg id 3033 "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is" warnMsg id "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 3034 "$(<file) to read files is" warnMsg id "$(<file) to read files is"
bashism (T_Backticked id [x]) | isOnlyRedirection x = bashism (T_Backticked id [x]) | isOnlyRedirection x =
warnMsg id 3035 "`<file` to read files is" warnMsg id "`<file` to read files is"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "echo" && argString `matches` flagRegex = | t `isCommand` "echo" && argString `matches` flagRegex =
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) 3036 "echo flags besides -n" warnMsg (getId arg) "echo flags besides -n"
else else
warnMsg (getId arg) 3037 "echo flags are" warnMsg (getId arg) "echo flags are"
where where
argString = concat $ oversimplify arg argString = concat $ oversimplify arg
flagRegex = mkRegex "^-[eEsn]+$" flagRegex = mkRegex "^-[eEsn]+$"
busyboxFlagRegex = mkRegex "^-[en]+$"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) = | t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
warnMsg (getId arg) 3038 "exec flags are" warnMsg (getId arg) "exec flags are"
bashism t@(T_SimpleCommand id _ _) bashism t@(T_SimpleCommand id _ _)
| t `isCommand` "let" = warnMsg id 3039 "'let' is" | t `isCommand` "let" = warnMsg id "'let' is"
bashism t@(T_SimpleCommand _ _ (cmd:args)) bashism t@(T_SimpleCommand _ _ (cmd:args))
| t `isCommand` "set" = unless isDash $ | t `isCommand` "set" = unless isDash $
checkOptions $ getLiteralArgs args checkOptions $ getLiteralArgs args
@ -342,17 +289,16 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
-- Get the literal options from a list of arguments, -- Get the literal options from a list of arguments,
-- up until the first non-literal one -- up until the first non-literal one
getLiteralArgs :: [Token] -> [(Id, String)] getLiteralArgs :: [Token] -> [(Id, String)]
getLiteralArgs = foldr go [] getLiteralArgs (first:rest) = fromMaybe [] $ do
where str <- getLiteralString first
go first rest = case getLiteralString first of return $ (getId first, str) : getLiteralArgs rest
Just str -> (getId first, str) : rest getLiteralArgs [] = []
Nothing -> []
-- Check a flag-option pair (such as -o errexit) -- Check a flag-option pair (such as -o errexit)
checkOptions (flag@(fid,flag') : opt@(oid,opt') : rest) checkOptions (flag@(fid,flag') : opt@(oid,opt') : rest)
| flag' `matches` oFlagRegex = do | flag' `matches` oFlagRegex = do
when (opt' `notElem` longOptions) $ when (opt' `notElem` longOptions) $
warnMsg oid 3040 $ "set option " <> opt' <> " is" warnMsg oid $ "set option " <> opt' <> " is"
checkFlags (flag:rest) checkFlags (flag:rest)
| otherwise = checkFlags (flag:opt:rest) | otherwise = checkFlags (flag:opt:rest)
checkOptions (flag:rest) = checkFlags (flag:rest) checkOptions (flag:rest) = checkFlags (flag:rest)
@ -365,10 +311,10 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
unless (flag' `matches` validFlagsRegex) $ unless (flag' `matches` validFlagsRegex) $
forM_ (tail flag') $ \letter -> forM_ (tail flag') $ \letter ->
when (letter `notElem` optionsSet) $ when (letter `notElem` optionsSet) $
warnMsg fid 3041 $ "set flag " <> ('-':letter:" is") warnMsg fid $ "set flag " <> ('-':letter:" is")
checkOptions rest checkOptions rest
| beginsWithDoubleDash flag' = do | beginsWithDoubleDash flag' = do
warnMsg fid 3042 $ "set flag " <> flag' <> " is" warnMsg fid $ "set flag " <> flag' <> " is"
checkOptions rest checkOptions rest
-- Either a word that doesn't start with a dash, or simply '--', -- Either a word that doesn't start with a dash, or simply '--',
-- so stop checking. -- so stop checking.
@ -390,20 +336,16 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
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 3044 $ "'" ++ name ++ "' is" warnMsg id $ "'" ++ name ++ "' is"
sequence_ $ do sequence_ $ do
allowed' <- Map.lookup name allowedFlags allowed' <- Map.lookup name allowedFlags
allowed <- allowed' allowed <- allowed'
(word, flag) <- find (word, flag) <- find
(\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is" return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"
when (name == "source" && not isBusyboxSh) $ when (name == "source") $ warnMsg id "'source' in place of '.' is"
warnMsg id 3046 "'source' in place of '.' is"
when (name == "trap") $ when (name == "trap") $
let let
check token = sequence_ $ do check token = sequence_ $ do
@ -411,12 +353,12 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
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) 3047 $ "trapping " ++ str ++ " is" warnMsg (getId token) $ "trapping " ++ str ++ " is"
when (not isBusyboxSh && "SIG" `isPrefixOf` upper) $ when ("SIG" `isPrefixOf` upper) $
warnMsg (getId token) 3048 warnMsg (getId token)
"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) 3049 warnMsg (getId token)
"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)
@ -425,16 +367,13 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> 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) 3050 "printf %q is" return $ warnMsg (getId format) "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 [ allowedFlags = Map.fromList [
("cd", Just ["L", "P"]), ("cd", Just ["L", "P"]),
("exec", Just []), ("exec", Just []),
@ -442,48 +381,38 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
("hash", Just $ if isDash then ["r", "v"] else ["r"]), ("hash", Just $ if isDash then ["r", "v"] else ["r"]),
("jobs", Just ["l", "p"]), ("jobs", Just ["l", "p"]),
("printf", Just []), ("printf", Just []),
("read", Just $ if isDash || isBusyboxSh then ["r", "p"] else ["r"]), ("read", Just $ if isDash then ["r", "p"] else ["r"]),
("readonly", Just ["p"]), ("readonly", Just ["p"]),
("trap", Just []), ("trap", Just []),
("type", Just $ if isBusyboxSh then ["p"] else []), ("type", Just []),
("ulimit", if isDash then Nothing else Just ["f"]), ("ulimit", if isDash then Nothing else Just ["f"]),
("umask", Just ["S"]), ("umask", Just ["S"]),
("unset", Just ["f", "v"]), ("unset", Just ["f", "v"]),
("wait", Just []) ("wait", Just [])
] ]
bashism t@(T_SourceCommand id src _) bashism t@(T_SourceCommand id src _) =
| getCommandName src == Just "source" = let name = fromMaybe "" $ getCommandName src
unless isBusyboxSh $ in when (name == "source") $ warnMsg id "'source' in place of '.' is"
warnMsg id 3051 "'source' in place of '.' is" bashism (TA_Expansion _ (T_Literal id str : _)) | str `matches` radix =
bashism (TA_Expansion _ (T_Literal id str : _)) when (str `matches` radix) $ warnMsg id "arithmetic base conversion is"
| str `matches` radix = warnMsg id 3052 "arithmetic base conversion is"
where where
radix = mkRegex "^[0-9]+#" radix = mkRegex "^[0-9]+#"
bashism _ = return () bashism _ = return ()
varChars="_0-9a-zA-Z" varChars="_0-9a-zA-Z"
advancedExpansions = let re = mkRegex in [ expansion = let re = mkRegex in [
(re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"), (re $ "^![" ++ varChars ++ "]", "indirect expansion is"),
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"), (re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"), (re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
(re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"), (re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is") (re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", "string indexing is"),
] (re $ "^([*@][%#]|#[@*])", "string operations on $@/$* are"),
simpleExpansions = let re = mkRegex in [ (re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")
(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 = [ "_" ]
@ -497,54 +426,10 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \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_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)" 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) = f (T_Redirecting id lefts r) =
@ -621,20 +506,20 @@ 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 _ _ l -> T_DollarBraced {} ->
when (isMultiDim l) $ about token when (isMultiDim token) $ 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 l = getBracedModifier (concat $ oversimplify l) `matches` re isMultiDim t = getBracedModifier (bracedString t) `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\\$ '"
prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '" prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '" prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '"
prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '" prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '"
prop_checkPS14a = verify checkPS1Assignments "export PS1=$'\\e[3m; '" prop_checkPS14a= verify checkPS1Assignments "export PS1=$'\\e[3m; '"
prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '" prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"
prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '" prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'" prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'"
@ -656,46 +541,5 @@ 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,27 +2,9 @@ module ShellCheck.Data where
import ShellCheck.Interface import ShellCheck.Interface
import Data.Version (showVersion) import Data.Version (showVersion)
{-
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
Use:
-- import Paths_ShellCheck (version)
shellcheckVersion = "kludge"
-}
import Paths_ShellCheck (version) import Paths_ShellCheck (version)
shellcheckVersion = showVersion version -- VERSIONSTRING
shellcheckVersion = showVersion version
internalVariables = [ internalVariables = [
-- Generic -- Generic
@ -30,27 +12,23 @@ internalVariables = [
-- Bash -- Bash
"BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC", "BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
"BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND", "BASH_ARGV", "BASH_CMDS", "BASH_COMMAND", "BASH_EXECUTION_STRING",
"BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_LOADABLES_PATH", "BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL",
"BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL", "BASH_VERSINFO", "BASH_VERSINFO", "BASH_VERSION", "COMP_CWORD", "COMP_KEY",
"BASH_VERSION", "COMP_CWORD", "COMP_KEY", "COMP_LINE", "COMP_POINT", "COMP_LINE", "COMP_POINT", "COMP_TYPE", "COMP_WORDBREAKS",
"COMP_TYPE", "COMP_WORDBREAKS", "COMP_WORDS", "COPROC", "DIRSTACK", "COMP_WORDS", "COPROC", "DIRSTACK", "EUID", "FUNCNAME", "GROUPS",
"EPOCHREALTIME", "EPOCHSECONDS", "EUID", "FUNCNAME", "GROUPS", "HISTCMD", "HISTCMD", "HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE",
"HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE", "OLDPWD", "OLDPWD", "OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD",
"OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD", "RANDOM", "RANDOM", "READLINE_LINE", "READLINE_POINT", "REPLY", "SECONDS",
"READLINE_ARGUMENT", "READLINE_LINE", "READLINE_MARK", "READLINE_POINT", "SHELLOPTS", "SHLVL", "UID", "BASH_ENV", "BASH_XTRACEFD", "CDPATH",
"REPLY", "SECONDS", "SHELLOPTS", "SHLVL", "SRANDOM", "UID", "BASH_COMPAT", "COLUMNS", "COMPREPLY", "EMACS", "ENV", "FCEDIT", "FIGNORE",
"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", "INSIDE_EMACS", "LANG", "LC_ALL", "LC_COLLATE", "IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
"LC_CTYPE", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LINES", "MAIL",
"LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH", "MAILCHECK", "MAILPATH", "OPTERR", "PATH", "POSIXLY_CORRECT",
"POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL",
"PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR", "TIMEFORMAT", "TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC",
"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",
@ -63,23 +41,15 @@ internalVariables = [
, "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP", , "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP",
"FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION", "FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION",
"flags_error", "flags_return" "flags_error", "flags_return"
-- Bats
,"stderr", "stderr_lines"
] ]
specialIntegerVariables = [ specialVariablesWithoutSpaces = [
"$", "?", "!", "#" "$", "-", "?", "!", "#"
] ]
specialVariablesWithoutSpaces = "-" : specialIntegerVariables
variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [ variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
"EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM", "OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
"READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS", "COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
"SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE",
"HISTSIZE", "LINES", "BASH_MONOSECONDS", "BASH_TRAPSIG"
-- shflags -- shflags
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE" , "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
@ -125,10 +95,10 @@ commonCommands = [
nonReadingCommands = [ nonReadingCommands = [
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown", "alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
"cp", "du", "echo", "export", "fg", "fuser", "getconf", "cp", "du", "echo", "export", "false", "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", "ulimit", "unalias", "uname" "set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname"
] ]
sampleWords = [ sampleWords = [
@ -160,18 +130,11 @@ shellForExecutable name =
"sh" -> return Sh "sh" -> return Sh
"bash" -> return Bash "bash" -> return Bash
"bats" -> 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
"oksh" -> return Ksh
_ -> Nothing _ -> Nothing
flagsForRead = "sreu:n:N:i:p:a:t:" flagsForRead = "sreu:n:N:i:p:a:t:"
flagsForMapfile = "d:n:O:s:u:C:c:t"
declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"]

View file

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

View file

@ -22,8 +22,6 @@
module ShellCheck.Fixer (applyFix, removeTabStops, mapPositions, Ranged(..), runTests) where module ShellCheck.Fixer (applyFix, removeTabStops, mapPositions, Ranged(..), runTests) where
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Prelude
import Control.Monad
import Control.Monad.State import Control.Monad.State
import Data.Array import Data.Array
import Data.List import Data.List
@ -37,7 +35,7 @@ class Ranged a where
end :: a -> Position end :: a -> Position
overlap :: a -> a -> Bool overlap :: a -> a -> Bool
overlap x y = overlap x y =
xEnd > yStart && yEnd > xStart (yStart >= xStart && yStart < xEnd) || (yStart < xStart && yEnd > xStart)
where where
yStart = start y yStart = start y
yEnd = end y yEnd = end y
@ -88,7 +86,6 @@ instance Ranged Replacement where
instance Monoid Fix where instance Monoid Fix where
mempty = newFix mempty = newFix
mappend = (<>) mappend = (<>)
mconcat = foldl mappend mempty -- fold left to right since <> discards right on overlap
instance Semigroup Fix where instance Semigroup Fix where
f1 <> f2 = f1 <> f2 =
@ -231,7 +228,7 @@ applyReplacement2 rep string = do
let (l1, l2) = tmap posLine originalPos in let (l1, l2) = tmap posLine originalPos in
when (l1 /= 1 || l2 /= 1) $ when (l1 /= 1 || l2 /= 1) $
error $ pleaseReport "bad cross-line fix" error "ShellCheck internal error, please report: bad cross-line fix"
let replacer = repString rep let replacer = repString rep
let shift = (length replacer) - (oldEnd - oldStart) let shift = (length replacer) - (oldEnd - oldStart)
@ -276,10 +273,10 @@ getPrefixSum = f 0
where where
f sum _ PSLeaf = sum f sum _ PSLeaf = sum
f sum target (PSBranch pivot left right cumulative) = f sum target (PSBranch pivot left right cumulative) =
case target `compare` pivot of case () of
LT -> f sum target left _ | target < pivot -> f sum target left
GT -> f (sum+cumulative) target right _ | target > pivot -> f (sum+cumulative) target right
EQ -> sum+cumulative _ -> sum+cumulative
-- Add a value to the Prefix Sum tree at the given index. -- Add a value to the Prefix Sum tree at the given index.
-- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5 -- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5
@ -288,10 +285,10 @@ addPSValue key value tree = if value == 0 then tree else f tree
where where
f PSLeaf = PSBranch key PSLeaf PSLeaf value f PSLeaf = PSBranch key PSLeaf PSLeaf value
f (PSBranch pivot left right sum) = f (PSBranch pivot left right sum) =
case key `compare` pivot of case () of
LT -> PSBranch pivot (f left) right (sum + value) _ | key < pivot -> PSBranch pivot (f left) right (sum + value)
GT -> PSBranch pivot left (f right) sum _ | key > pivot -> PSBranch pivot left (f right) sum
EQ -> PSBranch pivot left right (sum + value) _ -> PSBranch pivot left right (sum + value)
prop_pstreeSumsCorrectly kvs targets = prop_pstreeSumsCorrectly kvs targets =
let let

View file

@ -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 = NE.groupWith sourceFile comments fileGroups = groupWith sourceFile comments
outputGroup group = do outputGroup group = do
let filename = sourceFile (NE.head group) let filename = sourceFile (head group)
result <- siReadFile sys (Just True) filename result <- (siReadFile sys) filename
let contents = either (const "") id result let contents = either (const "") id result
outputFile filename contents (NE.toList group) outputFile filename contents group
outputFile filename contents warnings = do outputFile filename contents warnings = do
let comments = makeNonVirtual warnings contents let comments = makeNonVirtual warnings contents
@ -88,7 +88,7 @@ outputError file error = putStrLn $ concat [
attr s v = concat [ s, "='", escape v, "' " ] attr s v = concat [ s, "='", escape v, "' " ]
escape = concatMap escape' escape = concatMap escape'
escape' c = if isOk c then [c] else "&#" ++ show (ord c) ++ ";" escape' c = if isOk c then [c] else "&#" ++ show (ord c) ++ ";"
isOk x = any ($ x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")] isOk x = any ($x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")]
severity "error" = "error" severity "error" = "error"
severity "warning" = "warning" severity "warning" = "warning"

View file

@ -38,6 +38,9 @@ import System.FilePath
import Test.QuickCheck import Test.QuickCheck
import Debug.Trace
ltt x = trace (show x) x
format :: FormatterOptions -> IO Formatter format :: FormatterOptions -> IO Formatter
format options = do format options = do
foundIssues <- newIORef False foundIssues <- newIORef False
@ -87,7 +90,7 @@ reportResult foundIssues reportedIssues color result sys = do
mapM_ output $ M.toList fixmap mapM_ output $ M.toList fixmap
where where
output (name, fix) = do output (name, fix) = do
file <- siReadFile sys (Just True) name file <- (siReadFile sys) name
case file of case file of
Right contents -> do Right contents -> do
putStrLn $ formatDoc color $ makeDiff name contents fix putStrLn $ formatDoc color $ makeDiff name contents fix
@ -203,9 +206,10 @@ formatDoc color (DiffDoc name lf regions) =
buildFixMap :: [Fix] -> M.Map String Fix buildFixMap :: [Fix] -> M.Map String Fix
buildFixMap fixes = perFile buildFixMap fixes = perFile
where where
splitFixes = splitFixByFile $ mconcat fixes splitFixes = concatMap splitFixByFile fixes
perFile = groupByMap (posFile . repStartPos . head . fixReplacements) splitFixes perFile = groupByMap (posFile . repStartPos . head . fixReplacements) splitFixes
-- There are currently no multi-file fixes, but let's handle it anyways
splitFixByFile :: Fix -> [Fix] splitFixByFile :: Fix -> [Fix]
splitFixByFile fix = map makeFix $ groupBy sameFile (fixReplacements fix) splitFixByFile fix = map makeFix $ groupBy sameFile (fixReplacements fix)
where where

View file

@ -28,7 +28,6 @@ import Data.Array
import Data.List import Data.List
import System.IO import System.IO
import System.Info 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 {
@ -69,14 +68,12 @@ makeNonVirtual comments contents =
shouldOutputColor :: ColorOption -> IO Bool shouldOutputColor :: ColorOption -> IO Bool
shouldOutputColor colorOption = shouldOutputColor colorOption = do
case colorOption of term <- hIsTerminalDevice stdout
ColorAlways -> return True let windows = "mingw" `isPrefixOf` os
ColorNever -> return False let isUsableTty = term && not windows
ColorAuto -> do let useColor = case colorOption of
isTerminal <- hIsTerminalDevice stdout ColorAlways -> True
term <- lookupEnv "TERM" ColorNever -> False
let windows = "mingw" `isPrefixOf` os ColorAuto -> isUsableTty
let dumbTerm = term `elem` [Just "dumb", Just "", Nothing] return useColor
let isUsableTty = isTerminal && not windows && not dumbTerm
return isUsableTty

View file

@ -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 = NE.groupWith sourceFile comments groups = groupWith sourceFile comments
f :: NE.NonEmpty PositionedComment -> IO () f :: [PositionedComment] -> IO ()
f group = do f group = do
let filename = sourceFile (NE.head group) let filename = sourceFile (head group)
result <- siReadFile sys (Just True) filename result <- (siReadFile sys) filename
let contents = either (const "") id result let contents = either (const "") id result
outputResult filename contents (NE.toList group) outputResult filename contents group
outputResult filename contents warnings = do outputResult filename contents warnings = do
let comments = makeNonVirtual warnings contents let comments = makeNonVirtual warnings contents

View file

@ -23,7 +23,6 @@ module ShellCheck.Formatter.JSON (format) where
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Formatter.Format import ShellCheck.Formatter.Format
import Control.DeepSeq
import Data.Aeson import Data.Aeson
import Data.IORef import Data.IORef
import Data.Monoid import Data.Monoid
@ -104,7 +103,7 @@ collectResult ref cr sys = mapM_ f groups
comments = crComments cr comments = crComments cr
groups = groupWith sourceFile comments groups = groupWith sourceFile comments
f :: [PositionedComment] -> IO () f :: [PositionedComment] -> IO ()
f group = deepseq comments $ modifyIORef ref (\x -> comments ++ x) f group = modifyIORef ref (\x -> comments ++ x)
finish ref = do finish ref = do
list <- readIORef ref list <- readIORef ref

View file

@ -23,13 +23,12 @@ module ShellCheck.Formatter.JSON1 (format) where
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Formatter.Format import ShellCheck.Formatter.Format
import Control.DeepSeq
import Data.Aeson import Data.Aeson
import Data.IORef import Data.IORef
import Data.Monoid import Data.Monoid
import GHC.Exts
import System.IO import System.IO
import qualified Data.ByteString.Lazy.Char8 as BL import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.List.NonEmpty as NE
format :: IO Formatter format :: IO Formatter
format = do format = do
@ -114,14 +113,14 @@ outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
collectResult ref cr sys = mapM_ f groups collectResult ref cr sys = mapM_ f groups
where where
comments = crComments cr comments = crComments cr
groups = NE.groupWith sourceFile comments groups = groupWith sourceFile comments
f :: NE.NonEmpty PositionedComment -> IO () f :: [PositionedComment] -> IO ()
f group = do f group = do
let filename = sourceFile (NE.head group) let filename = sourceFile (head group)
result <- siReadFile sys (Just True) filename result <- siReadFile sys filename
let contents = either (const "") id result let contents = either (const "") id result
let comments' = makeNonVirtual comments contents let comments' = makeNonVirtual comments contents
deepseq comments' $ modifyIORef ref (\x -> comments' ++ x) modifyIORef ref (\x -> comments' ++ x)
finish ref = do finish ref = do
list <- readIORef ref list <- readIORef ref

View file

@ -23,7 +23,6 @@ import ShellCheck.Fixer
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Formatter.Format import ShellCheck.Formatter.Format
import Control.DeepSeq
import Control.Monad import Control.Monad
import Data.Array import Data.Array
import Data.Foldable import Data.Foldable
@ -31,9 +30,9 @@ import Data.Ord
import Data.IORef import Data.IORef
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import GHC.Exts
import System.IO import System.IO
import System.Info import System.Info
import qualified Data.List.NonEmpty as NE
wikiLink = "https://www.shellcheck.net/wiki/" wikiLink = "https://www.shellcheck.net/wiki/"
@ -89,7 +88,7 @@ rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err)
appendComments errRef comments max = do appendComments errRef comments max = do
previous <- readIORef errRef previous <- readIORef errRef
let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments
writeIORef errRef $! force . take max . nubBy equal . sort $ previous ++ current writeIORef errRef . take max . nubBy equal . sort $ previous ++ current
where where
fst3 (x,_,_) = x fst3 (x,_,_) = x
equal x y = fst3 x == fst3 y equal x y = fst3 x == fst3 y
@ -117,19 +116,19 @@ outputResult options ref result sys = do
color <- getColorFunc $ foColorOption options color <- getColorFunc $ foColorOption options
let comments = crComments result let comments = crComments result
appendComments ref comments (fromIntegral $ foWikiLinkCount options) appendComments ref comments (fromIntegral $ foWikiLinkCount options)
let fileGroups = NE.groupWith sourceFile comments let fileGroups = groupWith sourceFile comments
mapM_ (outputForFile color sys) fileGroups mapM_ (outputForFile color sys) fileGroups
outputForFile color sys comments = do outputForFile color sys comments = do
let fileName = sourceFile (NE.head comments) let fileName = sourceFile (head comments)
result <- siReadFile sys (Just True) fileName result <- (siReadFile sys) fileName
let contents = either (const "") id result let contents = either (const "") id result
let fileLinesList = lines contents let fileLinesList = lines contents
let lineCount = length fileLinesList let lineCount = length fileLinesList
let fileLines = listArray (1, lineCount) fileLinesList let fileLines = listArray (1, lineCount) fileLinesList
let groups = NE.groupWith lineNo comments let groups = groupWith lineNo comments
forM_ groups $ \commentsForLine -> do mapM_ (\commentsForLine -> do
let lineNum = fromIntegral $ lineNo (NE.head commentsForLine) let lineNum = fromIntegral $ lineNo (head commentsForLine)
let line = if lineNum < 1 || lineNum > lineCount let line = if lineNum < 1 || lineNum > lineCount
then "" then ""
else fileLines ! fromIntegral lineNum else fileLines ! fromIntegral lineNum
@ -137,9 +136,10 @@ outputForFile color sys comments = do
putStrLn $ color "message" $ putStrLn $ color "message" $
"In " ++ fileName ++" line " ++ show lineNum ++ ":" "In " ++ fileName ++" line " ++ show lineNum ++ ":"
putStrLn (color "source" line) putStrLn (color "source" line)
forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) commentsForLine
putStrLn "" putStrLn ""
showFixedString color (toList commentsForLine) (fromIntegral lineNum) fileLines showFixedString color commentsForLine (fromIntegral lineNum) fileLines
) groups
-- Pick out only the lines necessary to show a fix in action -- Pick out only the lines necessary to show a fix in action
sliceFile :: Fix -> Array Int String -> (Fix, Array Int String) sliceFile :: Fix -> Array Int String -> (Fix, Array Int String)
@ -169,13 +169,13 @@ showFixedString color comments lineNum fileLines =
-- and/or other unrelated lines. -- and/or other unrelated lines.
let (excerptFix, excerpt) = sliceFile mergedFix fileLines let (excerptFix, excerpt) = sliceFile mergedFix fileLines
-- in the spirit of error prone -- in the spirit of error prone
putStrLn $ color "message" "Did you mean:" putStrLn $ color "message" "Did you mean: "
putStrLn $ unlines $ applyFix excerptFix excerpt putStrLn $ unlines $ applyFix excerptFix excerpt
cuteIndent :: PositionedComment -> String cuteIndent :: PositionedComment -> String
cuteIndent comment = cuteIndent comment =
replicate (fromIntegral $ colNo comment - 1) ' ' ++ replicate (fromIntegral $ colNo comment - 1) ' ' ++
makeArrow ++ " " ++ code (codeNo comment) ++ " (" ++ severityText comment ++ "): " ++ messageText comment makeArrow ++ " " ++ code (codeNo comment) ++ ": " ++ messageText comment
where where
arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^" arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^"
makeArrow = makeArrow =

View file

@ -1,5 +1,5 @@
{- {-
Copyright 2012-2024 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,14 +21,14 @@
module ShellCheck.Interface module ShellCheck.Interface
( (
SystemInterface(..) SystemInterface(..)
, CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csExtendedAnalysis, csOptionalChecks) , CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csOptionalChecks)
, CheckResult(crFilename, crComments) , CheckResult(crFilename, crComments)
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride) , ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
, ParseResult(prComments, prTokenPositions, prRoot) , ParseResult(prComments, prTokenPositions, prRoot)
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asExtendedAnalysis, asOptionalChecks) , AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asOptionalChecks)
, AnalysisResult(arComments) , AnalysisResult(arComments)
, FormatterOptions(foColorOption, foWikiLinkCount) , FormatterOptions(foColorOption, foWikiLinkCount)
, Shell(Ksh, Sh, Bash, Dash, BusyboxSh) , Shell(Ksh, Sh, Bash, Dash)
, ExecutionMode(Executed, Sourced) , ExecutionMode(Executed, Sourced)
, ErrorMessage , ErrorMessage
, Code , Code
@ -39,12 +39,11 @@ module ShellCheck.Interface
, ColorOption(ColorAuto, ColorAlways, ColorNever) , ColorOption(ColorAuto, ColorAlways, ColorNever)
, TokenComment(tcId, tcComment, tcFix) , TokenComment(tcId, tcComment, tcFix)
, emptyCheckResult , emptyCheckResult
, newAnalysisResult
, newAnalysisSpec
, newFormatterOptions
, newParseResult , newParseResult
, newAnalysisSpec
, newAnalysisResult
, newFormatterOptions
, newPosition , newPosition
, newSystemInterface
, newTokenComment , newTokenComment
, mockedSystemInterface , mockedSystemInterface
, mockRcFile , mockRcFile
@ -74,19 +73,15 @@ import qualified Data.Map as Map
data SystemInterface m = SystemInterface { data SystemInterface m = SystemInterface {
-- | Given: -- Read a file by filename, or return an error
-- What annotations say about including external files (if anything) siReadFile :: String -> m (Either ErrorMessage String),
-- A resolved filename from siFindSource -- Given:
-- Read the file or return an error
siReadFile :: Maybe Bool -> String -> m (Either ErrorMessage String),
-- | Given:
-- the current script, -- the current script,
-- what annotations say about including external files (if anything)
-- a list of source-path annotations in effect, -- a list of source-path annotations in effect,
-- and a sourced file, -- and a sourced file,
-- find the sourced file -- find the sourced file
siFindSource :: String -> Maybe Bool -> [String] -> String -> m FilePath, siFindSource :: String -> [String] -> String -> m FilePath,
-- | Get the configuration file (name, contents) for a filename -- Get the configuration file (name, contents) for a filename
siGetConfig :: String -> m (Maybe (FilePath, String)) siGetConfig :: String -> m (Maybe (FilePath, String))
} }
@ -100,7 +95,6 @@ data CheckSpec = CheckSpec {
csIncludedWarnings :: Maybe [Integer], csIncludedWarnings :: Maybe [Integer],
csShellTypeOverride :: Maybe Shell, csShellTypeOverride :: Maybe Shell,
csMinSeverity :: Severity, csMinSeverity :: Severity,
csExtendedAnalysis :: Maybe Bool,
csOptionalChecks :: [String] csOptionalChecks :: [String]
} deriving (Show, Eq) } deriving (Show, Eq)
@ -125,7 +119,6 @@ emptyCheckSpec = CheckSpec {
csIncludedWarnings = Nothing, csIncludedWarnings = Nothing,
csShellTypeOverride = Nothing, csShellTypeOverride = Nothing,
csMinSeverity = StyleC, csMinSeverity = StyleC,
csExtendedAnalysis = Nothing,
csOptionalChecks = [] csOptionalChecks = []
} }
@ -138,14 +131,6 @@ newParseSpec = ParseSpec {
psShellTypeOverride = Nothing psShellTypeOverride = Nothing
} }
newSystemInterface :: Monad m => SystemInterface m
newSystemInterface =
SystemInterface {
siReadFile = \_ _ -> return $ Left "Not implemented",
siFindSource = \_ _ _ name -> return name,
siGetConfig = \_ -> return Nothing
}
-- Parser input and output -- Parser input and output
data ParseSpec = ParseSpec { data ParseSpec = ParseSpec {
psFilename :: String, psFilename :: String,
@ -176,7 +161,6 @@ data AnalysisSpec = AnalysisSpec {
asExecutionMode :: ExecutionMode, asExecutionMode :: ExecutionMode,
asCheckSourced :: Bool, asCheckSourced :: Bool,
asOptionalChecks :: [String], asOptionalChecks :: [String],
asExtendedAnalysis :: Maybe Bool,
asTokenPositions :: Map.Map Id (Position, Position) asTokenPositions :: Map.Map Id (Position, Position)
} }
@ -187,7 +171,6 @@ newAnalysisSpec token = AnalysisSpec {
asExecutionMode = Executed, asExecutionMode = Executed,
asCheckSourced = False, asCheckSourced = False,
asOptionalChecks = [], asOptionalChecks = [],
asExtendedAnalysis = Nothing,
asTokenPositions = Map.empty asTokenPositions = Map.empty
} }
@ -225,7 +208,7 @@ newCheckDescription = CheckDescription {
} }
-- Supporting data types -- Supporting data types
data Shell = Ksh | Sh | Bash | Dash | BusyboxSh deriving (Show, Eq) data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
data ExecutionMode = Executed | Sourced deriving (Show, Eq) data ExecutionMode = Executed | Sourced deriving (Show, Eq)
type ErrorMessage = String type ErrorMessage = String
@ -324,18 +307,19 @@ data ColorOption =
-- For testing -- For testing
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
mockedSystemInterface files = (newSystemInterface :: SystemInterface Identity) { mockedSystemInterface files = SystemInterface {
siReadFile = rf, siReadFile = rf,
siFindSource = fs, siFindSource = fs,
siGetConfig = const $ return Nothing siGetConfig = const $ return Nothing
} }
where where
rf _ file = return $ rf file = return $
case find ((== file) . fst) files of case find ((== file) . fst) files of
Nothing -> Left "File not included in mock." Nothing -> Left "File not included in mock."
Just (_, contents) -> Right contents Just (_, contents) -> Right contents
fs _ _ _ file = return file fs _ _ file = return file
mockRcFile rcfile mock = mock { mockRcFile rcfile mock = mock {
siGetConfig = const . return $ Just (".shellcheckrc", rcfile) siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
} }

File diff suppressed because it is too large Load diff

View file

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

@ -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-18.15 resolver: lts-13.26
# Local packages, usually specified by relative directory name # Local packages, usually specified by relative directory name
packages: packages:

View file

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

View file

@ -22,16 +22,13 @@ fi
cabal install --dependencies-only --enable-tests "${flags[@]}" || cabal install --dependencies-only --enable-tests "${flags[@]}" ||
cabal install --dependencies-only "${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 "${flags[@]}" || 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"
sc="$(find . -name shellcheck -type f -perm -111)" sc="$(find . -name shellcheck -type f -perm -111)"
[ -x "$sc" ] || die "Can't find executable" [ -x "$sc" ] || die "Can't find executable"

View file

@ -9,18 +9,7 @@ fail() {
if git diff | grep -q "" if git diff | grep -q ""
then then
fail "There are uncommitted changes" fail "There are uncommited 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 fi
current=$(git tag --points-at) current=$(git tag --points-at)
@ -45,38 +34,41 @@ then
fail "You are not on master" fail "You are not on master"
fi fi
version=${current#v}
if ! grep "Version:" ShellCheck.cabal | grep -qFw "$version"
then
fail "The cabal file does not match tag version $version"
fi
if ! grep -qF "## $current" CHANGELOG.md
then
fail "CHANGELOG.md does not contain '## $current'"
fi
if [[ $(git log -1 --pretty=%B) != "Stable version "* ]] if [[ $(git log -1 --pretty=%B) != "Stable version "* ]]
then then
fail "Expected git log message to be 'Stable version ...'" fail "Expected git log message to be 'Stable version ...'"
fi fi
if [[ $(git log -1 --pretty=%B) != *"CHANGELOG"* ]]
then
fail "Expected git log message to contain CHANGELOG"
fi
i=1 j=1 i=1 j=1
cat << EOF cat << EOF
Manual Checklist Manual Checklist
$((i++)). Make sure none of the automated checks above failed $((i++)). Make sure none of the automated checks above failed
$((i++)). Run \`build/build_builder build/*/\` to update all builder images. $((i++)). Make sure Travis build currently passes: https://travis-ci.org/koalaman/shellcheck
$((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++)). Make sure SnapCraft build currently works: https://build.snapcraft.io/user/koalaman
$((i++)). Run test/distrotest to ensure that most distros can build OOTB.
$((i++)). Format and read over the manual for bad formatting and outdated info. $((i++)). Format and read over the manual for bad formatting and outdated info.
$((i++)). Make sure the Hackage package builds locally. $((i++)). Make sure the Hackage package builds, so that all files are
Release Steps Release Steps
$((j++)). \`cabal sdist\` to generate a Hackage package $((j++)). \`cabal sdist\` to generate a Hackage package
$((j++)). \`git push --follow-tags\` to push commit $((j++)). \`git push --follow-tags\` to push commit
$((j++)). Wait for GitHub Actions to build. $((j++)). Wait for Travis to build
$((j++)). Verify release: $((j++)). Verify release:
a. Check that the new versions are uploaded: https://github.com/koalaman/shellcheck/tags a. Check that the new versions are uploaded: https://shellcheck.storage.googleapis.com/index.html
b. Check that the docker images have version tags: https://hub.docker.com/u/koalaman 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++)). If no disaster, upload to Hackage: http://hackage.haskell.org/upload
$((j++)). Push a new commit that updates CHANGELOG.md $((j++)). Push a new commit that updates CHANGELOG.md

View file

@ -17,20 +17,13 @@ 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. Also note that dist* will be deleted.
EOF EOF
exit 0 exit 0
} }
echo "Deleting 'dist', 'dist-newstyle', and '.stack-work'..." echo "Deleting 'dist' and 'dist-newstyle'..."
rm -rf dist dist-newstyle .stack-work rm -rf dist dist-newstyle
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"
@ -70,16 +63,14 @@ 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
haskell:latest true haskell:latest true
opensuse/leap:latest zypper install -y cabal-install ghc 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++ fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils
archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel archlinux/base:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
# Ubuntu LTS # Other versions we want to support
ubuntu:24.04 apt-get update && apt-get install -y cabal-install ubuntu:18.04 apt-get update && apt-get install -y cabal-install
ubuntu:22.04 apt-get update && apt-get install -y cabal-install
ubuntu:20.04 apt-get update && apt-get install -y cabal-install
# Stack on Ubuntu LTS # Misc Haskell including current and latest Stack build
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 ubuntu:18.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

@ -4,12 +4,8 @@ import Control.Monad
import System.Exit import System.Exit
import qualified ShellCheck.Analytics import qualified ShellCheck.Analytics
import qualified ShellCheck.AnalyzerLib import qualified ShellCheck.AnalyzerLib
import qualified ShellCheck.ASTLib
import qualified ShellCheck.CFG
import qualified ShellCheck.CFGAnalysis
import qualified ShellCheck.Checker 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.Custom
import qualified ShellCheck.Checks.ShellSupport import qualified ShellCheck.Checks.ShellSupport
import qualified ShellCheck.Fixer import qualified ShellCheck.Fixer
@ -18,24 +14,17 @@ import qualified ShellCheck.Parser
main = do main = do
putStrLn "Running ShellCheck tests..." putStrLn "Running ShellCheck tests..."
failures <- filter (not . snd) <$> mapM sequenceA tests results <- sequence [
if null failures then exitSuccess else do ShellCheck.Analytics.runTests
putStrLn "Tests failed for the following module(s):" ,ShellCheck.AnalyzerLib.runTests
mapM (putStrLn . ("- ShellCheck." ++) . fst) failures ,ShellCheck.Checker.runTests
exitFailure ,ShellCheck.Checks.Commands.runTests
where ,ShellCheck.Checks.Custom.runTests
tests = ,ShellCheck.Checks.ShellSupport.runTests
[ ("Analytics" , ShellCheck.Analytics.runTests) ,ShellCheck.Fixer.runTests
, ("AnalyzerLib" , ShellCheck.AnalyzerLib.runTests) ,ShellCheck.Formatter.Diff.runTests
, ("ASTLib" , ShellCheck.ASTLib.runTests) ,ShellCheck.Parser.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

View file

@ -3,7 +3,7 @@
# various resolvers. It's run via distrotest. # various resolvers. It's run via distrotest.
resolvers=( resolvers=(
# nightly-"$(date -d "3 days ago" +"%Y-%m-%d")" nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
) )
die() { echo "$*" >&2; exit 1; } die() { echo "$*" >&2; exit 1; }
@ -15,14 +15,13 @@ die() { echo "$*" >&2; exit 1; }
command -v stack || command -v stack ||
die "stack is missing" die "stack is missing"
stack setup --allow-different-user || die "Failed to setup with default resolver" stack setup || die "Failed to setup with default resolver"
stack build --test || die "Failed to build/test with default resolver" stack build --test || die "Failed to build/test with default resolver"
# Nice to haves, but not necessary
for resolver in "${resolvers[@]}" for resolver in "${resolvers[@]}"
do do
stack --resolver="$resolver" setup || die "Failed to setup $resolver. This probably doesn't matter." stack --resolver="$resolver" setup || die "Failed to setup $resolver"
stack --resolver="$resolver" build --test || die "Failed build/test with $resolver! This probably doesn't matter." stack --resolver="$resolver" build --test || die "Failed build/test with $resolver!"
done done
echo "Success" echo "Success"