mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 00:39:19 +08:00
Compare commits
157 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
4f5dc7094b | ||
|
670c1de01b | ||
|
b9b6975bfa | ||
|
8bb5e01401 | ||
|
15ff87cf80 | ||
|
dff8f9492a | ||
|
2e5c56b270 | ||
|
9584266a8b | ||
|
5fbaae2bb3 | ||
|
fbb14d6b38 | ||
|
2cfd1f2714 | ||
|
953d9bc56d | ||
|
e272fa04ee | ||
|
81e84c2939 | ||
|
34939ca0b7 | ||
|
e7820479f0 | ||
|
8480563672 | ||
|
dfbcc9595e | ||
|
2c0766825e | ||
|
cb4f4e7edc | ||
|
0607039d41 | ||
|
46f177b5be | ||
|
eaccd3d02c | ||
|
35033a9f2f | ||
|
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 | ||
|
00574dd1fc | ||
|
a82e606e8d | ||
|
fdd02c94c0 | ||
|
9423691039 |
@@ -1,73 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
build_linux() {
|
|
||||||
# Linux Docker image
|
|
||||||
name="$DOCKER_BASE"
|
|
||||||
DOCKER_BUILDS="$DOCKER_BUILDS $name"
|
|
||||||
docker build -t "$name:current" .
|
|
||||||
docker run "$name:current" --version
|
|
||||||
printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
|
|
||||||
docker run -v "$PWD:/mnt" "$name:current" myscript
|
|
||||||
|
|
||||||
# Copy static executable from docker image
|
|
||||||
id=$(docker create "$name:current")
|
|
||||||
docker cp "$id:/bin/shellcheck" "shellcheck"
|
|
||||||
docker rm "$id"
|
|
||||||
ls -l shellcheck
|
|
||||||
./shellcheck myscript
|
|
||||||
for tag in $TAGS
|
|
||||||
do
|
|
||||||
cp "shellcheck" "deploy/shellcheck-$tag.linux-x86_64";
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
build_aarch64() {
|
|
||||||
# Linux aarch64 static executable
|
|
||||||
docker run -v "$PWD:/mnt" koalaman/aarch64-builder 'buildsc'
|
|
||||||
for tag in $TAGS
|
|
||||||
do
|
|
||||||
cp "shellcheck" "deploy/shellcheck-$tag.linux-aarch64"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
build_armv6hf() {
|
|
||||||
# Linux armv6hf static executable
|
|
||||||
docker run -v "$PWD:/mnt" koalaman/armv6hf-builder -c 'compile-shellcheck'
|
|
||||||
for tag in $TAGS
|
|
||||||
do
|
|
||||||
cp "shellcheck" "deploy/shellcheck-$tag.linux-armv6hf";
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
build_windows() {
|
|
||||||
# Windows .exe
|
|
||||||
docker run -v "$PWD:/appdata" koalaman/winghc cuib
|
|
||||||
for tag in $TAGS
|
|
||||||
do
|
|
||||||
cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe";
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
build_osx() {
|
|
||||||
# Darwin x86_64 executable
|
|
||||||
brew update
|
|
||||||
brew install cabal-install pandoc gnu-tar
|
|
||||||
sudo ln -sf /usr/local/bin/gsha512sum /usr/local/bin/sha512sum
|
|
||||||
sudo ln -sf /usr/local/bin/gtar /usr/local/bin/tar
|
|
||||||
export PATH="/usr/local/bin:$PATH"
|
|
||||||
|
|
||||||
cabal update
|
|
||||||
cabal install --dependencies-only
|
|
||||||
cabal build shellcheck
|
|
||||||
|
|
||||||
# Cabal 3 no longer has a predictable output path
|
|
||||||
path="$(find . -name 'shellcheck' -type f -perm +111)"
|
|
||||||
[[ -e "$path" ]]
|
|
||||||
|
|
||||||
for tag in $TAGS
|
|
||||||
do
|
|
||||||
cp "$path" "deploy/shellcheck-$tag.darwin-x86_64";
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
4
.github/ISSUE_TEMPLATE.md
vendored
4
.github/ISSUE_TEMPLATE.md
vendored
@@ -2,10 +2,10 @@
|
|||||||
- Rule Id (if any, e.g. SC1000):
|
- Rule Id (if any, e.g. SC1000):
|
||||||
- My shellcheck version (`shellcheck --version` or "online"):
|
- My shellcheck version (`shellcheck --version` or "online"):
|
||||||
- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
|
- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
|
||||||
- [ ] I tried on shellcheck.net and verified that this is still a problem on the latest commit
|
- [ ] I tried on https://www.shellcheck.net/ and verified that this is still a problem on the latest commit
|
||||||
|
|
||||||
#### For new checks and feature suggestions
|
#### For new checks and feature suggestions
|
||||||
- [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this
|
- [ ] https://www.shellcheck.net/ (i.e. the latest commit) currently gives no useful warnings about this
|
||||||
- [ ] I searched through https://github.com/koalaman/shellcheck/issues and didn't find anything related
|
- [ ] I searched through https://github.com/koalaman/shellcheck/issues and didn't find anything related
|
||||||
|
|
||||||
|
|
||||||
|
124
.github/workflows/build.yml
vendored
Normal file
124
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
name: Build Lol
|
||||||
|
|
||||||
|
# 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/*.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 )
|
@@ -2,39 +2,10 @@
|
|||||||
set -x
|
set -x
|
||||||
shopt -s extglob
|
shopt -s extglob
|
||||||
|
|
||||||
if [[ "$TRAVIS_SECURE_ENV_VARS" != "true" ]]
|
|
||||||
then
|
|
||||||
echo >&2 "Missing TRAVIS_SECURE_ENV_VARS. Skipping GitHub deployment."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
install_deps() {
|
|
||||||
version="2.7.0" # 2.14.1 fails to overwrite duplicates
|
|
||||||
case "$(uname)" in
|
|
||||||
Linux)
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install curl
|
|
||||||
curl -L "https://github.com/github/hub/releases/download/v$version/hub-linux-amd64-$version.tgz" | tar xvz --strip-components=1 "hub-linux-amd64-$version/bin/hub"
|
|
||||||
;;
|
|
||||||
Darwin)
|
|
||||||
curl -L "https://github.com/github/hub/releases/download/v$version/hub-darwin-amd64-$version.tgz" | tar xvz --strip-components=1 "hub-darwin-amd64-$version/bin/hub"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown: $(uname)"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
hub_path="$PWD/bin/hub"
|
|
||||||
hub() {
|
|
||||||
"$hub_path" "$@"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
install_deps
|
|
||||||
|
|
||||||
export EDITOR="touch"
|
export EDITOR="touch"
|
||||||
|
|
||||||
# Sanity check
|
# Sanity check
|
||||||
|
gh --version || exit 1
|
||||||
hub release show latest || exit 1
|
hub release show latest || exit 1
|
||||||
|
|
||||||
for tag in $TAGS
|
for tag in $TAGS
|
||||||
@@ -50,8 +21,9 @@ do
|
|||||||
for file in deploy/*
|
for file in deploy/*
|
||||||
do
|
do
|
||||||
[[ $file == *.@(xz|gz|zip) ]] || continue
|
[[ $file == *.@(xz|gz|zip) ]] || continue
|
||||||
files+=(-a "$file")
|
[[ $file == *"$tag"* ]] || continue
|
||||||
|
files+=("$file")
|
||||||
done
|
done
|
||||||
hub release edit "${files[@]}" "$tag" || exit 1
|
gh release upload "$tag" "${files[@]}" --clobber || exit 1
|
||||||
done
|
done
|
||||||
|
|
||||||
|
@@ -1,12 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# This script builds and deploys multi-architecture docker images from the
|
# This script builds and deploys multi-architecture docker images from the
|
||||||
# binaries previously built and deployed to GCS by the Travis pipeline.
|
# binaries previously built and deployed to GitHub.
|
||||||
|
|
||||||
if [[ "$TRAVIS_SECURE_ENV_VARS" != "true" ]]
|
|
||||||
then
|
|
||||||
echo >&2 "Missing TRAVIS_SECURE_ENV_VARS. Skipping Docker builds."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
function multi_arch_docker::install_docker_buildx() {
|
function multi_arch_docker::install_docker_buildx() {
|
||||||
# Install up-to-date version of docker, with buildx support.
|
# Install up-to-date version of docker, with buildx support.
|
||||||
@@ -108,6 +102,5 @@ function multi_arch_docker::main() {
|
|||||||
multi_arch_docker::install_docker_buildx
|
multi_arch_docker::install_docker_buildx
|
||||||
multi_arch_docker::login_to_docker_hub
|
multi_arch_docker::login_to_docker_hub
|
||||||
multi_arch_docker::build_and_push_all
|
multi_arch_docker::build_and_push_all
|
||||||
set +x
|
|
||||||
multi_arch_docker::test_all
|
multi_arch_docker::test_all
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# This script packages up Travis compiled binaries
|
# This script packages up compiled binaries
|
||||||
set -ex
|
set -ex
|
||||||
shopt -s nullglob
|
shopt -s nullglob extglob
|
||||||
cd deploy
|
|
||||||
|
ls -l
|
||||||
|
|
||||||
cp ../LICENSE LICENSE.txt
|
cp ../LICENSE LICENSE.txt
|
||||||
sed -e $'s/$/\r/' > README.txt << END
|
sed -e $'s/$/\r/' > README.txt << END
|
||||||
@@ -22,44 +23,32 @@ This binary was compiled on $(date -u).
|
|||||||
$(git log -n 3)
|
$(git log -n 3)
|
||||||
END
|
END
|
||||||
|
|
||||||
for file in ./*.exe
|
for dir in */
|
||||||
do
|
do
|
||||||
zip "${file%.*}.zip" README.txt LICENSE.txt "$file"
|
cp LICENSE.txt README.txt "$dir"
|
||||||
done
|
done
|
||||||
|
|
||||||
for file in *.linux-x86_64
|
echo "Tags are $TAGS"
|
||||||
do
|
|
||||||
base="${file%.*}"
|
|
||||||
cp "$file" "shellcheck"
|
|
||||||
tar -cJf "$base.linux.x86_64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
|
|
||||||
rm "shellcheck"
|
|
||||||
done
|
|
||||||
|
|
||||||
for file in *.linux-aarch64
|
for tag in $TAGS
|
||||||
do
|
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
|
for dir in windows.*/
|
||||||
do
|
do
|
||||||
base="${file%.*}"
|
( cd "$dir" && zip "../shellcheck-$tag.zip" * )
|
||||||
cp "$file" "shellcheck"
|
done
|
||||||
tar -cJf "$base.linux.armv6hf.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
|
|
||||||
rm "shellcheck"
|
|
||||||
done
|
|
||||||
|
|
||||||
for file in *.darwin-x86_64
|
for dir in {linux,darwin}.*/
|
||||||
do
|
do
|
||||||
base="${file%.*}"
|
base="${dir%/}"
|
||||||
cp "$file" "shellcheck"
|
( cd "$dir" && tar -cJf "../shellcheck-$tag.$base.tar.xz" --transform="s:^:shellcheck-$tag/:" * )
|
||||||
tar -cJf "$base.darwin.x86_64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
|
done
|
||||||
rm "shellcheck"
|
|
||||||
done
|
done
|
||||||
|
|
||||||
for file in ./*
|
for file in ./*
|
||||||
do
|
do
|
||||||
|
[[ -f "$file" ]] || continue
|
||||||
sha512sum "$file" > "$file.sha512sum"
|
sha512sum "$file" > "$file.sha512sum"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
ls -l
|
||||||
|
64
.travis.yml
64
.travis.yml
@@ -1,64 +0,0 @@
|
|||||||
language: shell
|
|
||||||
os: linux
|
|
||||||
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
include:
|
|
||||||
- stage: Build
|
|
||||||
|
|
||||||
# This must weirdly not have a dash, otherwise an empty job is created
|
|
||||||
env: BUILD=linux
|
|
||||||
- env: BUILD=windows
|
|
||||||
- env: BUILD=armv6hf
|
|
||||||
- env: BUILD=aarch64
|
|
||||||
- env: BUILD=osx
|
|
||||||
os: osx
|
|
||||||
|
|
||||||
- stage: Deploy docker image
|
|
||||||
# Deploy only for pushes to master branch, not other branches, not PRs.
|
|
||||||
if: type = push
|
|
||||||
script:
|
|
||||||
- source ./.multi_arch_docker
|
|
||||||
- set -ex; multi_arch_docker::main; set +x
|
|
||||||
|
|
||||||
# This is in global context and runs for every stage that doesn't override it.
|
|
||||||
before_install: |
|
|
||||||
DOCKER_BASE="$DOCKER_USERNAME/shellcheck"
|
|
||||||
DOCKER_BUILDS=""
|
|
||||||
export TAGS=""
|
|
||||||
test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
|
|
||||||
test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
|
|
||||||
echo "Tags are $TAGS"
|
|
||||||
|
|
||||||
# This is in global context and runs for every stage that doesn't override it.
|
|
||||||
script:
|
|
||||||
- mkdir -p deploy
|
|
||||||
- source ./.compile_binaries
|
|
||||||
- ./striptests
|
|
||||||
- set -ex; build_"$BUILD"; set +x;
|
|
||||||
- ./.prepare_deploy
|
|
||||||
- ./.github_deploy
|
|
||||||
|
|
||||||
# This is in global context and runs for every stage that doesn't override it.
|
|
||||||
after_failure: |
|
|
||||||
id
|
|
||||||
pwd
|
|
||||||
df -h
|
|
||||||
find . -name '*.log' -type f -exec grep "" /dev/null {} +
|
|
||||||
find . -ls
|
|
||||||
|
|
||||||
# This is in global context and runs for every stage that doesn't override it.
|
|
||||||
deploy:
|
|
||||||
provider: gcs
|
|
||||||
skip_cleanup: true
|
|
||||||
access_key_id: GOOG7MDN7WEH6IIGBDCA
|
|
||||||
secret_access_key:
|
|
||||||
secure: Bcx2cT0/E2ikj7sdamVq52xlLZF9dz9ojGPtoKfPyQhkkZa+McVI4xgUSuyyoSxyKj77sofx2y8m6PJYYumT4g5hREV1tfeUkl0J2DQFMbGDYEt7kxVkXCxojNvhHwTzLFv0ezstrxWWxQm81BfQQ4U9lggRXtndAP4czZnOeHPINPSiue1QNwRAEw05r5UoIUJXy/5xyUrjIxn381pAs+gJqP2COeN9kTKYH53nS/AAws29RprfZFnPlo7xxWmcjRcdS5KPdGXI/c6tQp5zl2iTh510VC1PN2w1Wvnn/oNWhiNdqPyVDsojIX5+sS3nejzJA+KFMxXSBlyXIY3wPpS/MdscU79X6Q5f9ivsFfsm7gNBmxHUPNn0HAvU4ROT/CCE9j6jSbs5PC7QBo3CK4++jxAwE/pd9HUc2rs3k0ofx3rgveJ7txpy5yPKfwIIBi98kVKlC4w7dLvNTOfjW1Imt2yH87XTfsE0UIG9st1WII6s4l/WgBx2GuwKdt6+3QUYiAlCFckkxWi+fAvpHZUEL43Qxub5fN+ZV7Zib1n7opchH4QKGBb6/y0WaDCmtCfu0lppoe/TH6saOTjDFj67NJSElK6ZDxGZ3uw4R+ret2gm6WRKT2Oeub8J33VzSa7VkmFpMPrAAfPa9N1Z4ewBLoTmvxSg2A0dDrCdJio=
|
|
||||||
bucket: shellcheck-private
|
|
||||||
local_dir: deploy
|
|
||||||
on:
|
|
||||||
repo: koalaman/shellcheck
|
|
||||||
condition: $TRAVIS_BUILD_STAGE_NAME = Build
|
|
||||||
all_branches: true
|
|
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,3 +1,29 @@
|
|||||||
|
## Git
|
||||||
|
### Added
|
||||||
|
- `disable` directives can now be a range, e.g. `disable=SC3000-SC4000`
|
||||||
|
- 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
|
||||||
|
- Optional avoid-x-comparisons: Style warning SC2268 for `[ x$var = xval ]`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- SC1072/SC1073 now respond to disable annotations, though ignoring parse errors
|
||||||
|
is still purely cosmetic and does not allow ShellCheck to continue.
|
||||||
|
- Improved error reporting for trailing tokens after ]/]] and compound commands
|
||||||
|
- `#!/usr/bin/env -S shell` is now handled correctly
|
||||||
|
- Here docs with \r are now parsed correctly and give better warnings
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Assignments are now parsed to spec, without leniency for leading $ or spaces
|
||||||
|
- POSIX/dash unsupported feature warnings now have individual SC3xxx codes
|
||||||
|
- SC1090: A leading `$x/` or `$(x)/` is now treated as `./` when locating files
|
||||||
|
- SC2154: Variables appearing in -z/-n tests are no longer considered unassigned
|
||||||
|
- SC2270-SC2285: Improved warnings about misused =, e.g. `${var}=42`
|
||||||
|
|
||||||
|
|
||||||
## v0.7.1 - 2020-04-04
|
## v0.7.1 - 2020-04-04
|
||||||
### Fixed
|
### Fixed
|
||||||
- `-f diff` no longer claims that it found more issues when it didn't
|
- `-f diff` no longer claims that it found more issues when it didn't
|
||||||
|
29
Dockerfile
29
Dockerfile
@@ -1,29 +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 shellcheck.hs ./
|
|
||||||
COPY src src
|
|
||||||
RUN cabal build Paths_ShellCheck && \
|
|
||||||
ghc -optl-static -optl-pthread -isrc -idist/build/autogen --make shellcheck -split-sections -optc-Wl,--gc-sections -optlo-Os && \
|
|
||||||
strip --strip-all shellcheck
|
|
||||||
|
|
||||||
RUN mkdir -p /out/bin && \
|
|
||||||
cp shellcheck /out/bin/
|
|
||||||
|
|
||||||
# Resulting ShellCheck image
|
|
||||||
FROM scratch
|
|
||||||
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
|
|
||||||
WORKDIR /mnt
|
|
||||||
COPY --from=build /out /
|
|
||||||
ENTRYPOINT ["/bin/shellcheck"]
|
|
19
README.md
19
README.md
@@ -109,7 +109,8 @@ Services and platforms that have ShellCheck pre-installed and ready to use:
|
|||||||
* [Codacy](https://www.codacy.com/)
|
* [Codacy](https://www.codacy.com/)
|
||||||
* [Code Climate](https://codeclimate.com/)
|
* [Code Climate](https://codeclimate.com/)
|
||||||
* [Code Factor](https://www.codefactor.io/)
|
* [Code Factor](https://www.codefactor.io/)
|
||||||
* [Github](https://github.com/features/actions)(only Linux)
|
* [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck)
|
||||||
|
* [Github](https://github.com/features/actions) (only Linux)
|
||||||
|
|
||||||
Services and platforms with third party plugins:
|
Services and platforms with third party plugins:
|
||||||
|
|
||||||
@@ -148,7 +149,7 @@ On Arch Linux based distros:
|
|||||||
|
|
||||||
pacman -S shellcheck
|
pacman -S shellcheck
|
||||||
|
|
||||||
or get the dependency free [shellcheck-static](https://aur.archlinux.org/packages/shellcheck-static/) from the AUR.
|
or get the dependency free [shellcheck-bin](https://aur.archlinux.org/packages/shellcheck-bin/) from the AUR.
|
||||||
|
|
||||||
On Gentoo based distros:
|
On Gentoo based distros:
|
||||||
|
|
||||||
@@ -167,10 +168,14 @@ On FreeBSD:
|
|||||||
|
|
||||||
pkg install hs-ShellCheck
|
pkg install hs-ShellCheck
|
||||||
|
|
||||||
On OS X with homebrew:
|
On macOS (OS X) with Homebrew:
|
||||||
|
|
||||||
brew install shellcheck
|
brew install shellcheck
|
||||||
|
|
||||||
|
Or with MacPorts:
|
||||||
|
|
||||||
|
sudo port install shellcheck
|
||||||
|
|
||||||
On OpenBSD:
|
On OpenBSD:
|
||||||
|
|
||||||
pkg_add shellcheck
|
pkg_add shellcheck
|
||||||
@@ -197,6 +202,10 @@ Or Windows (via [scoop](http://scoop.sh)):
|
|||||||
C:\> scoop install shellcheck
|
C:\> scoop install shellcheck
|
||||||
```
|
```
|
||||||
|
|
||||||
|
From [conda-forge](https://anaconda.org/conda-forge/shellcheck):
|
||||||
|
|
||||||
|
conda install -c conda-forge shellcheck
|
||||||
|
|
||||||
From Snap Store:
|
From Snap Store:
|
||||||
|
|
||||||
snap install --channel=edge shellcheck
|
snap install --channel=edge shellcheck
|
||||||
@@ -220,7 +229,7 @@ Alternatively, you can download pre-compiled binaries for the latest release her
|
|||||||
* [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
|
* [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
|
||||||
* [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
|
* [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
|
||||||
* [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)
|
* [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)
|
||||||
* [MacOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz)
|
* [macOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz)
|
||||||
* [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip)
|
* [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip)
|
||||||
|
|
||||||
or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
|
or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
|
||||||
@@ -264,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`).
|
ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `brew`, `emerge`, `yum`, or `zypper`).
|
||||||
|
|
||||||
On MacOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source.
|
On macOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source.
|
||||||
|
|
||||||
$ brew install cabal-install
|
$ brew install cabal-install
|
||||||
|
|
||||||
|
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 $CABALOPTS --lib Diff-0.4.0 base-compat-0.11.2 base-orphans-0.8.4 dlist-1.0 hashable-1.3.0.0 indexed-traversable-0.1.1 integer-logarithms-1.0.3.1 primitive-0.7.1.0 regex-base-0.94.0.0 splitmix-0.1.0.3 tagged-0.8.6.1 th-abstraction-0.4.2.0 transformers-compat-0.6.6 base-compat-batteries-0.11.2 time-compat-1.9.5 unordered-containers-0.2.13.0 data-fix-0.3.1 vector-0.12.2.0 scientific-0.3.6.2 regex-tdfa-1.3.1.0 random-1.2.0 distributive-0.6.2.1 attoparsec-0.13.2.5 uuid-types-1.0.3 comonad-5.0.8 bifunctors-5.5.10 assoc-1.0.2 these-1.1.1.1 strict-0.4.0.1 aeson-1.5.5.1
|
||||||
|
|
||||||
|
# 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
|
||||||
|
./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 $CABALOPTS --lib Diff-0.4.0 base-compat-0.11.2 base-orphans-0.8.4 dlist-1.0 hashable-1.3.0.0 indexed-traversable-0.1.1 integer-logarithms-1.0.3.1 primitive-0.7.1.0 regex-base-0.94.0.0 splitmix-0.1.0.3 tagged-0.8.6.1 th-abstraction-0.4.2.0 transformers-compat-0.6.6 base-compat-batteries-0.11.2 time-compat-1.9.5 unordered-containers-0.2.13.0 data-fix-0.3.1 vector-0.12.2.0 scientific-0.3.6.2 regex-tdfa-1.3.1.0 random-1.2.0 distributive-0.6.2.1 attoparsec-0.13.2.5 uuid-types-1.0.3 comonad-5.0.8 bifunctors-5.5.10 assoc-1.0.2 these-1.1.1.1 strict-0.4.0.1 aeson-1.5.5.1
|
||||||
|
|
||||||
|
# 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
|
||||||
|
./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 --lib $CABALOPTS Diff-0.4.0 base-compat-0.11.2 base-orphans-0.8.4 dlist-1.0 hashable-1.3.1.0 indexed-traversable-0.1.1 integer-logarithms-1.0.3.1 primitive-0.7.1.0 regex-base-0.94.0.1 splitmix-0.1.0.3 tagged-0.8.6.1 th-abstraction-0.4.2.0 transformers-compat-0.6.6 base-compat-batteries-0.11.2 time-compat-1.9.5 unordered-containers-0.2.13.0 data-fix-0.3.1 vector-0.12.2.0 scientific-0.3.6.2 regex-tdfa-1.3.1.0 random-1.2.0 distributive-0.6.2.1 attoparsec-0.13.2.5 uuid-types-1.0.4 comonad-5.0.8 bifunctors-5.5.10 assoc-1.0.2 these-1.1.1.1 strict-0.4.0.1 aeson-1.5.6.0
|
||||||
|
|
||||||
|
# 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
|
||||||
|
./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
|
30
build/linux.ppc64le/Dockerfile
Normal file
30
build/linux.ppc64le/Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
FROM ubuntu:20.04
|
||||||
|
|
||||||
|
ENV TARGET powerpc64le-linux-gnu
|
||||||
|
ENV TARGETNAME linux.ppc64le
|
||||||
|
|
||||||
|
# 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" --enable-unregisterised
|
||||||
|
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;-optl-Wl,-fuse-ld=bfd -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 $CABALOPTS --lib Diff-0.4.0 base-compat-0.11.2 base-orphans-0.8.4 dlist-1.0 hashable-1.3.0.0 indexed-traversable-0.1.1 integer-logarithms-1.0.3.1 primitive-0.7.1.0 regex-base-0.94.0.0 splitmix-0.1.0.3 tagged-0.8.6.1 th-abstraction-0.4.2.0 transformers-compat-0.6.6 base-compat-batteries-0.11.2 time-compat-1.9.5 unordered-containers-0.2.13.0 data-fix-0.3.1 vector-0.12.2.0 scientific-0.3.6.2 regex-tdfa-1.3.1.0 random-1.2.0 distributive-0.6.2.1 attoparsec-0.13.2.5 uuid-types-1.0.3 comonad-5.0.8 bifunctors-5.5.10 assoc-1.0.2 these-1.1.1.1 strict-0.4.0.1 aeson-1.5.5.1
|
||||||
|
|
||||||
|
# Copy the build script
|
||||||
|
COPY build /usr/bin
|
||||||
|
|
||||||
|
WORKDIR /scratch
|
||||||
|
ENTRYPOINT ["/usr/bin/build"]
|
15
build/linux.ppc64le/build
Executable file
15
build/linux.ppc64le/build
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -xe
|
||||||
|
{
|
||||||
|
tar xzv --strip-components=1
|
||||||
|
./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-ppc64le-static "$TARGETNAME/shellcheck" --version
|
||||||
|
} >&2
|
||||||
|
tar czv "$TARGETNAME"
|
1
build/linux.ppc64le/tag
Normal file
1
build/linux.ppc64le/tag
Normal file
@@ -0,0 +1 @@
|
|||||||
|
koalaman/scbuilder-linux-ppc64le
|
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
|
||||||
|
./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
|
||||||
|
|
||||||
|
# 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. This list is just copied from `cabal build`
|
||||||
|
RUN wine /haskell/bin/cabal.exe update && IFS=';' && wine /haskell/bin/cabal.exe install $CABALOPTS --lib Diff-0.4.0 base-compat-0.11.2 base-orphans-0.8.4 dlist-1.0 hashable-1.3.0.0 indexed-traversable-0.1.1 integer-logarithms-1.0.3.1 primitive-0.7.1.0 regex-base-0.94.0.0 splitmix-0.1.0.3 tagged-0.8.6.1 th-abstraction-0.4.2.0 transformers-compat-0.6.6 base-compat-batteries-0.11.2 time-compat-1.9.5 unordered-containers-0.2.13.0 data-fix-0.3.1 vector-0.12.2.0 scientific-0.3.6.2 regex-tdfa-1.3.1.0 random-1.2.0 distributive-0.6.2.1 attoparsec-0.13.2.5 uuid-types-1.0.3 comonad-5.0.8 bifunctors-5.5.10 assoc-1.0.2 these-1.1.1.1 strict-0.4.0.1 aeson-1.5.5.1
|
||||||
|
|
||||||
|
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
|
||||||
|
./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
|
@@ -6,7 +6,7 @@ then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for i in 1 2
|
for i in 1 2 3
|
||||||
do
|
do
|
||||||
last=$(grep -hv "^prop" ./**/*.hs | grep -Ewo "${i}[0-9]{3}" | sort -n | tail -n 1)
|
last=$(grep -hv "^prop" ./**/*.hs | grep -Ewo "${i}[0-9]{3}" | sort -n | tail -n 1)
|
||||||
echo "Next ${i}xxx: $((last+1))"
|
echo "Next ${i}xxx: $((last+1))"
|
||||||
|
@@ -232,7 +232,8 @@ Valid keys are:
|
|||||||
**disable**
|
**disable**
|
||||||
: Disables a comma separated list of error codes for the following command.
|
: Disables a comma separated list of error codes for the following command.
|
||||||
The command can be a simple command like `echo foo`, or a compound command
|
The command can be a simple command like `echo foo`, or a compound command
|
||||||
like a function definition, subshell block or loop.
|
like a function definition, subshell block or loop. A range can be
|
||||||
|
be specified with a dash, e.g. `disable=SC3000-SC4000` to exclude 3xxx.
|
||||||
|
|
||||||
**enable**
|
**enable**
|
||||||
: Enable an optional check by name, as listed with **--list-optional**.
|
: Enable an optional check by name, as listed with **--list-optional**.
|
||||||
@@ -254,7 +255,7 @@ Valid keys are:
|
|||||||
**shell**
|
**shell**
|
||||||
: Overrides the shell detected from the shebang. This is useful for
|
: Overrides the shell detected from the shebang. This is useful for
|
||||||
files meant to be included (and thus lacking a shebang), or possibly
|
files meant to be included (and thus lacking a shebang), or possibly
|
||||||
as a more targeted alternative to 'disable=2039'.
|
as a more targeted alternative to 'disable=SC2039'.
|
||||||
|
|
||||||
# RC FILES
|
# RC FILES
|
||||||
|
|
||||||
|
@@ -507,7 +507,7 @@ ioInterface options files = do
|
|||||||
where
|
where
|
||||||
find filename deflt = do
|
find filename deflt = do
|
||||||
sources <- findM ((allowable inputs) `andM` doesFileExist) $
|
sources <- findM ((allowable inputs) `andM` doesFileExist) $
|
||||||
(adjustPath filename):(map (</> filename) $ map adjustPath $ sourcePathFlag ++ sourcePathAnnotation)
|
(adjustPath filename):(map ((</> filename) . adjustPath) $ sourcePathFlag ++ sourcePathAnnotation)
|
||||||
case sources of
|
case sources of
|
||||||
Nothing -> return deflt
|
Nothing -> return deflt
|
||||||
Just first -> return first
|
Just first -> return first
|
||||||
|
@@ -31,6 +31,8 @@ apps:
|
|||||||
shellcheck:
|
shellcheck:
|
||||||
command: usr/bin/shellcheck
|
command: usr/bin/shellcheck
|
||||||
plugs: [home, removable-media]
|
plugs: [home, removable-media]
|
||||||
|
environment:
|
||||||
|
LANG: C.UTF-8
|
||||||
|
|
||||||
parts:
|
parts:
|
||||||
shellcheck:
|
shellcheck:
|
||||||
|
@@ -145,7 +145,7 @@ data InnerToken t =
|
|||||||
deriving (Show, Eq, Functor, Foldable, Traversable)
|
deriving (Show, Eq, Functor, Foldable, Traversable)
|
||||||
|
|
||||||
data Annotation =
|
data Annotation =
|
||||||
DisableComment Integer
|
DisableComment Integer Integer -- [from, to)
|
||||||
| EnableComment String
|
| EnableComment String
|
||||||
| SourceOverride String
|
| SourceOverride String
|
||||||
| ShellOverride String
|
| ShellOverride String
|
||||||
|
@@ -28,6 +28,10 @@ import Data.Functor
|
|||||||
import Data.Functor.Identity
|
import Data.Functor.Identity
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import Numeric (showHex)
|
||||||
|
|
||||||
|
arguments (T_SimpleCommand _ _ (cmd:args)) = args
|
||||||
|
|
||||||
-- Is this a type of loop?
|
-- Is this a type of loop?
|
||||||
isLoop t = case t of
|
isLoop t = case t of
|
||||||
@@ -134,13 +138,95 @@ isUnquotedFlag token = fromMaybe False $ do
|
|||||||
str <- getLeadingUnquotedString token
|
str <- getLeadingUnquotedString token
|
||||||
return $ "-" `isPrefixOf` str
|
return $ "-" `isPrefixOf` str
|
||||||
|
|
||||||
-- Given a T_DollarBraced, return a simplified version of the string contents.
|
-- getGnuOpts "erd:u:" will parse a list of arguments tokens like `read`
|
||||||
bracedString (T_DollarBraced _ _ l) = concat $ oversimplify l
|
-- -re -d : -u 3 bar
|
||||||
bracedString _ = error "Internal shellcheck error, please report! (bracedString on non-variable)"
|
-- 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?
|
-- Is this an expansion of multiple items of an array?
|
||||||
isArrayExpansion t@(T_DollarBraced _ _ _) =
|
isArrayExpansion (T_DollarBraced _ _ l) =
|
||||||
let string = bracedString t in
|
let string = concat $ oversimplify l in
|
||||||
"@" `isPrefixOf` string ||
|
"@" `isPrefixOf` string ||
|
||||||
not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string
|
not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string
|
||||||
isArrayExpansion _ = False
|
isArrayExpansion _ = False
|
||||||
@@ -148,8 +234,8 @@ isArrayExpansion _ = False
|
|||||||
-- Is it possible that this arg becomes multiple args?
|
-- Is it possible that this arg becomes multiple args?
|
||||||
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
|
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
|
||||||
where
|
where
|
||||||
f t@(T_DollarBraced _ _ _) =
|
f (T_DollarBraced _ _ l) =
|
||||||
let string = bracedString t in
|
let string = concat $ oversimplify l in
|
||||||
"!" `isPrefixOf` string
|
"!" `isPrefixOf` string
|
||||||
f (T_DoubleQuoted _ parts) = any f parts
|
f (T_DoubleQuoted _ parts) = any f parts
|
||||||
f (T_NormalWord _ parts) = any f parts
|
f (T_NormalWord _ parts) = any f parts
|
||||||
@@ -193,6 +279,12 @@ getUnquotedLiteral (T_NormalWord _ list) =
|
|||||||
str _ = Nothing
|
str _ = Nothing
|
||||||
getUnquotedLiteral _ = Nothing
|
getUnquotedLiteral _ = Nothing
|
||||||
|
|
||||||
|
isQuotes t =
|
||||||
|
case t of
|
||||||
|
T_DoubleQuoted {} -> True
|
||||||
|
T_SingleQuoted {} -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
-- Get the last unquoted T_Literal in a word like "${var}foo"THIS
|
-- Get the last unquoted T_Literal in a word like "${var}foo"THIS
|
||||||
-- or nothing if the word does not end in an unquoted literal.
|
-- or nothing if the word does not end in an unquoted literal.
|
||||||
getTrailingUnquotedLiteral :: Token -> Maybe Token
|
getTrailingUnquotedLiteral :: Token -> Maybe Token
|
||||||
@@ -211,8 +303,11 @@ getTrailingUnquotedLiteral t =
|
|||||||
getLeadingUnquotedString :: Token -> Maybe String
|
getLeadingUnquotedString :: Token -> Maybe String
|
||||||
getLeadingUnquotedString t =
|
getLeadingUnquotedString t =
|
||||||
case t of
|
case t of
|
||||||
T_NormalWord _ ((T_Literal _ s) : _) -> return s
|
T_NormalWord _ ((T_Literal _ s) : rest) -> return $ s ++ from rest
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
where
|
||||||
|
from ((T_Literal _ s):rest) = s ++ from rest
|
||||||
|
from _ = ""
|
||||||
|
|
||||||
-- Maybe get the literal string of this token and any globs in it.
|
-- Maybe get the literal string of this token and any globs in it.
|
||||||
getGlobOrLiteralString = getLiteralStringExt f
|
getGlobOrLiteralString = getLiteralStringExt f
|
||||||
@@ -273,6 +368,37 @@ getLiteralStringExt more = g
|
|||||||
-- Is this token a string literal?
|
-- Is this token a string literal?
|
||||||
isLiteral t = isJust $ getLiteralString t
|
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]
|
-- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz]
|
||||||
getWordParts (T_NormalWord _ l) = concatMap getWordParts l
|
getWordParts (T_NormalWord _ l) = concatMap getWordParts l
|
||||||
@@ -301,7 +427,7 @@ getCommand t =
|
|||||||
|
|
||||||
-- Maybe get the command name string of a token representing a command
|
-- Maybe get the command name string of a token representing a command
|
||||||
getCommandName :: Token -> Maybe String
|
getCommandName :: Token -> Maybe String
|
||||||
getCommandName = fst . getCommandNameAndToken
|
getCommandName = fst . getCommandNameAndToken False
|
||||||
|
|
||||||
-- Maybe get the name+arguments of a command.
|
-- Maybe get the name+arguments of a command.
|
||||||
getCommandArgv t = do
|
getCommandArgv t = do
|
||||||
@@ -311,20 +437,38 @@ getCommandArgv t = do
|
|||||||
-- Get the command name token from a command, i.e.
|
-- Get the command name token from a command, i.e.
|
||||||
-- the token representing 'ls' in 'ls -la 2> foo'.
|
-- the token representing 'ls' in 'ls -la 2> foo'.
|
||||||
-- If it can't be determined, return the original token.
|
-- If it can't be determined, return the original token.
|
||||||
getCommandTokenOrThis = snd . getCommandNameAndToken
|
getCommandTokenOrThis = snd . getCommandNameAndToken False
|
||||||
|
|
||||||
getCommandNameAndToken :: Token -> (Maybe String, Token)
|
-- Given a command, get the string and token that represents the command name.
|
||||||
getCommandNameAndToken t = fromMaybe (Nothing, t) $ do
|
-- If direct, return the actual command (e.g. exec in 'exec ls')
|
||||||
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
|
-- If not, return the logical command (e.g. 'ls' in 'exec ls')
|
||||||
|
|
||||||
|
getCommandNameAndToken :: Bool -> Token -> (Maybe String, Token)
|
||||||
|
getCommandNameAndToken direct t = fromMaybe (Nothing, t) $ do
|
||||||
|
cmd@(T_SimpleCommand _ _ (w:rest)) <- getCommand t
|
||||||
s <- getLiteralString w
|
s <- getLiteralString w
|
||||||
if "busybox" `isSuffixOf` s || "builtin" == s
|
return $ fromMaybe (Just s, w) $ do
|
||||||
then
|
guard $ not direct
|
||||||
case rest of
|
actual <- getEffectiveCommandToken s cmd rest
|
||||||
(applet:_) -> return (getLiteralString applet, applet)
|
return (getLiteralString actual, actual)
|
||||||
_ -> return (Just s, w)
|
where
|
||||||
else
|
getEffectiveCommandToken str cmd args =
|
||||||
return (Just s, w)
|
let
|
||||||
|
firstArg = do
|
||||||
|
arg <- listToMaybe args
|
||||||
|
guard . not $ isFlag arg
|
||||||
|
return arg
|
||||||
|
in
|
||||||
|
case str of
|
||||||
|
"busybox" -> firstArg
|
||||||
|
"builtin" -> firstArg
|
||||||
|
"command" -> firstArg
|
||||||
|
"run" -> firstArg -- Used by bats
|
||||||
|
"exec" -> do
|
||||||
|
opts <- getBsdOpts "cla:" args
|
||||||
|
(_, (t, _)) <- find (null . fst) opts
|
||||||
|
return t
|
||||||
|
_ -> fail ""
|
||||||
|
|
||||||
-- If a command substitution is a single command, get its name.
|
-- If a command substitution is a single command, get its name.
|
||||||
-- $(date +%s) = Just "date"
|
-- $(date +%s) = Just "date"
|
||||||
@@ -341,8 +485,8 @@ getCommandNameFromExpansion t =
|
|||||||
|
|
||||||
-- Get the basename of a token representing a command
|
-- Get the basename of a token representing a command
|
||||||
getCommandBasename = fmap basename . getCommandName
|
getCommandBasename = fmap basename . getCommandName
|
||||||
where
|
|
||||||
basename = reverse . takeWhile (/= '/') . reverse
|
basename = reverse . takeWhile (/= '/') . reverse
|
||||||
|
|
||||||
isAssignment t =
|
isAssignment t =
|
||||||
case t of
|
case t of
|
||||||
@@ -400,10 +544,10 @@ getAssociativeArrays t =
|
|||||||
f t@T_SimpleCommand {} = sequence_ $ do
|
f t@T_SimpleCommand {} = sequence_ $ do
|
||||||
name <- getCommandName t
|
name <- getCommandName t
|
||||||
let assocNames = ["declare","local","typeset"]
|
let assocNames = ["declare","local","typeset"]
|
||||||
guard $ elem name assocNames
|
guard $ name `elem` assocNames
|
||||||
let flags = getAllFlags t
|
let flags = getAllFlags t
|
||||||
guard $ elem "A" $ map snd flags
|
guard $ "A" `elem` map snd flags
|
||||||
let args = map fst . filter ((==) "" . snd) $ flags
|
let args = [arg | (arg, "") <- flags]
|
||||||
let names = mapMaybe (getLiteralStringExt nameAssignments) args
|
let names = mapMaybe (getLiteralStringExt nameAssignments) args
|
||||||
return $ tell names
|
return $ tell names
|
||||||
f _ = return ()
|
f _ = return ()
|
||||||
@@ -421,38 +565,36 @@ data PseudoGlob = PGAny | PGMany | PGChar Char
|
|||||||
|
|
||||||
-- Turn a word into a PG pattern, replacing all unknown/runtime values with
|
-- Turn a word into a PG pattern, replacing all unknown/runtime values with
|
||||||
-- PGMany.
|
-- PGMany.
|
||||||
wordToPseudoGlob :: Token -> Maybe [PseudoGlob]
|
wordToPseudoGlob :: Token -> [PseudoGlob]
|
||||||
wordToPseudoGlob word =
|
wordToPseudoGlob = fromMaybe [PGMany] . wordToPseudoGlob' False
|
||||||
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
|
|
||||||
where
|
|
||||||
f x = case x of
|
|
||||||
T_Literal _ s -> return $ map PGChar s
|
|
||||||
T_SingleQuoted _ s -> return $ map PGChar s
|
|
||||||
|
|
||||||
T_DollarBraced {} -> return [PGMany]
|
|
||||||
T_DollarExpansion {} -> return [PGMany]
|
|
||||||
T_Backticked {} -> return [PGMany]
|
|
||||||
|
|
||||||
T_Glob _ "?" -> return [PGAny]
|
|
||||||
T_Glob _ ('[':_) -> return [PGAny]
|
|
||||||
T_Glob {} -> return [PGMany]
|
|
||||||
|
|
||||||
T_Extglob {} -> return [PGMany]
|
|
||||||
|
|
||||||
_ -> return [PGMany]
|
|
||||||
|
|
||||||
-- Turn a word into a PG pattern, but only if we can preserve
|
-- Turn a word into a PG pattern, but only if we can preserve
|
||||||
-- exact semantics.
|
-- exact semantics.
|
||||||
wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob]
|
wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob]
|
||||||
wordToExactPseudoGlob word =
|
wordToExactPseudoGlob = wordToPseudoGlob' True
|
||||||
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
|
|
||||||
|
wordToPseudoGlob' :: Bool -> Token -> Maybe [PseudoGlob]
|
||||||
|
wordToPseudoGlob' exact word =
|
||||||
|
simplifyPseudoGlob <$> toGlob word
|
||||||
where
|
where
|
||||||
|
toGlob :: Token -> Maybe [PseudoGlob]
|
||||||
|
toGlob word =
|
||||||
|
case word of
|
||||||
|
T_NormalWord _ (T_Literal _ ('~':str):rest) -> do
|
||||||
|
guard $ not exact
|
||||||
|
let this = (PGMany : (map PGChar $ dropWhile (/= '/') str))
|
||||||
|
tail <- concat <$> (mapM f $ concatMap getWordParts rest)
|
||||||
|
return $ this ++ tail
|
||||||
|
_ -> concat <$> (mapM f $ getWordParts word)
|
||||||
|
|
||||||
f x = case x of
|
f x = case x of
|
||||||
T_Literal _ s -> return $ map PGChar s
|
T_Literal _ s -> return $ map PGChar s
|
||||||
T_SingleQuoted _ s -> return $ map PGChar s
|
T_SingleQuoted _ s -> return $ map PGChar s
|
||||||
T_Glob _ "?" -> return [PGAny]
|
T_Glob _ "?" -> return [PGAny]
|
||||||
T_Glob _ "*" -> return [PGMany]
|
T_Glob _ "*" -> return [PGMany]
|
||||||
_ -> fail "Unknown token type"
|
T_Glob _ ('[':_) | not exact -> return [PGAny]
|
||||||
|
_ -> if exact then fail "" else return [PGMany]
|
||||||
|
|
||||||
|
|
||||||
-- Reorder a PseudoGlob for more efficient matching, e.g.
|
-- Reorder a PseudoGlob for more efficient matching, e.g.
|
||||||
-- f?*?**g -> f??*g
|
-- f?*?**g -> f??*g
|
||||||
@@ -502,8 +644,7 @@ pseudoGlobIsSuperSetof = matchable
|
|||||||
matchable (PGMany : rest) [] = matchable rest []
|
matchable (PGMany : rest) [] = matchable rest []
|
||||||
matchable _ _ = False
|
matchable _ _ = False
|
||||||
|
|
||||||
wordsCanBeEqual x y = fromMaybe True $
|
wordsCanBeEqual x y = pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
|
||||||
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
|
|
||||||
|
|
||||||
-- Is this an expansion that can be quoted,
|
-- Is this an expansion that can be quoted,
|
||||||
-- e.g. $(foo) `foo` $foo (but not {foo,})?
|
-- e.g. $(foo) `foo` $foo (but not {foo,})?
|
||||||
@@ -517,6 +658,11 @@ isCommandSubstitution t = case t of
|
|||||||
T_Backticked {} -> True
|
T_Backticked {} -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
|
-- Is this an expansion that results in a simple string?
|
||||||
|
isStringExpansion t = isCommandSubstitution t || case t of
|
||||||
|
T_DollarArithmetic {} -> True
|
||||||
|
T_DollarBraced {} -> not (isArrayExpansion t)
|
||||||
|
_ -> False
|
||||||
|
|
||||||
-- Is this a T_Annotation that ignores a specific code?
|
-- Is this a T_Annotation that ignores a specific code?
|
||||||
isAnnotationIgnoringCode code t =
|
isAnnotationIgnoringCode code t =
|
||||||
@@ -524,5 +670,5 @@ isAnnotationIgnoringCode code t =
|
|||||||
T_Annotation _ anns _ -> any hasNum anns
|
T_Annotation _ anns _ -> any hasNum anns
|
||||||
_ -> False
|
_ -> False
|
||||||
where
|
where
|
||||||
hasNum (DisableComment ts) = code == ts
|
hasNum (DisableComment from to) = code >= from && code < to
|
||||||
hasNum _ = False
|
hasNum _ = False
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -163,6 +163,8 @@ err id code str = addComment $ makeComment ErrorC id code str
|
|||||||
info id code str = addComment $ makeComment InfoC id code str
|
info id code str = addComment $ makeComment InfoC id code str
|
||||||
style id code str = addComment $ makeComment StyleC id code str
|
style id code str = addComment $ makeComment StyleC id code str
|
||||||
|
|
||||||
|
errWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
|
||||||
|
errWithFix = addCommentWithFix ErrorC
|
||||||
warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
|
warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
|
||||||
warnWithFix = addCommentWithFix WarningC
|
warnWithFix = addCommentWithFix WarningC
|
||||||
styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
|
styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
|
||||||
@@ -178,7 +180,7 @@ makeCommentWithFix severity id code str fix =
|
|||||||
withFix = comment {
|
withFix = comment {
|
||||||
tcFix = Just fix
|
tcFix = Just fix
|
||||||
}
|
}
|
||||||
in withFix `deepseq` withFix
|
in force withFix
|
||||||
|
|
||||||
makeParameters spec =
|
makeParameters spec =
|
||||||
let params = Parameters {
|
let params = Parameters {
|
||||||
@@ -236,6 +238,8 @@ prop_determineShell5 = determineShellTest "#shellcheck shell=sh\nfoo" == Sh
|
|||||||
prop_determineShell6 = determineShellTest "#! /bin/sh" == Sh
|
prop_determineShell6 = determineShellTest "#! /bin/sh" == Sh
|
||||||
prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
|
prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
|
||||||
prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh
|
prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh
|
||||||
|
prop_determineShell9 = determineShellTest "#!/bin/env -S dash -x" == Dash
|
||||||
|
prop_determineShell10 = determineShellTest "#!/bin/env --split-string= dash -x" == Dash
|
||||||
|
|
||||||
determineShellTest = determineShellTest' Nothing
|
determineShellTest = determineShellTest' Nothing
|
||||||
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
|
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
|
||||||
@@ -254,17 +258,19 @@ determineShell fallbackShell t = fromMaybe Bash $
|
|||||||
executableFromShebang :: String -> String
|
executableFromShebang :: String -> String
|
||||||
executableFromShebang = shellFor
|
executableFromShebang = shellFor
|
||||||
where
|
where
|
||||||
shellFor s | "/env " `isInfixOf` s = headOrDefault "" (drop 1 $ words s)
|
shellFor s | "/env " `isInfixOf` s = case matchRegex re s of
|
||||||
|
Just [flag, shell] -> shell
|
||||||
|
_ -> ""
|
||||||
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
|
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
|
||||||
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
|
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
|
||||||
|
re = mkRegex "/env +(-S|--split-string=?)? *([^ ]*)"
|
||||||
|
|
||||||
|
|
||||||
-- Given a root node, make a map from Id to parent Token.
|
-- Given a root node, make a map from Id to parent Token.
|
||||||
-- This is used to populate parentMap in Parameters
|
-- This is used to populate parentMap in Parameters
|
||||||
getParentTree :: Token -> Map.Map Id Token
|
getParentTree :: Token -> Map.Map Id Token
|
||||||
getParentTree t =
|
getParentTree t =
|
||||||
snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
|
snd $ execState (doStackAnalysis pre post t) ([], Map.empty)
|
||||||
where
|
where
|
||||||
pre t = modify (first ((:) t))
|
pre t = modify (first ((:) t))
|
||||||
post t = do
|
post t = do
|
||||||
@@ -293,15 +299,15 @@ isQuoteFree = isQuoteFreeNode False
|
|||||||
|
|
||||||
|
|
||||||
isQuoteFreeNode strict tree t =
|
isQuoteFreeNode strict tree t =
|
||||||
(isQuoteFreeElement t == Just True) ||
|
isQuoteFreeElement t ||
|
||||||
headOrDefault False (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t))
|
headOrDefault False (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t))
|
||||||
where
|
where
|
||||||
-- Is this node self-quoting in itself?
|
-- Is this node self-quoting in itself?
|
||||||
isQuoteFreeElement t =
|
isQuoteFreeElement t =
|
||||||
case t of
|
case t of
|
||||||
T_Assignment {} -> return True
|
T_Assignment {} -> True
|
||||||
T_FdRedirect {} -> return True
|
T_FdRedirect {} -> True
|
||||||
_ -> Nothing
|
_ -> False
|
||||||
|
|
||||||
-- Are any subnodes inherently self-quoting?
|
-- Are any subnodes inherently self-quoting?
|
||||||
isQuoteFreeContext t =
|
isQuoteFreeContext t =
|
||||||
@@ -354,8 +360,8 @@ getClosestCommand tree t =
|
|||||||
|
|
||||||
-- Like above, if koala_man knew Haskell when starting this project.
|
-- Like above, if koala_man knew Haskell when starting this project.
|
||||||
getClosestCommandM t = do
|
getClosestCommandM t = do
|
||||||
tree <- asks parentMap
|
params <- ask
|
||||||
return $ getClosestCommand tree t
|
return $ getClosestCommand (parentMap params) t
|
||||||
|
|
||||||
-- Is the token used as a command name (the first word in a T_SimpleCommand)?
|
-- Is the token used as a command name (the first word in a T_SimpleCommand)?
|
||||||
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
||||||
@@ -364,8 +370,8 @@ usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
|||||||
| currentId == getId word = go id rest
|
| currentId == getId word = go id rest
|
||||||
go currentId (T_DoubleQuoted id [word]:rest)
|
go currentId (T_DoubleQuoted id [word]:rest)
|
||||||
| currentId == getId word = go id rest
|
| currentId == getId word = go id rest
|
||||||
go currentId (T_SimpleCommand _ _ (word:_):_)
|
go currentId (t@(T_SimpleCommand _ _ (word:_)):_) =
|
||||||
| currentId == getId word = True
|
getId word == currentId || getId (getCommandTokenOrThis t) == currentId
|
||||||
go _ _ = False
|
go _ _ = False
|
||||||
|
|
||||||
-- A list of the element and all its parents up to the root node.
|
-- A list of the element and all its parents up to the root node.
|
||||||
@@ -377,8 +383,8 @@ getPath tree t = t :
|
|||||||
-- Version of the above taking the map from the current context
|
-- Version of the above taking the map from the current context
|
||||||
-- Todo: give this the name "getPath"
|
-- Todo: give this the name "getPath"
|
||||||
getPathM t = do
|
getPathM t = do
|
||||||
map <- asks parentMap
|
params <- ask
|
||||||
return $ getPath map t
|
return $ getPath (parentMap params) t
|
||||||
|
|
||||||
isParentOf tree parent child =
|
isParentOf tree parent child =
|
||||||
elem (getId parent) . map getId $ getPath tree child
|
elem (getId parent) . map getId $ getPath tree child
|
||||||
@@ -388,14 +394,13 @@ parents params = getPath (parentMap params)
|
|||||||
-- Find the first match in a list where the predicate is Just True.
|
-- Find the first match in a list where the predicate is Just True.
|
||||||
-- Stops if it's Just False and ignores Nothing.
|
-- Stops if it's Just False and ignores Nothing.
|
||||||
findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a
|
findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a
|
||||||
findFirst p l =
|
findFirst p = foldr go Nothing
|
||||||
case l of
|
where
|
||||||
[] -> Nothing
|
go x acc =
|
||||||
(x:xs) ->
|
case p x of
|
||||||
case p x of
|
Just True -> return x
|
||||||
Just True -> return x
|
Just False -> Nothing
|
||||||
Just False -> Nothing
|
Nothing -> acc
|
||||||
Nothing -> findFirst p xs
|
|
||||||
|
|
||||||
-- Check whether a word is entirely output from a single command
|
-- Check whether a word is entirely output from a single command
|
||||||
tokenIsJustCommandOutput t = case t of
|
tokenIsJustCommandOutput t = case t of
|
||||||
@@ -410,8 +415,7 @@ tokenIsJustCommandOutput t = case t of
|
|||||||
|
|
||||||
-- TODO: Replace this with a proper Control Flow Graph
|
-- TODO: Replace this with a proper Control Flow Graph
|
||||||
getVariableFlow params t =
|
getVariableFlow params t =
|
||||||
let (_, stack) = runState (doStackAnalysis startScope endScope t) []
|
reverse $ execState (doStackAnalysis startScope endScope t) []
|
||||||
in reverse stack
|
|
||||||
where
|
where
|
||||||
startScope t =
|
startScope t =
|
||||||
let scopeType = leadType params t
|
let scopeType = leadType params t
|
||||||
@@ -462,28 +466,22 @@ leadType params t =
|
|||||||
|
|
||||||
causesSubshell = do
|
causesSubshell = do
|
||||||
(T_Pipeline _ _ list) <- parentPipeline
|
(T_Pipeline _ _ list) <- parentPipeline
|
||||||
if length list <= 1
|
return $ case list of
|
||||||
then return False
|
_:_:_ -> not (hasLastpipe params) || getId (last list) /= getId t
|
||||||
else if not $ hasLastpipe params
|
_ -> False
|
||||||
then return True
|
|
||||||
else return . not $ (getId . head $ reverse list) == getId t
|
|
||||||
|
|
||||||
getModifiedVariables t =
|
getModifiedVariables t =
|
||||||
case t of
|
case t of
|
||||||
T_SimpleCommand _ vars [] ->
|
T_SimpleCommand _ vars [] ->
|
||||||
concatMap (\x -> case x of
|
[(x, x, name, dataTypeFrom DataString w) | x@(T_Assignment id _ name _ w) <- vars]
|
||||||
T_Assignment id _ name _ w ->
|
T_SimpleCommand {} ->
|
||||||
[(x, x, name, dataTypeFrom DataString w)]
|
getModifiedVariableCommand t
|
||||||
_ -> []
|
|
||||||
) vars
|
|
||||||
c@T_SimpleCommand {} ->
|
|
||||||
getModifiedVariableCommand c
|
|
||||||
|
|
||||||
TA_Unary _ "++|" v@(TA_Variable _ name _) ->
|
TA_Unary _ "++|" v@(TA_Variable _ name _) ->
|
||||||
[(t, v, name, DataString $ SourceFrom [v])]
|
[(t, v, name, DataString $ SourceFrom [v])]
|
||||||
TA_Unary _ "|++" v@(TA_Variable _ name _) ->
|
TA_Unary _ "|++" v@(TA_Variable _ name _) ->
|
||||||
[(t, v, name, DataString $ SourceFrom [v])]
|
[(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` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
||||||
return (t, t, name, DataString $ SourceFrom [rhs])
|
return (t, t, name, DataString $ SourceFrom [rhs])
|
||||||
|
|
||||||
@@ -495,26 +493,30 @@ getModifiedVariables t =
|
|||||||
|
|
||||||
-- Count [[ -v foo ]] as an "assignment".
|
-- Count [[ -v foo ]] as an "assignment".
|
||||||
-- This is to prevent [ -v foo ] being unassigned or unused.
|
-- This is to prevent [ -v foo ] being unassigned or unused.
|
||||||
TC_Unary id _ "-v" token -> maybeToList $ do
|
TC_Unary id _ "-v" token -> do
|
||||||
str <- fmap (takeWhile (/= '[')) $ -- Quoted index
|
str <- fmap (takeWhile (/= '[')) $ -- Quoted index
|
||||||
flip getLiteralStringExt token $ \x ->
|
flip getLiteralStringExt token $ \x ->
|
||||||
case x of
|
case x of
|
||||||
T_Glob _ s -> return s -- Unquoted index
|
T_Glob _ s -> return s -- Unquoted index
|
||||||
_ -> Nothing
|
_ -> []
|
||||||
|
|
||||||
guard . not . null $ str
|
guard . not . null $ str
|
||||||
return (t, token, str, DataString SourceChecked)
|
return (t, token, str, DataString SourceChecked)
|
||||||
|
|
||||||
|
TC_Unary _ _ "-n" token -> markAsChecked t token
|
||||||
|
TC_Unary _ _ "-z" token -> markAsChecked t token
|
||||||
|
TC_Nullary _ _ token -> markAsChecked t token
|
||||||
|
|
||||||
T_DollarBraced _ _ l -> maybeToList $ do
|
T_DollarBraced _ _ l -> maybeToList $ do
|
||||||
let string = bracedString t
|
let string = concat $ oversimplify l
|
||||||
let modifier = getBracedModifier string
|
let modifier = getBracedModifier string
|
||||||
guard $ any (`isPrefixOf` modifier) ["=", ":="]
|
guard $ any (`isPrefixOf` modifier) ["=", ":="]
|
||||||
return (t, t, getBracedReference string, DataString $ SourceFrom [l])
|
return (t, t, getBracedReference string, DataString $ SourceFrom [l])
|
||||||
|
|
||||||
t@(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, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
|
||||||
|
|
||||||
t@(T_CoProc _ name _) ->
|
T_CoProc _ name _ ->
|
||||||
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
|
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
|
||||||
|
|
||||||
--Points to 'for' rather than variable
|
--Points to 'for' rather than variable
|
||||||
@@ -522,6 +524,14 @@ getModifiedVariables t =
|
|||||||
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
|
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
|
||||||
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
|
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
|
||||||
_ -> []
|
_ -> []
|
||||||
|
where
|
||||||
|
markAsChecked place token = mapMaybe (f place) $ getWordParts token
|
||||||
|
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 =
|
isClosingFileOp op =
|
||||||
case op of
|
case op of
|
||||||
@@ -541,6 +551,9 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
|
|||||||
(not $ any (`elem` flags) ["f", "F"])
|
(not $ any (`elem` flags) ["f", "F"])
|
||||||
then concatMap getReference rest
|
then concatMap getReference rest
|
||||||
else []
|
else []
|
||||||
|
"local" -> if "x" `elem` flags
|
||||||
|
then concatMap getReference rest
|
||||||
|
else []
|
||||||
"trap" ->
|
"trap" ->
|
||||||
case rest of
|
case rest of
|
||||||
head:_ -> map (\x -> (base, head, x)) $ getVariablesFromLiteralToken head
|
head:_ -> map (\x -> (base, head, x)) $ getVariablesFromLiteralToken head
|
||||||
@@ -569,10 +582,14 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
|
|||||||
"builtin" ->
|
"builtin" ->
|
||||||
getModifiedVariableCommand $ T_SimpleCommand id cmdPrefix rest
|
getModifiedVariableCommand $ T_SimpleCommand id cmdPrefix rest
|
||||||
"read" ->
|
"read" ->
|
||||||
let params = map getLiteral rest
|
let fallback = catMaybes $ takeWhile isJust (reverse $ map getLiteral rest)
|
||||||
readArrayVars = getReadArrayVariables rest
|
in fromMaybe fallback $ do
|
||||||
in
|
parsed <- getGnuOpts flagsForRead rest
|
||||||
catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params
|
case lookup "a" parsed of
|
||||||
|
Just (_, var) -> (:[]) <$> getLiteralArray var
|
||||||
|
Nothing -> return $ catMaybes $
|
||||||
|
map (getLiteral . snd . snd) $ filter (null . fst) parsed
|
||||||
|
|
||||||
"getopts" ->
|
"getopts" ->
|
||||||
case rest of
|
case rest of
|
||||||
opts:var:_ -> maybeToList $ getLiteral var
|
opts:var:_ -> maybeToList $ getLiteral var
|
||||||
@@ -664,25 +681,28 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
|
|||||||
f [] = fail "not found"
|
f [] = fail "not found"
|
||||||
|
|
||||||
-- mapfile has some curious syntax allowing flags plus 0..n variable names
|
-- mapfile has some curious syntax allowing flags plus 0..n variable names
|
||||||
-- where only the first non-option one is used if any. Here we cheat and
|
-- where only the first non-option one is used if any.
|
||||||
-- just get the last one, if it's a variable name.
|
getMapfileArray base rest = parseArgs `mplus` fallback
|
||||||
getMapfileArray base arguments = do
|
where
|
||||||
lastArg <- listToMaybe (reverse arguments)
|
parseArgs :: Maybe (Token, Token, String, DataType)
|
||||||
name <- getLiteralString lastArg
|
parseArgs = do
|
||||||
guard $ isVariableName name
|
args <- getGnuOpts "d:n:O:s:u:C:c:t" rest
|
||||||
return (base, lastArg, name, DataArray SourceExternal)
|
case [y | ("",(_,y)) <- args] of
|
||||||
|
[] ->
|
||||||
-- get all the array variables used in read, e.g. read -a arr
|
return (base, base, "MAPFILE", DataArray SourceExternal)
|
||||||
getReadArrayVariables args =
|
first:_ -> do
|
||||||
map (getLiteralArray . snd)
|
name <- getLiteralString first
|
||||||
(filter (isArrayFlag . fst) (zip args (tail args)))
|
guard $ isVariableName name
|
||||||
|
return (base, first, name, DataArray SourceExternal)
|
||||||
isArrayFlag x = fromMaybe False $ do
|
-- If arg parsing fails (due to bad or new flags), get the last variable name
|
||||||
str <- getLiteralString x
|
fallback :: Maybe (Token, Token, String, DataType)
|
||||||
return $ case str of
|
fallback = do
|
||||||
'-':'-':_ -> False
|
(name, token) <- listToMaybe . mapMaybe f $ reverse rest
|
||||||
'-':str -> 'a' `elem` str
|
return (base, token, name, DataArray SourceExternal)
|
||||||
_ -> False
|
f arg = do
|
||||||
|
name <- getLiteralString arg
|
||||||
|
guard $ isVariableName name
|
||||||
|
return (name, arg)
|
||||||
|
|
||||||
-- get the FLAGS_ variable created by a shflags DEFINE_ call
|
-- get the FLAGS_ variable created by a shflags DEFINE_ call
|
||||||
getFlagVariable (n:v:_) = do
|
getFlagVariable (n:v:_) = do
|
||||||
@@ -713,7 +733,7 @@ getOffsetReferences mods = fromMaybe [] $ do
|
|||||||
|
|
||||||
getReferencedVariables parents t =
|
getReferencedVariables parents t =
|
||||||
case t of
|
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) :
|
(t, t, getBracedReference str) :
|
||||||
map (\x -> (l, l, x)) (
|
map (\x -> (l, l, x)) (
|
||||||
getIndexReferences str
|
getIndexReferences str
|
||||||
@@ -738,7 +758,7 @@ getReferencedVariables parents t =
|
|||||||
(t, t, "output")
|
(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]
|
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
|
||||||
x -> getReferencedVariableCommand x
|
x -> getReferencedVariableCommand x
|
||||||
where
|
where
|
||||||
@@ -755,9 +775,9 @@ getReferencedVariables parents t =
|
|||||||
|
|
||||||
literalizer t = case t of
|
literalizer t = case t of
|
||||||
T_Glob _ s -> return s -- Also when parsed as globs
|
T_Glob _ s -> return s -- Also when parsed as globs
|
||||||
_ -> Nothing
|
_ -> []
|
||||||
|
|
||||||
getIfReference context token = maybeToList $ do
|
getIfReference context token = do
|
||||||
str@(h:_) <- getLiteralStringExt literalizer token
|
str@(h:_) <- getLiteralStringExt literalizer token
|
||||||
when (isDigit h) $ fail "is a number"
|
when (isDigit h) $ fail "is a number"
|
||||||
return (context, token, getBracedReference str)
|
return (context, token, getBracedReference str)
|
||||||
@@ -808,7 +828,7 @@ getVariablesFromLiteralToken token =
|
|||||||
prop_getVariablesFromLiteral1 =
|
prop_getVariablesFromLiteral1 =
|
||||||
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
|
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
|
||||||
getVariablesFromLiteral string =
|
getVariablesFromLiteral string =
|
||||||
map (!! 0) $ matchAllSubgroups variableRegex string
|
map head $ matchAllSubgroups variableRegex string
|
||||||
where
|
where
|
||||||
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
|
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
|
||||||
|
|
||||||
@@ -824,27 +844,26 @@ prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
|
|||||||
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
|
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
|
||||||
prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
|
prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
|
||||||
prop_getBracedReference11= getBracedReference "!os*" == ""
|
prop_getBracedReference11= getBracedReference "!os*" == ""
|
||||||
|
prop_getBracedReference11b= getBracedReference "!os@" == ""
|
||||||
prop_getBracedReference12= getBracedReference "!os?bar**" == ""
|
prop_getBracedReference12= getBracedReference "!os?bar**" == ""
|
||||||
prop_getBracedReference13= getBracedReference "foo[bar]" == "foo"
|
prop_getBracedReference13= getBracedReference "foo[bar]" == "foo"
|
||||||
getBracedReference s = fromMaybe s $
|
getBracedReference s = fromMaybe s $
|
||||||
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
|
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
|
||||||
where
|
where
|
||||||
noPrefix = dropPrefix s
|
noPrefix = dropPrefix s
|
||||||
dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest
|
dropPrefix (c:rest) | c `elem` "!#" = rest
|
||||||
dropPrefix "" = ""
|
dropPrefix cs = cs
|
||||||
takeName s = do
|
takeName s = do
|
||||||
let name = takeWhile isVariableChar s
|
let name = takeWhile isVariableChar s
|
||||||
guard . not $ null name
|
guard . not $ null name
|
||||||
return name
|
return name
|
||||||
getSpecial (c:_) =
|
getSpecial (c:_) | c `elem` "*@#?-$!" = return [c]
|
||||||
if c `elem` "*@#?-$!" then return [c] else fail "not special"
|
getSpecial _ = fail "empty or not special"
|
||||||
getSpecial _ = fail "empty"
|
|
||||||
|
|
||||||
nameExpansion ('!':rest) = do -- e.g. ${!foo*bar*}
|
nameExpansion ('!':next:rest) = do -- e.g. ${!foo*bar*}
|
||||||
let suffix = dropWhile isVariableChar rest
|
guard $ isVariableChar next -- e.g. ${!@}
|
||||||
guard $ suffix /= rest -- e.g. ${!@}
|
first <- find (not . isVariableChar) rest
|
||||||
first <- suffix !!! 0
|
guard $ first `elem` "*?@"
|
||||||
guard $ first `elem` "*?"
|
|
||||||
return ""
|
return ""
|
||||||
nameExpansion _ = Nothing
|
nameExpansion _ = Nothing
|
||||||
|
|
||||||
@@ -877,8 +896,8 @@ headOrDefault def _ = def
|
|||||||
|
|
||||||
-- Run a command if the shell is in the given list
|
-- Run a command if the shell is in the given list
|
||||||
whenShell l c = do
|
whenShell l c = do
|
||||||
shell <- asks shellType
|
params <- ask
|
||||||
when (shell `elem` l ) c
|
when (shellType params `elem` l ) c
|
||||||
|
|
||||||
|
|
||||||
filterByAnnotation asSpec params =
|
filterByAnnotation asSpec params =
|
||||||
@@ -907,45 +926,15 @@ isCountingReference _ = False
|
|||||||
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
|
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
|
||||||
isQuotedAlternativeReference t =
|
isQuotedAlternativeReference t =
|
||||||
case t of
|
case t of
|
||||||
T_DollarBraced _ _ _ ->
|
T_DollarBraced _ _ l ->
|
||||||
getBracedModifier (bracedString t) `matches` re
|
getBracedModifier (concat $ oversimplify l) `matches` re
|
||||||
_ -> False
|
_ -> False
|
||||||
where
|
where
|
||||||
re = mkRegex "(^|\\]):?\\+"
|
re = mkRegex "(^|\\]):?\\+"
|
||||||
|
|
||||||
-- getGnuOpts "erd:u:" will parse a SimpleCommand like
|
supportsArrays Bash = True
|
||||||
-- read -re -d : -u 3 bar
|
supportsArrays Ksh = True
|
||||||
-- into
|
supportsArrays _ = False
|
||||||
-- 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 str t = getOpts str $ getAllFlags t
|
|
||||||
getBsdOpts str t = getOpts str $ getLeadingFlags t
|
|
||||||
getOpts :: String -> [(Token, String)] -> Maybe [(String, Token)]
|
|
||||||
getOpts string flags = process flags
|
|
||||||
where
|
|
||||||
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 $ null flag2
|
|
||||||
more <- process rest
|
|
||||||
return $ (flag1, token2) : more
|
|
||||||
else do
|
|
||||||
more <- process rest2
|
|
||||||
return $ (flag1, token1) : more
|
|
||||||
|
|
||||||
supportsArrays shell = shell == Bash || shell == Ksh
|
|
||||||
|
|
||||||
-- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh)
|
-- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh)
|
||||||
isBashLike :: Parameters -> Bool
|
isBashLike :: Parameters -> Bool
|
||||||
|
@@ -229,6 +229,12 @@ prop_cantSourceDynamic =
|
|||||||
prop_cantSourceDynamic2 =
|
prop_cantSourceDynamic2 =
|
||||||
[1090] == checkWithIncludes [("lib", "")] "source ~/foo"
|
[1090] == checkWithIncludes [("lib", "")] "source ~/foo"
|
||||||
|
|
||||||
|
prop_canStripPrefixAndSource =
|
||||||
|
null $ checkWithIncludes [("./lib", "")] "source \"$MYDIR/lib\""
|
||||||
|
|
||||||
|
prop_canStripPrefixAndSource2 =
|
||||||
|
null $ checkWithIncludes [("./utils.sh", "")] "source \"$(dirname \"${BASH_SOURCE[0]}\")/utils.sh\""
|
||||||
|
|
||||||
prop_canSourceDynamicWhenRedirected =
|
prop_canSourceDynamicWhenRedirected =
|
||||||
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
|
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
|
||||||
|
|
||||||
@@ -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"
|
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
|
||||||
|
|
||||||
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
|
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
|
||||||
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"
|
prop_spinBug1413 = null $ check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n"
|
||||||
|
|
||||||
@@ -295,6 +301,13 @@ prop_canDisableShebangWarning = null $ result
|
|||||||
csScript = "#shellcheck disable=SC2148\nfoo"
|
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]
|
prop_shExtensionDoesntMatter = result == [2148]
|
||||||
where
|
where
|
||||||
result = checkWithSpec [] emptyCheckSpec {
|
result = checkWithSpec [] emptyCheckSpec {
|
||||||
|
@@ -19,6 +19,7 @@
|
|||||||
-}
|
-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE MultiWayIf #-}
|
||||||
|
|
||||||
-- This module contains checks that examine specific commands by name.
|
-- This module contains checks that examine specific commands by name.
|
||||||
module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where
|
module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where
|
||||||
@@ -52,8 +53,6 @@ verify :: CommandCheck -> String -> Bool
|
|||||||
verify f s = producesComments (getChecker [f]) s == Just True
|
verify f s = producesComments (getChecker [f]) s == Just True
|
||||||
verifyNot f s = producesComments (getChecker [f]) s == Just False
|
verifyNot f s = producesComments (getChecker [f]) s == Just False
|
||||||
|
|
||||||
arguments (T_SimpleCommand _ _ (cmd:args)) = args
|
|
||||||
|
|
||||||
commandChecks :: [CommandCheck]
|
commandChecks :: [CommandCheck]
|
||||||
commandChecks = [
|
commandChecks = [
|
||||||
checkTr
|
checkTr
|
||||||
@@ -95,6 +94,7 @@ commandChecks = [
|
|||||||
,checkSudoArgs
|
,checkSudoArgs
|
||||||
,checkSourceArgs
|
,checkSourceArgs
|
||||||
,checkChmodDashr
|
,checkChmodDashr
|
||||||
|
,checkXargsDashi
|
||||||
]
|
]
|
||||||
|
|
||||||
optionalChecks = map fst optionalCommandChecks
|
optionalChecks = map fst optionalCommandChecks
|
||||||
@@ -115,6 +115,35 @@ prop_verifyOptionalExamples = all check optionalCommandChecks
|
|||||||
verify check (cdPositive desc)
|
verify check (cdPositive desc)
|
||||||
&& verifyNot check (cdNegative desc)
|
&& verifyNot check (cdNegative desc)
|
||||||
|
|
||||||
|
-- Run a check against the getopt parser. If it fails, the lists are empty.
|
||||||
|
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 :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
||||||
buildCommandMap = foldl' addCheck Map.empty
|
buildCommandMap = foldl' addCheck Map.empty
|
||||||
where
|
where
|
||||||
@@ -199,12 +228,11 @@ prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"
|
|||||||
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
|
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
|
||||||
acceptsGlob s = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
|
acceptsGlob s = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
|
||||||
f [] = return ()
|
f [] = return ()
|
||||||
f (x:xs) = g x xs
|
f (x:xs) = foldr g (const $ return ()) xs x
|
||||||
g _ [] = return ()
|
g b acc a = do
|
||||||
g a (b:r) = do
|
|
||||||
forM_ (getLiteralString a) $ \s -> when (acceptsGlob s && isGlob b) $
|
forM_ (getLiteralString a) $ \s -> when (acceptsGlob s && isGlob b) $
|
||||||
warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."
|
warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."
|
||||||
g b r
|
acc b
|
||||||
|
|
||||||
|
|
||||||
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
|
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
|
||||||
@@ -283,17 +311,11 @@ checkGrepRe = CommandCheck (Basename "grep") check where
|
|||||||
candidates =
|
candidates =
|
||||||
sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords
|
sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords
|
||||||
|
|
||||||
getSuspiciousRegexWildcard str =
|
getSuspiciousRegexWildcard str = case matchRegex suspicious str of
|
||||||
if not $ str `matches` contra
|
Just [[c]] | not (str `matches` contra) -> Just c
|
||||||
then do
|
_ -> fail "looks good"
|
||||||
match <- matchRegex suspicious str
|
suspicious = mkRegex "([A-Za-z1-9])\\*"
|
||||||
str <- match !!! 0
|
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
|
||||||
str !!! 0
|
|
||||||
else
|
|
||||||
fail "looks good"
|
|
||||||
where
|
|
||||||
suspicious = mkRegex "([A-Za-z1-9])\\*"
|
|
||||||
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
|
|
||||||
|
|
||||||
|
|
||||||
prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
|
prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
|
||||||
@@ -462,8 +484,8 @@ checkMkdirDashPM = CommandCheck (Basename "mkdir") check
|
|||||||
where
|
where
|
||||||
check t = sequence_ $ do
|
check t = sequence_ $ do
|
||||||
let flags = getAllFlags t
|
let flags = getAllFlags t
|
||||||
dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
|
dashP <- find (\(_,f) -> f == "p" || f == "parents") flags
|
||||||
dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
|
dashM <- find (\(_,f) -> f == "m" || f == "mode") flags
|
||||||
-- mkdir -pm 0700 dir is fine, so is ../dir, but dir/subdir is not.
|
-- mkdir -pm 0700 dir is fine, so is ../dir, but dir/subdir is not.
|
||||||
guard $ any couldHaveSubdirs (drop 1 $ arguments t)
|
guard $ any couldHaveSubdirs (drop 1 $ arguments t)
|
||||||
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
|
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
|
||||||
@@ -483,7 +505,7 @@ prop_checkNonportableSignals7 = verifyNot checkNonportableSignals "trap 'stop' i
|
|||||||
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
|
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
|
||||||
where
|
where
|
||||||
f args = case args of
|
f args = case args of
|
||||||
first:rest -> unless (isFlag first) $ mapM_ check rest
|
first:rest | not $ isFlag first -> mapM_ check rest
|
||||||
_ -> return ()
|
_ -> return ()
|
||||||
|
|
||||||
check param = sequence_ $ do
|
check param = sequence_ $ do
|
||||||
@@ -520,9 +542,9 @@ checkInteractiveSu = CommandCheck (Basename "su") f
|
|||||||
info (getId cmd) 2117
|
info (getId cmd) 2117
|
||||||
"To run commands as another user, use su -c or sudo."
|
"To run commands as another user, use su -c or sudo."
|
||||||
|
|
||||||
undirected (T_Pipeline _ _ l) = length l <= 1
|
undirected (T_Pipeline _ _ (_:_:_)) = False
|
||||||
-- This should really just be modifications to stdin, but meh
|
-- This should really just be modifications to stdin, but meh
|
||||||
undirected (T_Redirecting _ list _) = null list
|
undirected (T_Redirecting _ (_:_) _) = False
|
||||||
undirected _ = True
|
undirected _ = True
|
||||||
|
|
||||||
|
|
||||||
@@ -539,9 +561,8 @@ checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
|
|||||||
([], hostport:r@(_:_)) -> checkArg $ last r
|
([], hostport:r@(_:_)) -> checkArg $ last r
|
||||||
_ -> return ()
|
_ -> return ()
|
||||||
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
|
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
|
||||||
case filter (not . isConstant) parts of
|
forM_ (find (not . isConstant) parts) $
|
||||||
[] -> return ()
|
\x -> info (getId x) 2029
|
||||||
(x:_) -> info (getId x) 2029
|
|
||||||
"Note that, unescaped, this expands on the client side."
|
"Note that, unescaped, this expands on the client side."
|
||||||
checkArg _ = return ()
|
checkArg _ = return ()
|
||||||
|
|
||||||
@@ -567,6 +588,8 @@ prop_checkPrintfVar18= verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
|
|||||||
prop_checkPrintfVar19= verifyNot checkPrintfVar "printf '%(%s)T'"
|
prop_checkPrintfVar19= verifyNot checkPrintfVar "printf '%(%s)T'"
|
||||||
prop_checkPrintfVar20= verifyNot checkPrintfVar "printf '%d %(%s)T' 42"
|
prop_checkPrintfVar20= verifyNot checkPrintfVar "printf '%d %(%s)T' 42"
|
||||||
prop_checkPrintfVar21= verify checkPrintfVar "printf '%d %(%s)T'"
|
prop_checkPrintfVar21= verify checkPrintfVar "printf '%d %(%s)T'"
|
||||||
|
prop_checkPrintfVar22= verify checkPrintfVar "printf '%s\n%s' foo"
|
||||||
|
|
||||||
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||||
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
|
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
|
||||||
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
|
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
|
||||||
@@ -580,22 +603,21 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
|||||||
let formatCount = length formats
|
let formatCount = length formats
|
||||||
let argCount = length more
|
let argCount = length more
|
||||||
|
|
||||||
return $
|
return $ if
|
||||||
case () of
|
| argCount == 0 && formatCount == 0 ->
|
||||||
() | argCount == 0 && formatCount == 0 ->
|
return () -- This is fine
|
||||||
return () -- This is fine
|
| formatCount == 0 && argCount > 0 ->
|
||||||
() | formatCount == 0 && argCount > 0 ->
|
err (getId format) 2182
|
||||||
err (getId format) 2182
|
"This printf format string has no variables. Other arguments are ignored."
|
||||||
"This printf format string has no variables. Other arguments are ignored."
|
| any mayBecomeMultipleArgs more ->
|
||||||
() | any mayBecomeMultipleArgs more ->
|
return () -- We don't know so trust the user
|
||||||
return () -- We don't know so trust the user
|
| argCount < formatCount && onlyTrailingTs formats argCount ->
|
||||||
() | argCount < formatCount && onlyTrailingTs formats argCount ->
|
return () -- Allow trailing %()Ts since they use the current time
|
||||||
return () -- Allow trailing %()Ts since they use the current time
|
| argCount > 0 && argCount `mod` formatCount == 0 ->
|
||||||
() | argCount > 0 && argCount `mod` formatCount == 0 ->
|
return () -- Great: a suitable number of arguments
|
||||||
return () -- Great: a suitable number of arguments
|
| otherwise ->
|
||||||
() ->
|
warn (getId format) 2183 $
|
||||||
warn (getId format) 2183 $
|
"This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments."
|
||||||
"This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments."
|
|
||||||
|
|
||||||
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
|
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
|
||||||
info (getId format) 2059
|
info (getId format) 2059
|
||||||
@@ -610,6 +632,8 @@ prop_checkGetPrintfFormats2 = getPrintfFormats "%0*s" == "*s"
|
|||||||
prop_checkGetPrintfFormats3 = getPrintfFormats "%(%s)T" == "T"
|
prop_checkGetPrintfFormats3 = getPrintfFormats "%(%s)T" == "T"
|
||||||
prop_checkGetPrintfFormats4 = getPrintfFormats "%d%%%(%s)T" == "dT"
|
prop_checkGetPrintfFormats4 = getPrintfFormats "%d%%%(%s)T" == "dT"
|
||||||
prop_checkGetPrintfFormats5 = getPrintfFormats "%bPassed: %d, %bFailed: %d%b, Skipped: %d, %bErrored: %d%b\\n" == "bdbdbdbdb"
|
prop_checkGetPrintfFormats5 = getPrintfFormats "%bPassed: %d, %bFailed: %d%b, Skipped: %d, %bErrored: %d%b\\n" == "bdbdbdbdb"
|
||||||
|
prop_checkGetPrintfFormats6 = getPrintfFormats "%s%s" == "ss"
|
||||||
|
prop_checkGetPrintfFormats7 = getPrintfFormats "%s\n%s" == "ss"
|
||||||
getPrintfFormats = getFormats
|
getPrintfFormats = getFormats
|
||||||
where
|
where
|
||||||
-- Get the arguments in the string as a string of type characters,
|
-- Get the arguments in the string as a string of type characters,
|
||||||
@@ -628,17 +652,17 @@ getPrintfFormats = getFormats
|
|||||||
|
|
||||||
regexBasedGetFormats rest =
|
regexBasedGetFormats rest =
|
||||||
case matchRegex re rest of
|
case matchRegex re rest of
|
||||||
Just [width, precision, typ, rest] ->
|
Just [width, precision, typ, rest, _] ->
|
||||||
(if width == "*" then "*" else "") ++
|
(if width == "*" then "*" else "") ++
|
||||||
(if precision == "*" then "*" else "") ++
|
(if precision == "*" then "*" else "") ++
|
||||||
typ ++ getFormats rest
|
typ ++ getFormats rest
|
||||||
Nothing -> take 1 rest ++ getFormats rest
|
Nothing -> take 1 rest ++ getFormats rest
|
||||||
where
|
where
|
||||||
-- constructed based on specifications in "man printf"
|
-- constructed based on specifications in "man printf"
|
||||||
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)([diouxXfFeEgGaAcsbq])(.*)"
|
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)([diouxXfFeEgGaAcsbq])((\n|.)*)"
|
||||||
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \ /
|
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \______ /
|
||||||
-- V V V V V
|
-- V V V V V
|
||||||
-- flags field width precision format character rest
|
-- flags field width precision format character rest
|
||||||
-- field width and precision can be specified with a '*' instead of a digit,
|
-- field width and precision can be specified with a '*' instead of a digit,
|
||||||
-- in which case printf will accept one more argument for each '*' used
|
-- in which case printf will accept one more argument for each '*' used
|
||||||
|
|
||||||
@@ -663,17 +687,12 @@ prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'"
|
|||||||
prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"
|
prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"
|
||||||
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
|
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
|
||||||
where
|
where
|
||||||
f (var:value:rest) =
|
f (var:rest)
|
||||||
let str = literal var in
|
| (not (null rest) && isVariableName str) || isAssignment str =
|
||||||
when (isVariableName str || isAssignment str) $
|
warn (getId var) 2121 "To assign a variable, use just 'var=value', no 'set ..'."
|
||||||
msg (getId var)
|
where str = literal var
|
||||||
f (var:_) =
|
|
||||||
when (isAssignment $ literal var) $
|
|
||||||
msg (getId var)
|
|
||||||
f _ = return ()
|
f _ = return ()
|
||||||
|
|
||||||
msg id = warn id 2121 "To assign a variable, use just 'var=value', no 'set ..'."
|
|
||||||
|
|
||||||
isAssignment str = '=' `elem` str
|
isAssignment str = '=' `elem` str
|
||||||
literal (T_NormalWord _ l) = concatMap literal l
|
literal (T_NormalWord _ l) = concatMap literal l
|
||||||
literal (T_Literal _ str) = str
|
literal (T_Literal _ str) = str
|
||||||
@@ -687,8 +706,7 @@ prop_checkExportedExpansions4 = verifyNot checkExportedExpansions "export ${foo?
|
|||||||
checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
|
checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
|
||||||
where
|
where
|
||||||
check t = sequence_ $ do
|
check t = sequence_ $ do
|
||||||
var <- getSingleUnmodifiedVariable t
|
name <- getSingleUnmodifiedBracedString t
|
||||||
let name = bracedString var
|
|
||||||
return . warn (getId t) 2163 $
|
return . warn (getId t) 2163 $
|
||||||
"This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
"This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
||||||
|
|
||||||
@@ -704,26 +722,25 @@ checkReadExpansions = CommandCheck (Exactly "read") check
|
|||||||
where
|
where
|
||||||
options = getGnuOpts flagsForRead
|
options = getGnuOpts flagsForRead
|
||||||
getVars cmd = fromMaybe [] $ do
|
getVars cmd = fromMaybe [] $ do
|
||||||
opts <- options cmd
|
opts <- options $ arguments cmd
|
||||||
return [y | (x,y) <- opts, null x || x == "a"]
|
return [y | (x,(_, y)) <- opts, null x || x == "a"]
|
||||||
|
|
||||||
check cmd = mapM_ warning $ getVars cmd
|
check cmd = mapM_ warning $ getVars cmd
|
||||||
warning t = sequence_ $ do
|
warning t = sequence_ $ do
|
||||||
var <- getSingleUnmodifiedVariable t
|
name <- getSingleUnmodifiedBracedString t
|
||||||
let name = bracedString var
|
|
||||||
guard $ isVariableName name -- e.g. not $1
|
guard $ isVariableName name -- e.g. not $1
|
||||||
return . warn (getId t) 2229 $
|
return . warn (getId t) 2229 $
|
||||||
"This does not read '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
"This does not read '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
||||||
|
|
||||||
-- Return the single variable expansion that makes up this word, if any.
|
-- Return the single variable expansion that makes up this word, if any.
|
||||||
-- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing
|
-- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing
|
||||||
getSingleUnmodifiedVariable :: Token -> Maybe Token
|
getSingleUnmodifiedBracedString :: Token -> Maybe String
|
||||||
getSingleUnmodifiedVariable word =
|
getSingleUnmodifiedBracedString word =
|
||||||
case getWordParts word of
|
case getWordParts word of
|
||||||
[t@(T_DollarBraced {})] ->
|
[T_DollarBraced _ _ l] ->
|
||||||
let contents = bracedString t
|
let contents = concat $ oversimplify l
|
||||||
name = getBracedReference contents
|
name = getBracedReference contents
|
||||||
in guard (contents == name) >> return t
|
in guard (contents == name) >> return contents
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
|
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
|
||||||
@@ -883,12 +900,12 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
|
|||||||
notRequested = Map.difference handledMap requestedMap
|
notRequested = Map.difference handledMap requestedMap
|
||||||
|
|
||||||
warnUnhandled optId caseId str =
|
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) = sequence_ $ do
|
warnRedundant (Just str, expr)
|
||||||
str <- key
|
| str `notElem` ["*", ":", "?"] =
|
||||||
guard $ str `notElem` ["*", ":", "?"]
|
warn (getId expr) 2214 "This case is not specified by getopts."
|
||||||
return $ warn (getId expr) 2214 "This case is not specified by getopts."
|
warnRedundant _ = return ()
|
||||||
|
|
||||||
getHandledStrings (_, globs, _) =
|
getHandledStrings (_, globs, _) =
|
||||||
map (\x -> (literal x, x)) globs
|
map (\x -> (literal x, x)) globs
|
||||||
@@ -899,7 +916,7 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
|
|||||||
|
|
||||||
fromGlob t =
|
fromGlob t =
|
||||||
case t of
|
case t of
|
||||||
T_Glob _ ('[':c:']':[]) -> return [c]
|
T_Glob _ ['[', c, ']'] -> return [c]
|
||||||
T_Glob _ "*" -> return "*"
|
T_Glob _ "*" -> return "*"
|
||||||
T_Glob _ "?" -> return "?"
|
T_Glob _ "?" -> return "?"
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
@@ -934,7 +951,7 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
|
|||||||
when (isRecursive t) $
|
when (isRecursive t) $
|
||||||
mapM_ (mapM_ checkWord . braceExpand) $ arguments t
|
mapM_ (mapM_ checkWord . braceExpand) $ arguments t
|
||||||
where
|
where
|
||||||
isRecursive = any (`elem` ["r", "R", "recursive"]) . map snd . getAllFlags
|
isRecursive = any ((`elem` ["r", "R", "recursive"]) . snd) . getAllFlags
|
||||||
|
|
||||||
checkWord token =
|
checkWord token =
|
||||||
case getLiteralString token of
|
case getLiteralString token of
|
||||||
@@ -966,9 +983,9 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
|
|||||||
f _ = return ""
|
f _ = return ""
|
||||||
|
|
||||||
stripTrailing c = reverse . dropWhile (== c) . reverse
|
stripTrailing c = reverse . dropWhile (== c) . reverse
|
||||||
skipRepeating c (a:b:rest) | a == b && b == c = skipRepeating c (b:rest)
|
skipRepeating c = foldr go []
|
||||||
skipRepeating c (a:r) = a:skipRepeating c r
|
where
|
||||||
skipRepeating _ [] = []
|
go a r = a : case r of b:rest | b == c && a == b -> rest; _ -> r
|
||||||
|
|
||||||
paths = [
|
paths = [
|
||||||
"", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",
|
"", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",
|
||||||
@@ -1081,8 +1098,8 @@ prop_checkSudoArgs7 = verifyNot checkSudoArgs "sudo docker export foo"
|
|||||||
checkSudoArgs = CommandCheck (Basename "sudo") f
|
checkSudoArgs = CommandCheck (Basename "sudo") f
|
||||||
where
|
where
|
||||||
f t = sequence_ $ do
|
f t = sequence_ $ do
|
||||||
opts <- parseOpts t
|
opts <- parseOpts $ arguments t
|
||||||
let nonFlags = [x | ("",x) <- opts]
|
let nonFlags = [x | ("",(x, _)) <- opts]
|
||||||
commandArg <- nonFlags !!! 0
|
commandArg <- nonFlags !!! 0
|
||||||
command <- getLiteralString commandArg
|
command <- getLiteralString commandArg
|
||||||
guard $ command `elem` builtins
|
guard $ command `elem` builtins
|
||||||
@@ -1113,5 +1130,18 @@ checkChmodDashr = CommandCheck (Basename "chmod") f
|
|||||||
guard $ flag == "-r"
|
guard $ flag == "-r"
|
||||||
return $ warn (getId t) 2253 "Use -R to recurse, or explicitly a-r to remove read permissions."
|
return $ warn (getId t) 2253 "Use -R to recurse, or explicitly a-r to remove read permissions."
|
||||||
|
|
||||||
|
prop_checkXargsDashi1 = verify checkXargsDashi "xargs -i{} echo {}"
|
||||||
|
prop_checkXargsDashi2 = verifyNot checkXargsDashi "xargs -I{} echo {}"
|
||||||
|
prop_checkXargsDashi3 = verifyNot checkXargsDashi "xargs sed -i -e foo"
|
||||||
|
prop_checkXargsDashi4 = verify checkXargsDashi "xargs -e sed -i foo"
|
||||||
|
prop_checkXargsDashi5 = verifyNot checkXargsDashi "xargs -x sed -i foo"
|
||||||
|
checkXargsDashi = CommandCheck (Basename "xargs") f
|
||||||
|
where
|
||||||
|
f t = sequence_ $ do
|
||||||
|
opts <- parseOpts $ arguments t
|
||||||
|
(option, value) <- lookup "i" opts
|
||||||
|
return $ info (getId option) 2267 "GNU xargs -i is deprecated in favor of -I{}"
|
||||||
|
parseOpts = getBsdOpts "0oprtxadR:S:J:L:l:n:P:s:e:E:i:I:"
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||||
|
@@ -178,6 +178,9 @@ prop_checkBashisms93 = verify checkBashisms "#!/bin/sh\necho $(( 10#$(date +%m)
|
|||||||
prop_checkBashisms94 = verify checkBashisms "#!/bin/sh\n[ -v var ]"
|
prop_checkBashisms94 = verify checkBashisms "#!/bin/sh\n[ -v var ]"
|
||||||
prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_"
|
prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_"
|
||||||
prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
|
prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
|
||||||
|
prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}"
|
||||||
|
prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}"
|
||||||
|
prop_checkBashisms99 = verifyNot checkBashisms "#!/bin/dash\necho [^f]oo"
|
||||||
checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||||
params <- ask
|
params <- ask
|
||||||
kludge params t
|
kludge params t
|
||||||
@@ -186,102 +189,102 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||||||
kludge params = bashism
|
kludge params = bashism
|
||||||
where
|
where
|
||||||
isDash = shellType params == Dash
|
isDash = shellType params == Dash
|
||||||
warnMsg id s =
|
warnMsg id code s =
|
||||||
if isDash
|
if isDash
|
||||||
then warn id 2169 $ "In dash, " ++ s ++ " not supported."
|
then err id code $ "In dash, " ++ s ++ " not supported."
|
||||||
else warn id 2039 $ "In POSIX sh, " ++ s ++ " undefined."
|
else warn id code $ "In POSIX sh, " ++ s ++ " undefined."
|
||||||
|
|
||||||
bashism (T_ProcSub id _ _) = warnMsg id "process substitution is"
|
bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is"
|
||||||
bashism (T_Extglob id _ _) = warnMsg id "extglob is"
|
bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is"
|
||||||
bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is"
|
bashism (T_DollarSingleQuoted id _) = warnMsg id 3003 "$'..' is"
|
||||||
bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\" is"
|
bashism (T_DollarDoubleQuoted id _) = warnMsg id 3004 "$\"..\" is"
|
||||||
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id "arithmetic for loops are"
|
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id 3005 "arithmetic for loops are"
|
||||||
bashism (T_Arithmetic id _) = warnMsg id "standalone ((..)) is"
|
bashism (T_Arithmetic id _) = warnMsg id 3006 "standalone ((..)) is"
|
||||||
bashism (T_DollarBracket id _) = warnMsg id "$[..] in place of $((..)) is"
|
bashism (T_DollarBracket id _) = warnMsg id 3007 "$[..] in place of $((..)) is"
|
||||||
bashism (T_SelectIn id _ _ _) = warnMsg id "select loops are"
|
bashism (T_SelectIn id _ _ _) = warnMsg id 3008 "select loops are"
|
||||||
bashism (T_BraceExpansion id _) = warnMsg id "brace expansion is"
|
bashism (T_BraceExpansion id _) = warnMsg id 3009 "brace expansion is"
|
||||||
bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is"
|
bashism (T_Condition id DoubleBracket _) = warnMsg id 3010 "[[ ]] is"
|
||||||
bashism (T_HereString id _) = warnMsg id "here-strings are"
|
bashism (T_HereString id _) = warnMsg id 3011 "here-strings are"
|
||||||
bashism (TC_Binary id SingleBracket op _ _)
|
bashism (TC_Binary id SingleBracket op _ _)
|
||||||
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
|
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
|
||||||
unless isDash $ warnMsg id $ "lexicographical " ++ op ++ " is"
|
unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is"
|
||||||
bashism (TC_Binary id SingleBracket op _ _)
|
bashism (TC_Binary id SingleBracket op _ _)
|
||||||
| op `elem` [ "-ot", "-nt", "-ef" ] =
|
| op `elem` [ "-ot", "-nt", "-ef" ] =
|
||||||
unless isDash $ warnMsg id $ op ++ " is"
|
unless isDash $ warnMsg id 3013 $ op ++ " is"
|
||||||
bashism (TC_Binary id SingleBracket "==" _ _) =
|
bashism (TC_Binary id SingleBracket "==" _ _) =
|
||||||
warnMsg id "== in place of = is"
|
warnMsg id 3014 "== in place of = is"
|
||||||
bashism (TC_Binary id SingleBracket "=~" _ _) =
|
bashism (TC_Binary id SingleBracket "=~" _ _) =
|
||||||
warnMsg id "=~ regex matching is"
|
warnMsg id 3015 "=~ regex matching is"
|
||||||
bashism (TC_Unary id SingleBracket "-v" _) =
|
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" _) =
|
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 _)
|
bashism (TA_Unary id op _)
|
||||||
| op `elem` [ "|++", "|--", "++|", "--|"] =
|
| op `elem` [ "|++", "|--", "++|", "--|"] =
|
||||||
warnMsg id $ filter (/= '|') op ++ " is"
|
warnMsg id 3018 $ filter (/= '|') op ++ " is"
|
||||||
bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are"
|
bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
|
||||||
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is"
|
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id 3020 "&> is"
|
||||||
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id ">& is"
|
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id 3021 ">& is"
|
||||||
bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are"
|
bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are"
|
||||||
bashism (T_FdRedirect id num _)
|
bashism (T_FdRedirect id num _)
|
||||||
| all isDigit num && length num > 1 = warnMsg id "FDs outside 0-9 are"
|
| all isDigit num && length num > 1 = warnMsg id 3023 "FDs outside 0-9 are"
|
||||||
bashism (T_Assignment id Append _ _ _) =
|
bashism (T_Assignment id Append _ _ _) =
|
||||||
warnMsg id "+= is"
|
warnMsg id 3024 "+= is"
|
||||||
bashism (T_IoFile id _ word) | isNetworked =
|
bashism (T_IoFile id _ word) | isNetworked =
|
||||||
warnMsg id "/dev/{tcp,udp} is"
|
warnMsg id 3025 "/dev/{tcp,udp} is"
|
||||||
where
|
where
|
||||||
file = onlyLiteralString word
|
file = onlyLiteralString word
|
||||||
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
|
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
|
||||||
bashism (T_Glob id str) | "[^" `isInfixOf` str =
|
bashism (T_Glob id str) | not isDash && "[^" `isInfixOf` str =
|
||||||
warnMsg id "^ in place of ! in glob bracket expressions is"
|
warnMsg id 3026 "^ in place of ! in glob bracket expressions is"
|
||||||
|
|
||||||
bashism t@(TA_Variable id str _) | isBashVariable str =
|
bashism t@(TA_Variable id str _) | isBashVariable str =
|
||||||
warnMsg id $ str ++ " is"
|
warnMsg id 3028 $ str ++ " is"
|
||||||
|
|
||||||
bashism t@(T_DollarBraced id _ token) = do
|
bashism t@(T_DollarBraced id _ token) = do
|
||||||
mapM_ check expansion
|
mapM_ check expansion
|
||||||
when (isBashVariable var) $
|
when (isBashVariable var) $
|
||||||
warnMsg id $ var ++ " is"
|
warnMsg id 3028 $ var ++ " is"
|
||||||
where
|
where
|
||||||
str = bracedString t
|
str = concat $ oversimplify token
|
||||||
var = getBracedReference str
|
var = getBracedReference str
|
||||||
check (regex, feature) =
|
check (regex, code, feature) =
|
||||||
when (isJust $ matchRegex regex str) $ warnMsg id feature
|
when (isJust $ matchRegex regex str) $ warnMsg id code feature
|
||||||
|
|
||||||
bashism t@(T_Pipe id "|&") =
|
bashism t@(T_Pipe id "|&") =
|
||||||
warnMsg id "|& in place of 2>&1 | is"
|
warnMsg id 3029 "|& in place of 2>&1 | is"
|
||||||
bashism (T_Array id _) =
|
bashism (T_Array id _) =
|
||||||
warnMsg id "arrays are"
|
warnMsg id 3030 "arrays are"
|
||||||
bashism (T_IoFile id _ t) | isGlob t =
|
bashism (T_IoFile id _ t) | isGlob t =
|
||||||
warnMsg id "redirecting to/from globs is"
|
warnMsg id 3031 "redirecting to/from globs is"
|
||||||
bashism (T_CoProc id _ _) =
|
bashism (T_CoProc id _ _) =
|
||||||
warnMsg id "coproc is"
|
warnMsg id 3032 "coproc is"
|
||||||
|
|
||||||
bashism (T_Function id _ _ str _) | not (isVariableName str) =
|
bashism (T_Function id _ _ str _) | not (isVariableName str) =
|
||||||
warnMsg id "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
|
warnMsg id 3033 "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
|
||||||
|
|
||||||
bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
|
bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
|
||||||
warnMsg id "$(<file) to read files is"
|
warnMsg id 3034 "$(<file) to read files is"
|
||||||
bashism (T_Backticked id [x]) | isOnlyRedirection x =
|
bashism (T_Backticked id [x]) | isOnlyRedirection x =
|
||||||
warnMsg id "`<file` to read files is"
|
warnMsg id 3035 "`<file` to read files is"
|
||||||
|
|
||||||
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
||||||
| t `isCommand` "echo" && argString `matches` flagRegex =
|
| t `isCommand` "echo" && argString `matches` flagRegex =
|
||||||
if isDash
|
if isDash
|
||||||
then
|
then
|
||||||
when (argString /= "-n") $
|
when (argString /= "-n") $
|
||||||
warnMsg (getId arg) "echo flags besides -n"
|
warnMsg (getId arg) 3036 "echo flags besides -n"
|
||||||
else
|
else
|
||||||
warnMsg (getId arg) "echo flags are"
|
warnMsg (getId arg) 3037 "echo flags are"
|
||||||
where
|
where
|
||||||
argString = concat $ oversimplify arg
|
argString = concat $ oversimplify arg
|
||||||
flagRegex = mkRegex "^-[eEsn]+$"
|
flagRegex = mkRegex "^-[eEsn]+$"
|
||||||
|
|
||||||
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
||||||
| t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
|
| getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
|
||||||
warnMsg (getId arg) "exec flags are"
|
warnMsg (getId arg) 3038 "exec flags are"
|
||||||
bashism t@(T_SimpleCommand id _ _)
|
bashism t@(T_SimpleCommand id _ _)
|
||||||
| t `isCommand` "let" = warnMsg id "'let' is"
|
| t `isCommand` "let" = warnMsg id 3039 "'let' is"
|
||||||
bashism t@(T_SimpleCommand _ _ (cmd:args))
|
bashism t@(T_SimpleCommand _ _ (cmd:args))
|
||||||
| t `isCommand` "set" = unless isDash $
|
| t `isCommand` "set" = unless isDash $
|
||||||
checkOptions $ getLiteralArgs args
|
checkOptions $ getLiteralArgs args
|
||||||
@@ -289,16 +292,17 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||||||
-- Get the literal options from a list of arguments,
|
-- Get the literal options from a list of arguments,
|
||||||
-- up until the first non-literal one
|
-- up until the first non-literal one
|
||||||
getLiteralArgs :: [Token] -> [(Id, String)]
|
getLiteralArgs :: [Token] -> [(Id, String)]
|
||||||
getLiteralArgs (first:rest) = fromMaybe [] $ do
|
getLiteralArgs = foldr go []
|
||||||
str <- getLiteralString first
|
where
|
||||||
return $ (getId first, str) : getLiteralArgs rest
|
go first rest = case getLiteralString first of
|
||||||
getLiteralArgs [] = []
|
Just str -> (getId first, str) : rest
|
||||||
|
Nothing -> []
|
||||||
|
|
||||||
-- Check a flag-option pair (such as -o errexit)
|
-- Check a flag-option pair (such as -o errexit)
|
||||||
checkOptions (flag@(fid,flag') : opt@(oid,opt') : rest)
|
checkOptions (flag@(fid,flag') : opt@(oid,opt') : rest)
|
||||||
| flag' `matches` oFlagRegex = do
|
| flag' `matches` oFlagRegex = do
|
||||||
when (opt' `notElem` longOptions) $
|
when (opt' `notElem` longOptions) $
|
||||||
warnMsg oid $ "set option " <> opt' <> " is"
|
warnMsg oid 3040 $ "set option " <> opt' <> " is"
|
||||||
checkFlags (flag:rest)
|
checkFlags (flag:rest)
|
||||||
| otherwise = checkFlags (flag:opt:rest)
|
| otherwise = checkFlags (flag:opt:rest)
|
||||||
checkOptions (flag:rest) = checkFlags (flag:rest)
|
checkOptions (flag:rest) = checkFlags (flag:rest)
|
||||||
@@ -311,10 +315,10 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||||||
unless (flag' `matches` validFlagsRegex) $
|
unless (flag' `matches` validFlagsRegex) $
|
||||||
forM_ (tail flag') $ \letter ->
|
forM_ (tail flag') $ \letter ->
|
||||||
when (letter `notElem` optionsSet) $
|
when (letter `notElem` optionsSet) $
|
||||||
warnMsg fid $ "set flag " <> ('-':letter:" is")
|
warnMsg fid 3041 $ "set flag " <> ('-':letter:" is")
|
||||||
checkOptions rest
|
checkOptions rest
|
||||||
| beginsWithDoubleDash flag' = do
|
| beginsWithDoubleDash flag' = do
|
||||||
warnMsg fid $ "set flag " <> flag' <> " is"
|
warnMsg fid 3042 $ "set flag " <> flag' <> " is"
|
||||||
checkOptions rest
|
checkOptions rest
|
||||||
-- Either a word that doesn't start with a dash, or simply '--',
|
-- Either a word that doesn't start with a dash, or simply '--',
|
||||||
-- so stop checking.
|
-- so stop checking.
|
||||||
@@ -336,16 +340,19 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||||||
let name = fromMaybe "" $ getCommandName t
|
let name = fromMaybe "" $ getCommandName t
|
||||||
flags = getLeadingFlags t
|
flags = getLeadingFlags t
|
||||||
in do
|
in do
|
||||||
|
when (name == "local" && not isDash) $
|
||||||
|
-- This is so commonly accepted that we'll make it a special case
|
||||||
|
warnMsg id 3043 $ "'local' is"
|
||||||
when (name `elem` unsupportedCommands) $
|
when (name `elem` unsupportedCommands) $
|
||||||
warnMsg id $ "'" ++ name ++ "' is"
|
warnMsg id 3044 $ "'" ++ name ++ "' is"
|
||||||
sequence_ $ do
|
sequence_ $ do
|
||||||
allowed' <- Map.lookup name allowedFlags
|
allowed' <- Map.lookup name allowedFlags
|
||||||
allowed <- allowed'
|
allowed <- allowed'
|
||||||
(word, flag) <- find
|
(word, flag) <- find
|
||||||
(\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
|
(\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
|
||||||
return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"
|
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") $
|
when (name == "trap") $
|
||||||
let
|
let
|
||||||
check token = sequence_ $ do
|
check token = sequence_ $ do
|
||||||
@@ -353,12 +360,12 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||||||
let upper = map toUpper str
|
let upper = map toUpper str
|
||||||
return $ do
|
return $ do
|
||||||
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
|
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
|
||||||
warnMsg (getId token) $ "trapping " ++ str ++ " is"
|
warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is"
|
||||||
when ("SIG" `isPrefixOf` upper) $
|
when ("SIG" `isPrefixOf` upper) $
|
||||||
warnMsg (getId token)
|
warnMsg (getId token) 3048
|
||||||
"prefixing signal names with 'SIG' is"
|
"prefixing signal names with 'SIG' is"
|
||||||
when (not isDash && upper /= str) $
|
when (not isDash && upper /= str) $
|
||||||
warnMsg (getId token)
|
warnMsg (getId token) 3049
|
||||||
"using lower/mixed case for signal names is"
|
"using lower/mixed case for signal names is"
|
||||||
in
|
in
|
||||||
mapM_ check (drop 1 rest)
|
mapM_ check (drop 1 rest)
|
||||||
@@ -367,13 +374,13 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||||||
format <- rest !!! 0 -- flags are covered by allowedFlags
|
format <- rest !!! 0 -- flags are covered by allowedFlags
|
||||||
let literal = onlyLiteralString format
|
let literal = onlyLiteralString format
|
||||||
guard $ "%q" `isInfixOf` literal
|
guard $ "%q" `isInfixOf` literal
|
||||||
return $ warnMsg (getId format) "printf %q is"
|
return $ warnMsg (getId format) 3050 "printf %q is"
|
||||||
where
|
where
|
||||||
unsupportedCommands = [
|
unsupportedCommands = [
|
||||||
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
||||||
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
|
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
|
||||||
"typeset"
|
"typeset"
|
||||||
] ++ if not isDash then ["local"] else []
|
]
|
||||||
allowedFlags = Map.fromList [
|
allowedFlags = Map.fromList [
|
||||||
("cd", Just ["L", "P"]),
|
("cd", Just ["L", "P"]),
|
||||||
("exec", Just []),
|
("exec", Just []),
|
||||||
@@ -390,29 +397,35 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||||||
("unset", Just ["f", "v"]),
|
("unset", Just ["f", "v"]),
|
||||||
("wait", Just [])
|
("wait", Just [])
|
||||||
]
|
]
|
||||||
bashism t@(T_SourceCommand id src _) =
|
bashism t@(T_SourceCommand id src _)
|
||||||
let name = fromMaybe "" $ getCommandName src
|
| getCommandName src == Just "source" = warnMsg id 3051 "'source' in place of '.' is"
|
||||||
in when (name == "source") $ warnMsg id "'source' in place of '.' is"
|
bashism (TA_Expansion _ (T_Literal id str : _))
|
||||||
bashism (TA_Expansion _ (T_Literal id str : _)) | str `matches` radix =
|
| str `matches` radix = warnMsg id 3052 "arithmetic base conversion is"
|
||||||
when (str `matches` radix) $ warnMsg id "arithmetic base conversion is"
|
|
||||||
where
|
where
|
||||||
radix = mkRegex "^[0-9]+#"
|
radix = mkRegex "^[0-9]+#"
|
||||||
bashism _ = return ()
|
bashism _ = return ()
|
||||||
|
|
||||||
varChars="_0-9a-zA-Z"
|
varChars="_0-9a-zA-Z"
|
||||||
expansion = let re = mkRegex in [
|
expansion = let re = mkRegex in [
|
||||||
(re $ "^![" ++ varChars ++ "]", "indirect expansion is"),
|
(re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"),
|
||||||
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
|
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"),
|
||||||
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
|
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"),
|
||||||
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
|
(re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"),
|
||||||
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", "string indexing is"),
|
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", 3057, "string indexing is"),
|
||||||
(re $ "^([*@][%#]|#[@*])", "string operations on $@/$* are"),
|
(re $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"),
|
||||||
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")
|
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is"),
|
||||||
|
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is")
|
||||||
]
|
]
|
||||||
bashVars = [
|
bashVars = [
|
||||||
|
-- This list deliberately excludes $BASH_VERSION as it's often used
|
||||||
|
-- for shell identification.
|
||||||
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
||||||
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS",
|
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS",
|
||||||
"_"
|
"_", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
|
||||||
|
"BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND",
|
||||||
|
"BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE",
|
||||||
|
"BASH_SUBSHELL", "BASH_VERSINFO", "EPOCHREALTIME", "EPOCHSECONDS",
|
||||||
|
"FUNCNAME", "GROUPS", "MACHTYPE", "MAPFILE"
|
||||||
]
|
]
|
||||||
bashDynamicVars = [ "RANDOM", "SECONDS" ]
|
bashDynamicVars = [ "RANDOM", "SECONDS" ]
|
||||||
dashVars = [ "_" ]
|
dashVars = [ "_" ]
|
||||||
@@ -506,13 +519,13 @@ checkMultiDimensionalArrays = ForShell [Bash] f
|
|||||||
case token of
|
case token of
|
||||||
T_Assignment _ _ name (first:second:_) _ -> about second
|
T_Assignment _ _ name (first:second:_) _ -> about second
|
||||||
T_IndexedElement _ (first:second:_) _ -> about second
|
T_IndexedElement _ (first:second:_) _ -> about second
|
||||||
T_DollarBraced {} ->
|
T_DollarBraced _ _ l ->
|
||||||
when (isMultiDim token) $ about token
|
when (isMultiDim l) $ about token
|
||||||
_ -> return ()
|
_ -> return ()
|
||||||
about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays."
|
about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays."
|
||||||
|
|
||||||
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
|
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
|
||||||
isMultiDim t = getBracedModifier (bracedString t) `matches` re
|
isMultiDim l = getBracedModifier (concat $ oversimplify l) `matches` re
|
||||||
|
|
||||||
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
|
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
|
||||||
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
|
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
|
||||||
|
@@ -273,10 +273,10 @@ getPrefixSum = f 0
|
|||||||
where
|
where
|
||||||
f sum _ PSLeaf = sum
|
f sum _ PSLeaf = sum
|
||||||
f sum target (PSBranch pivot left right cumulative) =
|
f sum target (PSBranch pivot left right cumulative) =
|
||||||
case () of
|
case target `compare` pivot of
|
||||||
_ | target < pivot -> f sum target left
|
LT -> f sum target left
|
||||||
_ | target > pivot -> f (sum+cumulative) target right
|
GT -> f (sum+cumulative) target right
|
||||||
_ -> sum+cumulative
|
EQ -> sum+cumulative
|
||||||
|
|
||||||
-- Add a value to the Prefix Sum tree at the given index.
|
-- Add a value to the Prefix Sum tree at the given index.
|
||||||
-- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5
|
-- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5
|
||||||
@@ -285,10 +285,10 @@ addPSValue key value tree = if value == 0 then tree else f tree
|
|||||||
where
|
where
|
||||||
f PSLeaf = PSBranch key PSLeaf PSLeaf value
|
f PSLeaf = PSBranch key PSLeaf PSLeaf value
|
||||||
f (PSBranch pivot left right sum) =
|
f (PSBranch pivot left right sum) =
|
||||||
case () of
|
case key `compare` pivot of
|
||||||
_ | key < pivot -> PSBranch pivot (f left) right (sum + value)
|
LT -> PSBranch pivot (f left) right (sum + value)
|
||||||
_ | key > pivot -> PSBranch pivot left (f right) sum
|
GT -> PSBranch pivot left (f right) sum
|
||||||
_ -> PSBranch pivot left right (sum + value)
|
EQ -> PSBranch pivot left right (sum + value)
|
||||||
|
|
||||||
prop_pstreeSumsCorrectly kvs targets =
|
prop_pstreeSumsCorrectly kvs targets =
|
||||||
let
|
let
|
||||||
|
@@ -127,7 +127,7 @@ outputForFile color sys comments = do
|
|||||||
let lineCount = length fileLinesList
|
let lineCount = length fileLinesList
|
||||||
let fileLines = listArray (1, lineCount) fileLinesList
|
let fileLines = listArray (1, lineCount) fileLinesList
|
||||||
let groups = groupWith lineNo comments
|
let groups = groupWith lineNo comments
|
||||||
mapM_ (\commentsForLine -> do
|
forM_ groups $ \commentsForLine -> do
|
||||||
let lineNum = fromIntegral $ lineNo (head commentsForLine)
|
let lineNum = fromIntegral $ lineNo (head commentsForLine)
|
||||||
let line = if lineNum < 1 || lineNum > lineCount
|
let line = if lineNum < 1 || lineNum > lineCount
|
||||||
then ""
|
then ""
|
||||||
@@ -136,10 +136,9 @@ outputForFile color sys comments = do
|
|||||||
putStrLn $ color "message" $
|
putStrLn $ color "message" $
|
||||||
"In " ++ fileName ++" line " ++ show lineNum ++ ":"
|
"In " ++ fileName ++" line " ++ show lineNum ++ ":"
|
||||||
putStrLn (color "source" line)
|
putStrLn (color "source" line)
|
||||||
mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) commentsForLine
|
forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c
|
||||||
putStrLn ""
|
putStrLn ""
|
||||||
showFixedString color commentsForLine (fromIntegral lineNum) fileLines
|
showFixedString color commentsForLine (fromIntegral lineNum) fileLines
|
||||||
) groups
|
|
||||||
|
|
||||||
-- Pick out only the lines necessary to show a fix in action
|
-- Pick out only the lines necessary to show a fix in action
|
||||||
sliceFile :: Fix -> Array Int String -> (Fix, Array Int String)
|
sliceFile :: Fix -> Array Int String -> (Fix, Array Int String)
|
||||||
|
@@ -123,8 +123,10 @@ readUnicodeQuote = do
|
|||||||
return $ T_Literal id [c]
|
return $ T_Literal id [c]
|
||||||
|
|
||||||
carriageReturn = do
|
carriageReturn = do
|
||||||
parseNote ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ."
|
pos <- getPosition
|
||||||
char '\r'
|
char '\r'
|
||||||
|
parseProblemAt pos ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ."
|
||||||
|
return '\r'
|
||||||
|
|
||||||
almostSpace =
|
almostSpace =
|
||||||
choice [
|
choice [
|
||||||
@@ -209,8 +211,13 @@ startSpan = IncompleteInterval <$> getPosition
|
|||||||
|
|
||||||
endSpan (IncompleteInterval start) = do
|
endSpan (IncompleteInterval start) = do
|
||||||
endPos <- getPosition
|
endPos <- getPosition
|
||||||
id <- getNextIdBetween start endPos
|
getNextIdBetween start endPos
|
||||||
return id
|
|
||||||
|
getSpanPositionsFor m = do
|
||||||
|
start <- getPosition
|
||||||
|
m
|
||||||
|
end <- getPosition
|
||||||
|
return (start, end)
|
||||||
|
|
||||||
addToHereDocMap id list = do
|
addToHereDocMap id list = do
|
||||||
state <- getState
|
state <- getState
|
||||||
@@ -253,16 +260,22 @@ ignoreProblemsOf p = do
|
|||||||
shouldIgnoreCode code = do
|
shouldIgnoreCode code = do
|
||||||
context <- getCurrentContexts
|
context <- getCurrentContexts
|
||||||
checkSourced <- Mr.asks checkSourced
|
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
|
where
|
||||||
disabling checkSourced item =
|
disabling checkSourced item =
|
||||||
case item of
|
case item of
|
||||||
ContextAnnotation list -> any disabling' list
|
ContextAnnotation list -> any disabling' list
|
||||||
ContextSource _ -> not $ checkSourced
|
ContextSource _ -> not $ checkSourced
|
||||||
_ -> False
|
_ -> False
|
||||||
disabling' (DisableComment n) = code == n
|
disabling' (DisableComment n m) = code >= n && code < m
|
||||||
disabling' _ = False
|
disabling' _ = False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
getCurrentAnnotations includeSource =
|
getCurrentAnnotations includeSource =
|
||||||
concatMap get . takeWhile (not . isBoundary) <$> getCurrentContexts
|
concatMap get . takeWhile (not . isBoundary) <$> getCurrentContexts
|
||||||
where
|
where
|
||||||
@@ -373,16 +386,15 @@ parseNoteAtId id c l a = do
|
|||||||
parseNoteAtWithEnd start end c l a = addParseNote $ ParseNote start end c l a
|
parseNoteAtWithEnd start end c l a = addParseNote $ ParseNote start end c l a
|
||||||
|
|
||||||
--------- Convenient combinators
|
--------- Convenient combinators
|
||||||
thenSkip main follow = do
|
thenSkip main follow = main <* optional follow
|
||||||
r <- main
|
|
||||||
optional follow
|
|
||||||
return r
|
|
||||||
|
|
||||||
unexpecting s p = try $
|
unexpecting s p = try $
|
||||||
(try p >> fail ("Unexpected " ++ s)) <|> return ()
|
(try p >> fail ("Unexpected " ++ s)) <|> return ()
|
||||||
|
|
||||||
notFollowedBy2 = unexpecting ""
|
notFollowedBy2 = unexpecting ""
|
||||||
|
|
||||||
|
isFollowedBy p = (lookAhead . try $ p $> True) <|> return False
|
||||||
|
|
||||||
reluctantlyTill p end =
|
reluctantlyTill p end =
|
||||||
(lookAhead (void (try end) <|> eof) >> return []) <|> do
|
(lookAhead (void (try end) <|> eof) >> return []) <|> do
|
||||||
x <- p
|
x <- p
|
||||||
@@ -420,7 +432,7 @@ acceptButWarn parser level code note =
|
|||||||
|
|
||||||
parsecBracket before after op = do
|
parsecBracket before after op = do
|
||||||
val <- before
|
val <- before
|
||||||
(op val <* optional (after val)) <|> (after val *> fail "")
|
op val `thenSkip` after val <|> (after val *> fail "")
|
||||||
|
|
||||||
swapContext contexts p =
|
swapContext contexts p =
|
||||||
parsecBracket (getCurrentContexts <* setCurrentContexts contexts)
|
parsecBracket (getCurrentContexts <* setCurrentContexts contexts)
|
||||||
@@ -914,8 +926,9 @@ prop_readCondition20 = isOk readCondition "[[ echo_rc -eq 0 ]]"
|
|||||||
prop_readCondition21 = isOk readCondition "[[ $1 =~ ^(a\\ b)$ ]]"
|
prop_readCondition21 = isOk readCondition "[[ $1 =~ ^(a\\ b)$ ]]"
|
||||||
prop_readCondition22 = isOk readCondition "[[ $1 =~ \\.a\\.(\\.b\\.)\\.c\\. ]]"
|
prop_readCondition22 = isOk readCondition "[[ $1 =~ \\.a\\.(\\.b\\.)\\.c\\. ]]"
|
||||||
prop_readCondition23 = isOk readCondition "[[ -v arr[$var] ]]"
|
prop_readCondition23 = isOk readCondition "[[ -v arr[$var] ]]"
|
||||||
prop_readCondition24 = isWarning readCondition "[[ 1 == 2 ]]]"
|
|
||||||
prop_readCondition25 = isOk readCondition "[[ lex.yy.c -ot program.l ]]"
|
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
|
readCondition = called "test expression" $ do
|
||||||
opos <- getPosition
|
opos <- getPosition
|
||||||
start <- startSpan
|
start <- startSpan
|
||||||
@@ -942,15 +955,9 @@ readCondition = called "test expression" $ do
|
|||||||
cpos <- getPosition
|
cpos <- getPosition
|
||||||
close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here (don't wrap commands in []/[[]])"
|
close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here (don't wrap commands in []/[[]])"
|
||||||
id <- endSpan start
|
id <- endSpan start
|
||||||
when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "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 "Did you mean [[ ?"
|
when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Test expression was opened with single [ but closed with double ]]. Make sure they match."
|
||||||
optional $ lookAhead $ do
|
|
||||||
pos <- getPosition
|
|
||||||
notFollowedBy2 readCmdWord <|>
|
|
||||||
parseProblemAt pos ErrorC 1136
|
|
||||||
("Unexpected characters after terminating " ++ close ++ ". Missing semicolon/linefeed?")
|
|
||||||
spacing
|
spacing
|
||||||
many readCmdWord -- Read and throw away remainders to get then/do warnings. Fixme?
|
|
||||||
return $ T_Condition id typ condition
|
return $ T_Condition id typ condition
|
||||||
|
|
||||||
readAnnotationPrefix = do
|
readAnnotationPrefix = do
|
||||||
@@ -964,6 +971,7 @@ prop_readAnnotation3 = isOk readAnnotation "# shellcheck disable=SC1234 source=/
|
|||||||
prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=SC1234\n"
|
prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=SC1234\n"
|
||||||
prop_readAnnotation5 = isOk readAnnotation "# shellcheck disable=SC2002 # All cats are precious\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_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
|
readAnnotation = called "shellcheck directive" $ do
|
||||||
try readAnnotationPrefix
|
try readAnnotationPrefix
|
||||||
many1 linewhitespace
|
many1 linewhitespace
|
||||||
@@ -984,12 +992,16 @@ readAnnotationWithoutPrefix = do
|
|||||||
key <- many1 (letter <|> char '-')
|
key <- many1 (letter <|> char '-')
|
||||||
char '=' <|> fail "Expected '=' after directive key"
|
char '=' <|> fail "Expected '=' after directive key"
|
||||||
annotations <- case key of
|
annotations <- case key of
|
||||||
"disable" -> readCode `sepBy` char ','
|
"disable" -> readRange `sepBy` char ','
|
||||||
where
|
where
|
||||||
|
readRange = do
|
||||||
|
from <- readCode
|
||||||
|
to <- choice [ char '-' *> readCode, return $ from+1 ]
|
||||||
|
return $ DisableComment from to
|
||||||
readCode = do
|
readCode = do
|
||||||
optional $ string "SC"
|
optional $ string "SC"
|
||||||
int <- many1 digit
|
int <- many1 digit
|
||||||
return $ DisableComment (read int)
|
return $ read int
|
||||||
|
|
||||||
"enable" -> readName `sepBy` char ','
|
"enable" -> readName `sepBy` char ','
|
||||||
where
|
where
|
||||||
@@ -1554,7 +1566,7 @@ readDollarExpression = do
|
|||||||
readDollarExp = arithmetic <|> readDollarExpansion <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarVariable
|
readDollarExp = arithmetic <|> readDollarExpansion <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarVariable
|
||||||
where
|
where
|
||||||
arithmetic = readAmbiguous "$((" readDollarArithmetic readDollarExpansion (\pos ->
|
arithmetic = readAmbiguous "$((" readDollarArithmetic readDollarExpansion (\pos ->
|
||||||
parseNoteAt pos ErrorC 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'"
|
prop_readDollarSingleQuote = isOk readDollarSingleQuote "$'foo\\\'lol'"
|
||||||
readDollarSingleQuote = called "$'..' expression" $ do
|
readDollarSingleQuote = called "$'..' expression" $ do
|
||||||
@@ -1603,6 +1615,7 @@ readArithmeticExpression = called "((..)) command" $ do
|
|||||||
c <- readArithmeticContents
|
c <- readArithmeticContents
|
||||||
string "))"
|
string "))"
|
||||||
id <- endSpan start
|
id <- endSpan start
|
||||||
|
spacing
|
||||||
return (T_Arithmetic id c)
|
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
|
-- If the next characters match prefix, try two different parsers and warn if the alternate parser had to be used
|
||||||
@@ -1749,6 +1762,8 @@ prop_readHereDoc17= isWarning readScript "cat <<- ' foo'\nbar\n foo\n foo\n"
|
|||||||
prop_readHereDoc18= isOk readScript "cat <<'\"foo'\nbar\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_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_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
|
readHereDoc = called "here document" $ do
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
try $ string "<<"
|
try $ string "<<"
|
||||||
@@ -1777,7 +1792,9 @@ readHereDoc = called "here document" $ do
|
|||||||
-- Fun fact: bash considers << foo"" quoted, but not << <("foo").
|
-- Fun fact: bash considers << foo"" quoted, but not << <("foo").
|
||||||
readToken = do
|
readToken = do
|
||||||
str <- readStringForParser readNormalWord
|
str <- readStringForParser readNormalWord
|
||||||
return $ unquote 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
|
readPendingHereDocs = do
|
||||||
docs <- popPendingHereDocs
|
docs <- popPendingHereDocs
|
||||||
@@ -1892,14 +1909,14 @@ readPendingHereDocs = do
|
|||||||
debugHereDoc tokenId endToken doc
|
debugHereDoc tokenId endToken doc
|
||||||
| endToken `isInfixOf` doc =
|
| endToken `isInfixOf` doc =
|
||||||
let lookAt line = when (endToken `isInfixOf` line) $
|
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
|
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)
|
mapM_ lookAt (lines doc)
|
||||||
| map toLower endToken `isInfixOf` map toLower 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 =
|
| 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
|
readFilename = readNormalWord
|
||||||
@@ -1955,8 +1972,6 @@ readIoRedirect = do
|
|||||||
spacing
|
spacing
|
||||||
return $ T_FdRedirect id n redir
|
return $ T_FdRedirect id n redir
|
||||||
|
|
||||||
readRedirectList = many1 readIoRedirect
|
|
||||||
|
|
||||||
prop_readHereString = isOk readHereString "<<< \"Hello $world\""
|
prop_readHereString = isOk readHereString "<<< \"Hello $world\""
|
||||||
readHereString = called "here string" $ do
|
readHereString = called "here string" $ do
|
||||||
start <- startSpan
|
start <- startSpan
|
||||||
@@ -2079,10 +2094,6 @@ readSimpleCommand = called "simple command" $ do
|
|||||||
then action
|
then action
|
||||||
else getParser def cmd rest
|
else getParser def cmd rest
|
||||||
|
|
||||||
cStyleComment cmd =
|
|
||||||
case cmd of
|
|
||||||
_ -> False
|
|
||||||
|
|
||||||
validateCommand cmd =
|
validateCommand cmd =
|
||||||
case cmd of
|
case cmd of
|
||||||
(T_NormalWord _ [T_Literal _ "//"]) -> commentWarning (getId cmd)
|
(T_NormalWord _ [T_Literal _ "//"]) -> commentWarning (getId cmd)
|
||||||
@@ -2119,14 +2130,14 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
|
|||||||
let file = getFile file' rest'
|
let file = getFile file' rest'
|
||||||
override <- getSourceOverride
|
override <- getSourceOverride
|
||||||
let literalFile = do
|
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
|
-- Hack to avoid 'source ~/foo' trying to read from literal tilde
|
||||||
guard . not $ "~/" `isPrefixOf` name
|
guard . not $ "~/" `isPrefixOf` name
|
||||||
return name
|
return name
|
||||||
case literalFile of
|
case literalFile of
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
parseNoteAtId (getId file) WarningC 1090
|
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
|
return t
|
||||||
Just filename -> do
|
Just filename -> do
|
||||||
proceed <- shouldFollow filename
|
proceed <- shouldFollow filename
|
||||||
@@ -2179,6 +2190,16 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
|
|||||||
SourcePath x -> Just x
|
SourcePath x -> Just x
|
||||||
_ -> Nothing
|
_ -> 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 =
|
subRead name script =
|
||||||
withContext (ContextSource name) $
|
withContext (ContextSource name) $
|
||||||
inSeparateContext $
|
inSeparateContext $
|
||||||
@@ -2276,6 +2297,7 @@ readPipe = do
|
|||||||
|
|
||||||
readCommand = choice [
|
readCommand = choice [
|
||||||
readCompoundCommand,
|
readCompoundCommand,
|
||||||
|
readConditionCommand,
|
||||||
readCoProc,
|
readCoProc,
|
||||||
readSimpleCommand
|
readSimpleCommand
|
||||||
]
|
]
|
||||||
@@ -2388,6 +2410,7 @@ readSubshell = called "explicit subshell" $ do
|
|||||||
allspacing
|
allspacing
|
||||||
char ')' <|> fail "Expected ) closing the subshell"
|
char ')' <|> fail "Expected ) closing the subshell"
|
||||||
id <- endSpan start
|
id <- endSpan start
|
||||||
|
spacing
|
||||||
return $ T_Subshell id list
|
return $ T_Subshell id list
|
||||||
|
|
||||||
prop_readBraceGroup = isOk readBraceGroup "{ a; b | c | d; e; }"
|
prop_readBraceGroup = isOk readBraceGroup "{ a; b | c | d; e; }"
|
||||||
@@ -2408,6 +2431,7 @@ readBraceGroup = called "brace group" $ do
|
|||||||
parseProblem ErrorC 1056 "Expected a '}'. If you have one, try a ; or \\n in front of it."
|
parseProblem ErrorC 1056 "Expected a '}'. If you have one, try a ; or \\n in front of it."
|
||||||
fail "Missing '}'"
|
fail "Missing '}'"
|
||||||
id <- endSpan start
|
id <- endSpan start
|
||||||
|
spacing
|
||||||
return $ T_BraceGroup id list
|
return $ T_BraceGroup id list
|
||||||
|
|
||||||
prop_readBatsTest = isOk readBatsTest "@test 'can parse' {\n true\n}"
|
prop_readBatsTest = isOk readBatsTest "@test 'can parse' {\n true\n}"
|
||||||
@@ -2460,6 +2484,11 @@ readDoGroup kwId = do
|
|||||||
parseProblemAtId (getId doKw) ErrorC 1061 "Couldn't find 'done' for this 'do'."
|
parseProblemAtId (getId doKw) ErrorC 1061 "Couldn't find 'done' for this 'do'."
|
||||||
parseProblem ErrorC 1062 "Expected 'done' matching previously mentioned 'do'."
|
parseProblem ErrorC 1062 "Expected 'done' matching previously mentioned 'do'."
|
||||||
return "Expected 'done'"
|
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
|
return commands
|
||||||
|
|
||||||
|
|
||||||
@@ -2482,19 +2511,31 @@ readForClause = called "for loop" $ do
|
|||||||
readArithmetic id <|> readRegular id
|
readArithmetic id <|> readRegular id
|
||||||
where
|
where
|
||||||
readArithmetic id = called "arithmetic for condition" $ do
|
readArithmetic id = called "arithmetic for condition" $ do
|
||||||
try $ string "(("
|
readArithmeticDelimiter '(' "Missing second '(' to start arithmetic for ((;;)) loop"
|
||||||
x <- readArithmeticContents
|
x <- readArithmeticContents
|
||||||
char ';' >> spacing
|
char ';' >> spacing
|
||||||
y <- readArithmeticContents
|
y <- readArithmeticContents
|
||||||
char ';' >> spacing
|
char ';' >> spacing
|
||||||
z <- readArithmeticContents
|
z <- readArithmeticContents
|
||||||
spacing
|
spacing
|
||||||
string "))"
|
readArithmeticDelimiter ')' "Missing second ')' to terminate 'for ((;;))' loop condition"
|
||||||
spacing
|
spacing
|
||||||
optional $ readSequentialSep >> spacing
|
optional $ readSequentialSep >> spacing
|
||||||
group <- readBraced <|> readDoGroup id
|
group <- readBraced <|> readDoGroup id
|
||||||
return $ T_ForArithmetic id x y z group
|
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
|
readBraced = do
|
||||||
(T_BraceGroup _ list) <- readBraceGroup
|
(T_BraceGroup _ list) <- readBraceGroup
|
||||||
return list
|
return list
|
||||||
@@ -2625,7 +2666,7 @@ readFunctionDefinition = called "function" $ do
|
|||||||
|
|
||||||
readWithoutFunction = try $ do
|
readWithoutFunction = try $ do
|
||||||
name <- many1 functionChars
|
name <- many1 functionChars
|
||||||
guard $ name /= "time" -- Interfers with time ( foo )
|
guard $ name /= "time" -- Interferes with time ( foo )
|
||||||
spacing
|
spacing
|
||||||
readParens
|
readParens
|
||||||
return $ \id -> T_Function id (FunctionKeyword False) (FunctionParentheses True) name
|
return $ \id -> T_Function id (FunctionKeyword False) (FunctionParentheses True) name
|
||||||
@@ -2665,9 +2706,38 @@ readCoProc = called "coproc" $ do
|
|||||||
id <- endSpan start
|
id <- endSpan start
|
||||||
return $ T_CoProcBody id body
|
return $ T_CoProcBody id body
|
||||||
|
|
||||||
|
|
||||||
readPattern = (readPatternWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing)
|
readPattern = (readPatternWord `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"
|
prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null"
|
||||||
readCompoundCommand = do
|
readCompoundCommand = do
|
||||||
cmd <- choice [
|
cmd <- choice [
|
||||||
@@ -2675,7 +2745,6 @@ readCompoundCommand = do
|
|||||||
readAmbiguous "((" readArithmeticExpression readSubshell (\pos ->
|
readAmbiguous "((" readArithmeticExpression readSubshell (\pos ->
|
||||||
parseNoteAt pos ErrorC 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,
|
readSubshell,
|
||||||
readCondition,
|
|
||||||
readWhileClause,
|
readWhileClause,
|
||||||
readUntilClause,
|
readUntilClause,
|
||||||
readIfClause,
|
readIfClause,
|
||||||
@@ -2685,15 +2754,15 @@ readCompoundCommand = do
|
|||||||
readBatsTest,
|
readBatsTest,
|
||||||
readFunctionDefinition
|
readFunctionDefinition
|
||||||
]
|
]
|
||||||
spacing
|
|
||||||
redirs <- many readIoRedirect
|
redirs <- many readIoRedirect
|
||||||
id <- getNextIdSpanningTokenList (cmd:redirs)
|
id <- getNextIdSpanningTokenList (cmd:redirs)
|
||||||
unless (null redirs) $ optional $ do
|
optional . lookAhead $ do
|
||||||
lookAhead $ try (spacing >> needsSeparator)
|
notFollowedBy2 $ choice [readKeyword, g_Lbrace]
|
||||||
parseProblem WarningC 1013 "Bash requires ; or \\n here, after redirecting nested compound commands."
|
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
|
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
|
readCompoundList = readTerm
|
||||||
@@ -2763,17 +2832,13 @@ readLiteralForParser parser = do
|
|||||||
|
|
||||||
prop_readAssignmentWord = isOk readAssignmentWord "a=42"
|
prop_readAssignmentWord = isOk readAssignmentWord "a=42"
|
||||||
prop_readAssignmentWord2 = isOk readAssignmentWord "b=(1 2 3)"
|
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_readAssignmentWord5 = isOk readAssignmentWord "b+=lol"
|
||||||
prop_readAssignmentWord6 = isWarning readAssignmentWord "b += (1 2 3)"
|
|
||||||
prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42"
|
prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42"
|
||||||
prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42"
|
prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42"
|
||||||
prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "
|
prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "
|
||||||
prop_readAssignmentWord9a= isOk readAssignmentWord "foo="
|
prop_readAssignmentWord9a= isOk readAssignmentWord "foo="
|
||||||
prop_readAssignmentWord9b= isOk readAssignmentWord "foo= "
|
prop_readAssignmentWord9b= isOk readAssignmentWord "foo= "
|
||||||
prop_readAssignmentWord9c= isOk readAssignmentWord "foo= #bar"
|
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_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
|
||||||
prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'"
|
prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'"
|
||||||
prop_readAssignmentWord13= isOk readAssignmentWord "var=( (1 2) (3 4) )"
|
prop_readAssignmentWord13= isOk readAssignmentWord "var=( (1 2) (3 4) )"
|
||||||
@@ -2781,52 +2846,63 @@ prop_readAssignmentWord14= isOk readAssignmentWord "var=( 1 [2]=(3 4) )"
|
|||||||
prop_readAssignmentWord15= isOk readAssignmentWord "var=(1 [2]=(3 4))"
|
prop_readAssignmentWord15= isOk readAssignmentWord "var=(1 [2]=(3 4))"
|
||||||
readAssignmentWord = readAssignmentWordExt True
|
readAssignmentWord = readAssignmentWordExt True
|
||||||
readWellFormedAssignment = readAssignmentWordExt False
|
readWellFormedAssignment = readAssignmentWordExt False
|
||||||
readAssignmentWordExt lenient = try $ do
|
readAssignmentWordExt lenient = called "variable assignment" $ do
|
||||||
start <- startSpan
|
-- Parse up to and including the = in a 'try'
|
||||||
pos <- getPosition
|
(id, variable, op, indices) <- try $ do
|
||||||
when lenient $
|
start <- startSpan
|
||||||
optional (char '$' >> parseNote ErrorC 1066 "Don't use $ on the left side of assignments.")
|
pos <- getPosition
|
||||||
variable <- readVariableName
|
-- Check for a leading $ at parse time, to warn for $foo=(bar) which
|
||||||
when lenient $
|
-- would otherwise cause a parse failure so it can't be checked later.
|
||||||
optional (readNormalDollar >> parseNoteAt pos ErrorC
|
leadingDollarPos <-
|
||||||
1067 "For indirection, use arrays, declare \"var$n=value\", or (for sh) read/eval.")
|
if lenient
|
||||||
indices <- many readArrayIndex
|
then optionMaybe $ getSpanPositionsFor (char '$')
|
||||||
hasLeftSpace <- fmap (not . null) spacing
|
else return Nothing
|
||||||
pos <- getPosition
|
variable <- readVariableName
|
||||||
id <- endSpan start
|
indices <- many readArrayIndex
|
||||||
op <- readAssignmentOp
|
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
|
hasRightSpace <- fmap (not . null) spacing
|
||||||
|
rightPosEnd <- getPosition
|
||||||
isEndOfCommand <- fmap isJust $ optionMaybe (try . lookAhead $ (void (oneOf "\r\n;&|)") <|> eof))
|
isEndOfCommand <- fmap isJust $ optionMaybe (try . lookAhead $ (void (oneOf "\r\n;&|)") <|> eof))
|
||||||
if not hasLeftSpace && (hasRightSpace || isEndOfCommand)
|
|
||||||
|
if hasRightSpace || isEndOfCommand
|
||||||
then do
|
then do
|
||||||
when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $
|
when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $ do
|
||||||
parseNoteAt pos WarningC 1007
|
parseProblemAtWithEnd rightPosStart rightPosEnd WarningC 1007
|
||||||
"Remove space after = if trying to assign a value (for empty string, use var='' ... )."
|
"Remove space after = if trying to assign a value (for empty string, use var='' ... )."
|
||||||
value <- readEmptyLiteral
|
value <- readEmptyLiteral
|
||||||
return $ T_Assignment id op variable indices value
|
return $ T_Assignment id op variable indices value
|
||||||
else do
|
else do
|
||||||
when (hasLeftSpace || hasRightSpace) $
|
optional $ do
|
||||||
parseNoteAt pos ErrorC 1068 $
|
lookAhead $ char '='
|
||||||
"Don't put spaces around the "
|
parseProblem ErrorC 1097 "Unexpected ==. For assignment, use =. For comparison, use [/[[. Or quote for literal string."
|
||||||
++ (if op == Append
|
|
||||||
then "+= when appending"
|
|
||||||
else "= in assignments")
|
|
||||||
++ " (or quote to make it literal)."
|
|
||||||
value <- readArray <|> readNormalWord
|
value <- readArray <|> readNormalWord
|
||||||
spacing
|
spacing
|
||||||
return $ T_Assignment id op variable indices value
|
return $ T_Assignment id op variable indices value
|
||||||
where
|
where
|
||||||
readAssignmentOp = do
|
readAssignmentOp = do
|
||||||
pos <- getPosition
|
-- This is probably some kind of ascii art border
|
||||||
unexpecting "" $ string "==="
|
unexpecting "===" (string "===")
|
||||||
choice [
|
choice [
|
||||||
string "+=" >> return Append,
|
string "+=" >> return Append,
|
||||||
do
|
|
||||||
try (string "==")
|
|
||||||
parseProblemAt pos ErrorC 1097
|
|
||||||
"Unexpected ==. For assignment, use =. For comparison, use [/[[."
|
|
||||||
return Assign,
|
|
||||||
|
|
||||||
string "=" >> return Assign
|
string "=" >> return Assign
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3093,7 +3169,7 @@ readConfigFile filename = do
|
|||||||
let line = "line " ++ (show . sourceLine $ errorPos err)
|
let line = "line " ++ (show . sourceLine $ errorPos err)
|
||||||
suggestion = getStringFromParsec $ errorMessages err
|
suggestion = getStringFromParsec $ errorMessages err
|
||||||
in
|
in
|
||||||
"Failed to process " ++ filename ++ ", " ++ line ++ ": "
|
"Failed to process " ++ (e4m filename) ++ ", " ++ line ++ ": "
|
||||||
++ suggestion
|
++ suggestion
|
||||||
|
|
||||||
prop_readConfigKVs1 = isOk readConfigKVs "disable=1234"
|
prop_readConfigKVs1 = isOk readConfigKVs "disable=1234"
|
||||||
@@ -3114,6 +3190,7 @@ prop_readScript2 = isWarning readScript "#!/bin/bash\r\necho hello world\n"
|
|||||||
prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world"
|
prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world"
|
||||||
prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=("
|
prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=("
|
||||||
prop_readScript5 = isOk readScript "#!/bin/bash\n#This is an empty script\n\n"
|
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
|
readScriptFile sourced = do
|
||||||
start <- startSpan
|
start <- startSpan
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
@@ -3155,13 +3232,14 @@ readScriptFile sourced = do
|
|||||||
|
|
||||||
where
|
where
|
||||||
basename s = reverse . takeWhile (/= '/') . reverse $ s
|
basename s = reverse . takeWhile (/= '/') . reverse $ s
|
||||||
|
skipFlags = dropWhile ("-" `isPrefixOf`)
|
||||||
getShell sb =
|
getShell sb =
|
||||||
case words sb of
|
case words sb of
|
||||||
[] -> ""
|
[] -> ""
|
||||||
[x] -> basename x
|
[x] -> basename x
|
||||||
(first:second:_) ->
|
(first:args) ->
|
||||||
if basename first == "env"
|
if basename first == "env"
|
||||||
then second
|
then fromMaybe "" $ find (notElem '=') $ skipFlags args
|
||||||
else basename first
|
else basename first
|
||||||
|
|
||||||
verifyShebang pos s = do
|
verifyShebang pos s = do
|
||||||
@@ -3316,16 +3394,21 @@ parseShell env name contents = do
|
|||||||
prRoot = Just $
|
prRoot = Just $
|
||||||
reattachHereDocs script (hereDocMap userstate)
|
reattachHereDocs script (hereDocMap userstate)
|
||||||
}
|
}
|
||||||
Left err ->
|
Left err -> do
|
||||||
|
let context = contextStack state
|
||||||
return newParseResult {
|
return newParseResult {
|
||||||
prComments =
|
prComments =
|
||||||
map toPositionedComment $
|
map toPositionedComment $
|
||||||
notesForContext (contextStack state)
|
(filter (not . isIgnored context) $
|
||||||
++ [makeErrorFor err]
|
notesForContext context
|
||||||
|
++ [makeErrorFor err])
|
||||||
++ parseProblems state,
|
++ parseProblems state,
|
||||||
prTokenPositions = Map.empty,
|
prTokenPositions = Map.empty,
|
||||||
prRoot = Nothing
|
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
|
notesForContext list = zipWith ($) [first, second] $ filter isName list
|
||||||
where
|
where
|
||||||
|
@@ -9,7 +9,7 @@ fail() {
|
|||||||
|
|
||||||
if git diff | grep -q ""
|
if git diff | grep -q ""
|
||||||
then
|
then
|
||||||
fail "There are uncommited changes"
|
fail "There are uncommitted changes"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
current=$(git tag --points-at)
|
current=$(git tag --points-at)
|
||||||
|
Reference in New Issue
Block a user