mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 00:39:19 +08:00
Compare commits
336 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
cff3e22911 | ||
|
5669eb2203 | ||
|
b68df1882d | ||
|
087865c680 | ||
|
19c6f22c3f | ||
|
98952df35b | ||
|
a277efdbb1 | ||
|
45687b0548 | ||
|
ecdc21b0b7 | ||
|
4eb42fa3c1 | ||
|
f02c297fdd | ||
|
ea83b602d7 | ||
|
88cd21fd0f | ||
|
83435c4f2e | ||
|
4324b4a213 | ||
|
a69d6cb661 | ||
|
8442695b73 | ||
|
670c1de01b | ||
|
b9b6975bfa | ||
|
d6bb8fc0d8 | ||
|
8bb5e01401 | ||
|
2e59eba6eb | ||
|
15ff87cf80 | ||
|
99e9d5c54b | ||
|
dff8f9492a | ||
|
c5756760cb | ||
|
2e5c56b270 | ||
|
9584266a8b | ||
|
5fbaae2bb3 | ||
|
fbb14d6b38 | ||
|
2cfd1f2714 | ||
|
953d9bc56d | ||
|
e272fa04ee | ||
|
81e84c2939 | ||
|
34939ca0b7 | ||
|
e7820479f0 | ||
|
8480563672 | ||
|
dfbcc9595e | ||
|
2c0766825e | ||
|
cb4f4e7edc | ||
|
0607039d41 | ||
|
46f177b5be | ||
|
eaccd3d02c | ||
|
35033a9f2f | ||
|
19355226e1 | ||
|
4e7e3f9456 | ||
|
bd3299edd3 | ||
|
cc3884cf9f | ||
|
6ba1af0898 | ||
|
8e332ce879 | ||
|
7e40d97e7a | ||
|
775c0c11d7 | ||
|
5196ab1f95 | ||
|
b625562d60 | ||
|
18e80284ec | ||
|
65044c2568 | ||
|
61b7dd610d | ||
|
4b0e5ca119 | ||
|
619662adb6 | ||
|
28d3279ba6 | ||
|
256457c47a | ||
|
3104cec770 | ||
|
f100c2939e | ||
|
8d99926554 | ||
|
218deb6d01 | ||
|
c4cc2debb7 | ||
|
cfd68ee0c2 | ||
|
58783ab3cc | ||
|
43191fa71d | ||
|
c9be7ab2eb | ||
|
fb89cdf4ad | ||
|
9e59bcca91 | ||
|
a62d9f10c2 | ||
|
e72fbb2640 | ||
|
17e591233f | ||
|
50067ddf94 | ||
|
3fa5b7d3bd | ||
|
5e6d50f493 | ||
|
e779aedac3 | ||
|
3ef1175566 | ||
|
506ffa849b | ||
|
b864242caa | ||
|
3e50a2fce8 | ||
|
10c2d827fa | ||
|
e0e2edd525 | ||
|
c5b6d6f027 | ||
|
beee9b22ca | ||
|
1ac2c31728 | ||
|
cc81bdee31 | ||
|
34885142e7 | ||
|
14e6806092 | ||
|
5d753212fb | ||
|
5b86777f9d | ||
|
7a9dbc042b | ||
|
9793d94206 | ||
|
baab5b53e0 | ||
|
210cdcd01a | ||
|
1b884a17ea | ||
|
b52f58473d | ||
|
376e78b631 | ||
|
40aacc3345 | ||
|
739eaadbf5 | ||
|
6b88a341f3 | ||
|
a61d8a232c | ||
|
12d9c1b76d | ||
|
a2b5b6a500 | ||
|
5cf2c00ff7 | ||
|
a08ad3bee9 | ||
|
417e13f129 | ||
|
536cb584f4 | ||
|
c2a15ce8e9 | ||
|
d6adbfde78 | ||
|
2030b83607 | ||
|
8aa40c43ed | ||
|
5a42f4b938 | ||
|
a7a406c43c | ||
|
1d126960f3 | ||
|
60e80e4ce1 | ||
|
e0daa936d2 | ||
|
75863a887e | ||
|
413f0048b8 | ||
|
e7b5fb9742 | ||
|
30523555af | ||
|
58d3e50f43 | ||
|
73cc11fd0a | ||
|
163c710ba7 | ||
|
ab1610b004 | ||
|
148468be70 | ||
|
5eac721fcf | ||
|
b58bb4ba9d | ||
|
999b7e2596 | ||
|
a9d564a8bc | ||
|
8a7497c4f0 | ||
|
1eac0d7340 | ||
|
f8c1ffb0dc | ||
|
3e17a20965 | ||
|
1c6202dba4 | ||
|
64c31d9142 | ||
|
8a6679fd8a | ||
|
facf0d1e27 | ||
|
cd38afce26 | ||
|
5084ba8d7e | ||
|
ed331b816b | ||
|
cfa2a663af | ||
|
df4928f4e3 | ||
|
9747b1d5c3 | ||
|
fa841cb270 | ||
|
e8501151dd | ||
|
9027a9239f | ||
|
773e98868d | ||
|
d45ab327b0 | ||
|
0f9b0f18a4 | ||
|
322842b57e | ||
|
b6cff5ea0e | ||
|
8f105074fe | ||
|
d22e0aa4a7 | ||
|
fb55072302 | ||
|
0cc5ed4563 | ||
|
ca41440a67 | ||
|
1cf0aa25e9 | ||
|
4604066c37 | ||
|
2ebf522a52 | ||
|
e4eb2d157f | ||
|
f109f9ab92 | ||
|
67e091674e | ||
|
f833ee3d5a | ||
|
f55d8c45e5 | ||
|
14ee462ccd | ||
|
b3c04ce3d0 | ||
|
b0dbc79f69 | ||
|
2a8170ba05 | ||
|
01f4423465 | ||
|
d2fa88dd91 | ||
|
a30e42ab05 | ||
|
84d6e53659 | ||
|
f7547c9a5a | ||
|
bd717c9d1b | ||
|
da0931740f | ||
|
555f8a80dd | ||
|
a9c04e8a37 | ||
|
9378227570 | ||
|
a128796c0c | ||
|
a0005bfa5a | ||
|
37a72d05ec | ||
|
c60323fb25 | ||
|
db11e2f663 | ||
|
aac1d05a7e | ||
|
67f0dc4fd5 | ||
|
8cf037fe5e | ||
|
615063a9c3 | ||
|
37e78141bd | ||
|
9f833770b0 | ||
|
7963eeab9d | ||
|
7a5e261d03 | ||
|
9d5363377e | ||
|
86d470c74f | ||
|
acee69676b | ||
|
a57f6d2886 | ||
|
d28c8f883f | ||
|
c43b19f897 | ||
|
45a67e7c64 | ||
|
68a03e05e5 | ||
|
014a66f3f6 | ||
|
fee13732a4 | ||
|
741d499b3d | ||
|
9b66bc2f13 | ||
|
00574dd1fc | ||
|
7b998239af | ||
|
4c9210af79 | ||
|
a75219e525 | ||
|
99d6df8a08 | ||
|
106f321cf0 | ||
|
1da0becb0f | ||
|
472579052b | ||
|
c735bbf30a | ||
|
eecd003e2d | ||
|
440d0038aa | ||
|
12bc7a750c | ||
|
c2d67c15f8 | ||
|
6043deb8f2 | ||
|
83d329c8da | ||
|
d0beac6d0b | ||
|
b88b253cad | ||
|
a8f9f25ec9 | ||
|
85c49a8af9 | ||
|
42abcb7ae2 | ||
|
d5c5128115 | ||
|
6d06103cab | ||
|
c95914f9b3 | ||
|
ea24e25efd | ||
|
8f0448133c | ||
|
7fc9496320 | ||
|
962fad038c | ||
|
a223a7a5a5 | ||
|
8e9290badb | ||
|
292b0840d9 | ||
|
43c24cf79c | ||
|
21ad4196db | ||
|
172aa7c4fc | ||
|
c290eace54 | ||
|
a6efd02807 | ||
|
057cc714b3 | ||
|
0e00249eae | ||
|
0ca50159ec | ||
|
7e6a556ef1 | ||
|
4bfe6496d9 | ||
|
ffbbfcfe25 | ||
|
cc424bac11 | ||
|
cb01cbf7eb | ||
|
1e32139f66 | ||
|
4d92a2e15c | ||
|
f8648e5465 | ||
|
4fd8de058b | ||
|
aaffe38198 | ||
|
bd116f252b | ||
|
ef51ed3950 | ||
|
61b073d507 | ||
|
9d604ae732 | ||
|
1ca0b72329 | ||
|
474b23d6e7 | ||
|
fe2b4b5079 | ||
|
e820a5642b | ||
|
392b57b8e8 | ||
|
6595e14d25 | ||
|
115ef29079 | ||
|
76b798394f | ||
|
8a005526cc | ||
|
c29b6afa56 | ||
|
e6e89d68fd | ||
|
f25b8bd03a | ||
|
d7278b95f2 | ||
|
5487b3f229 | ||
|
28978a8b65 | ||
|
f5c6771016 | ||
|
0f48bb78a5 | ||
|
93be86f988 | ||
|
3449e6be21 | ||
|
2e52c2b56a | ||
|
1696296c0a | ||
|
a82e606e8d | ||
|
93486ed6ac | ||
|
499e0ceaba | ||
|
ff5f29f661 | ||
|
c7bf1fd96e | ||
|
b96b7f35f4 | ||
|
926ee54036 | ||
|
fdd02c94c0 | ||
|
9008a6833b | ||
|
ce60a1764f | ||
|
cbcca528ae | ||
|
83187dafd7 | ||
|
d919aaa847 | ||
|
3f296a08c1 | ||
|
0f15fa49ba | ||
|
0a4580e234 | ||
|
5c7d8129ad | ||
|
e075cde357 | ||
|
9f578f41a1 | ||
|
2c026f1ec7 | ||
|
874bdcb514 | ||
|
fa3eb47193 | ||
|
989ac32625 | ||
|
2bbfd0570d | ||
|
9b1befadc1 | ||
|
f44624a9c0 | ||
|
c75bbcbd60 | ||
|
daa9c08dd5 | ||
|
4da34fbc64 | ||
|
4a63a3a8bd | ||
|
2341a4c683 | ||
|
7eb6b35cb0 | ||
|
93eca1cb8e | ||
|
e701cf6fad | ||
|
5962b01816 | ||
|
5becc673b2 | ||
|
84ca7711c4 | ||
|
0e0de94045 | ||
|
699aac589a | ||
|
30c75340e6 | ||
|
4dfd7eb1cf | ||
|
79ba67dbd3 | ||
|
60f75e5b8a | ||
|
f042b0ebd1 | ||
|
764fdcb260 | ||
|
7473d4a743 | ||
|
91abd979f2 | ||
|
afea62de4e | ||
|
fa0f88c106 | ||
|
7fb399528c | ||
|
de9ab4e6ef | ||
|
ff1eab286c | ||
|
e01c470598 | ||
|
9423691039 | ||
|
71a4053e8c | ||
|
3fdc6babb2 | ||
|
c175971bf0 |
@@ -1,82 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
_cleanup(){
|
||||
rm -rf dist shellcheck || true
|
||||
}
|
||||
|
||||
build_linux() {
|
||||
# Linux Docker image
|
||||
name="$DOCKER_BASE"
|
||||
DOCKER_BUILDS="$DOCKER_BUILDS $name"
|
||||
docker build -t "$name:current" .
|
||||
docker run "$name:current" --version
|
||||
printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
|
||||
docker run -v "$PWD:/mnt" "$name:current" myscript
|
||||
|
||||
# Copy static executable from docker image
|
||||
id=$(docker create "$name:current")
|
||||
docker cp "$id:/bin/shellcheck" "shellcheck"
|
||||
docker rm "$id"
|
||||
ls -l shellcheck
|
||||
./shellcheck myscript
|
||||
for tag in $TAGS
|
||||
do
|
||||
cp "shellcheck" "deploy/shellcheck-$tag.linux-x86_64";
|
||||
done
|
||||
|
||||
# Linux Alpine based Docker image
|
||||
name="$DOCKER_BASE-alpine"
|
||||
DOCKER_BUILDS="$DOCKER_BUILDS $name"
|
||||
sed -e '/DELETE-MARKER/,$d' Dockerfile > Dockerfile.alpine
|
||||
docker build -f Dockerfile.alpine -t "$name:current" .
|
||||
docker run "$name:current" sh -c 'shellcheck --version'
|
||||
_cleanup
|
||||
}
|
||||
|
||||
build_aarch64() {
|
||||
# Linux aarch64 static executable
|
||||
docker run -v "$PWD:/mnt" koalaman/aarch64-builder 'buildsc'
|
||||
for tag in $TAGS
|
||||
do
|
||||
cp "shellcheck" "deploy/shellcheck-$tag.linux-aarch64"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
build_armv6hf() {
|
||||
# Linux armv6hf static executable
|
||||
docker run -v "$PWD:/mnt" koalaman/armv6hf-builder -c 'compile-shellcheck'
|
||||
for tag in $TAGS
|
||||
do
|
||||
cp "shellcheck" "deploy/shellcheck-$tag.linux-armv6hf";
|
||||
done
|
||||
_cleanup
|
||||
}
|
||||
|
||||
build_windows() {
|
||||
# Windows .exe
|
||||
docker run --user="$UID" -v "$PWD:/appdata" koalaman/winghc cuib
|
||||
for tag in $TAGS
|
||||
do
|
||||
cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe";
|
||||
done
|
||||
_cleanup
|
||||
}
|
||||
|
||||
build_osx() {
|
||||
# Darwin x86_64 static executable
|
||||
brew install cabal-install pandoc gnu-tar
|
||||
sudo ln -s /usr/local/bin/gsha512sum /usr/local/bin/sha512sum
|
||||
sudo ln -s /usr/local/bin/gtar /usr/local/bin/tar
|
||||
export PATH="/usr/local/bin:$PATH"
|
||||
|
||||
cabal update
|
||||
cabal install --dependencies-only
|
||||
cabal build shellcheck
|
||||
for tag in $TAGS
|
||||
do
|
||||
cp "dist/build/shellcheck/shellcheck" "deploy/shellcheck-$tag.darwin-x86_64";
|
||||
done
|
||||
_cleanup
|
||||
}
|
||||
|
4
.github/ISSUE_TEMPLATE.md
vendored
4
.github/ISSUE_TEMPLATE.md
vendored
@@ -2,10 +2,10 @@
|
||||
- Rule Id (if any, e.g. SC1000):
|
||||
- My shellcheck version (`shellcheck --version` or "online"):
|
||||
- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
|
||||
- [ ] I tried on shellcheck.net and verified that this is still a problem on the latest commit
|
||||
- [ ] I tried on https://www.shellcheck.net/ and verified that this is still a problem on the latest commit
|
||||
|
||||
#### For new checks and feature suggestions
|
||||
- [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this
|
||||
- [ ] https://www.shellcheck.net/ (i.e. the latest commit) currently gives no useful warnings about this
|
||||
- [ ] I searched through https://github.com/koalaman/shellcheck/issues and didn't find anything related
|
||||
|
||||
|
||||
|
124
.github/workflows/build.yml
vendored
Normal file
124
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
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@v2
|
||||
|
||||
- name: Package Source
|
||||
run: |
|
||||
mkdir source
|
||||
cabal sdist
|
||||
mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz
|
||||
|
||||
- name: Deduce tags
|
||||
run: |
|
||||
exec > source/tags
|
||||
echo "latest"
|
||||
if tag=$(git describe --exact-match --tags)
|
||||
then
|
||||
echo "stable"
|
||||
echo "$tag"
|
||||
fi
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: source
|
||||
path: source/
|
||||
|
||||
build_source:
|
||||
name: Build Source Code
|
||||
needs: package_source
|
||||
strategy:
|
||||
matrix:
|
||||
build: [linux.x86_64, linux.aarch64, linux.armv6hf, darwin.x86_64, windows.x86_64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
|
||||
- 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@v2
|
||||
with:
|
||||
name: bin
|
||||
path: bin/
|
||||
|
||||
package_binary:
|
||||
name: Package Binaries
|
||||
needs: build_source
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
|
||||
- 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@v2
|
||||
with:
|
||||
name: deploy
|
||||
path: deploy/
|
||||
|
||||
deploy:
|
||||
name: Deploy binaries
|
||||
needs: package_binary
|
||||
runs-on: ubuntu-latest
|
||||
environment: Deploy
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
|
||||
- name: Upload to GitHub
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
export TAGS="$(cat source/tags)"
|
||||
./.github_deploy
|
||||
|
||||
- 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 )
|
29
.github_deploy
Executable file
29
.github_deploy
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
set -x
|
||||
shopt -s extglob
|
||||
|
||||
export EDITOR="touch"
|
||||
|
||||
# Sanity check
|
||||
gh --version || exit 1
|
||||
hub release show latest || exit 1
|
||||
|
||||
for tag in $TAGS
|
||||
do
|
||||
if ! hub release show "$tag"
|
||||
then
|
||||
echo "Creating new release $tag"
|
||||
git show --no-patch --format='format:%B' > description
|
||||
hub release create -F description "$tag"
|
||||
fi
|
||||
|
||||
files=()
|
||||
for file in deploy/*
|
||||
do
|
||||
[[ $file == *.@(xz|gz|zip) ]] || continue
|
||||
[[ $file == *"$tag"* ]] || continue
|
||||
files+=("$file")
|
||||
done
|
||||
gh release upload "$tag" "${files[@]}" --clobber || exit 1
|
||||
done
|
||||
|
106
.multi_arch_docker
Executable file
106
.multi_arch_docker
Executable file
@@ -0,0 +1,106 @@
|
||||
#!/bin/bash
|
||||
# This script builds and deploys multi-architecture docker images from the
|
||||
# binaries previously built and deployed to GitHub.
|
||||
|
||||
function multi_arch_docker::install_docker_buildx() {
|
||||
# Install 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.
|
||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
|
||||
# Instantiate docker buildx builder with multi-architecture support.
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
docker buildx create --name mybuilder
|
||||
docker buildx use mybuilder
|
||||
# Start up buildx and verify that all is OK.
|
||||
docker buildx inspect --bootstrap
|
||||
}
|
||||
|
||||
# Log in to Docker Hub for deployment.
|
||||
function multi_arch_docker::login_to_docker_hub() {
|
||||
echo "$DOCKER_PASSWORD" | docker login -u="$DOCKER_USERNAME" --password-stdin
|
||||
}
|
||||
|
||||
# Run buildx build and push. Passed in arguments augment the command line.
|
||||
function multi_arch_docker::buildx() {
|
||||
mkdir -p /tmp/empty
|
||||
docker buildx build \
|
||||
--platform "${DOCKER_PLATFORMS// /,}" \
|
||||
--push \
|
||||
--progress plain \
|
||||
-f Dockerfile.multi-arch \
|
||||
"$@" \
|
||||
/tmp/empty
|
||||
rmdir /tmp/empty
|
||||
}
|
||||
|
||||
# Build and push plain and alpine docker images for all tags.
|
||||
function multi_arch_docker::build_and_push_all() {
|
||||
for tag in $TAGS; do
|
||||
multi_arch_docker::buildx -t "$DOCKER_BASE:$tag" --build-arg "tag=$tag"
|
||||
multi_arch_docker::buildx -t "$DOCKER_BASE-alpine:$tag" \
|
||||
--build-arg "tag=$tag" --target alpine
|
||||
done
|
||||
}
|
||||
|
||||
# Test all pushed docker images.
|
||||
function multi_arch_docker::test_all() {
|
||||
printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
|
||||
|
||||
for platform in $DOCKER_PLATFORMS; do
|
||||
for tag in $TAGS; do
|
||||
for ext in '-alpine' ''; do
|
||||
image="${DOCKER_BASE}${ext}:${tag}"
|
||||
msg="Testing docker image $image on platform $platform"
|
||||
line="${msg//?/=}"
|
||||
printf '\n%s\n%s\n%s\n' "${line}" "${msg}" "${line}"
|
||||
docker pull -q --platform "$platform" "$image"
|
||||
if [ -n "$ext" ]; then
|
||||
echo -n "Image architecture: "
|
||||
docker run --rm --entrypoint /bin/sh "$image" -c 'uname -m'
|
||||
version=$(docker run --rm "$image" shellcheck --version \
|
||||
| grep 'version:')
|
||||
else
|
||||
version=$(docker run --rm "$image" --version | grep 'version:')
|
||||
fi
|
||||
version=${version/#version: /v}
|
||||
echo "shellcheck version: $version"
|
||||
if [[ ! ("$tag" =~ ^(latest|stable)$) && "$tag" != "$version" ]]; then
|
||||
echo "Version mismatch: shellcheck $version tagged as $tag"
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$ext" ]; then
|
||||
docker run --rm -v "$PWD:/mnt" -w /mnt "$image" shellcheck myscript
|
||||
else
|
||||
docker run --rm -v "$PWD:/mnt" "$image" myscript
|
||||
fi
|
||||
done
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
function multi_arch_docker::main() {
|
||||
export DOCKER_PLATFORMS='linux/amd64'
|
||||
DOCKER_PLATFORMS+=' linux/arm64'
|
||||
DOCKER_PLATFORMS+=' linux/arm/v6'
|
||||
|
||||
multi_arch_docker::install_docker_buildx
|
||||
multi_arch_docker::login_to_docker_hub
|
||||
multi_arch_docker::build_and_push_all
|
||||
multi_arch_docker::test_all
|
||||
}
|
@@ -1,8 +1,9 @@
|
||||
#!/bin/bash
|
||||
# This script packages up Travis compiled binaries
|
||||
# This script packages up compiled binaries
|
||||
set -ex
|
||||
shopt -s nullglob
|
||||
cd deploy
|
||||
shopt -s nullglob extglob
|
||||
|
||||
ls -l
|
||||
|
||||
cp ../LICENSE LICENSE.txt
|
||||
sed -e $'s/$/\r/' > README.txt << END
|
||||
@@ -22,44 +23,32 @@ This binary was compiled on $(date -u).
|
||||
$(git log -n 3)
|
||||
END
|
||||
|
||||
for file in ./*.exe
|
||||
for dir in */
|
||||
do
|
||||
zip "${file%.*}.zip" README.txt LICENSE.txt "$file"
|
||||
cp LICENSE.txt README.txt "$dir"
|
||||
done
|
||||
|
||||
for file in *.linux-x86_64
|
||||
do
|
||||
base="${file%.*}"
|
||||
cp "$file" "shellcheck"
|
||||
tar -cJf "$base.linux.x86_64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
|
||||
rm "shellcheck"
|
||||
done
|
||||
echo "Tags are $TAGS"
|
||||
|
||||
for file in *.linux-aarch64
|
||||
for tag in $TAGS
|
||||
do
|
||||
base="${file%.*}"
|
||||
cp "$file" "shellcheck"
|
||||
tar -cJf "$base.linux.aarch64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
|
||||
rm "shellcheck"
|
||||
done
|
||||
|
||||
for file in *.linux-armv6hf
|
||||
do
|
||||
base="${file%.*}"
|
||||
cp "$file" "shellcheck"
|
||||
tar -cJf "$base.linux.armv6hf.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
|
||||
rm "shellcheck"
|
||||
done
|
||||
for dir in windows.*/
|
||||
do
|
||||
( cd "$dir" && zip "../shellcheck-$tag.zip" * )
|
||||
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"
|
||||
for dir in {linux,darwin}.*/
|
||||
do
|
||||
base="${dir%/}"
|
||||
( cd "$dir" && tar -cJf "../shellcheck-$tag.$base.tar.xz" --transform="s:^:shellcheck-$tag/:" * )
|
||||
done
|
||||
done
|
||||
|
||||
for file in ./*
|
||||
do
|
||||
[[ -f "$file" ]] || continue
|
||||
sha512sum "$file" > "$file.sha512sum"
|
||||
done
|
||||
|
||||
ls -l
|
||||
|
66
.travis.yml
66
.travis.yml
@@ -1,66 +0,0 @@
|
||||
|
||||
sudo: required
|
||||
|
||||
language: sh
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
env: BUILD=linux
|
||||
- os: linux
|
||||
env: BUILD=windows
|
||||
- os: linux
|
||||
env: BUILD=armv6hf
|
||||
- os: linux
|
||||
env: BUILD=aarch64
|
||||
- os: osx
|
||||
env: BUILD=osx
|
||||
|
||||
before_install: |
|
||||
DOCKER_BASE="$DOCKER_USERNAME/shellcheck"
|
||||
DOCKER_BUILDS=""
|
||||
TAGS=""
|
||||
test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
|
||||
test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
|
||||
echo "Tags are $TAGS"
|
||||
|
||||
script:
|
||||
- mkdir -p deploy
|
||||
- source ./.compile_binaries
|
||||
- ./striptests
|
||||
- set -x; build_"$BUILD"; set +x;
|
||||
- ./.prepare_deploy
|
||||
|
||||
after_success: |
|
||||
if [ "$BUILD" = "linux" ]; then
|
||||
docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
||||
for repo in $DOCKER_BUILDS; do
|
||||
for tag in $TAGS; do
|
||||
echo "Deploying $repo:current as $repo:$tag...";
|
||||
docker tag "$repo:current" "$repo:$tag" || exit 1;
|
||||
docker push "$repo:$tag" || exit 1;
|
||||
done;
|
||||
done;
|
||||
fi
|
||||
|
||||
after_failure: |
|
||||
id
|
||||
pwd
|
||||
df -h
|
||||
find . -name '*.log' -type f -exec grep "" /dev/null {} +
|
||||
find . -ls
|
||||
|
||||
deploy:
|
||||
provider: gcs
|
||||
skip_cleanup: true
|
||||
access_key_id: GOOG7MDN7WEH6IIGBDCA
|
||||
secret_access_key:
|
||||
secure: Bcx2cT0/E2ikj7sdamVq52xlLZF9dz9ojGPtoKfPyQhkkZa+McVI4xgUSuyyoSxyKj77sofx2y8m6PJYYumT4g5hREV1tfeUkl0J2DQFMbGDYEt7kxVkXCxojNvhHwTzLFv0ezstrxWWxQm81BfQQ4U9lggRXtndAP4czZnOeHPINPSiue1QNwRAEw05r5UoIUJXy/5xyUrjIxn381pAs+gJqP2COeN9kTKYH53nS/AAws29RprfZFnPlo7xxWmcjRcdS5KPdGXI/c6tQp5zl2iTh510VC1PN2w1Wvnn/oNWhiNdqPyVDsojIX5+sS3nejzJA+KFMxXSBlyXIY3wPpS/MdscU79X6Q5f9ivsFfsm7gNBmxHUPNn0HAvU4ROT/CCE9j6jSbs5PC7QBo3CK4++jxAwE/pd9HUc2rs3k0ofx3rgveJ7txpy5yPKfwIIBi98kVKlC4w7dLvNTOfjW1Imt2yH87XTfsE0UIG9st1WII6s4l/WgBx2GuwKdt6+3QUYiAlCFckkxWi+fAvpHZUEL43Qxub5fN+ZV7Zib1n7opchH4QKGBb6/y0WaDCmtCfu0lppoe/TH6saOTjDFj67NJSElK6ZDxGZ3uw4R+ret2gm6WRKT2Oeub8J33VzSa7VkmFpMPrAAfPa9N1Z4ewBLoTmvxSg2A0dDrCdJio=
|
||||
bucket: shellcheck
|
||||
local-dir: deploy
|
||||
on:
|
||||
repo: koalaman/shellcheck
|
||||
all_branches: true
|
52
CHANGELOG.md
52
CHANGELOG.md
@@ -1,3 +1,51 @@
|
||||
## v0.7.2 - 2021-04-19
|
||||
### Added
|
||||
- `disable` directives can now be a range, e.g. `disable=SC3000-SC4000`
|
||||
- SC1143: Warn about line continuations in comments
|
||||
- SC2259/SC2260: Warn when redirections override pipes
|
||||
- SC2261: Warn about multiple competing redirections
|
||||
- SC2262/SC2263: Warn about aliases declared and used in the same parsing unit
|
||||
- SC2264: Warn about wrapper functions that blatantly recurse
|
||||
- SC2265/SC2266: Warn when using & or | with test statements
|
||||
- SC2267: Warn when using xargs -i instead of -I
|
||||
- SC2268: Warn about unnecessary x-comparisons like `[ x$var = xval ]`
|
||||
|
||||
### Fixed
|
||||
- SC1072/SC1073 now respond to disable annotations, though ignoring parse errors
|
||||
is still purely cosmetic and does not allow ShellCheck to continue.
|
||||
- Improved error reporting for trailing tokens after ]/]] and compound commands
|
||||
- `#!/usr/bin/env -S shell` is now handled correctly
|
||||
- Here docs with \r are now parsed correctly and give better warnings
|
||||
|
||||
### Changed
|
||||
- Assignments are now parsed to spec, without leniency for leading $ or spaces
|
||||
- POSIX/dash unsupported feature warnings now have individual SC3xxx codes
|
||||
- SC1090: A leading `$x/` or `$(x)/` is now treated as `./` when locating files
|
||||
- SC2154: Variables appearing in -z/-n tests are no longer considered unassigned
|
||||
- SC2270-SC2285: Improved warnings about misused `=`, e.g. `${var}=42`
|
||||
|
||||
|
||||
## v0.7.1 - 2020-04-04
|
||||
### Fixed
|
||||
- `-f diff` no longer claims that it found more issues when it didn't
|
||||
- Known empty variables now correctly trigger SC2086
|
||||
- ShellCheck should now be compatible with Cabal 3
|
||||
- SC2154 and all command-specific checks now trigger for builtins
|
||||
called with `builtin`
|
||||
|
||||
### Added
|
||||
- SC1136: Warn about unexpected characters after ]/]]
|
||||
- SC2254: Suggest quoting expansions in case statements
|
||||
- SC2255: Suggest using `$((..))` in `[ 2*3 -eq 6 ]`
|
||||
- SC2256: Warn about translated strings that are known variables
|
||||
- SC2257: Warn about arithmetic mutation in redirections
|
||||
- SC2258: Warn about trailing commas in for loop elements
|
||||
|
||||
### Changed
|
||||
- SC2230: 'command -v' suggestion is now off by default (-i deprecate-which)
|
||||
- SC1081: Keywords are now correctly parsed case sensitively, with a warning
|
||||
|
||||
|
||||
## v0.7.0 - 2019-07-28
|
||||
### Added
|
||||
- Precompiled binaries for macOS and Linux aarch64
|
||||
@@ -117,7 +165,7 @@
|
||||
- SC2204/SC2205: Warn about `( -z foo )` and `( foo -eq bar )`
|
||||
- SC2200/SC2201: Warn about brace expansion in [/[[
|
||||
- SC2198/SC2199: Warn about arrays in [/[[
|
||||
- SC2196/SC2197: Warn about deprected egrep/fgrep
|
||||
- SC2196/SC2197: Warn about deprecated egrep/fgrep
|
||||
- SC2195: Warn about unmatchable case branches
|
||||
- SC2194: Warn about constant 'case' statements
|
||||
- SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables
|
||||
@@ -134,7 +182,7 @@
|
||||
### Fixed
|
||||
- `-c` no longer suggested when using `grep -o | wc`
|
||||
- Comments and whitespace are now allowed before filewide directives
|
||||
- Here doc delimters with esoteric quoting like `foo""` are now handled
|
||||
- Here doc delimiters with esoteric quoting like `foo""` are now handled
|
||||
- SC2095 about `ssh` in while read loops is now suppressed when using `-n`
|
||||
- `%(%Y%M%D)T` now recognized as a single formatter in `printf` checks
|
||||
- `grep -F` now suppresses regex related suggestions
|
||||
|
36
Dockerfile
36
Dockerfile
@@ -1,36 +0,0 @@
|
||||
# Build-only image
|
||||
FROM ubuntu:18.04 AS build
|
||||
USER root
|
||||
WORKDIR /opt/shellCheck
|
||||
|
||||
# Install OS deps
|
||||
RUN apt-get update && apt-get install -y ghc cabal-install
|
||||
|
||||
# Install Haskell deps
|
||||
# (This is a separate copy/run so that source changes don't require rebuilding)
|
||||
COPY ShellCheck.cabal ./
|
||||
RUN cabal update && cabal install --dependencies-only --ghc-options="-optlo-Os -split-sections"
|
||||
|
||||
# Copy source and build it
|
||||
COPY LICENSE Setup.hs shellcheck.hs ./
|
||||
COPY src src
|
||||
RUN cabal build Paths_ShellCheck && \
|
||||
ghc -optl-static -optl-pthread -isrc -idist/build/autogen --make shellcheck -split-sections -optc-Wl,--gc-sections -optlo-Os && \
|
||||
strip --strip-all shellcheck
|
||||
|
||||
RUN mkdir -p /out/bin && \
|
||||
cp shellcheck /out/bin/
|
||||
|
||||
# Resulting Alpine image
|
||||
FROM alpine:latest
|
||||
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
|
||||
COPY --from=build /out /
|
||||
|
||||
# DELETE-MARKER (Remove everything below to keep the alpine image)
|
||||
|
||||
# Resulting ShellCheck image
|
||||
FROM scratch
|
||||
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
|
||||
WORKDIR /mnt
|
||||
COPY --from=build /out /
|
||||
ENTRYPOINT ["/bin/shellcheck"]
|
26
Dockerfile.multi-arch
Normal file
26
Dockerfile.multi-arch
Normal file
@@ -0,0 +1,26 @@
|
||||
# Alpine image
|
||||
FROM alpine:latest AS alpine
|
||||
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
|
||||
ARG tag
|
||||
|
||||
# Put the right binary for each architecture into place for the
|
||||
# multi-architecture docker image.
|
||||
RUN set -x; \
|
||||
arch="$(uname -m)"; \
|
||||
echo "arch is $arch"; \
|
||||
if [ "${arch}" = 'armv7l' ]; then \
|
||||
arch='armv6hf'; \
|
||||
fi; \
|
||||
url_base='https://github.com/koalaman/shellcheck/releases/download/'; \
|
||||
tar_file="${tag}/shellcheck-${tag}.linux.${arch}.tar.xz"; \
|
||||
wget "${url_base}${tar_file}" -O - | tar xJf -; \
|
||||
mv "shellcheck-${tag}/shellcheck" /bin/; \
|
||||
rm -rf "shellcheck-${tag}"; \
|
||||
ls -laF /bin/shellcheck
|
||||
|
||||
# ShellCheck image
|
||||
FROM scratch
|
||||
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
|
||||
WORKDIR /mnt
|
||||
COPY --from=alpine /bin/shellcheck /bin/
|
||||
ENTRYPOINT ["/bin/shellcheck"]
|
38
README.md
38
README.md
@@ -109,6 +109,8 @@ Services and platforms that have ShellCheck pre-installed and ready to use:
|
||||
* [Codacy](https://www.codacy.com/)
|
||||
* [Code Climate](https://codeclimate.com/)
|
||||
* [Code Factor](https://www.codefactor.io/)
|
||||
* [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:
|
||||
|
||||
@@ -116,7 +118,7 @@ Services and platforms with third party plugins:
|
||||
|
||||
Most other services, including [GitLab](https://about.gitlab.com/), let you install
|
||||
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
|
||||
or by downloading and unpacking a [binary release](#installing-the-shellcheck-binary).
|
||||
or by downloading and unpacking a [binary release](#installing-a-pre-compiled-binary).
|
||||
|
||||
It's a good idea to manually install a specific ShellCheck version regardless. This avoids
|
||||
any surprise build breaks when a new version with new warnings is published.
|
||||
@@ -147,7 +149,7 @@ On Arch Linux based distros:
|
||||
|
||||
pacman -S shellcheck
|
||||
|
||||
or get the dependency free [shellcheck-static](https://aur.archlinux.org/packages/shellcheck-static/) from the AUR.
|
||||
or get the dependency free [shellcheck-bin](https://aur.archlinux.org/packages/shellcheck-bin/) from the AUR.
|
||||
|
||||
On Gentoo based distros:
|
||||
|
||||
@@ -166,10 +168,14 @@ On FreeBSD:
|
||||
|
||||
pkg install hs-ShellCheck
|
||||
|
||||
On OS X with homebrew:
|
||||
On macOS (OS X) with Homebrew:
|
||||
|
||||
brew install shellcheck
|
||||
|
||||
Or with MacPorts:
|
||||
|
||||
sudo port install shellcheck
|
||||
|
||||
On OpenBSD:
|
||||
|
||||
pkg_add shellcheck
|
||||
@@ -196,6 +202,10 @@ Or Windows (via [scoop](http://scoop.sh)):
|
||||
C:\> scoop install shellcheck
|
||||
```
|
||||
|
||||
From [conda-forge](https://anaconda.org/conda-forge/shellcheck):
|
||||
|
||||
conda install -c conda-forge shellcheck
|
||||
|
||||
From Snap Store:
|
||||
|
||||
snap install --channel=edge shellcheck
|
||||
@@ -209,15 +219,21 @@ docker run --rm -v "$PWD:/mnt" koalaman/shellcheck:stable myscript
|
||||
|
||||
or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend. It works exactly like a regular Alpine image, but has shellcheck preinstalled.
|
||||
|
||||
Using the [nix package manager](https://nixos.org/nix):
|
||||
```sh
|
||||
nix-env -iA nixpkgs.shellcheck
|
||||
```
|
||||
|
||||
Alternatively, you can download pre-compiled binaries for the latest release here:
|
||||
|
||||
* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
|
||||
* [Linux, armv6hf](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
|
||||
* [Linux, aarch64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz) aka ARM64 (statically linked)
|
||||
* [MacOS, x86_64](https://shellcheck.storage.googleapis.com/shellcheck-stable.darwin.x86_64.tar.xz)
|
||||
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-stable.zip)
|
||||
* [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, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)
|
||||
* [macOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz)
|
||||
* [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip)
|
||||
|
||||
or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds.
|
||||
or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
|
||||
(including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds).
|
||||
|
||||
Distro packages already come with a `man` page. If you are building from source, it can be installed with:
|
||||
|
||||
@@ -244,7 +260,7 @@ A simple installer may do something like:
|
||||
|
||||
```bash
|
||||
scversion="stable" # or "v0.4.7", or "latest"
|
||||
wget -qO- "https://storage.googleapis.com/shellcheck/shellcheck-${scversion?}.linux.x86_64.tar.xz" | tar -xJv
|
||||
wget -qO- "https://github.com/koalaman/shellcheck/releases/download/${scversion?}/shellcheck-${scversion?}.linux.x86_64.tar.xz" | tar -xJv
|
||||
cp "shellcheck-${scversion}/shellcheck" /usr/bin/
|
||||
shellcheck --version
|
||||
```
|
||||
@@ -257,7 +273,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`).
|
||||
|
||||
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
|
||||
|
||||
|
36
Setup.hs
36
Setup.hs
@@ -1,36 +0,0 @@
|
||||
import Distribution.PackageDescription (
|
||||
HookedBuildInfo,
|
||||
emptyHookedBuildInfo )
|
||||
import Distribution.Simple (
|
||||
Args,
|
||||
UserHooks ( preSDist ),
|
||||
defaultMainWithHooks,
|
||||
simpleUserHooks )
|
||||
import Distribution.Simple.Setup ( SDistFlags )
|
||||
|
||||
import System.Process ( system )
|
||||
|
||||
|
||||
main = defaultMainWithHooks myHooks
|
||||
where
|
||||
myHooks = simpleUserHooks { preSDist = myPreSDist }
|
||||
|
||||
-- | This hook will be executed before e.g. @cabal sdist@. It runs
|
||||
-- pandoc to create the man page from shellcheck.1.md. If the pandoc
|
||||
-- command is not found, this will fail with an error message:
|
||||
--
|
||||
-- /bin/sh: pandoc: command not found
|
||||
--
|
||||
-- Since the man page is listed in the Extra-Source-Files section of
|
||||
-- our cabal file, a failure here should result in a failure to
|
||||
-- create the distribution tarball (that's a good thing).
|
||||
--
|
||||
myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo
|
||||
myPreSDist _ _ = do
|
||||
putStrLn "Building the man page (shellcheck.1) with pandoc..."
|
||||
putStrLn pandoc_cmd
|
||||
result <- system pandoc_cmd
|
||||
putStrLn $ "pandoc exited with " ++ show result
|
||||
return emptyHookedBuildInfo
|
||||
where
|
||||
pandoc_cmd = "pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1"
|
@@ -1,5 +1,5 @@
|
||||
Name: ShellCheck
|
||||
Version: 0.7.0
|
||||
Version: 0.7.2
|
||||
Synopsis: Shell script analysis tool
|
||||
License: GPL-3
|
||||
License-file: LICENSE
|
||||
@@ -7,7 +7,7 @@ Category: Static Analysis
|
||||
Author: Vidar Holen
|
||||
Maintainer: vidar@vidarholen.net
|
||||
Homepage: https://www.shellcheck.net/
|
||||
Build-Type: Custom
|
||||
Build-Type: Simple
|
||||
Cabal-Version: >= 1.8
|
||||
Bug-reports: https://github.com/koalaman/shellcheck/issues
|
||||
Description:
|
||||
@@ -26,19 +26,13 @@ Extra-Source-Files:
|
||||
-- documentation
|
||||
README.md
|
||||
shellcheck.1.md
|
||||
-- built with a cabal sdist hook
|
||||
shellcheck.1
|
||||
-- A script to build the man page using pandoc
|
||||
manpage
|
||||
-- convenience script for stripping tests
|
||||
striptests
|
||||
-- tests
|
||||
test/shellcheck.hs
|
||||
|
||||
custom-setup
|
||||
setup-depends:
|
||||
base >= 4 && <5,
|
||||
process >= 1.0 && <1.7,
|
||||
Cabal >= 1.10 && <2.5
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: git://github.com/koalaman/shellcheck.git
|
||||
@@ -51,9 +45,7 @@ library
|
||||
build-depends:
|
||||
aeson,
|
||||
array,
|
||||
-- GHC 7.6.3 (base 4.6.0.1) is buggy (#1131, #1119) in optimized mode.
|
||||
-- Just disable that version entirely to fail fast.
|
||||
base > 4.6.0.1 && < 5,
|
||||
base >= 4.8.0.0 && < 5,
|
||||
bytestring,
|
||||
containers >= 0.5,
|
||||
deepseq >= 1.4.0.0,
|
||||
|
13
build/README.md
Normal file
13
build/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
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`.
|
12
build/build_builder
Executable file
12
build/build_builder
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
if [ $# -eq 0 ]
|
||||
then
|
||||
echo >&2 "No build image directories specified"
|
||||
echo >&2 "Example: $0 build/*/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for dir
|
||||
do
|
||||
( cd "$dir" && docker build -t "$(cat tag)" . ) || exit 1
|
||||
done
|
31
build/darwin.x86_64/Dockerfile
Normal file
31
build/darwin.x86_64/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
# DIGEST:sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76
|
||||
FROM liushuyu/osxcross:latest
|
||||
|
||||
ENV TARGET x86_64-apple-darwin18
|
||||
ENV TARGETNAME darwin.x86_64
|
||||
|
||||
# Build dependencies
|
||||
USER root
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
RUN apt-get update && apt-get install -y ghc automake autoconf llvm curl
|
||||
|
||||
# Build GHC
|
||||
WORKDIR /ghc
|
||||
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-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.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.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"]
|
14
build/darwin.x86_64/build
Executable file
14
build/darwin.x86_64/build
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
set -xe
|
||||
{
|
||||
tar xzv --strip-components=1
|
||||
chmod +x striptests && ./striptests
|
||||
mkdir "$TARGETNAME"
|
||||
cabal update
|
||||
( IFS=';'; cabal build $CABALOPTS )
|
||||
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
|
||||
ls -l "$TARGETNAME"
|
||||
"$TARGET-strip" -Sx "$TARGETNAME/shellcheck"
|
||||
ls -l "$TARGETNAME"
|
||||
} >&2
|
||||
tar czv "$TARGETNAME"
|
1
build/darwin.x86_64/tag
Normal file
1
build/darwin.x86_64/tag
Normal file
@@ -0,0 +1 @@
|
||||
koalaman/scbuilder-darwin-x86_64
|
30
build/linux.aarch64/Dockerfile
Normal file
30
build/linux.aarch64/Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
||||
FROM ubuntu:20.04
|
||||
|
||||
ENV TARGET aarch64-linux-gnu
|
||||
ENV TARGETNAME linux.aarch64
|
||||
|
||||
# Build dependencies
|
||||
USER root
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
RUN apt-get update && apt-get install -y ghc automake autoconf build-essential llvm curl qemu-user-static gcc-$TARGET
|
||||
|
||||
# Build GHC
|
||||
WORKDIR /ghc
|
||||
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-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.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.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;--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"]
|
15
build/linux.aarch64/build
Executable file
15
build/linux.aarch64/build
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
set -xe
|
||||
{
|
||||
tar xzv --strip-components=1
|
||||
chmod +x striptests && ./striptests
|
||||
mkdir "$TARGETNAME"
|
||||
cabal update
|
||||
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
|
||||
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
|
||||
ls -l "$TARGETNAME"
|
||||
"$TARGET-strip" -s "$TARGETNAME/shellcheck"
|
||||
ls -l "$TARGETNAME"
|
||||
qemu-aarch64-static "$TARGETNAME/shellcheck" --version
|
||||
} >&2
|
||||
tar czv "$TARGETNAME"
|
1
build/linux.aarch64/tag
Normal file
1
build/linux.aarch64/tag
Normal file
@@ -0,0 +1 @@
|
||||
koalaman/scbuilder-linux-aarch64
|
59
build/linux.armv6hf/Dockerfile
Normal file
59
build/linux.armv6hf/Dockerfile
Normal file
@@ -0,0 +1,59 @@
|
||||
# I've again spent days trying to get a working armv6hf compiler going.
|
||||
# God only knows how many recompilations of GCC, GHC, libraries, and
|
||||
# ShellCheck itself, has gone into it.
|
||||
#
|
||||
# I tried Debian's toolchain. I tried my custom one built according to
|
||||
# RPi `gcc -v`. I tried GHC9, glibc, musl, registerised vs not, but
|
||||
# nothing has yielded an armv6hf binary that does not immediately
|
||||
# segfault on qemu-arm-static or the RPi itself.
|
||||
#
|
||||
# I then tried the same but with armv7hf. Same story.
|
||||
#
|
||||
# Emulating the entire userspace with balenalib again? Very strange build
|
||||
# failures where programs would fail to execute with > ~100 arguments.
|
||||
#
|
||||
# Finally, creating our own appears to work when using a custom QEmu
|
||||
# patched to follow execve calls.
|
||||
#
|
||||
# PS: $100 bounty for getting a RPi1 compatible static build going
|
||||
# with cross-compilation, similar to what the aarch64 build does.
|
||||
#
|
||||
|
||||
FROM ubuntu:20.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 build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev
|
||||
WORKDIR /build
|
||||
RUN git clone --depth 1 https://github.com/koalaman/qemu
|
||||
RUN cd qemu && ./configure --static && cd build && ninja qemu-arm
|
||||
RUN cp qemu/build/qemu-arm /build/qemu-arm-static
|
||||
ENV QEMU_EXECVE 1
|
||||
|
||||
# Set up an armv6 userspace
|
||||
WORKDIR /
|
||||
RUN apt-get install -y debootstrap qemu-user-static
|
||||
# We expect this to fail if the host doesn't have binfmt qemu support
|
||||
RUN qemu-debootstrap --arch armhf bullseye pi http://mirrordirector.raspbian.org/raspbian || [ -e /pi/etc/issue ]
|
||||
RUN cp /build/qemu-arm-static /pi/usr/bin/qemu-arm-static
|
||||
RUN printf > /bin/pirun '%s\n' '#!/bin/sh' 'chroot /pi /usr/bin/qemu-arm-static /usr/bin/env "$@"' && chmod +x /bin/pirun
|
||||
# If the debootstrap process didn't finish, continue it
|
||||
RUN [ ! -e /pi/debootstrap ] || pirun '/debootstrap/debootstrap' --second-stage
|
||||
|
||||
# Install deps in the chroot
|
||||
RUN pirun apt-get update
|
||||
RUN pirun apt-get install -y ghc cabal-install
|
||||
|
||||
# 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"
|
||||
RUN pirun cabal update
|
||||
RUN IFS=";" && pirun cabal install --dependencies-only $CABALOPTS ShellCheck
|
||||
|
||||
# Copy the build script
|
||||
WORKDIR /pi/scratch
|
||||
COPY build /pi/usr/bin
|
||||
ENTRYPOINT ["/bin/pirun", "/usr/bin/build"]
|
16
build/linux.armv6hf/build
Executable file
16
build/linux.armv6hf/build
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
set -xe
|
||||
cd /scratch
|
||||
{
|
||||
tar xzv --strip-components=1
|
||||
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"
|
1
build/linux.armv6hf/tag
Normal file
1
build/linux.armv6hf/tag
Normal file
@@ -0,0 +1 @@
|
||||
koalaman/scbuilder-linux-armv6hf
|
26
build/linux.x86_64/Dockerfile
Normal file
26
build/linux.x86_64/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM ubuntu:20.04
|
||||
|
||||
ENV TARGETNAME linux.x86_64
|
||||
|
||||
# Install GHC and cabal
|
||||
USER root
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
RUN apt-get update && apt-get install -y ghc curl xz-utils
|
||||
|
||||
# So we'd like a later version of Cabal that supports --enable-executable-static,
|
||||
# but we can't use Ubuntu 20.10 because coreutils has switched to new syscalls that
|
||||
# the TravisCI kernel doesn't support. Download it manually.
|
||||
RUN curl "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/bin
|
||||
|
||||
# Use ld.bfd instead of ld.gold due to
|
||||
# x86_64-linux-gnu/libpthread.a(pthread_cond_init.o)(.note.stapsdt+0x14): error:
|
||||
# relocation refers to local symbol "" [2], which is defined in a discarded section
|
||||
ENV CABALOPTS "--ghc-options;-optl-Wl,-fuse-ld=bfd -split-sections -optc-Os -optc-Wl,--gc-sections"
|
||||
|
||||
# Other archs pre-build dependencies here, but this one doesn't to detect ecosystem movement
|
||||
|
||||
# Copy the build script
|
||||
COPY build /usr/bin
|
||||
|
||||
WORKDIR /scratch
|
||||
ENTRYPOINT ["/usr/bin/build"]
|
15
build/linux.x86_64/build
Executable file
15
build/linux.x86_64/build
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
set -xe
|
||||
{
|
||||
tar xzv --strip-components=1
|
||||
chmod +x striptests && ./striptests
|
||||
mkdir "$TARGETNAME"
|
||||
cabal update
|
||||
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
|
||||
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
|
||||
ls -l "$TARGETNAME"
|
||||
strip -s "$TARGETNAME/shellcheck"
|
||||
ls -l "$TARGETNAME"
|
||||
"$TARGETNAME/shellcheck" --version
|
||||
} >&2
|
||||
tar czv "$TARGETNAME"
|
1
build/linux.x86_64/tag
Normal file
1
build/linux.x86_64/tag
Normal file
@@ -0,0 +1 @@
|
||||
koalaman/scbuilder-linux-x86_64
|
30
build/run_builder
Executable file
30
build/run_builder
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
if [ $# -lt 2 ]
|
||||
then
|
||||
echo >&2 "This script builds a source archive (as produced by cabal sdist)"
|
||||
echo >&2 "Usage: $0 sourcefile.tar.gz builddir..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
file=$(realpath "$1")
|
||||
shift
|
||||
|
||||
if [ ! -e "$file" ]
|
||||
then
|
||||
echo >&2 "$file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -ex -o pipefail
|
||||
|
||||
for dir
|
||||
do
|
||||
tagfile="$dir/tag"
|
||||
if [ ! -e "$tagfile" ]
|
||||
then
|
||||
echo >&2 "$tagfile does not exist"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
docker run -i "$(< "$tagfile")" < "$file" | tar xz
|
||||
done
|
27
build/windows.x86_64/Dockerfile
Normal file
27
build/windows.x86_64/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
FROM ubuntu:20.04
|
||||
|
||||
ENV TARGETNAME windows.x86_64
|
||||
|
||||
# We don't need wine32, even though it complains
|
||||
USER root
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
RUN apt-get update && apt-get install -y curl busybox wine winbind
|
||||
|
||||
# Fetch Windows version, will be available under z:\haskell
|
||||
WORKDIR /haskell
|
||||
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-x86_64-unknown-mingw32.tar.xz" | tar xJ --strip-components=1
|
||||
WORKDIR /haskell/bin
|
||||
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-mingw32.zip" | busybox unzip -
|
||||
RUN curl -L "https://curl.se/windows/dl-7.75.0/curl-7.75.0-win64-mingw.zip" | busybox unzip - && mv curl-7.75.0-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"]
|
19
build/windows.x86_64/build
Executable file
19
build/windows.x86_64/build
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
cabal() {
|
||||
wine /haskell/bin/cabal.exe "$@"
|
||||
}
|
||||
|
||||
set -xe
|
||||
{
|
||||
tar xzv --strip-components=1
|
||||
chmod +x striptests && ./striptests
|
||||
mkdir "$TARGETNAME"
|
||||
cabal update
|
||||
( IFS=';'; cabal build $CABALOPTS )
|
||||
find dist*/ -name shellcheck.exe -type f -ls -exec mv {} "$TARGETNAME/" \;
|
||||
ls -l "$TARGETNAME"
|
||||
wine "/haskell/mingw/bin/strip.exe" -s "$TARGETNAME/shellcheck.exe"
|
||||
ls -l "$TARGETNAME"
|
||||
wine "$TARGETNAME/shellcheck.exe" --version
|
||||
} >&2
|
||||
tar czv "$TARGETNAME"
|
1
build/windows.x86_64/tag
Normal file
1
build/windows.x86_64/tag
Normal file
@@ -0,0 +1 @@
|
||||
koalaman/scbuilder-windows-x86_64
|
4
manpage
Executable file
4
manpage
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
echo >&2 "Generating man page using pandoc"
|
||||
pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1 || exit
|
||||
echo >&2 "Done. You can read it with: man ./shellcheck.1"
|
@@ -6,8 +6,8 @@ then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for i in 1 2
|
||||
for i in 1 2 3
|
||||
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))"
|
||||
done
|
||||
|
@@ -107,7 +107,7 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
||||
|
||||
**-x**,\ **--external-sources**
|
||||
|
||||
: Follow 'source' statements even when the file is not specified as input.
|
||||
: Follow `source` statements even when the file is not specified as input.
|
||||
By default, `shellcheck` will only follow files specified on the command
|
||||
line (plus `/dev/null`). This option allows following any file the script
|
||||
may `source`.
|
||||
@@ -232,7 +232,8 @@ Valid keys are:
|
||||
**disable**
|
||||
: 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
|
||||
like a function definition, subshell block or loop.
|
||||
like a function definition, subshell block or loop. A range can be
|
||||
be specified with a dash, e.g. `disable=SC3000-SC4000` to exclude 3xxx.
|
||||
|
||||
**enable**
|
||||
: Enable an optional check by name, as listed with **--list-optional**.
|
||||
@@ -254,7 +255,7 @@ Valid keys are:
|
||||
**shell**
|
||||
: Overrides the shell detected from the shebang. This is useful for
|
||||
files meant to be included (and thus lacking a shebang), or possibly
|
||||
as a more targeted alternative to 'disable=2039'.
|
||||
as a more targeted alternative to 'disable=SC2039'.
|
||||
|
||||
# RC FILES
|
||||
|
||||
@@ -275,8 +276,8 @@ Here is an example `.shellcheckrc`:
|
||||
# Turn on warnings for unassigned uppercase variables
|
||||
enable=check-unassigned-uppercase
|
||||
|
||||
# Allow using `which` since it gives full paths and is common enough
|
||||
disable=SC2230
|
||||
# Allow [ ! -z foo ] instead of suggesting -n
|
||||
disable=SC2236
|
||||
|
||||
If no `.shellcheckrc` is found in any of the parent directories, ShellCheck
|
||||
will look in `~/.shellcheckrc` followed by the XDG config directory
|
||||
@@ -301,7 +302,7 @@ invocation.
|
||||
|
||||
# RETURN VALUES
|
||||
|
||||
ShellCheck uses the follow exit codes:
|
||||
ShellCheck uses the following exit codes:
|
||||
|
||||
+ 0: All files successfully scanned with no issues.
|
||||
+ 1: All files successfully scanned with some issues.
|
||||
|
@@ -491,6 +491,12 @@ ioInterface options files = do
|
||||
first <- a arg
|
||||
if not first then return False else b arg
|
||||
|
||||
findM p = foldr go (pure Nothing)
|
||||
where
|
||||
go x acc = do
|
||||
b <- p x
|
||||
if b then pure (Just x) else acc
|
||||
|
||||
findSourceFile inputs sourcePathFlag currentScript sourcePathAnnotation original =
|
||||
if isAbsolute original
|
||||
then
|
||||
@@ -500,11 +506,11 @@ ioInterface options files = do
|
||||
find original original
|
||||
where
|
||||
find filename deflt = do
|
||||
sources <- filterM ((allowable inputs) `andM` doesFileExist) $
|
||||
(adjustPath filename):(map (</> filename) $ map adjustPath $ sourcePathFlag ++ sourcePathAnnotation)
|
||||
sources <- findM ((allowable inputs) `andM` doesFileExist) $
|
||||
(adjustPath filename):(map ((</> filename) . adjustPath) $ sourcePathFlag ++ sourcePathAnnotation)
|
||||
case sources of
|
||||
[] -> return deflt
|
||||
(first:_) -> return first
|
||||
Nothing -> return deflt
|
||||
Just first -> return first
|
||||
scriptdir = dropFileName currentScript
|
||||
adjustPath str =
|
||||
case (splitDirectories str) of
|
||||
|
@@ -31,6 +31,8 @@ apps:
|
||||
shellcheck:
|
||||
command: usr/bin/shellcheck
|
||||
plugs: [home, removable-media]
|
||||
environment:
|
||||
LANG: C.UTF-8
|
||||
|
||||
parts:
|
||||
shellcheck:
|
||||
|
@@ -17,7 +17,7 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric, DeriveAnyClass, DeriveTraversable, PatternSynonyms #-}
|
||||
module ShellCheck.AST where
|
||||
|
||||
import GHC.Generics (Generic)
|
||||
@@ -37,113 +37,115 @@ newtype FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
|
||||
data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
|
||||
|
||||
newtype Root = Root Token
|
||||
data Token =
|
||||
TA_Binary Id String Token Token
|
||||
| TA_Assignment Id String Token Token
|
||||
| TA_Variable Id String [Token]
|
||||
| TA_Expansion Id [Token]
|
||||
| TA_Sequence Id [Token]
|
||||
| TA_Trinary Id Token Token Token
|
||||
| TA_Unary Id String Token
|
||||
| TC_And Id ConditionType String Token Token
|
||||
| TC_Binary Id ConditionType String Token Token
|
||||
| TC_Group Id ConditionType Token
|
||||
| TC_Nullary Id ConditionType Token
|
||||
| TC_Or Id ConditionType String Token Token
|
||||
| TC_Unary Id ConditionType String Token
|
||||
| TC_Empty Id ConditionType
|
||||
| T_AND_IF Id
|
||||
| T_AndIf Id Token Token
|
||||
| T_Arithmetic Id Token
|
||||
| T_Array Id [Token]
|
||||
| T_IndexedElement Id [Token] Token
|
||||
data Token = OuterToken Id (InnerToken Token) deriving (Show)
|
||||
|
||||
data InnerToken t =
|
||||
Inner_TA_Binary String t t
|
||||
| Inner_TA_Assignment String t t
|
||||
| Inner_TA_Variable String [t]
|
||||
| Inner_TA_Expansion [t]
|
||||
| Inner_TA_Sequence [t]
|
||||
| Inner_TA_Trinary t t t
|
||||
| Inner_TA_Unary String t
|
||||
| Inner_TC_And ConditionType String t t
|
||||
| Inner_TC_Binary ConditionType String t t
|
||||
| Inner_TC_Group ConditionType t
|
||||
| Inner_TC_Nullary ConditionType t
|
||||
| Inner_TC_Or ConditionType String t t
|
||||
| Inner_TC_Unary ConditionType String t
|
||||
| Inner_TC_Empty ConditionType
|
||||
| Inner_T_AND_IF
|
||||
| Inner_T_AndIf t t
|
||||
| Inner_T_Arithmetic t
|
||||
| Inner_T_Array [t]
|
||||
| Inner_T_IndexedElement [t] t
|
||||
-- Store the index as string, and parse as arithmetic or string later
|
||||
| T_UnparsedIndex Id SourcePos String
|
||||
| T_Assignment Id AssignmentMode String [Token] Token
|
||||
| T_Backgrounded Id Token
|
||||
| T_Backticked Id [Token]
|
||||
| T_Bang Id
|
||||
| T_Banged Id Token
|
||||
| T_BraceExpansion Id [Token]
|
||||
| T_BraceGroup Id [Token]
|
||||
| T_CLOBBER Id
|
||||
| T_Case Id
|
||||
| T_CaseExpression Id Token [(CaseType, [Token], [Token])]
|
||||
| T_Condition Id ConditionType Token
|
||||
| T_DGREAT Id
|
||||
| T_DLESS Id
|
||||
| T_DLESSDASH Id
|
||||
| T_DSEMI Id
|
||||
| T_Do Id
|
||||
| T_DollarArithmetic Id Token
|
||||
| T_DollarBraced Id Bool Token
|
||||
| T_DollarBracket Id Token
|
||||
| T_DollarDoubleQuoted Id [Token]
|
||||
| T_DollarExpansion Id [Token]
|
||||
| T_DollarSingleQuoted Id String
|
||||
| T_DollarBraceCommandExpansion Id [Token]
|
||||
| T_Done Id
|
||||
| T_DoubleQuoted Id [Token]
|
||||
| T_EOF Id
|
||||
| T_Elif Id
|
||||
| T_Else Id
|
||||
| T_Esac Id
|
||||
| T_Extglob Id String [Token]
|
||||
| T_FdRedirect Id String Token
|
||||
| T_Fi Id
|
||||
| T_For Id
|
||||
| T_ForArithmetic Id Token Token Token [Token]
|
||||
| T_ForIn Id String [Token] [Token]
|
||||
| T_Function Id FunctionKeyword FunctionParentheses String Token
|
||||
| T_GREATAND Id
|
||||
| T_Glob Id String
|
||||
| T_Greater Id
|
||||
| T_HereDoc Id Dashed Quoted String [Token]
|
||||
| T_HereString Id Token
|
||||
| T_If Id
|
||||
| T_IfExpression Id [([Token],[Token])] [Token]
|
||||
| T_In Id
|
||||
| T_IoFile Id Token Token
|
||||
| T_IoDuplicate Id Token String
|
||||
| T_LESSAND Id
|
||||
| T_LESSGREAT Id
|
||||
| T_Lbrace Id
|
||||
| T_Less Id
|
||||
| T_Literal Id String
|
||||
| T_Lparen Id
|
||||
| T_NEWLINE Id
|
||||
| T_NormalWord Id [Token]
|
||||
| T_OR_IF Id
|
||||
| T_OrIf Id Token Token
|
||||
| T_ParamSubSpecialChar Id String -- e.g. '%' in ${foo%bar} or '/' in ${foo/bar/baz}
|
||||
| T_Pipeline Id [Token] [Token] -- [Pipe separators] [Commands]
|
||||
| T_ProcSub Id String [Token]
|
||||
| T_Rbrace Id
|
||||
| T_Redirecting Id [Token] Token
|
||||
| T_Rparen Id
|
||||
| T_Script Id Token [Token] -- Shebang T_Literal, followed by script.
|
||||
| T_Select Id
|
||||
| T_SelectIn Id String [Token] [Token]
|
||||
| T_Semi Id
|
||||
| T_SimpleCommand Id [Token] [Token]
|
||||
| T_SingleQuoted Id String
|
||||
| T_Subshell Id [Token]
|
||||
| T_Then Id
|
||||
| T_Until Id
|
||||
| T_UntilExpression Id [Token] [Token]
|
||||
| T_While Id
|
||||
| T_WhileExpression Id [Token] [Token]
|
||||
| T_Annotation Id [Annotation] Token
|
||||
| T_Pipe Id String
|
||||
| T_CoProc Id (Maybe String) Token
|
||||
| T_CoProcBody Id Token
|
||||
| T_Include Id Token
|
||||
| T_SourceCommand Id Token Token
|
||||
| T_BatsTest Id Token Token
|
||||
deriving (Show)
|
||||
| Inner_T_UnparsedIndex SourcePos String
|
||||
| Inner_T_Assignment AssignmentMode String [t] t
|
||||
| Inner_T_Backgrounded t
|
||||
| Inner_T_Backticked [t]
|
||||
| Inner_T_Bang
|
||||
| Inner_T_Banged t
|
||||
| Inner_T_BraceExpansion [t]
|
||||
| Inner_T_BraceGroup [t]
|
||||
| Inner_T_CLOBBER
|
||||
| Inner_T_Case
|
||||
| Inner_T_CaseExpression t [(CaseType, [t], [t])]
|
||||
| Inner_T_Condition ConditionType t
|
||||
| Inner_T_DGREAT
|
||||
| Inner_T_DLESS
|
||||
| Inner_T_DLESSDASH
|
||||
| Inner_T_DSEMI
|
||||
| Inner_T_Do
|
||||
| Inner_T_DollarArithmetic t
|
||||
| Inner_T_DollarBraced Bool t
|
||||
| Inner_T_DollarBracket t
|
||||
| Inner_T_DollarDoubleQuoted [t]
|
||||
| Inner_T_DollarExpansion [t]
|
||||
| Inner_T_DollarSingleQuoted String
|
||||
| Inner_T_DollarBraceCommandExpansion [t]
|
||||
| Inner_T_Done
|
||||
| Inner_T_DoubleQuoted [t]
|
||||
| Inner_T_EOF
|
||||
| Inner_T_Elif
|
||||
| Inner_T_Else
|
||||
| Inner_T_Esac
|
||||
| Inner_T_Extglob String [t]
|
||||
| Inner_T_FdRedirect String t
|
||||
| Inner_T_Fi
|
||||
| Inner_T_For
|
||||
| Inner_T_ForArithmetic t t t [t]
|
||||
| Inner_T_ForIn String [t] [t]
|
||||
| Inner_T_Function FunctionKeyword FunctionParentheses String t
|
||||
| Inner_T_GREATAND
|
||||
| Inner_T_Glob String
|
||||
| Inner_T_Greater
|
||||
| Inner_T_HereDoc Dashed Quoted String [t]
|
||||
| Inner_T_HereString t
|
||||
| Inner_T_If
|
||||
| Inner_T_IfExpression [([t],[t])] [t]
|
||||
| Inner_T_In
|
||||
| Inner_T_IoFile t t
|
||||
| Inner_T_IoDuplicate t String
|
||||
| Inner_T_LESSAND
|
||||
| Inner_T_LESSGREAT
|
||||
| Inner_T_Lbrace
|
||||
| Inner_T_Less
|
||||
| Inner_T_Literal String
|
||||
| Inner_T_Lparen
|
||||
| Inner_T_NEWLINE
|
||||
| Inner_T_NormalWord [t]
|
||||
| Inner_T_OR_IF
|
||||
| Inner_T_OrIf t t
|
||||
| Inner_T_ParamSubSpecialChar String -- e.g. '%' in ${foo%bar} or '/' in ${foo/bar/baz}
|
||||
| Inner_T_Pipeline [t] [t] -- [Pipe separators] [Commands]
|
||||
| Inner_T_ProcSub String [t]
|
||||
| Inner_T_Rbrace
|
||||
| Inner_T_Redirecting [t] t
|
||||
| Inner_T_Rparen
|
||||
| Inner_T_Script t [t] -- Shebang T_Literal, followed by script.
|
||||
| Inner_T_Select
|
||||
| Inner_T_SelectIn String [t] [t]
|
||||
| Inner_T_Semi
|
||||
| Inner_T_SimpleCommand [t] [t]
|
||||
| Inner_T_SingleQuoted String
|
||||
| Inner_T_Subshell [t]
|
||||
| Inner_T_Then
|
||||
| Inner_T_Until
|
||||
| Inner_T_UntilExpression [t] [t]
|
||||
| Inner_T_While
|
||||
| Inner_T_WhileExpression [t] [t]
|
||||
| Inner_T_Annotation [Annotation] t
|
||||
| Inner_T_Pipe String
|
||||
| Inner_T_CoProc (Maybe String) t
|
||||
| Inner_T_CoProcBody t
|
||||
| Inner_T_Include t
|
||||
| Inner_T_SourceCommand t t
|
||||
| Inner_T_BatsTest t t
|
||||
deriving (Show, Eq, Functor, Foldable, Traversable)
|
||||
|
||||
data Annotation =
|
||||
DisableComment Integer
|
||||
DisableComment Integer Integer -- [from, to)
|
||||
| EnableComment String
|
||||
| SourceOverride String
|
||||
| ShellOverride String
|
||||
@@ -151,240 +153,125 @@ data Annotation =
|
||||
deriving (Show, Eq)
|
||||
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
|
||||
|
||||
-- This is an abomination.
|
||||
tokenEquals :: Token -> Token -> Bool
|
||||
tokenEquals a b = kludge a == kludge b
|
||||
where kludge s = Re.subRegex (Re.mkRegex "\\(Id [0-9]+\\)") (show s) "(Id 0)"
|
||||
pattern T_AND_IF id = OuterToken id Inner_T_AND_IF
|
||||
pattern T_Bang id = OuterToken id Inner_T_Bang
|
||||
pattern T_Case id = OuterToken id Inner_T_Case
|
||||
pattern TC_Empty id typ = OuterToken id (Inner_TC_Empty typ)
|
||||
pattern T_CLOBBER id = OuterToken id Inner_T_CLOBBER
|
||||
pattern T_DGREAT id = OuterToken id Inner_T_DGREAT
|
||||
pattern T_DLESS id = OuterToken id Inner_T_DLESS
|
||||
pattern T_DLESSDASH id = OuterToken id Inner_T_DLESSDASH
|
||||
pattern T_Do id = OuterToken id Inner_T_Do
|
||||
pattern T_DollarSingleQuoted id str = OuterToken id (Inner_T_DollarSingleQuoted str)
|
||||
pattern T_Done id = OuterToken id Inner_T_Done
|
||||
pattern T_DSEMI id = OuterToken id Inner_T_DSEMI
|
||||
pattern T_Elif id = OuterToken id Inner_T_Elif
|
||||
pattern T_Else id = OuterToken id Inner_T_Else
|
||||
pattern T_EOF id = OuterToken id Inner_T_EOF
|
||||
pattern T_Esac id = OuterToken id Inner_T_Esac
|
||||
pattern T_Fi id = OuterToken id Inner_T_Fi
|
||||
pattern T_For id = OuterToken id Inner_T_For
|
||||
pattern T_Glob id str = OuterToken id (Inner_T_Glob str)
|
||||
pattern T_GREATAND id = OuterToken id Inner_T_GREATAND
|
||||
pattern T_Greater id = OuterToken id Inner_T_Greater
|
||||
pattern T_If id = OuterToken id Inner_T_If
|
||||
pattern T_In id = OuterToken id Inner_T_In
|
||||
pattern T_Lbrace id = OuterToken id Inner_T_Lbrace
|
||||
pattern T_Less id = OuterToken id Inner_T_Less
|
||||
pattern T_LESSAND id = OuterToken id Inner_T_LESSAND
|
||||
pattern T_LESSGREAT id = OuterToken id Inner_T_LESSGREAT
|
||||
pattern T_Literal id str = OuterToken id (Inner_T_Literal str)
|
||||
pattern T_Lparen id = OuterToken id Inner_T_Lparen
|
||||
pattern T_NEWLINE id = OuterToken id Inner_T_NEWLINE
|
||||
pattern T_OR_IF id = OuterToken id Inner_T_OR_IF
|
||||
pattern T_ParamSubSpecialChar id str = OuterToken id (Inner_T_ParamSubSpecialChar str)
|
||||
pattern T_Pipe id str = OuterToken id (Inner_T_Pipe str)
|
||||
pattern T_Rbrace id = OuterToken id Inner_T_Rbrace
|
||||
pattern T_Rparen id = OuterToken id Inner_T_Rparen
|
||||
pattern T_Select id = OuterToken id Inner_T_Select
|
||||
pattern T_Semi id = OuterToken id Inner_T_Semi
|
||||
pattern T_SingleQuoted id str = OuterToken id (Inner_T_SingleQuoted str)
|
||||
pattern T_Then id = OuterToken id Inner_T_Then
|
||||
pattern T_UnparsedIndex id pos str = OuterToken id (Inner_T_UnparsedIndex pos str)
|
||||
pattern T_Until id = OuterToken id Inner_T_Until
|
||||
pattern T_While id = OuterToken id Inner_T_While
|
||||
pattern TA_Assignment id op t1 t2 = OuterToken id (Inner_TA_Assignment op t1 t2)
|
||||
pattern TA_Binary id op t1 t2 = OuterToken id (Inner_TA_Binary op t1 t2)
|
||||
pattern TA_Expansion id t = OuterToken id (Inner_TA_Expansion t)
|
||||
pattern T_AndIf id t u = OuterToken id (Inner_T_AndIf t u)
|
||||
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_Array id t = OuterToken id (Inner_T_Array t)
|
||||
pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)
|
||||
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_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1)
|
||||
pattern TA_Variable id str t = OuterToken id (Inner_TA_Variable str t)
|
||||
pattern T_Backgrounded id l = OuterToken id (Inner_T_Backgrounded l)
|
||||
pattern T_Backticked id list = OuterToken id (Inner_T_Backticked list)
|
||||
pattern T_Banged id l = OuterToken id (Inner_T_Banged l)
|
||||
pattern T_BatsTest id name t = OuterToken id (Inner_T_BatsTest name t)
|
||||
pattern T_BraceExpansion id list = OuterToken id (Inner_T_BraceExpansion list)
|
||||
pattern T_BraceGroup id l = OuterToken id (Inner_T_BraceGroup l)
|
||||
pattern TC_And id typ str t1 t2 = OuterToken id (Inner_TC_And typ str t1 t2)
|
||||
pattern T_CaseExpression id word cases = OuterToken id (Inner_T_CaseExpression word cases)
|
||||
pattern TC_Binary id typ op lhs rhs = OuterToken id (Inner_TC_Binary typ op lhs rhs)
|
||||
pattern TC_Group id typ token = OuterToken id (Inner_TC_Group typ token)
|
||||
pattern TC_Nullary id typ token = OuterToken id (Inner_TC_Nullary typ token)
|
||||
pattern T_Condition id typ token = OuterToken id (Inner_T_Condition typ token)
|
||||
pattern T_CoProcBody id t = OuterToken id (Inner_T_CoProcBody t)
|
||||
pattern T_CoProc id var body = OuterToken id (Inner_T_CoProc var body)
|
||||
pattern TC_Or id typ str t1 t2 = OuterToken id (Inner_TC_Or typ str t1 t2)
|
||||
pattern TC_Unary id typ op token = OuterToken id (Inner_TC_Unary typ op token)
|
||||
pattern T_DollarArithmetic id c = OuterToken id (Inner_T_DollarArithmetic c)
|
||||
pattern T_DollarBraceCommandExpansion id list = OuterToken id (Inner_T_DollarBraceCommandExpansion list)
|
||||
pattern T_DollarBraced id braced op = OuterToken id (Inner_T_DollarBraced braced op)
|
||||
pattern T_DollarBracket id c = OuterToken id (Inner_T_DollarBracket c)
|
||||
pattern T_DollarDoubleQuoted id list = OuterToken id (Inner_T_DollarDoubleQuoted list)
|
||||
pattern T_DollarExpansion id list = OuterToken id (Inner_T_DollarExpansion list)
|
||||
pattern T_DoubleQuoted id list = OuterToken id (Inner_T_DoubleQuoted list)
|
||||
pattern T_Extglob id str l = OuterToken id (Inner_T_Extglob str l)
|
||||
pattern T_FdRedirect id v t = OuterToken id (Inner_T_FdRedirect v t)
|
||||
pattern T_ForArithmetic id a b c group = OuterToken id (Inner_T_ForArithmetic a b c group)
|
||||
pattern T_ForIn id v w l = OuterToken id (Inner_T_ForIn v w l)
|
||||
pattern T_Function id a b name body = OuterToken id (Inner_T_Function a b name body)
|
||||
pattern T_HereDoc id d q str l = OuterToken id (Inner_T_HereDoc d q str l)
|
||||
pattern T_HereString id word = OuterToken id (Inner_T_HereString word)
|
||||
pattern T_IfExpression id conditions elses = OuterToken id (Inner_T_IfExpression conditions elses)
|
||||
pattern T_Include id script = OuterToken id (Inner_T_Include script)
|
||||
pattern T_IndexedElement id indices t = OuterToken id (Inner_T_IndexedElement indices t)
|
||||
pattern T_IoDuplicate id op num = OuterToken id (Inner_T_IoDuplicate op num)
|
||||
pattern T_IoFile id op file = OuterToken id (Inner_T_IoFile op file)
|
||||
pattern T_NormalWord id list = OuterToken id (Inner_T_NormalWord list)
|
||||
pattern T_OrIf id t u = OuterToken id (Inner_T_OrIf t u)
|
||||
pattern T_Pipeline id l1 l2 = OuterToken id (Inner_T_Pipeline l1 l2)
|
||||
pattern T_ProcSub id typ l = OuterToken id (Inner_T_ProcSub typ l)
|
||||
pattern T_Redirecting id redirs cmd = OuterToken id (Inner_T_Redirecting redirs cmd)
|
||||
pattern T_Script id shebang list = OuterToken id (Inner_T_Script shebang list)
|
||||
pattern T_SelectIn id v w l = OuterToken id (Inner_T_SelectIn v w l)
|
||||
pattern T_SimpleCommand id vars cmds = OuterToken id (Inner_T_SimpleCommand vars cmds)
|
||||
pattern T_SourceCommand id includer t_include = OuterToken id (Inner_T_SourceCommand includer t_include)
|
||||
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_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, 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
|
||||
(==) = tokenEquals
|
||||
OuterToken _ a == OuterToken _ b = a == b
|
||||
|
||||
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> m Token) -> Token -> m Token
|
||||
analyze f g i =
|
||||
round
|
||||
where
|
||||
round t = do
|
||||
round t@(OuterToken id it) = do
|
||||
f t
|
||||
newT <- delve t
|
||||
newIt <- traverse round it
|
||||
g t
|
||||
i newT
|
||||
roundAll = mapM round
|
||||
|
||||
dl l v = do
|
||||
x <- roundAll l
|
||||
return $ v x
|
||||
dll l m v = do
|
||||
x <- roundAll l
|
||||
y <- roundAll m
|
||||
return $ v x y
|
||||
d1 t v = do
|
||||
x <- round t
|
||||
return $ v x
|
||||
d2 t1 t2 v = do
|
||||
x <- round t1
|
||||
y <- round t2
|
||||
return $ v x y
|
||||
|
||||
delve (T_NormalWord id list) = dl list $ T_NormalWord id
|
||||
delve (T_DoubleQuoted id list) = dl list $ T_DoubleQuoted id
|
||||
delve (T_DollarDoubleQuoted id list) = dl list $ T_DollarDoubleQuoted id
|
||||
delve (T_DollarExpansion id list) = dl list $ T_DollarExpansion id
|
||||
delve (T_DollarBraceCommandExpansion id list) = dl list $ T_DollarBraceCommandExpansion id
|
||||
delve (T_BraceExpansion id list) = dl list $ T_BraceExpansion id
|
||||
delve (T_Backticked id list) = dl list $ T_Backticked id
|
||||
delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id
|
||||
delve (T_DollarBracket id c) = d1 c $ T_DollarBracket id
|
||||
delve (T_IoFile id op file) = d2 op file $ T_IoFile id
|
||||
delve (T_IoDuplicate id op num) = d1 op $ \x -> T_IoDuplicate id x num
|
||||
delve (T_HereString id word) = d1 word $ T_HereString id
|
||||
delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v
|
||||
delve (T_Assignment id mode var indices value) = do
|
||||
a <- roundAll indices
|
||||
b <- round value
|
||||
return $ T_Assignment id mode var a b
|
||||
delve (T_Array id t) = dl t $ T_Array id
|
||||
delve (T_IndexedElement id indices t) = do
|
||||
a <- roundAll indices
|
||||
b <- round t
|
||||
return $ T_IndexedElement id a b
|
||||
delve (T_Redirecting id redirs cmd) = do
|
||||
a <- roundAll redirs
|
||||
b <- round cmd
|
||||
return $ T_Redirecting id a b
|
||||
delve (T_SimpleCommand id vars cmds) = dll vars cmds $ T_SimpleCommand id
|
||||
delve (T_Pipeline id l1 l2) = dll l1 l2 $ T_Pipeline id
|
||||
delve (T_Banged id l) = d1 l $ T_Banged id
|
||||
delve (T_AndIf id t u) = d2 t u $ T_AndIf id
|
||||
delve (T_OrIf id t u) = d2 t u $ T_OrIf id
|
||||
delve (T_Backgrounded id l) = d1 l $ T_Backgrounded id
|
||||
delve (T_Subshell id l) = dl l $ T_Subshell id
|
||||
delve (T_ProcSub id typ l) = dl l $ T_ProcSub id typ
|
||||
delve (T_Arithmetic id c) = d1 c $ T_Arithmetic id
|
||||
delve (T_IfExpression id conditions elses) = do
|
||||
newConds <- mapM (\(c, t) -> do
|
||||
x <- mapM round c
|
||||
y <- mapM round t
|
||||
return (x,y)
|
||||
) conditions
|
||||
newElses <- roundAll elses
|
||||
return $ T_IfExpression id newConds newElses
|
||||
delve (T_BraceGroup id l) = dl l $ T_BraceGroup id
|
||||
delve (T_WhileExpression id c l) = dll c l $ T_WhileExpression id
|
||||
delve (T_UntilExpression id c l) = dll c l $ T_UntilExpression id
|
||||
delve (T_ForIn id v w l) = dll w l $ T_ForIn id v
|
||||
delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v
|
||||
delve (T_CaseExpression id word cases) = do
|
||||
newWord <- round word
|
||||
newCases <- mapM (\(o, c, t) -> do
|
||||
x <- mapM round c
|
||||
y <- mapM round t
|
||||
return (o, x,y)
|
||||
) cases
|
||||
return $ T_CaseExpression id newWord newCases
|
||||
|
||||
delve (T_ForArithmetic id a b c group) = do
|
||||
x <- round a
|
||||
y <- round b
|
||||
z <- round c
|
||||
list <- mapM round group
|
||||
return $ T_ForArithmetic id x y z list
|
||||
|
||||
delve (T_Script id s l) = dl l $ T_Script id s
|
||||
delve (T_Function id a b name body) = d1 body $ T_Function id a b name
|
||||
delve (T_Condition id typ token) = d1 token $ T_Condition id typ
|
||||
delve (T_Extglob id str l) = dl l $ T_Extglob id str
|
||||
delve (T_DollarBraced id braced op) = d1 op $ T_DollarBraced id braced
|
||||
delve (T_HereDoc id d q str l) = dl l $ T_HereDoc id d q str
|
||||
|
||||
delve (TC_And id typ str t1 t2) = d2 t1 t2 $ TC_And id typ str
|
||||
delve (TC_Or id typ str t1 t2) = d2 t1 t2 $ TC_Or id typ str
|
||||
delve (TC_Group id typ token) = d1 token $ TC_Group id typ
|
||||
delve (TC_Binary id typ op lhs rhs) = d2 lhs rhs $ TC_Binary id typ op
|
||||
delve (TC_Unary id typ op token) = d1 token $ TC_Unary id typ op
|
||||
delve (TC_Nullary id typ token) = d1 token $ TC_Nullary id typ
|
||||
|
||||
delve (TA_Binary id op t1 t2) = d2 t1 t2 $ TA_Binary id op
|
||||
delve (TA_Assignment id op t1 t2) = d2 t1 t2 $ TA_Assignment id op
|
||||
delve (TA_Unary id op t1) = d1 t1 $ TA_Unary id op
|
||||
delve (TA_Sequence id l) = dl l $ TA_Sequence id
|
||||
delve (TA_Trinary id t1 t2 t3) = do
|
||||
a <- round t1
|
||||
b <- round t2
|
||||
c <- round t3
|
||||
return $ TA_Trinary id a b c
|
||||
delve (TA_Expansion id t) = dl t $ TA_Expansion id
|
||||
delve (TA_Variable id str t) = dl t $ TA_Variable id str
|
||||
delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns
|
||||
delve (T_CoProc id var body) = d1 body $ T_CoProc id var
|
||||
delve (T_CoProcBody id t) = d1 t $ T_CoProcBody id
|
||||
delve (T_Include id script) = d1 script $ T_Include id
|
||||
delve (T_SourceCommand id includer t_include) = d2 includer t_include $ T_SourceCommand id
|
||||
delve (T_BatsTest id name t) = d2 name t $ T_BatsTest id
|
||||
delve t = return t
|
||||
i (OuterToken id newIt)
|
||||
|
||||
getId :: Token -> Id
|
||||
getId t = case t of
|
||||
T_AND_IF id -> id
|
||||
T_OR_IF id -> id
|
||||
T_DSEMI id -> id
|
||||
T_Semi id -> id
|
||||
T_DLESS id -> id
|
||||
T_DGREAT id -> id
|
||||
T_LESSAND id -> id
|
||||
T_GREATAND id -> id
|
||||
T_LESSGREAT id -> id
|
||||
T_DLESSDASH id -> id
|
||||
T_CLOBBER id -> id
|
||||
T_If id -> id
|
||||
T_Then id -> id
|
||||
T_Else id -> id
|
||||
T_Elif id -> id
|
||||
T_Fi id -> id
|
||||
T_Do id -> id
|
||||
T_Done id -> id
|
||||
T_Case id -> id
|
||||
T_Esac id -> id
|
||||
T_While id -> id
|
||||
T_Until id -> id
|
||||
T_For id -> id
|
||||
T_Select id -> id
|
||||
T_Lbrace id -> id
|
||||
T_Rbrace id -> id
|
||||
T_Lparen id -> id
|
||||
T_Rparen id -> id
|
||||
T_Bang id -> id
|
||||
T_In id -> id
|
||||
T_NEWLINE id -> id
|
||||
T_EOF id -> id
|
||||
T_Less id -> id
|
||||
T_Greater id -> id
|
||||
T_SingleQuoted id _ -> id
|
||||
T_Literal id _ -> id
|
||||
T_NormalWord id _ -> id
|
||||
T_DoubleQuoted id _ -> id
|
||||
T_DollarExpansion id _ -> id
|
||||
T_DollarBraced id _ _ -> id
|
||||
T_DollarArithmetic id _ -> id
|
||||
T_BraceExpansion id _ -> id
|
||||
T_ParamSubSpecialChar id _ -> id
|
||||
T_DollarBraceCommandExpansion id _ -> id
|
||||
T_IoFile id _ _ -> id
|
||||
T_IoDuplicate id _ _ -> id
|
||||
T_HereDoc id _ _ _ _ -> id
|
||||
T_HereString id _ -> id
|
||||
T_FdRedirect id _ _ -> id
|
||||
T_Assignment id _ _ _ _ -> id
|
||||
T_Array id _ -> id
|
||||
T_IndexedElement id _ _ -> id
|
||||
T_Redirecting id _ _ -> id
|
||||
T_SimpleCommand id _ _ -> id
|
||||
T_Pipeline id _ _ -> id
|
||||
T_Banged id _ -> id
|
||||
T_AndIf id _ _ -> id
|
||||
T_OrIf id _ _ -> id
|
||||
T_Backgrounded id _ -> id
|
||||
T_IfExpression id _ _ -> id
|
||||
T_Subshell id _ -> id
|
||||
T_BraceGroup id _ -> id
|
||||
T_WhileExpression id _ _ -> id
|
||||
T_UntilExpression id _ _ -> id
|
||||
T_ForIn id _ _ _ -> id
|
||||
T_SelectIn id _ _ _ -> id
|
||||
T_CaseExpression id _ _ -> id
|
||||
T_Function id _ _ _ _ -> id
|
||||
T_Arithmetic id _ -> id
|
||||
T_Script id _ _ -> id
|
||||
T_Condition id _ _ -> id
|
||||
T_Extglob id _ _ -> id
|
||||
T_Backticked id _ -> id
|
||||
TC_And id _ _ _ _ -> id
|
||||
TC_Or id _ _ _ _ -> id
|
||||
TC_Group id _ _ -> id
|
||||
TC_Binary id _ _ _ _ -> id
|
||||
TC_Unary id _ _ _ -> id
|
||||
TC_Nullary id _ _ -> id
|
||||
TA_Binary id _ _ _ -> id
|
||||
TA_Assignment id _ _ _ -> id
|
||||
TA_Unary id _ _ -> id
|
||||
TA_Sequence id _ -> id
|
||||
TA_Trinary id _ _ _ -> id
|
||||
TA_Expansion id _ -> id
|
||||
T_ProcSub id _ _ -> id
|
||||
T_Glob id _ -> id
|
||||
T_ForArithmetic id _ _ _ _ -> id
|
||||
T_DollarSingleQuoted id _ -> id
|
||||
T_DollarDoubleQuoted id _ -> id
|
||||
T_DollarBracket id _ -> id
|
||||
T_Annotation id _ _ -> id
|
||||
T_Pipe id _ -> id
|
||||
T_CoProc id _ _ -> id
|
||||
T_CoProcBody id _ -> id
|
||||
T_Include id _ -> id
|
||||
T_SourceCommand id _ _ -> id
|
||||
T_UnparsedIndex id _ _ -> id
|
||||
TC_Empty id _ -> id
|
||||
TA_Variable id _ _ -> id
|
||||
T_BatsTest id _ _ -> id
|
||||
getId (OuterToken id _) = id
|
||||
|
||||
blank :: Monad m => Token -> m ()
|
||||
blank = const $ return ()
|
||||
|
@@ -17,16 +17,25 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
module ShellCheck.ASTLib where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Monad.Writer
|
||||
import Control.Monad
|
||||
import Data.Char
|
||||
import Data.Functor
|
||||
import Data.Functor.Identity
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import qualified Data.Map as Map
|
||||
import Numeric (showHex)
|
||||
|
||||
import Test.QuickCheck
|
||||
|
||||
arguments (T_SimpleCommand _ _ (cmd:args)) = args
|
||||
|
||||
-- Is this a type of loop?
|
||||
isLoop t = case t of
|
||||
@@ -46,6 +55,7 @@ willSplit x =
|
||||
T_BraceExpansion {} -> True
|
||||
T_Glob {} -> True
|
||||
T_Extglob {} -> True
|
||||
T_DoubleQuoted _ l -> any willBecomeMultipleArgs l
|
||||
T_NormalWord _ l -> any willSplit l
|
||||
_ -> False
|
||||
|
||||
@@ -132,13 +142,95 @@ isUnquotedFlag token = fromMaybe False $ do
|
||||
str <- getLeadingUnquotedString token
|
||||
return $ "-" `isPrefixOf` str
|
||||
|
||||
-- Given a T_DollarBraced, return a simplified version of the string contents.
|
||||
bracedString (T_DollarBraced _ _ l) = concat $ oversimplify l
|
||||
bracedString _ = error "Internal shellcheck error, please report! (bracedString on non-variable)"
|
||||
-- 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)))
|
||||
|
||||
-- Is this an expansion of multiple items of an array?
|
||||
isArrayExpansion t@(T_DollarBraced _ _ _) =
|
||||
let string = bracedString t in
|
||||
isArrayExpansion (T_DollarBraced _ _ l) =
|
||||
let string = concat $ oversimplify l in
|
||||
"@" `isPrefixOf` string ||
|
||||
not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string
|
||||
isArrayExpansion _ = False
|
||||
@@ -146,8 +238,8 @@ isArrayExpansion _ = False
|
||||
-- Is it possible that this arg becomes multiple args?
|
||||
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
|
||||
where
|
||||
f t@(T_DollarBraced _ _ _) =
|
||||
let string = bracedString t in
|
||||
f (T_DollarBraced _ _ l) =
|
||||
let string = concat $ oversimplify l in
|
||||
"!" `isPrefixOf` string
|
||||
f (T_DoubleQuoted _ parts) = any f parts
|
||||
f (T_NormalWord _ parts) = any f parts
|
||||
@@ -175,9 +267,13 @@ willConcatInAssignment token =
|
||||
getLiteralString :: Token -> Maybe String
|
||||
getLiteralString = getLiteralStringExt (const Nothing)
|
||||
|
||||
-- Definitely get a literal string, with a given default for all non-literals
|
||||
getLiteralStringDef :: String -> Token -> String
|
||||
getLiteralStringDef x = runIdentity . getLiteralStringExt (const $ return x)
|
||||
|
||||
-- Definitely get a literal string, skipping over all non-literals
|
||||
onlyLiteralString :: Token -> String
|
||||
onlyLiteralString = fromJust . getLiteralStringExt (const $ return "")
|
||||
onlyLiteralString = getLiteralStringDef ""
|
||||
|
||||
-- Maybe get a literal string, but only if it's an unquoted argument.
|
||||
getUnquotedLiteral (T_NormalWord _ list) =
|
||||
@@ -187,6 +283,12 @@ getUnquotedLiteral (T_NormalWord _ list) =
|
||||
str _ = 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
|
||||
-- or nothing if the word does not end in an unquoted literal.
|
||||
getTrailingUnquotedLiteral :: Token -> Maybe Token
|
||||
@@ -205,8 +307,11 @@ getTrailingUnquotedLiteral t =
|
||||
getLeadingUnquotedString :: Token -> Maybe String
|
||||
getLeadingUnquotedString t =
|
||||
case t of
|
||||
T_NormalWord _ ((T_Literal _ s) : _) -> return s
|
||||
T_NormalWord _ ((T_Literal _ s) : rest) -> return $ s ++ from rest
|
||||
_ -> Nothing
|
||||
where
|
||||
from ((T_Literal _ s):rest) = s ++ from rest
|
||||
from _ = ""
|
||||
|
||||
-- Maybe get the literal string of this token and any globs in it.
|
||||
getGlobOrLiteralString = getLiteralStringExt f
|
||||
@@ -216,7 +321,7 @@ getGlobOrLiteralString = getLiteralStringExt f
|
||||
|
||||
-- Maybe get the literal value of a token, using a custom function
|
||||
-- to map unrecognized Tokens into strings.
|
||||
getLiteralStringExt :: (Token -> Maybe String) -> Token -> Maybe String
|
||||
getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String
|
||||
getLiteralStringExt more = g
|
||||
where
|
||||
allInList = fmap concat . mapM g
|
||||
@@ -267,6 +372,37 @@ getLiteralStringExt more = g
|
||||
-- Is this token a string literal?
|
||||
isLiteral t = isJust $ getLiteralString t
|
||||
|
||||
-- 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]
|
||||
getWordParts (T_NormalWord _ l) = concatMap getWordParts l
|
||||
@@ -295,25 +431,48 @@ getCommand t =
|
||||
|
||||
-- Maybe get the command name string of a token representing a command
|
||||
getCommandName :: Token -> Maybe String
|
||||
getCommandName = fst . getCommandNameAndToken
|
||||
getCommandName = fst . getCommandNameAndToken False
|
||||
|
||||
-- Maybe get the name+arguments of a command.
|
||||
getCommandArgv t = do
|
||||
(T_SimpleCommand _ _ args@(_:_)) <- getCommand t
|
||||
return args
|
||||
|
||||
-- Get the command name token from a command, i.e.
|
||||
-- the token representing 'ls' in 'ls -la 2> foo'.
|
||||
-- If it can't be determined, return the original token.
|
||||
getCommandTokenOrThis = snd . getCommandNameAndToken
|
||||
getCommandTokenOrThis = snd . getCommandNameAndToken False
|
||||
|
||||
getCommandNameAndToken :: Token -> (Maybe String, Token)
|
||||
getCommandNameAndToken t = fromMaybe (Nothing, t) $ do
|
||||
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
|
||||
-- Given a command, get the string and token that represents the command name.
|
||||
-- If direct, return the actual command (e.g. exec in 'exec ls')
|
||||
-- If not, return the logical command (e.g. 'ls' in 'exec ls')
|
||||
|
||||
getCommandNameAndToken :: Bool -> Token -> (Maybe String, Token)
|
||||
getCommandNameAndToken direct t = fromMaybe (Nothing, t) $ do
|
||||
cmd@(T_SimpleCommand _ _ (w:rest)) <- getCommand t
|
||||
s <- getLiteralString w
|
||||
if "busybox" `isSuffixOf` s || "builtin" == s
|
||||
then
|
||||
case rest of
|
||||
(applet:_) -> return (getLiteralString applet, applet)
|
||||
_ -> return (Just s, w)
|
||||
else
|
||||
return (Just s, w)
|
||||
|
||||
return $ fromMaybe (Just s, w) $ do
|
||||
guard $ not direct
|
||||
actual <- getEffectiveCommandToken s cmd rest
|
||||
return (getLiteralString actual, actual)
|
||||
where
|
||||
getEffectiveCommandToken str cmd args =
|
||||
let
|
||||
firstArg = do
|
||||
arg <- listToMaybe args
|
||||
guard . not $ isFlag arg
|
||||
return arg
|
||||
in
|
||||
case str of
|
||||
"busybox" -> firstArg
|
||||
"builtin" -> firstArg
|
||||
"command" -> firstArg
|
||||
"run" -> firstArg -- Used by bats
|
||||
"exec" -> do
|
||||
opts <- getBsdOpts "cla:" args
|
||||
(_, (t, _)) <- find (null . fst) opts
|
||||
return t
|
||||
_ -> fail ""
|
||||
|
||||
-- If a command substitution is a single command, get its name.
|
||||
-- $(date +%s) = Just "date"
|
||||
@@ -330,8 +489,8 @@ getCommandNameFromExpansion t =
|
||||
|
||||
-- Get the basename of a token representing a command
|
||||
getCommandBasename = fmap basename . getCommandName
|
||||
where
|
||||
basename = reverse . takeWhile (/= '/') . reverse
|
||||
|
||||
basename = reverse . takeWhile (/= '/') . reverse
|
||||
|
||||
isAssignment t =
|
||||
case t of
|
||||
@@ -362,19 +521,23 @@ isFunctionLike t =
|
||||
isBraceExpansion t = case t of T_BraceExpansion {} -> True; _ -> False
|
||||
|
||||
-- Get the lists of commands from tokens that contain them, such as
|
||||
-- the body of while loops or branches of if statements.
|
||||
-- the conditions and bodies of while loops or branches of if statements.
|
||||
getCommandSequences :: Token -> [[Token]]
|
||||
getCommandSequences t =
|
||||
case t of
|
||||
T_Script _ _ cmds -> [cmds]
|
||||
T_BraceGroup _ cmds -> [cmds]
|
||||
T_Subshell _ cmds -> [cmds]
|
||||
T_WhileExpression _ _ cmds -> [cmds]
|
||||
T_UntilExpression _ _ cmds -> [cmds]
|
||||
T_WhileExpression _ cond cmds -> [cond, cmds]
|
||||
T_UntilExpression _ cond cmds -> [cond, cmds]
|
||||
T_ForIn _ _ _ cmds -> [cmds]
|
||||
T_ForArithmetic _ _ _ _ cmds -> [cmds]
|
||||
T_IfExpression _ thens elses -> map snd thens ++ [elses]
|
||||
T_IfExpression _ thens elses -> (concatMap (\(a,b) -> [a,b]) thens) ++ [elses]
|
||||
T_Annotation _ _ t -> getCommandSequences t
|
||||
|
||||
T_DollarExpansion _ cmds -> [cmds]
|
||||
T_DollarBraceCommandExpansion _ cmds -> [cmds]
|
||||
T_Backticked _ cmds -> [cmds]
|
||||
_ -> []
|
||||
|
||||
-- Get a list of names of associative arrays
|
||||
@@ -382,13 +545,13 @@ getAssociativeArrays t =
|
||||
nub . execWriter $ doAnalysis f t
|
||||
where
|
||||
f :: Token -> Writer [String] ()
|
||||
f t@T_SimpleCommand {} = fromMaybe (return ()) $ do
|
||||
f t@T_SimpleCommand {} = sequence_ $ do
|
||||
name <- getCommandName t
|
||||
let assocNames = ["declare","local","typeset"]
|
||||
guard $ elem name assocNames
|
||||
guard $ name `elem` assocNames
|
||||
let flags = getAllFlags t
|
||||
guard $ elem "A" $ map snd flags
|
||||
let args = map fst . filter ((==) "" . snd) $ flags
|
||||
guard $ "A" `elem` map snd flags
|
||||
let args = [arg | (arg, "") <- flags]
|
||||
let names = mapMaybe (getLiteralStringExt nameAssignments) args
|
||||
return $ tell names
|
||||
f _ = return ()
|
||||
@@ -406,38 +569,36 @@ data PseudoGlob = PGAny | PGMany | PGChar Char
|
||||
|
||||
-- Turn a word into a PG pattern, replacing all unknown/runtime values with
|
||||
-- PGMany.
|
||||
wordToPseudoGlob :: Token -> Maybe [PseudoGlob]
|
||||
wordToPseudoGlob word =
|
||||
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
|
||||
where
|
||||
f x = case x of
|
||||
T_Literal _ s -> return $ map PGChar s
|
||||
T_SingleQuoted _ s -> return $ map PGChar s
|
||||
|
||||
T_DollarBraced {} -> return [PGMany]
|
||||
T_DollarExpansion {} -> return [PGMany]
|
||||
T_Backticked {} -> return [PGMany]
|
||||
|
||||
T_Glob _ "?" -> return [PGAny]
|
||||
T_Glob _ ('[':_) -> return [PGAny]
|
||||
T_Glob {} -> return [PGMany]
|
||||
|
||||
T_Extglob {} -> return [PGMany]
|
||||
|
||||
_ -> return [PGMany]
|
||||
wordToPseudoGlob :: Token -> [PseudoGlob]
|
||||
wordToPseudoGlob = fromMaybe [PGMany] . wordToPseudoGlob' False
|
||||
|
||||
-- Turn a word into a PG pattern, but only if we can preserve
|
||||
-- exact semantics.
|
||||
wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob]
|
||||
wordToExactPseudoGlob word =
|
||||
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
|
||||
wordToExactPseudoGlob = wordToPseudoGlob' True
|
||||
|
||||
wordToPseudoGlob' :: Bool -> Token -> Maybe [PseudoGlob]
|
||||
wordToPseudoGlob' exact word =
|
||||
simplifyPseudoGlob <$> toGlob word
|
||||
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
|
||||
T_Literal _ s -> return $ map PGChar s
|
||||
T_Literal _ s -> return $ map PGChar s
|
||||
T_SingleQuoted _ s -> return $ map PGChar s
|
||||
T_Glob _ "?" -> return [PGAny]
|
||||
T_Glob _ "*" -> return [PGMany]
|
||||
_ -> fail "Unknown token type"
|
||||
T_Glob _ "?" -> return [PGAny]
|
||||
T_Glob _ "*" -> return [PGMany]
|
||||
T_Glob _ ('[':_) | not exact -> return [PGAny]
|
||||
_ -> if exact then fail "" else return [PGMany]
|
||||
|
||||
|
||||
-- Reorder a PseudoGlob for more efficient matching, e.g.
|
||||
-- f?*?**g -> f??*g
|
||||
@@ -487,8 +648,7 @@ pseudoGlobIsSuperSetof = matchable
|
||||
matchable (PGMany : rest) [] = matchable rest []
|
||||
matchable _ _ = False
|
||||
|
||||
wordsCanBeEqual x y = fromMaybe True $
|
||||
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
|
||||
wordsCanBeEqual x y = pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
|
||||
|
||||
-- Is this an expansion that can be quoted,
|
||||
-- e.g. $(foo) `foo` $foo (but not {foo,})?
|
||||
@@ -502,6 +662,11 @@ isCommandSubstitution t = case t of
|
||||
T_Backticked {} -> True
|
||||
_ -> False
|
||||
|
||||
-- Is this an expansion that results in a simple string?
|
||||
isStringExpansion t = isCommandSubstitution t || case t of
|
||||
T_DollarArithmetic {} -> True
|
||||
T_DollarBraced {} -> not (isArrayExpansion t)
|
||||
_ -> False
|
||||
|
||||
-- Is this a T_Annotation that ignores a specific code?
|
||||
isAnnotationIgnoringCode code t =
|
||||
@@ -509,5 +674,45 @@ isAnnotationIgnoringCode code t =
|
||||
T_Annotation _ anns _ -> any hasNum anns
|
||||
_ -> False
|
||||
where
|
||||
hasNum (DisableComment ts) = code == ts
|
||||
hasNum (DisableComment from to) = code >= from && code < to
|
||||
hasNum _ = False
|
||||
|
||||
prop_executableFromShebang1 = executableFromShebang "/bin/sh" == "sh"
|
||||
prop_executableFromShebang2 = executableFromShebang "/bin/bash" == "bash"
|
||||
prop_executableFromShebang3 = executableFromShebang "/usr/bin/env ksh" == "ksh"
|
||||
prop_executableFromShebang4 = executableFromShebang "/usr/bin/env -S foo=bar bash -x" == "bash"
|
||||
prop_executableFromShebang5 = executableFromShebang "/usr/bin/env --split-string=bash -x" == "bash"
|
||||
prop_executableFromShebang6 = executableFromShebang "/usr/bin/env --split-string=foo=bar bash -x" == "bash"
|
||||
prop_executableFromShebang7 = executableFromShebang "/usr/bin/env --split-string bash -x" == "bash"
|
||||
prop_executableFromShebang8 = executableFromShebang "/usr/bin/env --split-string foo=bar bash -x" == "bash"
|
||||
prop_executableFromShebang9 = executableFromShebang "/usr/bin/env foo=bar dash" == "dash"
|
||||
prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "ash"
|
||||
prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "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" -> "ash" -- busybox sh is 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`)
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -35,17 +35,18 @@ analyzeScript spec = newAnalysisResult {
|
||||
arComments =
|
||||
filterByAnnotation spec params . nub $
|
||||
runAnalytics spec
|
||||
++ runChecker params (checkers params)
|
||||
++ runChecker params (checkers spec params)
|
||||
}
|
||||
where
|
||||
params = makeParameters spec
|
||||
|
||||
checkers params = mconcat $ map ($ params) [
|
||||
ShellCheck.Checks.Commands.checker,
|
||||
checkers spec params = mconcat $ map ($ params) [
|
||||
ShellCheck.Checks.Commands.checker spec,
|
||||
ShellCheck.Checks.Custom.checker,
|
||||
ShellCheck.Checks.ShellSupport.checker
|
||||
]
|
||||
|
||||
optionalChecks = mconcat $ [
|
||||
ShellCheck.Analytics.optionalChecks
|
||||
ShellCheck.Analytics.optionalChecks,
|
||||
ShellCheck.Checks.Commands.optionalChecks
|
||||
]
|
||||
|
@@ -163,6 +163,8 @@ err id code str = addComment $ makeComment ErrorC id code str
|
||||
info id code str = addComment $ makeComment InfoC id code str
|
||||
style id code str = addComment $ makeComment StyleC id code str
|
||||
|
||||
errWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
|
||||
errWithFix = addCommentWithFix ErrorC
|
||||
warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
|
||||
warnWithFix = addCommentWithFix WarningC
|
||||
styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
|
||||
@@ -178,7 +180,7 @@ makeCommentWithFix severity id code str fix =
|
||||
withFix = comment {
|
||||
tcFix = Just fix
|
||||
}
|
||||
in withFix `deepseq` withFix
|
||||
in force withFix
|
||||
|
||||
makeParameters spec =
|
||||
let params = Parameters {
|
||||
@@ -236,40 +238,28 @@ prop_determineShell5 = determineShellTest "#shellcheck shell=sh\nfoo" == Sh
|
||||
prop_determineShell6 = determineShellTest "#! /bin/sh" == Sh
|
||||
prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
|
||||
prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh
|
||||
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" == Dash -- busybox sh is a specific shell, not posix sh
|
||||
prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == Dash
|
||||
|
||||
determineShellTest = determineShellTest' Nothing
|
||||
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
|
||||
determineShell fallbackShell t = fromMaybe Bash $ do
|
||||
shellString <- foldl mplus Nothing $ getCandidates t
|
||||
determineShell fallbackShell t = fromMaybe Bash $
|
||||
shellForExecutable shellString `mplus` fallbackShell
|
||||
where
|
||||
forAnnotation t =
|
||||
case t of
|
||||
(ShellOverride s) -> return s
|
||||
_ -> fail ""
|
||||
getCandidates :: Token -> [Maybe String]
|
||||
getCandidates t@T_Script {} = [Just $ fromShebang t]
|
||||
getCandidates (T_Annotation _ annotations s) =
|
||||
map forAnnotation annotations ++
|
||||
[Just $ fromShebang s]
|
||||
shellString = getCandidate t
|
||||
getCandidate :: Token -> String
|
||||
getCandidate t@T_Script {} = fromShebang t
|
||||
getCandidate (T_Annotation _ annotations s) =
|
||||
headOrDefault (fromShebang s) [s | ShellOverride s <- annotations]
|
||||
fromShebang (T_Script _ (T_Literal _ s) _) = executableFromShebang s
|
||||
|
||||
-- Given a string like "/bin/bash" or "/usr/bin/env dash",
|
||||
-- return the shell basename like "bash" or "dash"
|
||||
executableFromShebang :: String -> String
|
||||
executableFromShebang = shellFor
|
||||
where
|
||||
shellFor s | "/env " `isInfixOf` s = head (drop 1 (words s)++[""])
|
||||
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
|
||||
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
|
||||
|
||||
|
||||
|
||||
-- Given a root node, make a map from Id to parent Token.
|
||||
-- This is used to populate parentMap in Parameters
|
||||
getParentTree :: Token -> Map.Map Id Token
|
||||
getParentTree t =
|
||||
snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
|
||||
snd $ execState (doStackAnalysis pre post t) ([], Map.empty)
|
||||
where
|
||||
pre t = modify (first ((:) t))
|
||||
post t = do
|
||||
@@ -298,15 +288,15 @@ isQuoteFree = isQuoteFreeNode False
|
||||
|
||||
|
||||
isQuoteFreeNode strict tree t =
|
||||
(isQuoteFreeElement t == Just True) ||
|
||||
head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False])
|
||||
isQuoteFreeElement t ||
|
||||
headOrDefault False (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t))
|
||||
where
|
||||
-- Is this node self-quoting in itself?
|
||||
isQuoteFreeElement t =
|
||||
case t of
|
||||
T_Assignment {} -> return True
|
||||
T_FdRedirect {} -> return True
|
||||
_ -> Nothing
|
||||
T_Assignment {} -> True
|
||||
T_FdRedirect {} -> True
|
||||
_ -> False
|
||||
|
||||
-- Are any subnodes inherently self-quoting?
|
||||
isQuoteFreeContext t =
|
||||
@@ -359,8 +349,8 @@ getClosestCommand tree t =
|
||||
|
||||
-- Like above, if koala_man knew Haskell when starting this project.
|
||||
getClosestCommandM t = do
|
||||
tree <- asks parentMap
|
||||
return $ getClosestCommand tree t
|
||||
params <- ask
|
||||
return $ getClosestCommand (parentMap params) t
|
||||
|
||||
-- Is the token used as a command name (the first word in a T_SimpleCommand)?
|
||||
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
||||
@@ -369,8 +359,8 @@ usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
||||
| currentId == getId word = go id rest
|
||||
go currentId (T_DoubleQuoted id [word]:rest)
|
||||
| currentId == getId word = go id rest
|
||||
go currentId (T_SimpleCommand _ _ (word:_):_)
|
||||
| currentId == getId word = True
|
||||
go currentId (t@(T_SimpleCommand _ _ (word:_)):_) =
|
||||
getId word == currentId || getId (getCommandTokenOrThis t) == currentId
|
||||
go _ _ = False
|
||||
|
||||
-- A list of the element and all its parents up to the root node.
|
||||
@@ -382,8 +372,8 @@ getPath tree t = t :
|
||||
-- Version of the above taking the map from the current context
|
||||
-- Todo: give this the name "getPath"
|
||||
getPathM t = do
|
||||
map <- asks parentMap
|
||||
return $ getPath map t
|
||||
params <- ask
|
||||
return $ getPath (parentMap params) t
|
||||
|
||||
isParentOf tree parent child =
|
||||
elem (getId parent) . map getId $ getPath tree child
|
||||
@@ -393,14 +383,13 @@ parents params = getPath (parentMap params)
|
||||
-- Find the first match in a list where the predicate is Just True.
|
||||
-- Stops if it's Just False and ignores Nothing.
|
||||
findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a
|
||||
findFirst p l =
|
||||
case l of
|
||||
[] -> Nothing
|
||||
(x:xs) ->
|
||||
case p x of
|
||||
Just True -> return x
|
||||
Just False -> Nothing
|
||||
Nothing -> findFirst p xs
|
||||
findFirst p = foldr go Nothing
|
||||
where
|
||||
go x acc =
|
||||
case p x of
|
||||
Just True -> return x
|
||||
Just False -> Nothing
|
||||
Nothing -> acc
|
||||
|
||||
-- Check whether a word is entirely output from a single command
|
||||
tokenIsJustCommandOutput t = case t of
|
||||
@@ -415,8 +404,7 @@ tokenIsJustCommandOutput t = case t of
|
||||
|
||||
-- TODO: Replace this with a proper Control Flow Graph
|
||||
getVariableFlow params t =
|
||||
let (_, stack) = runState (doStackAnalysis startScope endScope t) []
|
||||
in reverse stack
|
||||
reverse $ execState (doStackAnalysis startScope endScope t) []
|
||||
where
|
||||
startScope t =
|
||||
let scopeType = leadType params t
|
||||
@@ -454,7 +442,7 @@ leadType params t =
|
||||
T_BatsTest {} -> SubshellScope "@bats test"
|
||||
T_CoProcBody _ _ -> SubshellScope "coproc"
|
||||
T_Redirecting {} ->
|
||||
if fromMaybe False causesSubshell
|
||||
if causesSubshell == Just True
|
||||
then SubshellScope "pipeline"
|
||||
else NoneScope
|
||||
_ -> NoneScope
|
||||
@@ -467,28 +455,22 @@ leadType params t =
|
||||
|
||||
causesSubshell = do
|
||||
(T_Pipeline _ _ list) <- parentPipeline
|
||||
if length list <= 1
|
||||
then return False
|
||||
else if not $ hasLastpipe params
|
||||
then return True
|
||||
else return . not $ (getId . head $ reverse list) == getId t
|
||||
return $ case list of
|
||||
_:_:_ -> not (hasLastpipe params) || getId (last list) /= getId t
|
||||
_ -> False
|
||||
|
||||
getModifiedVariables t =
|
||||
case t of
|
||||
T_SimpleCommand _ vars [] ->
|
||||
concatMap (\x -> case x of
|
||||
T_Assignment id _ name _ w ->
|
||||
[(x, x, name, dataTypeFrom DataString w)]
|
||||
_ -> []
|
||||
) vars
|
||||
c@T_SimpleCommand {} ->
|
||||
getModifiedVariableCommand c
|
||||
[(x, x, name, dataTypeFrom DataString w) | x@(T_Assignment id _ name _ w) <- vars]
|
||||
T_SimpleCommand {} ->
|
||||
getModifiedVariableCommand t
|
||||
|
||||
TA_Unary _ "++|" v@(TA_Variable _ name _) ->
|
||||
[(t, v, name, DataString $ SourceFrom [v])]
|
||||
TA_Unary _ "|++" v@(TA_Variable _ name _) ->
|
||||
[(t, v, name, DataString $ SourceFrom [v])]
|
||||
TA_Assignment _ op (TA_Variable _ name _) rhs -> maybeToList $ do
|
||||
TA_Assignment _ op (TA_Variable _ name _) rhs -> do
|
||||
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
||||
return (t, t, name, DataString $ SourceFrom [rhs])
|
||||
|
||||
@@ -500,26 +482,30 @@ getModifiedVariables t =
|
||||
|
||||
-- Count [[ -v foo ]] as an "assignment".
|
||||
-- This is to prevent [ -v foo ] being unassigned or unused.
|
||||
TC_Unary id _ "-v" token -> maybeToList $ do
|
||||
TC_Unary id _ "-v" token -> do
|
||||
str <- fmap (takeWhile (/= '[')) $ -- Quoted index
|
||||
flip getLiteralStringExt token $ \x ->
|
||||
case x of
|
||||
T_Glob _ s -> return s -- Unquoted index
|
||||
_ -> Nothing
|
||||
_ -> []
|
||||
|
||||
guard . not . null $ str
|
||||
return (t, token, str, DataString SourceChecked)
|
||||
|
||||
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
|
||||
let string = bracedString t
|
||||
let string = concat $ oversimplify l
|
||||
let modifier = getBracedModifier string
|
||||
guard $ ":=" `isPrefixOf` modifier
|
||||
guard $ any (`isPrefixOf` modifier) ["=", ":="]
|
||||
return (t, t, getBracedReference string, DataString $ SourceFrom [l])
|
||||
|
||||
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo
|
||||
T_FdRedirect _ ('{':var) op -> -- {foo}>&2 modifies foo
|
||||
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
|
||||
|
||||
t@(T_CoProc _ name _) ->
|
||||
T_CoProc _ name _ ->
|
||||
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
|
||||
|
||||
--Points to 'for' rather than variable
|
||||
@@ -527,6 +513,14 @@ getModifiedVariables t =
|
||||
T_ForIn 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
|
||||
f place t = case t of
|
||||
T_DollarBraced _ _ l ->
|
||||
let str = getBracedReference $ concat $ oversimplify l in do
|
||||
guard $ isVariableName str
|
||||
return (place, t, str, DataString SourceChecked)
|
||||
_ -> Nothing
|
||||
|
||||
isClosingFileOp op =
|
||||
case op of
|
||||
@@ -546,10 +540,14 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
|
||||
(not $ any (`elem` flags) ["f", "F"])
|
||||
then concatMap getReference rest
|
||||
else []
|
||||
"local" -> if "x" `elem` flags
|
||||
then concatMap getReference rest
|
||||
else []
|
||||
"trap" ->
|
||||
case rest of
|
||||
head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head
|
||||
head:_ -> map (\x -> (base, head, x)) $ getVariablesFromLiteralToken head
|
||||
_ -> []
|
||||
"alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token]
|
||||
_ -> []
|
||||
where
|
||||
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
|
||||
@@ -567,14 +565,20 @@ getReferencedVariableCommand _ = []
|
||||
-- VariableName :: String, -- The variable name, i.e. foo
|
||||
-- VariableValue :: DataType -- A description of the value being assigned, i.e. "Literal string with value foo"
|
||||
-- )
|
||||
getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
||||
getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
||||
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
|
||||
case x of
|
||||
"builtin" ->
|
||||
getModifiedVariableCommand $ T_SimpleCommand id cmdPrefix rest
|
||||
"read" ->
|
||||
let params = map getLiteral rest
|
||||
readArrayVars = getReadArrayVariables rest
|
||||
in
|
||||
catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params
|
||||
let fallback = catMaybes $ takeWhile isJust (reverse $ map getLiteral rest)
|
||||
in fromMaybe fallback $ do
|
||||
parsed <- getGnuOpts flagsForRead rest
|
||||
case lookup "a" parsed of
|
||||
Just (_, var) -> (:[]) <$> getLiteralArray var
|
||||
Nothing -> return $ catMaybes $
|
||||
map (getLiteral . snd . snd) $ filter (null . fst) parsed
|
||||
|
||||
"getopts" ->
|
||||
case rest of
|
||||
opts:var:_ -> maybeToList $ getLiteral var
|
||||
@@ -610,8 +614,7 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
|
||||
_ -> []
|
||||
where
|
||||
flags = map snd $ getAllFlags base
|
||||
stripEquals s = let rest = dropWhile (/= '=') s in
|
||||
if rest == "" then "" else tail rest
|
||||
stripEquals s = drop 1 $ dropWhile (/= '=') s
|
||||
stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) =
|
||||
T_NormalWord id1 (T_Literal id2 (stripEquals s):rs)
|
||||
stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) =
|
||||
@@ -642,7 +645,7 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
|
||||
getModifierParam _ _ = []
|
||||
|
||||
letParamToLiteral token =
|
||||
if var == ""
|
||||
if null var
|
||||
then []
|
||||
else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])]
|
||||
where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token
|
||||
@@ -667,25 +670,28 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
|
||||
f [] = fail "not found"
|
||||
|
||||
-- mapfile has some curious syntax allowing flags plus 0..n variable names
|
||||
-- where only the first non-option one is used if any. Here we cheat and
|
||||
-- just get the last one, if it's a variable name.
|
||||
getMapfileArray base arguments = do
|
||||
lastArg <- listToMaybe (reverse arguments)
|
||||
name <- getLiteralString lastArg
|
||||
guard $ isVariableName name
|
||||
return (base, lastArg, name, DataArray SourceExternal)
|
||||
|
||||
-- get all the array variables used in read, e.g. read -a arr
|
||||
getReadArrayVariables args =
|
||||
map (getLiteralArray . snd)
|
||||
(filter (isArrayFlag . fst) (zip args (tail args)))
|
||||
|
||||
isArrayFlag x = fromMaybe False $ do
|
||||
str <- getLiteralString x
|
||||
return $ case str of
|
||||
'-':'-':_ -> False
|
||||
'-':str -> 'a' `elem` str
|
||||
_ -> False
|
||||
-- where only the first non-option one is used if any.
|
||||
getMapfileArray base rest = parseArgs `mplus` fallback
|
||||
where
|
||||
parseArgs :: Maybe (Token, Token, String, DataType)
|
||||
parseArgs = do
|
||||
args <- getGnuOpts "d:n:O:s:u:C:c:t" rest
|
||||
case [y | ("",(_,y)) <- args] of
|
||||
[] ->
|
||||
return (base, base, "MAPFILE", DataArray SourceExternal)
|
||||
first:_ -> do
|
||||
name <- getLiteralString first
|
||||
guard $ isVariableName name
|
||||
return (base, first, name, DataArray SourceExternal)
|
||||
-- If arg parsing fails (due to bad or new flags), get the last variable name
|
||||
fallback :: Maybe (Token, Token, String, DataType)
|
||||
fallback = do
|
||||
(name, token) <- listToMaybe . mapMaybe f $ reverse rest
|
||||
return (base, token, name, DataArray SourceExternal)
|
||||
f arg = do
|
||||
name <- getLiteralString arg
|
||||
guard $ isVariableName name
|
||||
return (name, arg)
|
||||
|
||||
-- get the FLAGS_ variable created by a shflags DEFINE_ call
|
||||
getFlagVariable (n:v:_) = do
|
||||
@@ -716,7 +722,7 @@ getOffsetReferences mods = fromMaybe [] $ do
|
||||
|
||||
getReferencedVariables parents t =
|
||||
case t of
|
||||
T_DollarBraced id _ l -> let str = bracedString t in
|
||||
T_DollarBraced id _ l -> let str = concat $ oversimplify l in
|
||||
(t, t, getBracedReference str) :
|
||||
map (\x -> (l, l, x)) (
|
||||
getIndexReferences str
|
||||
@@ -741,7 +747,7 @@ getReferencedVariables parents t =
|
||||
(t, t, "output")
|
||||
]
|
||||
|
||||
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
|
||||
T_FdRedirect _ ('{':var) op -> -- {foo}>&- references and closes foo
|
||||
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
|
||||
x -> getReferencedVariableCommand x
|
||||
where
|
||||
@@ -758,12 +764,11 @@ getReferencedVariables parents t =
|
||||
|
||||
literalizer t = case t of
|
||||
T_Glob _ s -> return s -- Also when parsed as globs
|
||||
_ -> Nothing
|
||||
_ -> []
|
||||
|
||||
getIfReference context token = maybeToList $ do
|
||||
str <- getLiteralStringExt literalizer token
|
||||
guard . not $ null str
|
||||
when (isDigit $ head str) $ fail "is a number"
|
||||
getIfReference context token = do
|
||||
str@(h:_) <- getLiteralStringExt literalizer token
|
||||
when (isDigit h) $ fail "is a number"
|
||||
return (context, token, getBracedReference str)
|
||||
|
||||
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
|
||||
@@ -783,8 +788,8 @@ isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `
|
||||
-- Compare a command to a literal. Like above, but checks full path.
|
||||
isUnqualifiedCommand token str = isCommandMatch token (== str)
|
||||
|
||||
isCommandMatch token matcher = fromMaybe False $
|
||||
fmap matcher (getCommandName token)
|
||||
isCommandMatch token matcher = maybe False
|
||||
matcher (getCommandName token)
|
||||
|
||||
-- Does this regex look like it was intended as a glob?
|
||||
-- True: *foo*
|
||||
@@ -805,14 +810,14 @@ isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
|
||||
isVariableName _ = False
|
||||
|
||||
getVariablesFromLiteralToken token =
|
||||
getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token)
|
||||
getVariablesFromLiteral (getLiteralStringDef " " token)
|
||||
|
||||
-- Try to get referenced variables from a literal string like "$foo"
|
||||
-- Ignores tons of cases like arithmetic evaluation and array indices.
|
||||
prop_getVariablesFromLiteral1 =
|
||||
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
|
||||
getVariablesFromLiteral string =
|
||||
map (!! 0) $ matchAllSubgroups variableRegex string
|
||||
map head $ matchAllSubgroups variableRegex string
|
||||
where
|
||||
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
|
||||
|
||||
@@ -828,34 +833,33 @@ 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) = if c `elem` "!#" then rest else c:rest
|
||||
dropPrefix "" = ""
|
||||
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:_) =
|
||||
if c `elem` "*@#?-$!" then return [c] else fail "not special"
|
||||
getSpecial _ = fail "empty"
|
||||
getSpecial (c:_) | c `elem` "*@#?-$!" = return [c]
|
||||
getSpecial _ = fail "empty or not special"
|
||||
|
||||
nameExpansion ('!':rest) = do -- e.g. ${!foo*bar*}
|
||||
let suffix = dropWhile isVariableChar rest
|
||||
guard $ suffix /= rest -- e.g. ${!@}
|
||||
first <- suffix !!! 0
|
||||
guard $ first `elem` "*?"
|
||||
nameExpansion ('!':next:rest) = do -- e.g. ${!foo*bar*}
|
||||
guard $ isVariableChar next -- e.g. ${!@}
|
||||
first <- find (not . isVariableChar) rest
|
||||
guard $ first `elem` "*?@"
|
||||
return ""
|
||||
nameExpansion _ = Nothing
|
||||
|
||||
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
|
||||
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
|
||||
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
|
||||
getBracedModifier s = fromMaybe "" . listToMaybe $ do
|
||||
getBracedModifier s = headOrDefault "" $ do
|
||||
let var = getBracedReference s
|
||||
a <- dropModifier s
|
||||
dropPrefix var a
|
||||
@@ -869,15 +873,6 @@ getBracedModifier s = fromMaybe "" . listToMaybe $ do
|
||||
|
||||
-- Useful generic functions.
|
||||
|
||||
-- Run an action in a Maybe (or do nothing).
|
||||
-- Example:
|
||||
-- potentially $ do
|
||||
-- s <- getLiteralString cmd
|
||||
-- guard $ s `elem` ["--recursive", "-r"]
|
||||
-- return $ warn .. "Something something recursive"
|
||||
potentially :: Monad m => Maybe (m ()) -> m ()
|
||||
potentially = fromMaybe (return ())
|
||||
|
||||
-- Get element 0 or a default. Like `head` but safe.
|
||||
headOrDefault _ (a:_) = a
|
||||
headOrDefault def _ = def
|
||||
@@ -890,8 +885,8 @@ headOrDefault def _ = def
|
||||
|
||||
-- Run a command if the shell is in the given list
|
||||
whenShell l c = do
|
||||
shell <- asks shellType
|
||||
when (shell `elem` l ) c
|
||||
params <- ask
|
||||
when (shellType params `elem` l ) c
|
||||
|
||||
|
||||
filterByAnnotation asSpec params =
|
||||
@@ -920,46 +915,15 @@ isCountingReference _ = False
|
||||
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
|
||||
isQuotedAlternativeReference t =
|
||||
case t of
|
||||
T_DollarBraced _ _ _ ->
|
||||
getBracedModifier (bracedString t) `matches` re
|
||||
T_DollarBraced _ _ l ->
|
||||
getBracedModifier (concat $ oversimplify l) `matches` re
|
||||
_ -> False
|
||||
where
|
||||
re = mkRegex "(^|\\]):?\\+"
|
||||
|
||||
-- getGnuOpts "erd:u:" will parse a SimpleCommand like
|
||||
-- read -re -d : -u 3 bar
|
||||
-- into
|
||||
-- Just [("r", -re), ("e", -re), ("d", :), ("u", 3), ("", bar)]
|
||||
-- where flags with arguments map to arguments, while others map to themselves.
|
||||
-- Any unrecognized flag will result in Nothing.
|
||||
getGnuOpts = getOpts getAllFlags
|
||||
getBsdOpts = getOpts getLeadingFlags
|
||||
getOpts :: (Token -> [(Token, String)]) -> String -> Token -> Maybe [(String, Token)]
|
||||
getOpts flagTokenizer string cmd = process flags
|
||||
where
|
||||
flags = flagTokenizer cmd
|
||||
flagList (c:':':rest) = ([c], True) : flagList rest
|
||||
flagList (c:rest) = ([c], False) : flagList rest
|
||||
flagList [] = []
|
||||
flagMap = Map.fromList $ ("", False) : flagList string
|
||||
|
||||
process [] = return []
|
||||
process [(token, flag)] = do
|
||||
takesArg <- Map.lookup flag flagMap
|
||||
guard $ not takesArg
|
||||
return [(flag, token)]
|
||||
process ((token1, flag1):rest2@((token2, flag2):rest)) = do
|
||||
takesArg <- Map.lookup flag1 flagMap
|
||||
if takesArg
|
||||
then do
|
||||
guard $ flag2 == ""
|
||||
more <- process rest
|
||||
return $ (flag1, token2) : more
|
||||
else do
|
||||
more <- process rest2
|
||||
return $ (flag1, token1) : more
|
||||
|
||||
supportsArrays shell = shell == Bash || shell == Ksh
|
||||
supportsArrays Bash = True
|
||||
supportsArrays Ksh = True
|
||||
supportsArrays _ = False
|
||||
|
||||
-- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh)
|
||||
isBashLike :: Parameters -> Bool
|
||||
|
@@ -48,7 +48,7 @@ tokenToPosition startMap t = fromMaybe fail $ do
|
||||
where
|
||||
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
|
||||
|
||||
shellFromFilename filename = foldl mplus Nothing candidates
|
||||
shellFromFilename filename = listToMaybe candidates
|
||||
where
|
||||
shellExtensions = [(".ksh", Ksh)
|
||||
,(".bash", Bash)
|
||||
@@ -57,7 +57,7 @@ shellFromFilename filename = foldl mplus Nothing candidates
|
||||
-- The `.sh` is too generic to determine the shell:
|
||||
-- We fallback to Bash in this case and emit SC2148 if there is no shebang
|
||||
candidates =
|
||||
map (\(ext,sh) -> if ext `isSuffixOf` filename then Just sh else Nothing) shellExtensions
|
||||
[sh | (ext,sh) <- shellExtensions, ext `isSuffixOf` filename]
|
||||
|
||||
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
|
||||
checkScript sys spec = do
|
||||
@@ -88,9 +88,9 @@ checkScript sys spec = do
|
||||
asOptionalChecks = csOptionalChecks spec
|
||||
} where as = newAnalysisSpec root
|
||||
let analysisMessages =
|
||||
fromMaybe [] $
|
||||
maybe []
|
||||
(arComments . analyzeScript . analysisSpec)
|
||||
<$> prRoot result
|
||||
$ prRoot result
|
||||
let translator = tokenToPosition tokenPositions
|
||||
return . nub . sortMessages . filter shouldInclude $
|
||||
(parseMessages ++ map translator analysisMessages)
|
||||
@@ -104,7 +104,7 @@ checkScript sys spec = do
|
||||
code = cCode (pcComment pc)
|
||||
severity = cSeverity (pcComment pc)
|
||||
|
||||
sortMessages = sortBy (comparing order)
|
||||
sortMessages = sortOn order
|
||||
order pc =
|
||||
let pos = pcStartPos pc
|
||||
comment = pcComment pc in
|
||||
@@ -198,11 +198,11 @@ prop_optionDisablesBadShebang =
|
||||
}
|
||||
|
||||
prop_annotationDisablesBadShebang =
|
||||
[] == check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
|
||||
null $ check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
|
||||
|
||||
|
||||
prop_canParseDevNull =
|
||||
[] == check "source /dev/null"
|
||||
null $ check "source /dev/null"
|
||||
|
||||
prop_failsWhenNotSourcing =
|
||||
[1091, 2154] == check "source lol; echo \"$bar\""
|
||||
@@ -218,7 +218,7 @@ prop_worksWhenDotting =
|
||||
|
||||
-- FIXME: This should really be giving [1093], "recursively sourced"
|
||||
prop_noInfiniteSourcing =
|
||||
[] == checkWithIncludes [("lib", "source lib")] "source lib"
|
||||
null $ checkWithIncludes [("lib", "source lib")] "source lib"
|
||||
|
||||
prop_canSourceBadSyntax =
|
||||
[1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
|
||||
@@ -229,6 +229,12 @@ prop_cantSourceDynamic =
|
||||
prop_cantSourceDynamic2 =
|
||||
[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 =
|
||||
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
|
||||
|
||||
@@ -239,10 +245,10 @@ prop_recursiveParsing =
|
||||
[1037] == checkRecursive [("lib", "echo \"$10\"")] "source lib"
|
||||
|
||||
prop_nonRecursiveAnalysis =
|
||||
[] == checkWithIncludes [("lib", "echo $1")] "source lib"
|
||||
null $ checkWithIncludes [("lib", "echo $1")] "source lib"
|
||||
|
||||
prop_nonRecursiveParsing =
|
||||
[] == checkWithIncludes [("lib", "echo \"$10\"")] "source lib"
|
||||
null $ checkWithIncludes [("lib", "echo \"$10\"")] "source lib"
|
||||
|
||||
prop_sourceDirectiveDoesntFollowFile =
|
||||
null $ checkWithIncludes
|
||||
@@ -270,7 +276,7 @@ prop_filewideAnnotation8 = null $
|
||||
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
|
||||
|
||||
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
|
||||
2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
|
||||
3046 `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"
|
||||
|
||||
@@ -288,6 +294,20 @@ prop_deducesTypeFromExtension2 = result == [2079]
|
||||
csScript = "(( 3.14 ))"
|
||||
}
|
||||
|
||||
prop_canDisableShebangWarning = null $ result
|
||||
where
|
||||
result = checkWithSpec [] emptyCheckSpec {
|
||||
csFilename = "file.sh",
|
||||
csScript = "#shellcheck disable=SC2148\nfoo"
|
||||
}
|
||||
|
||||
prop_canDisableParseErrors = null $ result
|
||||
where
|
||||
result = checkWithSpec [] emptyCheckSpec {
|
||||
csFilename = "file.sh",
|
||||
csScript = "#shellcheck disable=SC1073,SC1072,SC2148\n()"
|
||||
}
|
||||
|
||||
prop_shExtensionDoesntMatter = result == [2148]
|
||||
where
|
||||
result = checkWithSpec [] emptyCheckSpec {
|
||||
@@ -328,7 +348,7 @@ prop_optionIncludes4 =
|
||||
[2154] == checkOptionIncludes (Just [2154]) "#!/bin/sh\n var='a b'\n echo $var\n echo $bar"
|
||||
|
||||
|
||||
prop_readsRcFile = result == []
|
||||
prop_readsRcFile = null result
|
||||
where
|
||||
result = checkWithRc "disable=2086" emptyCheckSpec {
|
||||
csScript = "#!/bin/sh\necho $1",
|
||||
|
@@ -19,9 +19,10 @@
|
||||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
|
||||
-- This module contains checks that examine specific commands by name.
|
||||
module ShellCheck.Checks.Commands (checker , ShellCheck.Checks.Commands.runTests) where
|
||||
module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
@@ -34,6 +35,7 @@ import ShellCheck.Regex
|
||||
import Control.Monad
|
||||
import Control.Monad.RWS
|
||||
import Data.Char
|
||||
import Data.Functor.Identity
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import qualified Data.Map.Strict as Map
|
||||
@@ -51,8 +53,6 @@ verify :: CommandCheck -> String -> Bool
|
||||
verify f s = producesComments (getChecker [f]) s == Just True
|
||||
verifyNot f s = producesComments (getChecker [f]) s == Just False
|
||||
|
||||
arguments (T_SimpleCommand _ _ (cmd:args)) = args
|
||||
|
||||
commandChecks :: [CommandCheck]
|
||||
commandChecks = [
|
||||
checkTr
|
||||
@@ -90,13 +90,60 @@ commandChecks = [
|
||||
,checkMvArguments, checkCpArguments, checkLnArguments
|
||||
,checkFindRedirections
|
||||
,checkReadExpansions
|
||||
,checkWhich
|
||||
,checkSudoRedirect
|
||||
,checkSudoArgs
|
||||
,checkSourceArgs
|
||||
,checkChmodDashr
|
||||
,checkXargsDashi
|
||||
]
|
||||
|
||||
optionalChecks = map fst optionalCommandChecks
|
||||
optionalCommandChecks :: [(CheckDescription, CommandCheck)]
|
||||
optionalCommandChecks = [
|
||||
(newCheckDescription {
|
||||
cdName = "deprecate-which",
|
||||
cdDescription = "Suggest 'command -v' instead of 'which'",
|
||||
cdPositive = "which javac",
|
||||
cdNegative = "command -v javac"
|
||||
}, checkWhich)
|
||||
]
|
||||
optionalCheckMap = Map.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks
|
||||
|
||||
prop_verifyOptionalExamples = all check optionalCommandChecks
|
||||
where
|
||||
check (desc, check) =
|
||||
verify check (cdPositive desc)
|
||||
&& verifyNot check (cdNegative desc)
|
||||
|
||||
-- Run a check against the getopt parser. If it fails, the lists are empty.
|
||||
checkGetOpts str flags args f =
|
||||
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:" []
|
||||
|
||||
-- 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) "" []
|
||||
|
||||
-- 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" []
|
||||
|
||||
|
||||
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
||||
buildCommandMap = foldl' addCheck Map.empty
|
||||
where
|
||||
@@ -105,12 +152,16 @@ buildCommandMap = foldl' addCheck Map.empty
|
||||
|
||||
|
||||
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
|
||||
checkCommand map t@(T_SimpleCommand id _ (cmd:rest)) = fromMaybe (return ()) $ do
|
||||
checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do
|
||||
name <- getLiteralString cmd
|
||||
return $
|
||||
if '/' `elem` name
|
||||
then
|
||||
Map.findWithDefault nullCheck (Basename $ basename name) map t
|
||||
else if name == "builtin" && not (null rest) then
|
||||
let t' = T_SimpleCommand id cmdPrefix rest
|
||||
selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest
|
||||
in Map.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
|
||||
else do
|
||||
Map.findWithDefault nullCheck (Exactly name) map t
|
||||
Map.findWithDefault nullCheck (Basename name) map t
|
||||
@@ -128,8 +179,14 @@ getChecker list = Checker {
|
||||
map = buildCommandMap list
|
||||
|
||||
|
||||
checker :: Parameters -> Checker
|
||||
checker params = getChecker commandChecks
|
||||
checker :: AnalysisSpec -> Parameters -> Checker
|
||||
checker spec params = getChecker $ commandChecks ++ optionals
|
||||
where
|
||||
keys = asOptionalChecks spec
|
||||
optionals =
|
||||
if "all" `elem` keys
|
||||
then map snd optionalCommandChecks
|
||||
else mapMaybe (\x -> Map.lookup x optionalCheckMap) keys
|
||||
|
||||
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
|
||||
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
|
||||
@@ -169,15 +226,13 @@ prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php"
|
||||
prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)"
|
||||
prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"
|
||||
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
|
||||
acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
|
||||
acceptsGlob _ = False
|
||||
acceptsGlob s = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
|
||||
f [] = return ()
|
||||
f [x] = return ()
|
||||
f (a:b:r) = do
|
||||
when (acceptsGlob (getLiteralString a) && isGlob b) $ do
|
||||
let (Just s) = getLiteralString a
|
||||
f (x:xs) = foldr g (const $ return ()) xs x
|
||||
g b acc a = do
|
||||
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."
|
||||
f (b:r)
|
||||
acc b
|
||||
|
||||
|
||||
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
|
||||
@@ -221,13 +276,12 @@ prop_checkGrepRe23= verifyNot checkGrepRe "grep '.*' file"
|
||||
checkGrepRe = CommandCheck (Basename "grep") check where
|
||||
check cmd = f cmd (arguments cmd)
|
||||
-- --regex=*(extglob) doesn't work. Fixme?
|
||||
skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s
|
||||
skippable _ = False
|
||||
skippable s = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s
|
||||
f _ [] = return ()
|
||||
f cmd (x:r) =
|
||||
let str = getLiteralStringExt (const $ return "_") x
|
||||
let str = getLiteralStringDef "_" x
|
||||
in
|
||||
if str `elem` [Just "--", Just "-e", Just "--regex"]
|
||||
if str `elem` ["--", "-e", "--regex"]
|
||||
then checkRE cmd r -- Regex is *after* this
|
||||
else
|
||||
if skippable str
|
||||
@@ -243,7 +297,7 @@ checkGrepRe = CommandCheck (Basename "grep") check where
|
||||
let string = concat $ oversimplify re
|
||||
if isConfusedGlobRegex string then
|
||||
warn (getId re) 2063 "Grep uses regex, but this looks like a glob."
|
||||
else potentially $ do
|
||||
else sequence_ $ do
|
||||
char <- getSuspiciousRegexWildcard string
|
||||
return $ info (getId re) 2022 $
|
||||
"Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'."
|
||||
@@ -252,22 +306,16 @@ checkGrepRe = CommandCheck (Basename "grep") check where
|
||||
grepGlobFlags = ["fixed-strings", "F", "include", "exclude", "exclude-dir", "o", "only-matching"]
|
||||
|
||||
wordStartingWith c =
|
||||
head . filter ([c] `isPrefixOf`) $ candidates
|
||||
headOrDefault (c:"test") . filter ([c] `isPrefixOf`) $ candidates
|
||||
where
|
||||
candidates =
|
||||
sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords ++ [c:"test"]
|
||||
sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords
|
||||
|
||||
getSuspiciousRegexWildcard str =
|
||||
if not $ str `matches` contra
|
||||
then do
|
||||
match <- matchRegex suspicious str
|
||||
str <- match !!! 0
|
||||
str !!! 0
|
||||
else
|
||||
fail "looks good"
|
||||
where
|
||||
suspicious = mkRegex "([A-Za-z1-9])\\*"
|
||||
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
|
||||
getSuspiciousRegexWildcard str = case matchRegex suspicious str of
|
||||
Just [[c]] | not (str `matches` contra) -> Just c
|
||||
_ -> fail "looks good"
|
||||
suspicious = mkRegex "([A-Za-z1-9])\\*"
|
||||
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
|
||||
|
||||
|
||||
prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
|
||||
@@ -318,10 +366,10 @@ returnOrExit multi invalid = (f . arguments)
|
||||
invalid (getId value)
|
||||
f _ = return ()
|
||||
|
||||
isInvalid s = s == "" || any (not . isDigit) s || length s > 5
|
||||
isInvalid s = null s || any (not . isDigit) s || length s > 5
|
||||
|| let value = (read s :: Integer) in value > 255
|
||||
|
||||
literal token = fromJust $ getLiteralStringExt lit token
|
||||
literal token = runIdentity $ getLiteralStringExt lit token
|
||||
lit (T_DollarBraced {}) = return "0"
|
||||
lit (T_DollarArithmetic {}) = return "0"
|
||||
lit (T_DollarExpansion {}) = return "0"
|
||||
@@ -338,7 +386,7 @@ checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments)
|
||||
check (exec:arg:term:_) = do
|
||||
execS <- getLiteralString exec
|
||||
termS <- getLiteralString term
|
||||
cmdS <- getLiteralStringExt (const $ return " ") arg
|
||||
let cmdS = getLiteralStringDef " " arg
|
||||
|
||||
guard $ execS `elem` ["-exec", "-execdir"] && termS `elem` [";", "+"]
|
||||
guard $ cmdS `matches` commandRegex
|
||||
@@ -434,10 +482,10 @@ prop_checkMkdirDashPM20 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 .././bin"
|
||||
prop_checkMkdirDashPM21 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../../bin"
|
||||
checkMkdirDashPM = CommandCheck (Basename "mkdir") check
|
||||
where
|
||||
check t = potentially $ do
|
||||
check t = sequence_ $ do
|
||||
let flags = getAllFlags t
|
||||
dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
|
||||
dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
|
||||
dashP <- find (\(_,f) -> f == "p" || f == "parents") flags
|
||||
dashM <- find (\(_,f) -> f == "m" || f == "mode") flags
|
||||
-- mkdir -pm 0700 dir is fine, so is ../dir, but dir/subdir is not.
|
||||
guard $ any couldHaveSubdirs (drop 1 $ arguments t)
|
||||
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
|
||||
@@ -457,10 +505,10 @@ prop_checkNonportableSignals7 = verifyNot checkNonportableSignals "trap 'stop' i
|
||||
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
|
||||
where
|
||||
f args = case args of
|
||||
first:rest -> unless (isFlag first) $ mapM_ check rest
|
||||
first:rest | not $ isFlag first -> mapM_ check rest
|
||||
_ -> return ()
|
||||
|
||||
check param = potentially $ do
|
||||
check param = sequence_ $ do
|
||||
str <- getLiteralString param
|
||||
let id = getId param
|
||||
return $ sequence_ $ mapMaybe (\f -> f id str) [
|
||||
@@ -494,9 +542,9 @@ checkInteractiveSu = CommandCheck (Basename "su") f
|
||||
info (getId cmd) 2117
|
||||
"To run commands as another user, use su -c or sudo."
|
||||
|
||||
undirected (T_Pipeline _ _ l) = length l <= 1
|
||||
undirected (T_Pipeline _ _ (_:_:_)) = False
|
||||
-- This should really just be modifications to stdin, but meh
|
||||
undirected (T_Redirecting _ list _) = null list
|
||||
undirected (T_Redirecting _ (_:_) _) = False
|
||||
undirected _ = True
|
||||
|
||||
|
||||
@@ -513,9 +561,8 @@ checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
|
||||
([], hostport:r@(_:_)) -> checkArg $ last r
|
||||
_ -> return ()
|
||||
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
|
||||
case filter (not . isConstant) parts of
|
||||
[] -> return ()
|
||||
(x:_) -> info (getId x) 2029
|
||||
forM_ (find (not . isConstant) parts) $
|
||||
\x -> info (getId x) 2029
|
||||
"Note that, unescaped, this expands on the client side."
|
||||
checkArg _ = return ()
|
||||
|
||||
@@ -541,6 +588,8 @@ prop_checkPrintfVar18= verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
|
||||
prop_checkPrintfVar19= verifyNot checkPrintfVar "printf '%(%s)T'"
|
||||
prop_checkPrintfVar20= verifyNot checkPrintfVar "printf '%d %(%s)T' 42"
|
||||
prop_checkPrintfVar21= verify checkPrintfVar "printf '%d %(%s)T'"
|
||||
prop_checkPrintfVar22= verify checkPrintfVar "printf '%s\n%s' foo"
|
||||
|
||||
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
|
||||
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
|
||||
@@ -548,32 +597,31 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||
f _ = return ()
|
||||
|
||||
check format more = do
|
||||
fromMaybe (return ()) $ do
|
||||
sequence_ $ do
|
||||
string <- getLiteralString format
|
||||
let formats = getPrintfFormats string
|
||||
let formatCount = length formats
|
||||
let argCount = length more
|
||||
|
||||
return $
|
||||
case () of
|
||||
() | argCount == 0 && formatCount == 0 ->
|
||||
return () -- This is fine
|
||||
() | formatCount == 0 && argCount > 0 ->
|
||||
err (getId format) 2182
|
||||
"This printf format string has no variables. Other arguments are ignored."
|
||||
() | any mayBecomeMultipleArgs more ->
|
||||
return () -- We don't know so trust the user
|
||||
() | argCount < formatCount && onlyTrailingTs formats argCount ->
|
||||
return () -- Allow trailing %()Ts since they use the current time
|
||||
() | argCount > 0 && argCount `mod` formatCount == 0 ->
|
||||
return () -- Great: a suitable number of arguments
|
||||
() ->
|
||||
warn (getId format) 2183 $
|
||||
"This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments."
|
||||
return $ if
|
||||
| argCount == 0 && formatCount == 0 ->
|
||||
return () -- This is fine
|
||||
| formatCount == 0 && argCount > 0 ->
|
||||
err (getId format) 2182
|
||||
"This printf format string has no variables. Other arguments are ignored."
|
||||
| any mayBecomeMultipleArgs more ->
|
||||
return () -- We don't know so trust the user
|
||||
| argCount < formatCount && onlyTrailingTs formats argCount ->
|
||||
return () -- Allow trailing %()Ts since they use the current time
|
||||
| argCount > 0 && argCount `mod` formatCount == 0 ->
|
||||
return () -- Great: a suitable number of arguments
|
||||
| otherwise ->
|
||||
warn (getId format) 2183 $
|
||||
"This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments."
|
||||
|
||||
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
|
||||
info (getId format) 2059
|
||||
"Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"."
|
||||
"Don't use variables in the printf format string. Use printf '..%s..' \"$foo\"."
|
||||
where
|
||||
onlyTrailingTs format argCount =
|
||||
all (== 'T') $ drop argCount format
|
||||
@@ -584,6 +632,8 @@ prop_checkGetPrintfFormats2 = getPrintfFormats "%0*s" == "*s"
|
||||
prop_checkGetPrintfFormats3 = getPrintfFormats "%(%s)T" == "T"
|
||||
prop_checkGetPrintfFormats4 = getPrintfFormats "%d%%%(%s)T" == "dT"
|
||||
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
|
||||
where
|
||||
-- Get the arguments in the string as a string of type characters,
|
||||
@@ -602,17 +652,17 @@ getPrintfFormats = getFormats
|
||||
|
||||
regexBasedGetFormats rest =
|
||||
case matchRegex re rest of
|
||||
Just [width, precision, typ, rest] ->
|
||||
Just [width, precision, typ, rest, _] ->
|
||||
(if width == "*" then "*" else "") ++
|
||||
(if precision == "*" then "*" else "") ++
|
||||
typ ++ getFormats rest
|
||||
Nothing -> take 1 rest ++ getFormats rest
|
||||
where
|
||||
-- constructed based on specifications in "man printf"
|
||||
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)([diouxXfFeEgGaAcsbq])(.*)"
|
||||
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \ /
|
||||
-- V V V V V
|
||||
-- flags field width precision format character rest
|
||||
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)([diouxXfFeEgGaAcsbq])((\n|.)*)"
|
||||
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \______ /
|
||||
-- V V V V V
|
||||
-- flags field width precision format character rest
|
||||
-- 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
|
||||
|
||||
@@ -637,17 +687,12 @@ prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'"
|
||||
prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"
|
||||
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
|
||||
where
|
||||
f (var:value:rest) =
|
||||
let str = literal var in
|
||||
when (isVariableName str || isAssignment str) $
|
||||
msg (getId var)
|
||||
f (var:_) =
|
||||
when (isAssignment $ literal var) $
|
||||
msg (getId var)
|
||||
f (var:rest)
|
||||
| (not (null rest) && isVariableName str) || isAssignment str =
|
||||
warn (getId var) 2121 "To assign a variable, use just 'var=value', no 'set ..'."
|
||||
where str = literal var
|
||||
f _ = return ()
|
||||
|
||||
msg id = warn id 2121 "To assign a variable, use just 'var=value', no 'set ..'."
|
||||
|
||||
isAssignment str = '=' `elem` str
|
||||
literal (T_NormalWord _ l) = concatMap literal l
|
||||
literal (T_Literal _ str) = str
|
||||
@@ -660,9 +705,8 @@ prop_checkExportedExpansions3 = verifyNot checkExportedExpansions "export foo"
|
||||
prop_checkExportedExpansions4 = verifyNot checkExportedExpansions "export ${foo?}"
|
||||
checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
|
||||
where
|
||||
check t = potentially $ do
|
||||
var <- getSingleUnmodifiedVariable t
|
||||
let name = bracedString var
|
||||
check t = sequence_ $ do
|
||||
name <- getSingleUnmodifiedBracedString t
|
||||
return . warn (getId t) 2163 $
|
||||
"This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
||||
|
||||
@@ -676,28 +720,27 @@ prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1"
|
||||
prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}"
|
||||
checkReadExpansions = CommandCheck (Exactly "read") check
|
||||
where
|
||||
options = getGnuOpts "sreu:n:N:i:p:a:"
|
||||
options = getGnuOpts flagsForRead
|
||||
getVars cmd = fromMaybe [] $ do
|
||||
opts <- options cmd
|
||||
return . map snd $ filter (\(x,_) -> x == "" || x == "a") opts
|
||||
opts <- options $ arguments cmd
|
||||
return [y | (x,(_, y)) <- opts, null x || x == "a"]
|
||||
|
||||
check cmd = mapM_ warning $ getVars cmd
|
||||
warning t = potentially $ do
|
||||
var <- getSingleUnmodifiedVariable t
|
||||
let name = bracedString var
|
||||
warning t = sequence_ $ do
|
||||
name <- getSingleUnmodifiedBracedString t
|
||||
guard $ isVariableName name -- e.g. not $1
|
||||
return . warn (getId t) 2229 $
|
||||
"This does not read '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
||||
|
||||
-- Return the single variable expansion that makes up this word, if any.
|
||||
-- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing
|
||||
getSingleUnmodifiedVariable :: Token -> Maybe Token
|
||||
getSingleUnmodifiedVariable word =
|
||||
getSingleUnmodifiedBracedString :: Token -> Maybe String
|
||||
getSingleUnmodifiedBracedString word =
|
||||
case getWordParts word of
|
||||
[t@(T_DollarBraced {})] ->
|
||||
let contents = bracedString t
|
||||
[T_DollarBraced _ _ l] ->
|
||||
let contents = concat $ oversimplify l
|
||||
name = getBracedReference contents
|
||||
in guard (contents == name) >> return t
|
||||
in guard (contents == name) >> return contents
|
||||
_ -> Nothing
|
||||
|
||||
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
|
||||
@@ -708,7 +751,7 @@ checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments)
|
||||
re = mkRegex "\\$\\{?[0-9*@]"
|
||||
f = mapM_ checkArg
|
||||
checkArg arg =
|
||||
let string = fromJust $ getLiteralStringExt (const $ return "_") arg in
|
||||
let string = getLiteralStringDef "_" arg in
|
||||
when ('=' `elem` string && string `matches` re) $
|
||||
err (getId arg) 2142
|
||||
"Aliases can't use positional parameters. Use a function."
|
||||
@@ -721,7 +764,7 @@ checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
|
||||
where
|
||||
f = mapM_ checkArg
|
||||
checkArg arg | '=' `elem` concat (oversimplify arg) =
|
||||
forM_ (take 1 $ filter (not . isLiteral) $ getWordParts arg) $
|
||||
forM_ (find (not . isLiteral) $ getWordParts arg) $
|
||||
\x -> warn (getId x) 2139 "This expands when defined, not when used. Consider escaping."
|
||||
checkArg _ = return ()
|
||||
|
||||
@@ -754,7 +797,7 @@ checkFindWithoutPath = CommandCheck (Basename "find") f
|
||||
-- path. We assume that all the pre-path flags are single characters from a
|
||||
-- list of GNU and macOS flags.
|
||||
hasPath (first:rest) =
|
||||
let flag = fromJust $ getLiteralStringExt (const $ return "___") first in
|
||||
let flag = getLiteralStringDef "___" first in
|
||||
not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest
|
||||
hasPath [] = False
|
||||
isLeadingFlag flag = length flag <= 2 || all (`elem` leadingFlagChars) flag
|
||||
@@ -832,7 +875,7 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
|
||||
f :: Token -> Analysis
|
||||
f t@(T_SimpleCommand _ _ (cmd:arg1:_)) = do
|
||||
path <- getPathM t
|
||||
potentially $ do
|
||||
sequence_ $ do
|
||||
options <- getLiteralString arg1
|
||||
(T_WhileExpression _ _ body) <- findFirst whileLoop path
|
||||
caseCmd <- mapMaybe findCase body !!! 0
|
||||
@@ -857,12 +900,12 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
|
||||
notRequested = Map.difference handledMap requestedMap
|
||||
|
||||
warnUnhandled optId caseId str =
|
||||
warn caseId 2213 $ "getopts specified -" ++ str ++ ", but it's not handled by this 'case'."
|
||||
warn caseId 2213 $ "getopts specified -" ++ (e4m str) ++ ", but it's not handled by this 'case'."
|
||||
|
||||
warnRedundant (key, expr) = potentially $ do
|
||||
str <- key
|
||||
guard $ str `notElem` ["*", ":", "?"]
|
||||
return $ warn (getId expr) 2214 "This case is not specified by getopts."
|
||||
warnRedundant (Just str, expr)
|
||||
| str `notElem` ["*", ":", "?"] =
|
||||
warn (getId expr) 2214 "This case is not specified by getopts."
|
||||
warnRedundant _ = return ()
|
||||
|
||||
getHandledStrings (_, globs, _) =
|
||||
map (\x -> (literal x, x)) globs
|
||||
@@ -873,7 +916,7 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
|
||||
|
||||
fromGlob t =
|
||||
case t of
|
||||
T_Glob _ ('[':c:']':[]) -> return [c]
|
||||
T_Glob _ ['[', c, ']'] -> return [c]
|
||||
T_Glob _ "*" -> return "*"
|
||||
T_Glob _ "?" -> return "?"
|
||||
_ -> Nothing
|
||||
@@ -908,7 +951,7 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
|
||||
when (isRecursive t) $
|
||||
mapM_ (mapM_ checkWord . braceExpand) $ arguments t
|
||||
where
|
||||
isRecursive = any (`elem` ["r", "R", "recursive"]) . map snd . getAllFlags
|
||||
isRecursive = any ((`elem` ["r", "R", "recursive"]) . snd) . getAllFlags
|
||||
|
||||
checkWord token =
|
||||
case getLiteralString token of
|
||||
@@ -918,7 +961,7 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
|
||||
Nothing ->
|
||||
checkWord' token
|
||||
|
||||
checkWord' token = fromMaybe (return ()) $ do
|
||||
checkWord' token = sequence_ $ do
|
||||
filename <- getPotentialPath token
|
||||
let path = fixPath filename
|
||||
return . when (path `elem` importantPaths) $
|
||||
@@ -940,9 +983,9 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
|
||||
f _ = return ""
|
||||
|
||||
stripTrailing c = reverse . dropWhile (== c) . reverse
|
||||
skipRepeating c (a:b:rest) | a == b && b == c = skipRepeating c (b:rest)
|
||||
skipRepeating c (a:r) = a:skipRepeating c r
|
||||
skipRepeating _ [] = []
|
||||
skipRepeating c = foldr go []
|
||||
where
|
||||
go a r = a : case r of b:rest | b == c && a == b -> rest; _ -> r
|
||||
|
||||
paths = [
|
||||
"", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",
|
||||
@@ -968,10 +1011,9 @@ missingDestination handler token = do
|
||||
_ -> return ()
|
||||
where
|
||||
args = getAllFlags token
|
||||
params = map fst $ filter (\(_,x) -> x == "") args
|
||||
params = [x | (x,"") <- args]
|
||||
hasTarget =
|
||||
any (\x -> x /= "" && x `isPrefixOf` "target-directory") $
|
||||
map snd args
|
||||
any (\(_,x) -> x /= "" && x `isPrefixOf` "target-directory") args
|
||||
|
||||
prop_checkMvArguments1 = verify checkMvArguments "mv 'foo bar'"
|
||||
prop_checkMvArguments2 = verifyNot checkMvArguments "mv foo bar"
|
||||
@@ -1014,7 +1056,7 @@ checkFindRedirections = CommandCheck (Basename "find") f
|
||||
|
||||
prop_checkWhich = verify checkWhich "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_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
|
||||
@@ -1031,7 +1073,7 @@ checkSudoRedirect = CommandCheck (Basename "sudo") f
|
||||
Just (T_Redirecting _ redirs _) ->
|
||||
mapM_ warnAbout redirs
|
||||
warnAbout (T_FdRedirect _ s (T_IoFile id op file))
|
||||
| (s == "" || s == "&") && not (special file) =
|
||||
| (null s || s == "&") && not (special file) =
|
||||
case op of
|
||||
T_Less _ ->
|
||||
info (getId op) 2024
|
||||
@@ -1055,9 +1097,9 @@ prop_checkSudoArgs6 = verifyNot checkSudoArgs "sudo -n -u export ls"
|
||||
prop_checkSudoArgs7 = verifyNot checkSudoArgs "sudo docker export foo"
|
||||
checkSudoArgs = CommandCheck (Basename "sudo") f
|
||||
where
|
||||
f t = potentially $ do
|
||||
opts <- parseOpts t
|
||||
let nonFlags = map snd $ filter (\(flag, _) -> flag == "") opts
|
||||
f t = sequence_ $ do
|
||||
opts <- parseOpts $ arguments t
|
||||
let nonFlags = [x | ("",(x, _)) <- opts]
|
||||
commandArg <- nonFlags !!! 0
|
||||
command <- getLiteralString commandArg
|
||||
guard $ command `elem` builtins
|
||||
@@ -1083,10 +1125,23 @@ prop_checkChmodDashr3 = verifyNot checkChmodDashr "chmod a-r dir"
|
||||
checkChmodDashr = CommandCheck (Basename "chmod") f
|
||||
where
|
||||
f t = mapM_ check $ arguments t
|
||||
check t = potentially $ do
|
||||
check t = sequence_ $ do
|
||||
flag <- getLiteralString t
|
||||
guard $ flag == "-r"
|
||||
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:"
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||
|
@@ -30,6 +30,7 @@ import ShellCheck.Regex
|
||||
import Control.Monad
|
||||
import Control.Monad.RWS
|
||||
import Data.Char
|
||||
import Data.Functor.Identity
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import qualified Data.Map as Map
|
||||
@@ -73,7 +74,7 @@ prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
|
||||
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
|
||||
checkForDecimals = ForShell [Sh, Dash, Bash] f
|
||||
where
|
||||
f t@(TA_Expansion id _) = potentially $ do
|
||||
f t@(TA_Expansion id _) = sequence_ $ do
|
||||
str <- getLiteralString t
|
||||
first <- str !!! 0
|
||||
guard $ isDigit first && '.' `elem` str
|
||||
@@ -177,6 +178,9 @@ prop_checkBashisms93 = verify checkBashisms "#!/bin/sh\necho $(( 10#$(date +%m)
|
||||
prop_checkBashisms94 = verify checkBashisms "#!/bin/sh\n[ -v var ]"
|
||||
prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_"
|
||||
prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
|
||||
prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}"
|
||||
prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}"
|
||||
prop_checkBashisms99 = verifyNot checkBashisms "#!/bin/dash\necho [^f]oo"
|
||||
checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
params <- ask
|
||||
kludge params t
|
||||
@@ -185,102 +189,102 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
kludge params = bashism
|
||||
where
|
||||
isDash = shellType params == Dash
|
||||
warnMsg id s =
|
||||
warnMsg id code s =
|
||||
if isDash
|
||||
then warn id 2169 $ "In dash, " ++ s ++ " not supported."
|
||||
else warn id 2039 $ "In POSIX sh, " ++ s ++ " undefined."
|
||||
then err id code $ "In dash, " ++ s ++ " not supported."
|
||||
else warn id code $ "In POSIX sh, " ++ s ++ " undefined."
|
||||
|
||||
bashism (T_ProcSub id _ _) = warnMsg id "process substitution is"
|
||||
bashism (T_Extglob id _ _) = warnMsg id "extglob is"
|
||||
bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is"
|
||||
bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\" is"
|
||||
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id "arithmetic for loops are"
|
||||
bashism (T_Arithmetic id _) = warnMsg id "standalone ((..)) is"
|
||||
bashism (T_DollarBracket id _) = warnMsg id "$[..] in place of $((..)) is"
|
||||
bashism (T_SelectIn id _ _ _) = warnMsg id "select loops are"
|
||||
bashism (T_BraceExpansion id _) = warnMsg id "brace expansion is"
|
||||
bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is"
|
||||
bashism (T_HereString id _) = warnMsg id "here-strings are"
|
||||
bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is"
|
||||
bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is"
|
||||
bashism (T_DollarSingleQuoted id _) = 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 _) = warnMsg id 3010 "[[ ]] is"
|
||||
bashism (T_HereString id _) = warnMsg id 3011 "here-strings are"
|
||||
bashism (TC_Binary id SingleBracket op _ _)
|
||||
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
|
||||
unless isDash $ warnMsg id $ "lexicographical " ++ op ++ " is"
|
||||
unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is"
|
||||
bashism (TC_Binary id SingleBracket op _ _)
|
||||
| op `elem` [ "-nt", "-ef" ] =
|
||||
unless isDash $ warnMsg id $ op ++ " is"
|
||||
| op `elem` [ "-ot", "-nt", "-ef" ] =
|
||||
unless isDash $ warnMsg id 3013 $ op ++ " is"
|
||||
bashism (TC_Binary id SingleBracket "==" _ _) =
|
||||
warnMsg id "== in place of = is"
|
||||
warnMsg id 3014 "== in place of = is"
|
||||
bashism (TC_Binary id SingleBracket "=~" _ _) =
|
||||
warnMsg id "=~ regex matching is"
|
||||
warnMsg id 3015 "=~ regex matching is"
|
||||
bashism (TC_Unary id SingleBracket "-v" _) =
|
||||
warnMsg id "unary -v (in place of [ -n \"${var+x}\" ]) is"
|
||||
warnMsg id 3016 "unary -v (in place of [ -n \"${var+x}\" ]) is"
|
||||
bashism (TC_Unary id _ "-a" _) =
|
||||
warnMsg id "unary -a in place of -e is"
|
||||
warnMsg id 3017 "unary -a in place of -e is"
|
||||
bashism (TA_Unary id op _)
|
||||
| op `elem` [ "|++", "|--", "++|", "--|"] =
|
||||
warnMsg id $ filter (/= '|') op ++ " is"
|
||||
bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are"
|
||||
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is"
|
||||
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id ">& is"
|
||||
bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are"
|
||||
warnMsg id 3018 $ filter (/= '|') op ++ " is"
|
||||
bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
|
||||
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id 3020 "&> is"
|
||||
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id 3021 ">& is"
|
||||
bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are"
|
||||
bashism (T_FdRedirect id num _)
|
||||
| all isDigit num && length num > 1 = warnMsg id "FDs outside 0-9 are"
|
||||
| all isDigit num && length num > 1 = warnMsg id 3023 "FDs outside 0-9 are"
|
||||
bashism (T_Assignment id Append _ _ _) =
|
||||
warnMsg id "+= is"
|
||||
warnMsg id 3024 "+= is"
|
||||
bashism (T_IoFile id _ word) | isNetworked =
|
||||
warnMsg id "/dev/{tcp,udp} is"
|
||||
warnMsg id 3025 "/dev/{tcp,udp} is"
|
||||
where
|
||||
file = onlyLiteralString word
|
||||
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
|
||||
bashism (T_Glob id str) | "[^" `isInfixOf` str =
|
||||
warnMsg id "^ in place of ! in glob bracket expressions is"
|
||||
bashism (T_Glob id str) | not isDash && "[^" `isInfixOf` str =
|
||||
warnMsg id 3026 "^ in place of ! in glob bracket expressions is"
|
||||
|
||||
bashism t@(TA_Variable id str _) | isBashVariable str =
|
||||
warnMsg id $ str ++ " is"
|
||||
warnMsg id 3028 $ str ++ " is"
|
||||
|
||||
bashism t@(T_DollarBraced id _ token) = do
|
||||
mapM_ check expansion
|
||||
when (isBashVariable var) $
|
||||
warnMsg id $ var ++ " is"
|
||||
warnMsg id 3028 $ var ++ " is"
|
||||
where
|
||||
str = bracedString t
|
||||
str = concat $ oversimplify token
|
||||
var = getBracedReference str
|
||||
check (regex, feature) =
|
||||
when (isJust $ matchRegex regex str) $ warnMsg id feature
|
||||
check (regex, code, feature) =
|
||||
when (isJust $ matchRegex regex str) $ warnMsg id code feature
|
||||
|
||||
bashism t@(T_Pipe id "|&") =
|
||||
warnMsg id "|& in place of 2>&1 | is"
|
||||
warnMsg id 3029 "|& in place of 2>&1 | is"
|
||||
bashism (T_Array id _) =
|
||||
warnMsg id "arrays are"
|
||||
warnMsg id 3030 "arrays are"
|
||||
bashism (T_IoFile id _ t) | isGlob t =
|
||||
warnMsg id "redirecting to/from globs is"
|
||||
warnMsg id 3031 "redirecting to/from globs is"
|
||||
bashism (T_CoProc id _ _) =
|
||||
warnMsg id "coproc is"
|
||||
warnMsg id 3032 "coproc is"
|
||||
|
||||
bashism (T_Function id _ _ str _) | not (isVariableName str) =
|
||||
warnMsg id "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
|
||||
warnMsg id 3033 "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
|
||||
|
||||
bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
|
||||
warnMsg id "$(<file) to read files is"
|
||||
warnMsg id 3034 "$(<file) to read files is"
|
||||
bashism (T_Backticked id [x]) | isOnlyRedirection x =
|
||||
warnMsg id "`<file` to read files is"
|
||||
warnMsg id 3035 "`<file` to read files is"
|
||||
|
||||
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
||||
| t `isCommand` "echo" && argString `matches` flagRegex =
|
||||
if isDash
|
||||
then
|
||||
when (argString /= "-n") $
|
||||
warnMsg (getId arg) "echo flags besides -n"
|
||||
warnMsg (getId arg) 3036 "echo flags besides -n"
|
||||
else
|
||||
warnMsg (getId arg) "echo flags are"
|
||||
warnMsg (getId arg) 3037 "echo flags are"
|
||||
where
|
||||
argString = concat $ oversimplify arg
|
||||
flagRegex = mkRegex "^-[eEsn]+$"
|
||||
|
||||
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
||||
| t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
|
||||
warnMsg (getId arg) "exec flags are"
|
||||
| getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
|
||||
warnMsg (getId arg) 3038 "exec flags are"
|
||||
bashism t@(T_SimpleCommand id _ _)
|
||||
| t `isCommand` "let" = warnMsg id "'let' is"
|
||||
| t `isCommand` "let" = warnMsg id 3039 "'let' is"
|
||||
bashism t@(T_SimpleCommand _ _ (cmd:args))
|
||||
| t `isCommand` "set" = unless isDash $
|
||||
checkOptions $ getLiteralArgs args
|
||||
@@ -288,16 +292,17 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
-- Get the literal options from a list of arguments,
|
||||
-- up until the first non-literal one
|
||||
getLiteralArgs :: [Token] -> [(Id, String)]
|
||||
getLiteralArgs (first:rest) = fromMaybe [] $ do
|
||||
str <- getLiteralString first
|
||||
return $ (getId first, str) : getLiteralArgs rest
|
||||
getLiteralArgs [] = []
|
||||
getLiteralArgs = foldr go []
|
||||
where
|
||||
go first rest = case getLiteralString first of
|
||||
Just str -> (getId first, str) : rest
|
||||
Nothing -> []
|
||||
|
||||
-- Check a flag-option pair (such as -o errexit)
|
||||
checkOptions (flag@(fid,flag') : opt@(oid,opt') : rest)
|
||||
| flag' `matches` oFlagRegex = do
|
||||
when (opt' `notElem` longOptions) $
|
||||
warnMsg oid $ "set option " <> opt' <> " is"
|
||||
warnMsg oid 3040 $ "set option " <> opt' <> " is"
|
||||
checkFlags (flag:rest)
|
||||
| otherwise = checkFlags (flag:opt:rest)
|
||||
checkOptions (flag:rest) = checkFlags (flag:rest)
|
||||
@@ -310,10 +315,10 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
unless (flag' `matches` validFlagsRegex) $
|
||||
forM_ (tail flag') $ \letter ->
|
||||
when (letter `notElem` optionsSet) $
|
||||
warnMsg fid $ "set flag " <> ('-':letter:" is")
|
||||
warnMsg fid 3041 $ "set flag " <> ('-':letter:" is")
|
||||
checkOptions rest
|
||||
| beginsWithDoubleDash flag' = do
|
||||
warnMsg fid $ "set flag " <> flag' <> " is"
|
||||
warnMsg fid 3042 $ "set flag " <> flag' <> " is"
|
||||
checkOptions rest
|
||||
-- Either a word that doesn't start with a dash, or simply '--',
|
||||
-- so stop checking.
|
||||
@@ -335,44 +340,47 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
let name = fromMaybe "" $ getCommandName t
|
||||
flags = getLeadingFlags t
|
||||
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) $
|
||||
warnMsg id $ "'" ++ name ++ "' is"
|
||||
potentially $ do
|
||||
warnMsg id 3044 $ "'" ++ name ++ "' is"
|
||||
sequence_ $ do
|
||||
allowed' <- Map.lookup name allowedFlags
|
||||
allowed <- allowed'
|
||||
(word, flag) <- listToMaybe $
|
||||
filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
|
||||
return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"
|
||||
(word, flag) <- find
|
||||
(\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
|
||||
return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is"
|
||||
|
||||
when (name == "source") $ warnMsg id "'source' in place of '.' is"
|
||||
when (name == "source") $ warnMsg id 3046 "'source' in place of '.' is"
|
||||
when (name == "trap") $
|
||||
let
|
||||
check token = potentially $ do
|
||||
check token = sequence_ $ do
|
||||
str <- getLiteralString token
|
||||
let upper = map toUpper str
|
||||
return $ do
|
||||
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
|
||||
warnMsg (getId token) $ "trapping " ++ str ++ " is"
|
||||
warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is"
|
||||
when ("SIG" `isPrefixOf` upper) $
|
||||
warnMsg (getId token)
|
||||
warnMsg (getId token) 3048
|
||||
"prefixing signal names with 'SIG' is"
|
||||
when (not isDash && upper /= str) $
|
||||
warnMsg (getId token)
|
||||
warnMsg (getId token) 3049
|
||||
"using lower/mixed case for signal names is"
|
||||
in
|
||||
mapM_ check (drop 1 rest)
|
||||
|
||||
when (name == "printf") $ potentially $ do
|
||||
when (name == "printf") $ sequence_ $ do
|
||||
format <- rest !!! 0 -- flags are covered by allowedFlags
|
||||
let literal = onlyLiteralString format
|
||||
guard $ "%q" `isInfixOf` literal
|
||||
return $ warnMsg (getId format) "printf %q is"
|
||||
return $ warnMsg (getId format) 3050 "printf %q is"
|
||||
where
|
||||
unsupportedCommands = [
|
||||
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
||||
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
|
||||
"typeset"
|
||||
] ++ if not isDash then ["local"] else []
|
||||
]
|
||||
allowedFlags = Map.fromList [
|
||||
("cd", Just ["L", "P"]),
|
||||
("exec", Just []),
|
||||
@@ -389,29 +397,35 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
("unset", Just ["f", "v"]),
|
||||
("wait", Just [])
|
||||
]
|
||||
bashism t@(T_SourceCommand id src _) =
|
||||
let name = fromMaybe "" $ getCommandName src
|
||||
in when (name == "source") $ warnMsg id "'source' in place of '.' is"
|
||||
bashism (TA_Expansion _ (T_Literal id str : _)) | str `matches` radix =
|
||||
when (str `matches` radix) $ warnMsg id "arithmetic base conversion is"
|
||||
bashism t@(T_SourceCommand id src _)
|
||||
| getCommandName src == Just "source" = warnMsg id 3051 "'source' in place of '.' is"
|
||||
bashism (TA_Expansion _ (T_Literal id str : _))
|
||||
| str `matches` radix = warnMsg id 3052 "arithmetic base conversion is"
|
||||
where
|
||||
radix = mkRegex "^[0-9]+#"
|
||||
bashism _ = return ()
|
||||
|
||||
varChars="_0-9a-zA-Z"
|
||||
expansion = let re = mkRegex in [
|
||||
(re $ "^![" ++ varChars ++ "]", "indirect expansion is"),
|
||||
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
|
||||
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
|
||||
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
|
||||
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", "string indexing is"),
|
||||
(re $ "^([*@][%#]|#[@*])", "string operations on $@/$* are"),
|
||||
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")
|
||||
(re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"),
|
||||
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"),
|
||||
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"),
|
||||
(re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"),
|
||||
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", 3057, "string indexing is"),
|
||||
(re $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"),
|
||||
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is"),
|
||||
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is")
|
||||
]
|
||||
bashVars = [
|
||||
-- This list deliberately excludes $BASH_VERSION as it's often used
|
||||
-- for shell identification.
|
||||
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
||||
"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" ]
|
||||
dashVars = [ "_" ]
|
||||
@@ -456,11 +470,10 @@ checkEchoSed = ForShell [Bash, Ksh] f
|
||||
|
||||
-- This should have used backreferences, but TDFA doesn't support them
|
||||
sedRe = mkRegex "^s(.)([^\n]*)g?$"
|
||||
isSimpleSed s = fromMaybe False $ do
|
||||
[first,rest] <- matchRegex sedRe s
|
||||
let delimiters = filter (== head first) rest
|
||||
isSimpleSed s = isJust $ do
|
||||
[h:_,rest] <- matchRegex sedRe s
|
||||
let delimiters = filter (== h) rest
|
||||
guard $ length delimiters == 2
|
||||
return True
|
||||
checkIn id s =
|
||||
when (isSimpleSed s) $
|
||||
style id 2001 "See if you can use ${variable//search/replace} instead."
|
||||
@@ -487,11 +500,11 @@ checkBraceExpansionVars = ForShell [Bash] f
|
||||
T_DollarBraced {} -> return "$"
|
||||
T_DollarExpansion {} -> return "$"
|
||||
T_DollarArithmetic {} -> return "$"
|
||||
otherwise -> return "-"
|
||||
toString t = fromJust $ getLiteralStringExt literalExt t
|
||||
_ -> return "-"
|
||||
toString t = runIdentity $ getLiteralStringExt literalExt t
|
||||
isEvaled t = do
|
||||
cmd <- getClosestCommandM t
|
||||
return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval"
|
||||
return $ maybe False (`isUnqualifiedCommand` "eval") cmd
|
||||
|
||||
|
||||
prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3"
|
||||
@@ -506,13 +519,13 @@ checkMultiDimensionalArrays = ForShell [Bash] f
|
||||
case token of
|
||||
T_Assignment _ _ name (first:second:_) _ -> about second
|
||||
T_IndexedElement _ (first:second:_) _ -> about second
|
||||
T_DollarBraced {} ->
|
||||
when (isMultiDim token) $ about token
|
||||
T_DollarBraced _ _ l ->
|
||||
when (isMultiDim l) $ about token
|
||||
_ -> return ()
|
||||
about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays."
|
||||
|
||||
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
|
||||
isMultiDim t = getBracedModifier (bracedString t) `matches` re
|
||||
isMultiDim l = getBracedModifier (concat $ oversimplify l) `matches` re
|
||||
|
||||
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
|
||||
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
|
||||
|
@@ -114,6 +114,10 @@ binaryTestOps = [
|
||||
"-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="
|
||||
]
|
||||
|
||||
arithmeticBinaryTestOps = [
|
||||
"-eq", "-ne", "-lt", "-le", "-gt", "-ge"
|
||||
]
|
||||
|
||||
unaryTestOps = [
|
||||
"!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p",
|
||||
"-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n",
|
||||
@@ -131,4 +135,6 @@ shellForExecutable name =
|
||||
"ksh" -> return Ksh
|
||||
"ksh88" -> return Ksh
|
||||
"ksh93" -> return Ksh
|
||||
otherwise -> Nothing
|
||||
_ -> Nothing
|
||||
|
||||
flagsForRead = "sreu:n:N:i:p:a:t:"
|
||||
|
@@ -200,7 +200,7 @@ doReplace start end o r =
|
||||
let si = fromIntegral (start-1)
|
||||
ei = fromIntegral (end-1)
|
||||
(x, xs) = splitAt si o
|
||||
(y, z) = splitAt (ei - si) xs
|
||||
z = drop (ei - si) xs
|
||||
in
|
||||
x ++ r ++ z
|
||||
|
||||
@@ -273,10 +273,10 @@ getPrefixSum = f 0
|
||||
where
|
||||
f sum _ PSLeaf = sum
|
||||
f sum target (PSBranch pivot left right cumulative) =
|
||||
case () of
|
||||
_ | target < pivot -> f sum target left
|
||||
_ | target > pivot -> f (sum+cumulative) target right
|
||||
_ -> sum+cumulative
|
||||
case target `compare` pivot of
|
||||
LT -> f sum target left
|
||||
GT -> f (sum+cumulative) target right
|
||||
EQ -> sum+cumulative
|
||||
|
||||
-- Add a value to the Prefix Sum tree at the given index.
|
||||
-- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5
|
||||
@@ -285,17 +285,17 @@ addPSValue key value tree = if value == 0 then tree else f tree
|
||||
where
|
||||
f PSLeaf = PSBranch key PSLeaf PSLeaf value
|
||||
f (PSBranch pivot left right sum) =
|
||||
case () of
|
||||
_ | key < pivot -> PSBranch pivot (f left) right (sum + value)
|
||||
_ | key > pivot -> PSBranch pivot left (f right) sum
|
||||
_ -> PSBranch pivot left right (sum + value)
|
||||
case key `compare` pivot of
|
||||
LT -> PSBranch pivot (f left) right (sum + value)
|
||||
GT -> PSBranch pivot left (f right) sum
|
||||
EQ -> PSBranch pivot left right (sum + value)
|
||||
|
||||
prop_pstreeSumsCorrectly kvs targets =
|
||||
let
|
||||
-- Trivial O(n * m) implementation
|
||||
dumbPrefixSums :: [(Int, Int)] -> [Int] -> [Int]
|
||||
dumbPrefixSums kvs targets =
|
||||
let prefixSum target = sum . map snd . filter (\(k,v) -> k <= target) $ kvs
|
||||
let prefixSum target = sum [v | (k,v) <- kvs, k <= target]
|
||||
in map prefixSum targets
|
||||
-- PSTree O(n * log m) implementation
|
||||
smartPrefixSums :: [(Int, Int)] -> [Int] -> [Int]
|
||||
|
@@ -43,14 +43,15 @@ ltt x = trace (show x) x
|
||||
|
||||
format :: FormatterOptions -> IO Formatter
|
||||
format options = do
|
||||
didOutput <- newIORef False
|
||||
foundIssues <- newIORef False
|
||||
reportedIssues <- newIORef False
|
||||
shouldColor <- shouldOutputColor (foColorOption options)
|
||||
let color = if shouldColor then colorize else nocolor
|
||||
return Formatter {
|
||||
header = return (),
|
||||
footer = checkFooter didOutput color,
|
||||
footer = checkFooter foundIssues reportedIssues color,
|
||||
onFailure = reportFailure color,
|
||||
onResult = reportResult didOutput color
|
||||
onResult = reportResult foundIssues reportedIssues color
|
||||
}
|
||||
|
||||
|
||||
@@ -69,9 +70,10 @@ printErr :: ColorFunc -> String -> IO ()
|
||||
printErr color = hPutStrLn stderr . color bold . color red
|
||||
reportFailure color file msg = printErr color $ file ++ ": " ++ msg
|
||||
|
||||
checkFooter didOutput color = do
|
||||
output <- readIORef didOutput
|
||||
unless output $
|
||||
checkFooter foundIssues reportedIssues color = do
|
||||
found <- readIORef foundIssues
|
||||
output <- readIORef reportedIssues
|
||||
when (found && not output) $
|
||||
printErr color "Issues were detected, but none were auto-fixable. Use another format to see them."
|
||||
|
||||
type ColorFunc = (Int -> String -> String)
|
||||
@@ -79,9 +81,10 @@ data LFStatus = LinefeedMissing | LinefeedOk
|
||||
data DiffDoc a = DiffDoc String LFStatus [DiffRegion a]
|
||||
data DiffRegion a = DiffRegion (Int, Int) (Int, Int) [Diff a]
|
||||
|
||||
reportResult :: (IORef Bool) -> ColorFunc -> CheckResult -> SystemInterface IO -> IO ()
|
||||
reportResult didOutput color result sys = do
|
||||
reportResult :: (IORef Bool) -> (IORef Bool) -> ColorFunc -> CheckResult -> SystemInterface IO -> IO ()
|
||||
reportResult foundIssues reportedIssues color result sys = do
|
||||
let comments = crComments result
|
||||
unless (null comments) $ writeIORef foundIssues True
|
||||
let suggestedFixes = mapMaybe pcFix comments
|
||||
let fixmap = buildFixMap suggestedFixes
|
||||
mapM_ output $ M.toList fixmap
|
||||
@@ -91,7 +94,7 @@ reportResult didOutput color result sys = do
|
||||
case file of
|
||||
Right contents -> do
|
||||
putStrLn $ formatDoc color $ makeDiff name contents fix
|
||||
writeIORef didOutput True
|
||||
writeIORef reportedIssues True
|
||||
Left msg -> reportFailure color name msg
|
||||
|
||||
hasTrailingLinefeed str =
|
||||
|
@@ -127,7 +127,7 @@ outputForFile color sys comments = do
|
||||
let lineCount = length fileLinesList
|
||||
let fileLines = listArray (1, lineCount) fileLinesList
|
||||
let groups = groupWith lineNo comments
|
||||
mapM_ (\commentsForLine -> do
|
||||
forM_ groups $ \commentsForLine -> do
|
||||
let lineNum = fromIntegral $ lineNo (head commentsForLine)
|
||||
let line = if lineNum < 1 || lineNum > lineCount
|
||||
then ""
|
||||
@@ -136,10 +136,9 @@ outputForFile color sys comments = do
|
||||
putStrLn $ color "message" $
|
||||
"In " ++ fileName ++" line " ++ show lineNum ++ ":"
|
||||
putStrLn (color "source" line)
|
||||
mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) commentsForLine
|
||||
forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c
|
||||
putStrLn ""
|
||||
showFixedString color commentsForLine (fromIntegral lineNum) fileLines
|
||||
) groups
|
||||
|
||||
-- Pick out only the lines necessary to show a fix in action
|
||||
sliceFile :: Fix -> Array Int String -> (Fix, Array Int String)
|
||||
|
@@ -73,15 +73,15 @@ import qualified Data.Map as Map
|
||||
|
||||
|
||||
data SystemInterface m = SystemInterface {
|
||||
-- Read a file by filename, or return an error
|
||||
-- | Read a file by filename, or return an error
|
||||
siReadFile :: String -> m (Either ErrorMessage String),
|
||||
-- Given:
|
||||
-- | Given:
|
||||
-- the current script,
|
||||
-- a list of source-path annotations in effect,
|
||||
-- and a sourced file,
|
||||
-- find the sourced file
|
||||
siFindSource :: String -> [String] -> String -> m FilePath,
|
||||
-- Get the configuration file (name, contents) for a filename
|
||||
-- | Get the configuration file (name, contents) for a filename
|
||||
siGetConfig :: String -> m (Maybe (FilePath, String))
|
||||
}
|
||||
|
||||
@@ -256,9 +256,6 @@ data Replacement = Replacement {
|
||||
data InsertionPoint = InsertBefore | InsertAfter
|
||||
deriving (Show, Eq, Generic, NFData)
|
||||
|
||||
instance Ord Replacement where
|
||||
compare r1 r2 = (repStartPos r1) `compare` (repStartPos r2)
|
||||
|
||||
newReplacement = Replacement {
|
||||
repStartPos = newPosition,
|
||||
repEndPos = newPosition,
|
||||
@@ -316,10 +313,10 @@ mockedSystemInterface files = SystemInterface {
|
||||
siGetConfig = const $ return Nothing
|
||||
}
|
||||
where
|
||||
rf file =
|
||||
case filter ((== file) . fst) files of
|
||||
[] -> return $ Left "File not included in mock."
|
||||
[(_, contents)] -> return $ Right contents
|
||||
rf file = return $
|
||||
case find ((== file) . fst) files of
|
||||
Nothing -> Left "File not included in mock."
|
||||
Just (_, contents) -> Right contents
|
||||
fs _ _ file = return file
|
||||
|
||||
mockRcFile rcfile mock = mock {
|
||||
|
@@ -24,7 +24,7 @@
|
||||
module ShellCheck.Parser (parseScript, runTests) where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.ASTLib hiding (runTests)
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
|
||||
@@ -34,7 +34,7 @@ import Control.Monad.Identity
|
||||
import Control.Monad.Trans
|
||||
import Data.Char
|
||||
import Data.Functor
|
||||
import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub)
|
||||
import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub, find)
|
||||
import Data.Maybe
|
||||
import Data.Monoid
|
||||
import Debug.Trace
|
||||
@@ -87,11 +87,23 @@ extglobStart = oneOf extglobStartChars
|
||||
unicodeDoubleQuotes = "\x201C\x201D\x2033\x2036"
|
||||
unicodeSingleQuotes = "\x2018\x2019"
|
||||
|
||||
prop_spacing = isOk spacing " \\\n # Comment"
|
||||
prop_spacing1 = isOk spacing " \\\n # Comment"
|
||||
prop_spacing2 = isOk spacing "# We can continue lines with \\"
|
||||
prop_spacing3 = isWarning spacing " \\\n # --verbose=true \\"
|
||||
spacing = do
|
||||
x <- many (many1 linewhitespace <|> try (string "\\\n" >> return ""))
|
||||
x <- many (many1 linewhitespace <|> continuation)
|
||||
optional readComment
|
||||
return $ concat x
|
||||
where
|
||||
continuation = do
|
||||
try (string "\\\n")
|
||||
-- The line was continued. Warn if this next line is a comment with a trailing \
|
||||
whitespace <- many linewhitespace
|
||||
optional $ do
|
||||
x <- readComment
|
||||
when ("\\" `isSuffixOf` x) $
|
||||
parseProblem ErrorC 1143 "This backslash is part of a comment and does not continue the line."
|
||||
return whitespace
|
||||
|
||||
spacing1 = do
|
||||
spacing <- spacing
|
||||
@@ -123,8 +135,10 @@ readUnicodeQuote = do
|
||||
return $ T_Literal id [c]
|
||||
|
||||
carriageReturn = do
|
||||
parseNote ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ."
|
||||
pos <- getPosition
|
||||
char '\r'
|
||||
parseProblemAt pos ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ."
|
||||
return '\r'
|
||||
|
||||
almostSpace =
|
||||
choice [
|
||||
@@ -186,12 +200,12 @@ getNextIdSpanningTokens startTok endTok = do
|
||||
|
||||
-- Get an ID starting from the first token of the list, and ending after the last
|
||||
getNextIdSpanningTokenList list =
|
||||
if null list
|
||||
then do
|
||||
case list of
|
||||
[] -> do
|
||||
pos <- getPosition
|
||||
getNextIdBetween pos pos
|
||||
else
|
||||
getNextIdSpanningTokens (head list) (last list)
|
||||
(h:_) ->
|
||||
getNextIdSpanningTokens h (last list)
|
||||
|
||||
-- Get the span covered by an id
|
||||
getSpanForId :: Monad m => Id -> SCParser m (SourcePos, SourcePos)
|
||||
@@ -209,8 +223,13 @@ startSpan = IncompleteInterval <$> getPosition
|
||||
|
||||
endSpan (IncompleteInterval start) = do
|
||||
endPos <- getPosition
|
||||
id <- getNextIdBetween start endPos
|
||||
return id
|
||||
getNextIdBetween start endPos
|
||||
|
||||
getSpanPositionsFor m = do
|
||||
start <- getPosition
|
||||
m
|
||||
end <- getPosition
|
||||
return (start, end)
|
||||
|
||||
addToHereDocMap id list = do
|
||||
state <- getState
|
||||
@@ -246,19 +265,29 @@ addParseNote n = do
|
||||
parseNotes = n : parseNotes state
|
||||
}
|
||||
|
||||
ignoreProblemsOf p = do
|
||||
systemState <- lift . lift $ Ms.get
|
||||
p <* (lift . lift . Ms.put $ systemState)
|
||||
|
||||
shouldIgnoreCode code = do
|
||||
context <- getCurrentContexts
|
||||
checkSourced <- Mr.asks checkSourced
|
||||
return $ any (disabling checkSourced) context
|
||||
return $ any (contextItemDisablesCode checkSourced code) context
|
||||
|
||||
-- Does this item on the context stack disable warnings for 'code'?
|
||||
contextItemDisablesCode :: Bool -> Integer -> Context -> Bool
|
||||
contextItemDisablesCode alsoCheckSourced code = disabling alsoCheckSourced
|
||||
where
|
||||
disabling checkSourced item =
|
||||
case item of
|
||||
ContextAnnotation list -> any disabling' list
|
||||
ContextSource _ -> not $ checkSourced
|
||||
_ -> False
|
||||
disabling' (DisableComment n) = code == n
|
||||
disabling' (DisableComment n m) = code >= n && code < m
|
||||
disabling' _ = False
|
||||
|
||||
|
||||
|
||||
getCurrentAnnotations includeSource =
|
||||
concatMap get . takeWhile (not . isBoundary) <$> getCurrentContexts
|
||||
where
|
||||
@@ -321,16 +350,15 @@ parseProblem level code msg = do
|
||||
parseProblemAt pos level code msg
|
||||
|
||||
setCurrentContexts c = Ms.modify (\state -> state { contextStack = c })
|
||||
getCurrentContexts = contextStack <$> Ms.get
|
||||
getCurrentContexts = Ms.gets contextStack
|
||||
|
||||
popContext = do
|
||||
v <- getCurrentContexts
|
||||
if not $ null v
|
||||
then do
|
||||
let (a:r) = v
|
||||
case v of
|
||||
(a:r) -> do
|
||||
setCurrentContexts r
|
||||
return $ Just a
|
||||
else
|
||||
[] ->
|
||||
return Nothing
|
||||
|
||||
pushContext c = do
|
||||
@@ -370,16 +398,15 @@ parseNoteAtId id c l a = do
|
||||
parseNoteAtWithEnd start end c l a = addParseNote $ ParseNote start end c l a
|
||||
|
||||
--------- Convenient combinators
|
||||
thenSkip main follow = do
|
||||
r <- main
|
||||
optional follow
|
||||
return r
|
||||
thenSkip main follow = main <* optional follow
|
||||
|
||||
unexpecting s p = try $
|
||||
(try p >> fail ("Unexpected " ++ s)) <|> return ()
|
||||
|
||||
notFollowedBy2 = unexpecting ""
|
||||
|
||||
isFollowedBy p = (lookAhead . try $ p $> True) <|> return False
|
||||
|
||||
reluctantlyTill p end =
|
||||
(lookAhead (void (try end) <|> eof) >> return []) <|> do
|
||||
x <- p
|
||||
@@ -417,7 +444,7 @@ acceptButWarn parser level code note =
|
||||
|
||||
parsecBracket before after op = do
|
||||
val <- before
|
||||
(op val <* optional (after val)) <|> (after val *> fail "")
|
||||
op val `thenSkip` after val <|> (after val *> fail "")
|
||||
|
||||
swapContext contexts p =
|
||||
parsecBracket (getCurrentContexts <* setCurrentContexts contexts)
|
||||
@@ -437,6 +464,7 @@ readConditionContents single =
|
||||
readCondContents `attempting` lookAhead (do
|
||||
pos <- getPosition
|
||||
s <- readVariableName
|
||||
spacing1
|
||||
when (s `elem` commonCommands) $
|
||||
parseProblemAt pos WarningC 1014 "Use 'if cmd; then ..' to check exit code, or 'if [[ $(cmd) == .. ]]' to check output.")
|
||||
|
||||
@@ -582,9 +610,9 @@ readConditionContents single =
|
||||
return $ TC_Nullary id typ x
|
||||
)
|
||||
|
||||
checkTrailingOp x = fromMaybe (return ()) $ do
|
||||
checkTrailingOp x = sequence_ $ do
|
||||
(T_Literal id str) <- getTrailingUnquotedLiteral x
|
||||
trailingOp <- listToMaybe (filter (`isSuffixOf` str) binaryTestOps)
|
||||
trailingOp <- find (`isSuffixOf` str) binaryTestOps
|
||||
return $ parseProblemAtId id ErrorC 1108 $
|
||||
"You need a space before and after the " ++ trailingOp ++ " ."
|
||||
|
||||
@@ -910,6 +938,9 @@ prop_readCondition20 = isOk readCondition "[[ echo_rc -eq 0 ]]"
|
||||
prop_readCondition21 = isOk readCondition "[[ $1 =~ ^(a\\ b)$ ]]"
|
||||
prop_readCondition22 = isOk readCondition "[[ $1 =~ \\.a\\.(\\.b\\.)\\.c\\. ]]"
|
||||
prop_readCondition23 = isOk readCondition "[[ -v arr[$var] ]]"
|
||||
prop_readCondition25 = isOk readCondition "[[ lex.yy.c -ot program.l ]]"
|
||||
prop_readCondition26 = isOk readScript "[[ foo ]]\\\n && bar"
|
||||
prop_readCondition27 = not $ isOk readConditionCommand "[[ x ]] foo"
|
||||
readCondition = called "test expression" $ do
|
||||
opos <- getPosition
|
||||
start <- startSpan
|
||||
@@ -936,10 +967,9 @@ readCondition = called "test expression" $ do
|
||||
cpos <- getPosition
|
||||
close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here (don't wrap commands in []/[[]])"
|
||||
id <- endSpan start
|
||||
when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Did you mean ]] ?"
|
||||
when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?"
|
||||
when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Test expression was opened with double [[ but closed with single ]. Make sure they match."
|
||||
when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Test expression was opened with single [ but closed with double ]]. Make sure they match."
|
||||
spacing
|
||||
many readCmdWord -- Read and throw away remainders to get then/do warnings. Fixme?
|
||||
return $ T_Condition id typ condition
|
||||
|
||||
readAnnotationPrefix = do
|
||||
@@ -953,6 +983,7 @@ prop_readAnnotation3 = isOk readAnnotation "# shellcheck disable=SC1234 source=/
|
||||
prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=SC1234\n"
|
||||
prop_readAnnotation5 = isOk readAnnotation "# shellcheck disable=SC2002 # All cats are precious\n"
|
||||
prop_readAnnotation6 = isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\n"
|
||||
prop_readAnnotation7 = isOk readAnnotation "# shellcheck disable=SC1000,SC2000-SC3000,SC1001\n"
|
||||
readAnnotation = called "shellcheck directive" $ do
|
||||
try readAnnotationPrefix
|
||||
many1 linewhitespace
|
||||
@@ -973,12 +1004,16 @@ readAnnotationWithoutPrefix = do
|
||||
key <- many1 (letter <|> char '-')
|
||||
char '=' <|> fail "Expected '=' after directive key"
|
||||
annotations <- case key of
|
||||
"disable" -> readCode `sepBy` char ','
|
||||
"disable" -> readRange `sepBy` char ','
|
||||
where
|
||||
readRange = do
|
||||
from <- readCode
|
||||
to <- choice [ char '-' *> readCode, return $ from+1 ]
|
||||
return $ DisableComment from to
|
||||
readCode = do
|
||||
optional $ string "SC"
|
||||
int <- many1 digit
|
||||
return $ DisableComment (read int)
|
||||
return $ read int
|
||||
|
||||
"enable" -> readName `sepBy` char ','
|
||||
where
|
||||
@@ -1016,6 +1051,7 @@ readComment = do
|
||||
unexpecting "shellcheck annotation" readAnnotationPrefix
|
||||
readAnyComment
|
||||
|
||||
prop_readAnyComment = isOk readAnyComment "# Comment"
|
||||
readAnyComment = do
|
||||
char '#'
|
||||
many $ noneOf "\r\n"
|
||||
@@ -1032,14 +1068,16 @@ prop_readNormalWord9 = isOk readSubshell "(foo\\ ;\nbar)"
|
||||
prop_readNormalWord10 = isWarning readNormalWord "\x201Chello\x201D"
|
||||
prop_readNormalWord11 = isWarning readNormalWord "\x2018hello\x2019"
|
||||
prop_readNormalWord12 = isWarning readNormalWord "hello\x2018"
|
||||
readNormalWord = readNormalishWord ""
|
||||
readNormalWord = readNormalishWord "" ["do", "done", "then", "fi", "esac"]
|
||||
|
||||
readNormalishWord end = do
|
||||
readPatternWord = readNormalishWord "" ["esac"]
|
||||
|
||||
readNormalishWord end terms = do
|
||||
start <- startSpan
|
||||
pos <- getPosition
|
||||
x <- many1 (readNormalWordPart end)
|
||||
id <- endSpan start
|
||||
checkPossibleTermination pos x
|
||||
checkPossibleTermination pos x terms
|
||||
return $ T_NormalWord id x
|
||||
|
||||
readIndexSpan = do
|
||||
@@ -1059,10 +1097,10 @@ readIndexSpan = do
|
||||
id <- endSpan start
|
||||
return $ T_Literal id str
|
||||
|
||||
checkPossibleTermination pos [T_Literal _ x] =
|
||||
when (x `elem` ["do", "done", "then", "fi", "esac"]) $
|
||||
checkPossibleTermination pos [T_Literal _ x] terminators =
|
||||
when (x `elem` terminators) $
|
||||
parseProblemAt pos WarningC 1010 $ "Use semicolon or linefeed before '" ++ x ++ "' (or quote to make it literal)."
|
||||
checkPossibleTermination _ _ = return ()
|
||||
checkPossibleTermination _ _ _ = return ()
|
||||
|
||||
readNormalWordPart end = do
|
||||
notFollowedBy2 $ oneOf end
|
||||
@@ -1379,6 +1417,8 @@ readNormalEscaped = called "escaped char" $ do
|
||||
do
|
||||
next <- quotable <|> oneOf "?*@!+[]{}.,~#"
|
||||
when (next == ' ') $ checkTrailingSpaces pos <|> return ()
|
||||
-- Check if this line is followed by a commented line with a trailing backslash
|
||||
when (next == '\n') $ try . lookAhead $ void spacing
|
||||
return $ if next == '\n' then "" else [next]
|
||||
<|>
|
||||
do
|
||||
@@ -1541,7 +1581,7 @@ readDollarExpression = do
|
||||
readDollarExp = arithmetic <|> readDollarExpansion <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarVariable
|
||||
where
|
||||
arithmetic = readAmbiguous "$((" readDollarArithmetic readDollarExpansion (\pos ->
|
||||
parseNoteAt pos WarningC 1102 "Shells disambiguate $(( differently or not at all. For $(command substition), add space after $( . For $((arithmetics)), fix parsing errors.")
|
||||
parseNoteAt pos ErrorC 1102 "Shells disambiguate $(( differently or not at all. For $(command substitution), add space after $( . For $((arithmetics)), fix parsing errors.")
|
||||
|
||||
prop_readDollarSingleQuote = isOk readDollarSingleQuote "$'foo\\\'lol'"
|
||||
readDollarSingleQuote = called "$'..' expression" $ do
|
||||
@@ -1590,6 +1630,7 @@ readArithmeticExpression = called "((..)) command" $ do
|
||||
c <- readArithmeticContents
|
||||
string "))"
|
||||
id <- endSpan start
|
||||
spacing
|
||||
return (T_Arithmetic id c)
|
||||
|
||||
-- If the next characters match prefix, try two different parsers and warn if the alternate parser had to be used
|
||||
@@ -1733,8 +1774,11 @@ prop_readHereDoc14= isWarning readScript "cat << foo\nbar\nfoo \n"
|
||||
prop_readHereDoc15= isWarning readScript "cat <<foo\nbar\nfoo bar\nfoo"
|
||||
prop_readHereDoc16= isOk readScript "cat <<- ' foo'\nbar\n foo\n"
|
||||
prop_readHereDoc17= isWarning readScript "cat <<- ' foo'\nbar\n foo\n foo\n"
|
||||
prop_readHereDoc18= isOk readScript "cat <<'\"foo'\nbar\n\"foo\n"
|
||||
prop_readHereDoc20= isWarning readScript "cat << foo\n foo\n()\nfoo\n"
|
||||
prop_readHereDoc21= isOk readScript "# shellcheck disable=SC1039\ncat << foo\n foo\n()\nfoo\n"
|
||||
prop_readHereDoc22 = isWarning readScript "cat << foo\r\ncow\r\nfoo\r\n"
|
||||
prop_readHereDoc23 = isNotOk readScript "cat << foo \r\ncow\r\nfoo\r\n"
|
||||
readHereDoc = called "here document" $ do
|
||||
pos <- getPosition
|
||||
try $ string "<<"
|
||||
@@ -1753,14 +1797,19 @@ readHereDoc = called "here document" $ do
|
||||
addPendingHereDoc doc
|
||||
return doc
|
||||
where
|
||||
quotes = "\"'\\"
|
||||
unquote :: String -> (Quoted, String)
|
||||
unquote "" = (Unquoted, "")
|
||||
unquote [c] = (Unquoted, [c])
|
||||
unquote s@(cl:tl) =
|
||||
case reverse tl of
|
||||
(cr:tr) | cr == cl && cl `elem` "\"'" -> (Quoted, reverse tr)
|
||||
_ -> (if '\\' `elem` s then (Quoted, filter ((/=) '\\') s) else (Unquoted, s))
|
||||
-- Fun fact: bash considers << foo"" quoted, but not << <("foo").
|
||||
-- Instead of replicating this, just read a token and strip quotes.
|
||||
readToken = do
|
||||
str <- readStringForParser readNormalWord
|
||||
return (if any (`elem` quotes) str then Quoted else Unquoted,
|
||||
filter (not . (`elem` quotes)) str)
|
||||
|
||||
-- A here doc actually works with \r\n because the \r becomes part of the token
|
||||
crstr <- (carriageReturn >> (return $ str ++ "\r")) <|> return str
|
||||
return $ unquote crstr
|
||||
|
||||
readPendingHereDocs = do
|
||||
docs <- popPendingHereDocs
|
||||
@@ -1809,7 +1858,7 @@ readPendingHereDocs = do
|
||||
let thereIsNoTrailer = null trailingSpace && null trailer
|
||||
let leaderIsOk = null leadingSpace
|
||||
|| dashed == Dashed && leadingSpacesAreTabs
|
||||
let trailerStart = if null trailer then '\0' else head trailer
|
||||
let trailerStart = case trailer of [] -> '\0'; (h:_) -> h
|
||||
let hasTrailingSpace = not $ null trailingSpace
|
||||
let hasTrailer = not $ null trailer
|
||||
let ppt = parseProblemAt trailerPos ErrorC
|
||||
@@ -1875,14 +1924,14 @@ readPendingHereDocs = do
|
||||
debugHereDoc tokenId endToken doc
|
||||
| endToken `isInfixOf` doc =
|
||||
let lookAt line = when (endToken `isInfixOf` line) $
|
||||
parseProblemAtId tokenId ErrorC 1042 ("Close matches include '" ++ line ++ "' (!= '" ++ endToken ++ "').")
|
||||
parseProblemAtId tokenId ErrorC 1042 ("Close matches include '" ++ (e4m line) ++ "' (!= '" ++ (e4m endToken) ++ "').")
|
||||
in do
|
||||
parseProblemAtId tokenId ErrorC 1041 ("Found '" ++ endToken ++ "' further down, but not on a separate line.")
|
||||
parseProblemAtId tokenId ErrorC 1041 ("Found '" ++ (e4m endToken) ++ "' further down, but not on a separate line.")
|
||||
mapM_ lookAt (lines doc)
|
||||
| map toLower endToken `isInfixOf` map toLower doc =
|
||||
parseProblemAtId tokenId ErrorC 1043 ("Found " ++ endToken ++ " further down, but with wrong casing.")
|
||||
parseProblemAtId tokenId ErrorC 1043 ("Found " ++ (e4m endToken) ++ " further down, but with wrong casing.")
|
||||
| otherwise =
|
||||
parseProblemAtId tokenId ErrorC 1044 ("Couldn't find end token `" ++ endToken ++ "' in the here document.")
|
||||
parseProblemAtId tokenId ErrorC 1044 ("Couldn't find end token `" ++ (e4m endToken) ++ "' in the here document.")
|
||||
|
||||
|
||||
readFilename = readNormalWord
|
||||
@@ -1938,8 +1987,6 @@ readIoRedirect = do
|
||||
spacing
|
||||
return $ T_FdRedirect id n redir
|
||||
|
||||
readRedirectList = many1 readIoRedirect
|
||||
|
||||
prop_readHereString = isOk readHereString "<<< \"Hello $world\""
|
||||
readHereString = called "here string" $ do
|
||||
start <- startSpan
|
||||
@@ -2035,7 +2082,11 @@ readSimpleCommand = called "simple command" $ do
|
||||
|
||||
Just cmd -> do
|
||||
validateCommand cmd
|
||||
suffix <- option [] $ getParser readCmdSuffix cmd [
|
||||
-- We have to ignore possible parsing problems from the lookAhead parser
|
||||
firstArgument <- ignoreProblemsOf . optionMaybe . try . lookAhead $ readCmdWord
|
||||
suffix <- option [] $ getParser readCmdSuffix
|
||||
-- If `export` or other modifier commands are called with `builtin` we have to look at the first argument
|
||||
(if isCommand ["builtin"] cmd then fromMaybe cmd firstArgument else cmd) [
|
||||
(["declare", "export", "local", "readonly", "typeset"], readModifierSuffix),
|
||||
(["time"], readTimeSuffix),
|
||||
(["let"], readLetSuffix),
|
||||
@@ -2058,10 +2109,6 @@ readSimpleCommand = called "simple command" $ do
|
||||
then action
|
||||
else getParser def cmd rest
|
||||
|
||||
cStyleComment cmd =
|
||||
case cmd of
|
||||
_ -> False
|
||||
|
||||
validateCommand cmd =
|
||||
case cmd of
|
||||
(T_NormalWord _ [T_Literal _ "//"]) -> commentWarning (getId cmd)
|
||||
@@ -2098,14 +2145,14 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
|
||||
let file = getFile file' rest'
|
||||
override <- getSourceOverride
|
||||
let literalFile = do
|
||||
name <- override `mplus` getLiteralString file
|
||||
name <- override `mplus` getLiteralString file `mplus` stripDynamicPrefix file
|
||||
-- Hack to avoid 'source ~/foo' trying to read from literal tilde
|
||||
guard . not $ "~/" `isPrefixOf` name
|
||||
return name
|
||||
case literalFile of
|
||||
Nothing -> do
|
||||
parseNoteAtId (getId file) WarningC 1090
|
||||
"Can't follow non-constant source. Use a directive to specify location."
|
||||
"ShellCheck can't follow non-constant source. Use a directive to specify location."
|
||||
return t
|
||||
Just filename -> do
|
||||
proceed <- shouldFollow filename
|
||||
@@ -2158,6 +2205,16 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
|
||||
SourcePath x -> Just x
|
||||
_ -> Nothing
|
||||
|
||||
-- If the word has a single expansion as the directory, try stripping it
|
||||
-- This affects `$foo/bar` but not `${foo}-dir/bar` or `/foo/$file`
|
||||
stripDynamicPrefix word =
|
||||
case getWordParts word of
|
||||
exp : rest | isStringExpansion exp -> do
|
||||
str <- getLiteralString (T_NormalWord (Id 0) rest)
|
||||
guard $ "/" `isPrefixOf` str
|
||||
return $ "." ++ str
|
||||
_ -> Nothing
|
||||
|
||||
subRead name script =
|
||||
withContext (ContextSource name) $
|
||||
inSeparateContext $
|
||||
@@ -2255,6 +2312,7 @@ readPipe = do
|
||||
|
||||
readCommand = choice [
|
||||
readCompoundCommand,
|
||||
readConditionCommand,
|
||||
readCoProc,
|
||||
readSimpleCommand
|
||||
]
|
||||
@@ -2283,6 +2341,7 @@ prop_readIfClause2 = isWarning readIfClause "if false; then; echo oo; fi"
|
||||
prop_readIfClause3 = isWarning readIfClause "if false; then true; else; echo lol; fi"
|
||||
prop_readIfClause4 = isWarning readIfClause "if false; then true; else if true; then echo lol; fi; fi"
|
||||
prop_readIfClause5 = isOk readIfClause "if false; then true; else\nif true; then echo lol; fi; fi"
|
||||
prop_readIfClause6 = isWarning readIfClause "if true\nthen\nDo the thing\nfi"
|
||||
readIfClause = called "if expression" $ do
|
||||
start <- startSpan
|
||||
pos <- getPosition
|
||||
@@ -2366,6 +2425,7 @@ readSubshell = called "explicit subshell" $ do
|
||||
allspacing
|
||||
char ')' <|> fail "Expected ) closing the subshell"
|
||||
id <- endSpan start
|
||||
spacing
|
||||
return $ T_Subshell id list
|
||||
|
||||
prop_readBraceGroup = isOk readBraceGroup "{ a; b | c | d; e; }"
|
||||
@@ -2386,6 +2446,7 @@ readBraceGroup = called "brace group" $ do
|
||||
parseProblem ErrorC 1056 "Expected a '}'. If you have one, try a ; or \\n in front of it."
|
||||
fail "Missing '}'"
|
||||
id <- endSpan start
|
||||
spacing
|
||||
return $ T_BraceGroup id list
|
||||
|
||||
prop_readBatsTest = isOk readBatsTest "@test 'can parse' {\n true\n}"
|
||||
@@ -2438,10 +2499,16 @@ readDoGroup kwId = do
|
||||
parseProblemAtId (getId doKw) ErrorC 1061 "Couldn't find 'done' for this 'do'."
|
||||
parseProblem ErrorC 1062 "Expected 'done' matching previously mentioned 'do'."
|
||||
return "Expected 'done'"
|
||||
|
||||
optional . lookAhead $ do
|
||||
pos <- getPosition
|
||||
try $ string "<("
|
||||
parseProblemAt pos ErrorC 1142 "Use 'done < <(cmd)' to redirect from process substitution (currently missing one '<')."
|
||||
return commands
|
||||
|
||||
|
||||
prop_readForClause = isOk readForClause "for f in *; do rm \"$f\"; done"
|
||||
prop_readForClause1 = isOk readForClause "for f in *; { rm \"$f\"; }"
|
||||
prop_readForClause3 = isOk readForClause "for f; do foo; done"
|
||||
prop_readForClause4 = isOk readForClause "for((i=0; i<10; i++)); do echo $i; done"
|
||||
prop_readForClause5 = isOk readForClause "for ((i=0;i<10 && n>x;i++,--n))\ndo \necho $i\ndone"
|
||||
@@ -2459,19 +2526,31 @@ readForClause = called "for loop" $ do
|
||||
readArithmetic id <|> readRegular id
|
||||
where
|
||||
readArithmetic id = called "arithmetic for condition" $ do
|
||||
try $ string "(("
|
||||
readArithmeticDelimiter '(' "Missing second '(' to start arithmetic for ((;;)) loop"
|
||||
x <- readArithmeticContents
|
||||
char ';' >> spacing
|
||||
y <- readArithmeticContents
|
||||
char ';' >> spacing
|
||||
z <- readArithmeticContents
|
||||
spacing
|
||||
string "))"
|
||||
readArithmeticDelimiter ')' "Missing second ')' to terminate 'for ((;;))' loop condition"
|
||||
spacing
|
||||
optional $ readSequentialSep >> spacing
|
||||
group <- readBraced <|> readDoGroup id
|
||||
return $ T_ForArithmetic id x y z group
|
||||
|
||||
-- For c='(' read "((" and be lenient about spaces
|
||||
readArithmeticDelimiter c msg = do
|
||||
char c
|
||||
startPos <- getPosition
|
||||
sp <- spacing
|
||||
endPos <- getPosition
|
||||
char c <|> do
|
||||
parseProblemAt startPos ErrorC 1137 msg
|
||||
fail ""
|
||||
unless (null sp) $
|
||||
parseProblemAtWithEnd startPos endPos ErrorC 1138 $ "Remove spaces between " ++ [c,c] ++ " in arithmetic for loop."
|
||||
|
||||
readBraced = do
|
||||
(T_BraceGroup _ list) <- readBraceGroup
|
||||
return list
|
||||
@@ -2481,7 +2560,7 @@ readForClause = called "for loop" $ do
|
||||
"Don't use $ on the iterator name in for loops."
|
||||
name <- readVariableName `thenSkip` allspacing
|
||||
values <- readInClause <|> (optional readSequentialSep >> return [])
|
||||
group <- readDoGroup id
|
||||
group <- readBraced <|> readDoGroup id
|
||||
return $ T_ForIn id name values group
|
||||
|
||||
prop_readSelectClause1 = isOk readSelectClause "select foo in *; do echo $foo; done"
|
||||
@@ -2519,6 +2598,7 @@ prop_readCaseClause2 = isOk readCaseClause "case foo\n in * ) echo bar;; esac"
|
||||
prop_readCaseClause3 = isOk readCaseClause "case foo\n in * ) echo bar & ;; esac"
|
||||
prop_readCaseClause4 = isOk readCaseClause "case foo\n in *) echo bar ;& bar) foo; esac"
|
||||
prop_readCaseClause5 = isOk readCaseClause "case foo\n in *) echo bar;;& foo) baz;; esac"
|
||||
prop_readCaseClause6 = isOk readCaseClause "case foo\n in if) :;; done) :;; esac"
|
||||
readCaseClause = called "case expression" $ do
|
||||
start <- startSpan
|
||||
g_Case
|
||||
@@ -2601,7 +2681,7 @@ readFunctionDefinition = called "function" $ do
|
||||
|
||||
readWithoutFunction = try $ do
|
||||
name <- many1 functionChars
|
||||
guard $ name /= "time" -- Interfers with time ( foo )
|
||||
guard $ name /= "time" -- Interferes with time ( foo )
|
||||
spacing
|
||||
readParens
|
||||
return $ \id -> T_Function id (FunctionKeyword False) (FunctionParentheses True) name
|
||||
@@ -2641,17 +2721,45 @@ readCoProc = called "coproc" $ do
|
||||
id <- endSpan start
|
||||
return $ T_CoProcBody id body
|
||||
|
||||
readPattern = (readPatternWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing)
|
||||
|
||||
readPattern = (readNormalWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing)
|
||||
prop_readConditionCommand = isOk readConditionCommand "[[ x ]] > foo 2>&1"
|
||||
readConditionCommand = do
|
||||
cmd <- readCondition
|
||||
redirs <- many readIoRedirect
|
||||
id <- getNextIdSpanningTokenList (cmd:redirs)
|
||||
|
||||
pos <- getPosition
|
||||
hasDashAo <- isFollowedBy $ do
|
||||
c <- choice $ try . string <$> ["-o", "-a", "or", "and"]
|
||||
posEnd <- getPosition
|
||||
parseProblemAtWithEnd pos posEnd ErrorC 1139 $
|
||||
"Use " ++ alt c ++ " instead of '" ++ c ++ "' between test commands."
|
||||
|
||||
-- If the next word is a keyword, readNormalWord will trigger a warning
|
||||
hasKeyword <- isFollowedBy readKeyword
|
||||
hasWord <- isFollowedBy readNormalWord
|
||||
|
||||
when (hasWord && not (hasKeyword || hasDashAo)) $ do
|
||||
-- We have other words following, and no error has been emitted.
|
||||
posEnd <- getPosition
|
||||
parseProblemAtWithEnd pos posEnd ErrorC 1140 "Unexpected parameters after condition. Missing &&/||, or bad expression?"
|
||||
|
||||
return $ T_Redirecting id redirs cmd
|
||||
where
|
||||
alt "or" = "||"
|
||||
alt "-o" = "||"
|
||||
alt "and" = "&&"
|
||||
alt "-a" = "&&"
|
||||
alt _ = "|| or &&"
|
||||
|
||||
prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null"
|
||||
readCompoundCommand = do
|
||||
cmd <- choice [
|
||||
readBraceGroup,
|
||||
readAmbiguous "((" readArithmeticExpression readSubshell (\pos ->
|
||||
parseNoteAt pos WarningC 1105 "Shells disambiguate (( differently or not at all. For subshell, add spaces around ( . For ((, fix parsing errors."),
|
||||
parseNoteAt pos ErrorC 1105 "Shells disambiguate (( differently or not at all. For subshell, add spaces around ( . For ((, fix parsing errors."),
|
||||
readSubshell,
|
||||
readCondition,
|
||||
readWhileClause,
|
||||
readUntilClause,
|
||||
readIfClause,
|
||||
@@ -2661,15 +2769,15 @@ readCompoundCommand = do
|
||||
readBatsTest,
|
||||
readFunctionDefinition
|
||||
]
|
||||
spacing
|
||||
redirs <- many readIoRedirect
|
||||
id <- getNextIdSpanningTokenList (cmd:redirs)
|
||||
unless (null redirs) $ optional $ do
|
||||
lookAhead $ try (spacing >> needsSeparator)
|
||||
parseProblem WarningC 1013 "Bash requires ; or \\n here, after redirecting nested compound commands."
|
||||
optional . lookAhead $ do
|
||||
notFollowedBy2 $ choice [readKeyword, g_Lbrace]
|
||||
pos <- getPosition
|
||||
many1 readNormalWord
|
||||
posEnd <- getPosition
|
||||
parseProblemAtWithEnd pos posEnd ErrorC 1141 "Unexpected tokens after compound command. Bad redirection or missing ;/&&/||/|?"
|
||||
return $ T_Redirecting id redirs cmd
|
||||
where
|
||||
needsSeparator = choice [ g_Then, g_Else, g_Elif, g_Fi, g_Do, g_Done, g_Esac, g_Rbrace ]
|
||||
|
||||
|
||||
readCompoundList = readTerm
|
||||
@@ -2739,17 +2847,13 @@ readLiteralForParser parser = do
|
||||
|
||||
prop_readAssignmentWord = isOk readAssignmentWord "a=42"
|
||||
prop_readAssignmentWord2 = isOk readAssignmentWord "b=(1 2 3)"
|
||||
prop_readAssignmentWord3 = isWarning readAssignmentWord "$b = 13"
|
||||
prop_readAssignmentWord4 = isWarning readAssignmentWord "b = $(lol)"
|
||||
prop_readAssignmentWord5 = isOk readAssignmentWord "b+=lol"
|
||||
prop_readAssignmentWord6 = isWarning readAssignmentWord "b += (1 2 3)"
|
||||
prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42"
|
||||
prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42"
|
||||
prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "
|
||||
prop_readAssignmentWord9a= isOk readAssignmentWord "foo="
|
||||
prop_readAssignmentWord9b= isOk readAssignmentWord "foo= "
|
||||
prop_readAssignmentWord9c= isOk readAssignmentWord "foo= #bar"
|
||||
prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42"
|
||||
prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
|
||||
prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'"
|
||||
prop_readAssignmentWord13= isOk readAssignmentWord "var=( (1 2) (3 4) )"
|
||||
@@ -2757,52 +2861,63 @@ prop_readAssignmentWord14= isOk readAssignmentWord "var=( 1 [2]=(3 4) )"
|
||||
prop_readAssignmentWord15= isOk readAssignmentWord "var=(1 [2]=(3 4))"
|
||||
readAssignmentWord = readAssignmentWordExt True
|
||||
readWellFormedAssignment = readAssignmentWordExt False
|
||||
readAssignmentWordExt lenient = try $ do
|
||||
start <- startSpan
|
||||
pos <- getPosition
|
||||
when lenient $
|
||||
optional (char '$' >> parseNote ErrorC 1066 "Don't use $ on the left side of assignments.")
|
||||
variable <- readVariableName
|
||||
when lenient $
|
||||
optional (readNormalDollar >> parseNoteAt pos ErrorC
|
||||
1067 "For indirection, use arrays, declare \"var$n=value\", or (for sh) read/eval.")
|
||||
indices <- many readArrayIndex
|
||||
hasLeftSpace <- fmap (not . null) spacing
|
||||
pos <- getPosition
|
||||
id <- endSpan start
|
||||
op <- readAssignmentOp
|
||||
readAssignmentWordExt lenient = called "variable assignment" $ do
|
||||
-- Parse up to and including the = in a 'try'
|
||||
(id, variable, op, indices) <- try $ do
|
||||
start <- startSpan
|
||||
pos <- getPosition
|
||||
-- Check for a leading $ at parse time, to warn for $foo=(bar) which
|
||||
-- would otherwise cause a parse failure so it can't be checked later.
|
||||
leadingDollarPos <-
|
||||
if lenient
|
||||
then optionMaybe $ getSpanPositionsFor (char '$')
|
||||
else return Nothing
|
||||
variable <- readVariableName
|
||||
indices <- many readArrayIndex
|
||||
hasLeftSpace <- fmap (not . null) spacing
|
||||
opStart <- getPosition
|
||||
id <- endSpan start
|
||||
op <- readAssignmentOp
|
||||
opEnd <- getPosition
|
||||
|
||||
when (isJust leadingDollarPos || hasLeftSpace) $ do
|
||||
hasParen <- isFollowedBy (spacing >> char '(')
|
||||
when hasParen $
|
||||
sequence_ $ do
|
||||
(l, r) <- leadingDollarPos
|
||||
return $ parseProblemAtWithEnd l r ErrorC 1066 "Don't use $ on the left side of assignments."
|
||||
|
||||
-- Fail so that this is not parsed as an assignment.
|
||||
fail ""
|
||||
-- At this point we know for sure.
|
||||
return (id, variable, op, indices)
|
||||
|
||||
rightPosStart <- getPosition
|
||||
hasRightSpace <- fmap (not . null) spacing
|
||||
rightPosEnd <- getPosition
|
||||
isEndOfCommand <- fmap isJust $ optionMaybe (try . lookAhead $ (void (oneOf "\r\n;&|)") <|> eof))
|
||||
if not hasLeftSpace && (hasRightSpace || isEndOfCommand)
|
||||
|
||||
if hasRightSpace || isEndOfCommand
|
||||
then do
|
||||
when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $
|
||||
parseNoteAt pos WarningC 1007
|
||||
when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $ do
|
||||
parseProblemAtWithEnd rightPosStart rightPosEnd WarningC 1007
|
||||
"Remove space after = if trying to assign a value (for empty string, use var='' ... )."
|
||||
value <- readEmptyLiteral
|
||||
return $ T_Assignment id op variable indices value
|
||||
else do
|
||||
when (hasLeftSpace || hasRightSpace) $
|
||||
parseNoteAt pos ErrorC 1068 $
|
||||
"Don't put spaces around the "
|
||||
++ (if op == Append
|
||||
then "+= when appending"
|
||||
else "= in assignments")
|
||||
++ " (or quote to make it literal)."
|
||||
optional $ do
|
||||
lookAhead $ char '='
|
||||
parseProblem ErrorC 1097 "Unexpected ==. For assignment, use =. For comparison, use [/[[. Or quote for literal string."
|
||||
|
||||
value <- readArray <|> readNormalWord
|
||||
spacing
|
||||
return $ T_Assignment id op variable indices value
|
||||
where
|
||||
readAssignmentOp = do
|
||||
pos <- getPosition
|
||||
unexpecting "" $ string "==="
|
||||
-- This is probably some kind of ascii art border
|
||||
unexpecting "===" (string "===")
|
||||
choice [
|
||||
string "+=" >> return Append,
|
||||
do
|
||||
try (string "==")
|
||||
parseProblemAt pos ErrorC 1097
|
||||
"Unexpected ==. For assignment, use =. For comparison, use [/[[."
|
||||
return Assign,
|
||||
|
||||
string "=" >> return Assign
|
||||
]
|
||||
|
||||
@@ -2867,6 +2982,7 @@ redirToken c t = try $ do
|
||||
|
||||
tryWordToken s t = tryParseWordToken s t `thenSkip` spacing
|
||||
tryParseWordToken keyword t = try $ do
|
||||
pos <- getPosition
|
||||
start <- startSpan
|
||||
str <- anycaseString keyword
|
||||
id <- endSpan start
|
||||
@@ -2882,9 +2998,10 @@ tryParseWordToken keyword t = try $ do
|
||||
_ -> return ()
|
||||
|
||||
lookAhead keywordSeparator
|
||||
when (str /= keyword) $
|
||||
parseProblem ErrorC 1081 $
|
||||
"Scripts are case sensitive. Use '" ++ keyword ++ "', not '" ++ str ++ "'."
|
||||
when (str /= keyword) $ do
|
||||
parseProblemAt pos ErrorC 1081 $
|
||||
"Scripts are case sensitive. Use '" ++ keyword ++ "', not '" ++ str ++ "' (or quote if literal)."
|
||||
fail ""
|
||||
return $ t id
|
||||
|
||||
anycaseString =
|
||||
@@ -3067,7 +3184,7 @@ readConfigFile filename = do
|
||||
let line = "line " ++ (show . sourceLine $ errorPos err)
|
||||
suggestion = getStringFromParsec $ errorMessages err
|
||||
in
|
||||
"Failed to process " ++ filename ++ ", " ++ line ++ ": "
|
||||
"Failed to process " ++ (e4m filename) ++ ", " ++ line ++ ": "
|
||||
++ suggestion
|
||||
|
||||
prop_readConfigKVs1 = isOk readConfigKVs "disable=1234"
|
||||
@@ -3088,6 +3205,7 @@ prop_readScript2 = isWarning readScript "#!/bin/bash\r\necho hello world\n"
|
||||
prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world"
|
||||
prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=("
|
||||
prop_readScript5 = isOk readScript "#!/bin/bash\n#This is an empty script\n\n"
|
||||
prop_readScript6 = isOk readScript "#!/usr/bin/env -S X=FOO bash\n#This is an empty script\n\n"
|
||||
readScriptFile sourced = do
|
||||
start <- startSpan
|
||||
pos <- getPosition
|
||||
@@ -3113,8 +3231,8 @@ readScriptFile sourced = do
|
||||
let ignoreShebang = shellAnnotationSpecified || shellFlagSpecified
|
||||
|
||||
unless ignoreShebang $
|
||||
verifyShebang pos (getShell shebangString)
|
||||
if ignoreShebang || isValidShell (getShell shebangString) /= Just False
|
||||
verifyShebang pos (executableFromShebang shebangString)
|
||||
if ignoreShebang || isValidShell (executableFromShebang shebangString) /= Just False
|
||||
then do
|
||||
commands <- withAnnotations annotations readCompoundListOrEmpty
|
||||
id <- endSpan start
|
||||
@@ -3128,16 +3246,6 @@ readScriptFile sourced = do
|
||||
return $ T_Script id shebang []
|
||||
|
||||
where
|
||||
basename s = reverse . takeWhile (/= '/') . reverse $ s
|
||||
getShell sb =
|
||||
case words sb of
|
||||
[] -> ""
|
||||
[x] -> basename x
|
||||
(first:second:_) ->
|
||||
if basename first == "env"
|
||||
then second
|
||||
else basename first
|
||||
|
||||
verifyShebang pos s = do
|
||||
case isValidShell s of
|
||||
Just True -> return ()
|
||||
@@ -3145,7 +3253,7 @@ readScriptFile sourced = do
|
||||
Nothing -> parseProblemAt pos ErrorC 1008 "This shebang was unrecognized. ShellCheck only supports sh/bash/dash/ksh. Add a 'shell' directive to specify."
|
||||
|
||||
isValidShell s =
|
||||
let good = s == "" || any (`isPrefixOf` s) goodShells
|
||||
let good = null s || any (`isPrefixOf` s) goodShells
|
||||
bad = any (`isPrefixOf` s) badShells
|
||||
in
|
||||
if good
|
||||
@@ -3290,16 +3398,21 @@ parseShell env name contents = do
|
||||
prRoot = Just $
|
||||
reattachHereDocs script (hereDocMap userstate)
|
||||
}
|
||||
Left err ->
|
||||
Left err -> do
|
||||
let context = contextStack state
|
||||
return newParseResult {
|
||||
prComments =
|
||||
map toPositionedComment $
|
||||
notesForContext (contextStack state)
|
||||
++ [makeErrorFor err]
|
||||
(filter (not . isIgnored context) $
|
||||
notesForContext context
|
||||
++ [makeErrorFor err])
|
||||
++ parseProblems state,
|
||||
prTokenPositions = Map.empty,
|
||||
prRoot = Nothing
|
||||
}
|
||||
where
|
||||
-- A final pass for ignoring parse errors after failed parsing
|
||||
isIgnored stack note = any (contextItemDisablesCode False (codeForParseNote note)) stack
|
||||
|
||||
notesForContext list = zipWith ($) [first, second] $ filter isName list
|
||||
where
|
||||
@@ -3425,4 +3538,3 @@ tryWithErrors parser = do
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
||||
|
||||
|
@@ -11,21 +11,34 @@ command -v cabal ||
|
||||
|
||||
cabal update ||
|
||||
die "can't update"
|
||||
cabal install --dependencies-only --enable-tests ||
|
||||
die "can't install dependencies"
|
||||
cabal configure --enable-tests ||
|
||||
|
||||
if [ -e /etc/arch-release ]
|
||||
then
|
||||
# Arch has an unconventional packaging setup
|
||||
flags=(--disable-library-vanilla --enable-shared --enable-executable-dynamic --ghc-options=-dynamic)
|
||||
else
|
||||
flags=()
|
||||
fi
|
||||
|
||||
cabal install --dependencies-only --enable-tests "${flags[@]}" ||
|
||||
cabal install --dependencies-only "${flags[@]}" ||
|
||||
die "can't install dependencies"
|
||||
cabal configure --enable-tests "${flags[@]}" ||
|
||||
die "configure failed"
|
||||
cabal build ||
|
||||
die "build failed"
|
||||
cabal test ||
|
||||
die "test failed"
|
||||
|
||||
dist/build/shellcheck/shellcheck - << 'EOF' || die "execution failed"
|
||||
sc="$(find . -name shellcheck -type f -perm -111)"
|
||||
[ -x "$sc" ] || die "Can't find executable"
|
||||
|
||||
"$sc" - << 'EOF' || die "execution failed"
|
||||
#!/bin/sh
|
||||
echo "Hello World"
|
||||
EOF
|
||||
|
||||
dist/build/shellcheck/shellcheck - << 'EOF' && die "negative execution failed"
|
||||
"$sc" - << 'EOF' && die "negative execution failed"
|
||||
#!/bin/sh
|
||||
echo $1
|
||||
EOF
|
||||
|
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2257
|
||||
|
||||
failed=0
|
||||
fail() {
|
||||
@@ -8,7 +9,7 @@ fail() {
|
||||
|
||||
if git diff | grep -q ""
|
||||
then
|
||||
fail "There are uncommited changes"
|
||||
fail "There are uncommitted changes"
|
||||
fi
|
||||
|
||||
current=$(git tag --points-at)
|
||||
|
@@ -17,13 +17,13 @@ and is still highly experimental.
|
||||
Make sure you're plugged in and have screen/tmux in place,
|
||||
then re-run with $0 --run to continue.
|
||||
|
||||
Also note that 'dist' will be deleted.
|
||||
Also note that dist* will be deleted.
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
echo "Deleting 'dist'..."
|
||||
rm -rf dist
|
||||
echo "Deleting 'dist' and 'dist-newstyle'..."
|
||||
rm -rf dist dist-newstyle
|
||||
|
||||
log=$(mktemp) || die "Can't create temp file"
|
||||
date >> "$log" || die "Can't write to log"
|
||||
@@ -61,20 +61,16 @@ done << EOF
|
||||
debian:stable apt-get update && apt-get install -y cabal-install
|
||||
debian:testing apt-get update && apt-get install -y cabal-install
|
||||
ubuntu:latest apt-get update && apt-get install -y cabal-install
|
||||
haskell:latest true
|
||||
opensuse/leap:latest zypper install -y cabal-install ghc
|
||||
fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils
|
||||
archlinux/base:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
|
||||
|
||||
# Other Ubuntu versions we want to support
|
||||
ubuntu:19.04 apt-get update && apt-get install -y cabal-install
|
||||
ubuntu:18.10 apt-get update && apt-get install -y cabal-install
|
||||
# Other versions we want to support
|
||||
ubuntu:18.04 apt-get update && apt-get install -y cabal-install
|
||||
|
||||
# Misc Haskell including current and latest Stack build
|
||||
ubuntu:18.10 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
|
||||
haskell:latest true
|
||||
|
||||
# Known to currently fail
|
||||
centos:latest yum install -y epel-release && yum install -y cabal-install
|
||||
fedora:latest dnf install -y cabal-install
|
||||
archlinux/base:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
|
||||
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
|
||||
|
||||
exit "$final"
|
||||
|
@@ -4,6 +4,7 @@ import Control.Monad
|
||||
import System.Exit
|
||||
import qualified ShellCheck.Analytics
|
||||
import qualified ShellCheck.AnalyzerLib
|
||||
import qualified ShellCheck.ASTLib
|
||||
import qualified ShellCheck.Checker
|
||||
import qualified ShellCheck.Checks.Commands
|
||||
import qualified ShellCheck.Checks.Custom
|
||||
@@ -17,6 +18,7 @@ main = do
|
||||
results <- sequence [
|
||||
ShellCheck.Analytics.runTests
|
||||
,ShellCheck.AnalyzerLib.runTests
|
||||
,ShellCheck.ASTLib.runTests
|
||||
,ShellCheck.Checker.runTests
|
||||
,ShellCheck.Checks.Commands.runTests
|
||||
,ShellCheck.Checks.Custom.runTests
|
||||
|
Reference in New Issue
Block a user