Compare commits

..

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

39 changed files with 268 additions and 877 deletions

View file

@ -15,7 +15,7 @@ jobs:
sudo apt-get install cabal-install sudo apt-get install cabal-install
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
@ -37,47 +37,24 @@ jobs:
mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: source name: source
path: source/ path: source/
run_tests:
name: Run tests
needs: package_source
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
- name: Install dependencies
run: |
sudo apt-get update && sudo apt-get install ghc cabal-install
cabal update
- name: Unpack source
run: |
cd source
tar xvf source.tar.gz --strip-components=1
- name: Build and run tests
run: |
cd source
cabal test
build_source: build_source:
name: Build name: Build Source Code
needs: package_source needs: package_source
strategy: strategy:
matrix: matrix:
build: [linux.x86_64, linux.aarch64, linux.armv6hf, linux.riscv64, darwin.x86_64, darwin.aarch64, windows.x86_64] build: [linux.x86_64, linux.aarch64, linux.armv6hf, darwin.x86_64, darwin.aarch64, windows.x86_64]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
- name: Build source - name: Build source
run: | run: |
@ -86,9 +63,9 @@ jobs:
( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} ) ( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} )
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: ${{matrix.build}}.bin name: bin
path: bin/ path: bin/
package_binary: package_binary:
@ -97,25 +74,25 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
- name: Work around GitHub permissions bug - name: Work around GitHub permissions bug
run: chmod +x *.bin/*/shellcheck* run: chmod +x bin/*/shellcheck*
- name: Package binaries - name: Package binaries
run: | run: |
export TAGS="$(cat source/tags)" export TAGS="$(cat source/tags)"
mkdir -p deploy mkdir -p deploy
cp -r *.bin/* deploy cp -r bin/* deploy
cd deploy cd deploy
../.prepare_deploy ../.prepare_deploy
rm -rf */ README* LICENSE* rm -rf */ README* LICENSE*
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: deploy name: deploy
path: deploy/ path: deploy/
@ -132,10 +109,10 @@ jobs:
sudo apt-get install hub sudo apt-get install hub
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
- name: Upload to GitHub - name: Upload to GitHub
env: env:

View file

@ -80,7 +80,6 @@ function multi_arch_docker::main() {
export DOCKER_PLATFORMS='linux/amd64' export DOCKER_PLATFORMS='linux/amd64'
DOCKER_PLATFORMS+=' linux/arm64' DOCKER_PLATFORMS+=' linux/arm64'
DOCKER_PLATFORMS+=' linux/arm/v6' DOCKER_PLATFORMS+=' linux/arm/v6'
DOCKER_PLATFORMS+=' linux/riscv64'
multi_arch_docker::install_docker_buildx multi_arch_docker::install_docker_buildx
multi_arch_docker::login_to_docker_hub multi_arch_docker::login_to_docker_hub

View file

@ -1,25 +1,3 @@
## Git
### Added
- SC2327/SC2328: Warn about capturing the output of redirected commands.
- SC2329: Warn when (non-escaping) functions are never invoked.
- SC2330: Warn about unsupported glob matches with [[ .. ]] in BusyBox.
- SC2331: Suggest using standard -e instead of unary -a in tests.
- SC2332: Warn about `[ ! -o opt ]` being unconditionally true in Bash.
- SC3062: Warn about bashism `[ -o opt ]`.
- Precompiled binaries for Linux riscv64 (linux.riscv64)
### Changed
- SC2002 about Useless Use Of Cat is now disabled by default. It can be
re-enabled with `--enable=useless-use-of-cat` or equivalent directive.
- SC2015 about `A && B || C` no longer triggers when B is a test command.
- SC3012: Do not warn about `\<` and `\>` in test/[] as specified in POSIX.1-2024
### Fixed
- SC2218 about function use-before-define is now more accurate.
- SC2317 about unreachable commands is now less spammy for nested ones.
- SC2292, optional suggestion for [[ ]], now triggers for Busybox.
### Removed
- SC3013: removed since the operators `-ot/-nt/-ef` are specified in POSIX.1-2024
## v0.10.0 - 2024-03-07 ## v0.10.0 - 2024-03-07
### Added ### Added
- Precompiled binaries for macOS ARM64 (darwin.aarch64) - Precompiled binaries for macOS ARM64 (darwin.aarch64)

View file

@ -110,11 +110,9 @@ Services and platforms that have ShellCheck pre-installed and ready to use:
* [Codacy](https://www.codacy.com/) * [Codacy](https://www.codacy.com/)
* [Code Climate](https://codeclimate.com/) * [Code Climate](https://codeclimate.com/)
* [Code Factor](https://www.codefactor.io/) * [Code Factor](https://www.codefactor.io/)
* [Codety](https://www.codety.io/) via the [Codety Scanner](https://github.com/codetyio/codety-scanner)
* [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck) * [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck)
* [Github](https://github.com/features/actions) (only Linux) * [Github](https://github.com/features/actions) (only Linux)
* [Trunk Check](https://trunk.io/products/check) (universal linter; [allows you to explicitly version your shellcheck install](https://github.com/trunk-io/plugins/blob/bcbb361dcdbe4619af51ea7db474d7fb87540d20/.trunk/trunk.yaml#L32)) via the [shellcheck plugin](https://github.com/trunk-io/plugins/blob/main/linters/shellcheck/plugin.yaml) * [Trunk Check](https://trunk.io/products/check) (universal linter; [allows you to explicitly version your shellcheck install](https://github.com/trunk-io/plugins/blob/bcbb361dcdbe4619af51ea7db474d7fb87540d20/.trunk/trunk.yaml#L32)) via the [shellcheck plugin](https://github.com/trunk-io/plugins/blob/main/linters/shellcheck/plugin.yaml)
* [CodeRabbit](https://coderabbit.ai/)
Most other services, including [GitLab](https://about.gitlab.com/), let you install Most other services, including [GitLab](https://about.gitlab.com/), let you install
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)), ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
@ -230,17 +228,11 @@ Using the [nix package manager](https://nixos.org/nix):
nix-env -iA nixpkgs.shellcheck nix-env -iA nixpkgs.shellcheck
``` ```
Using the [Flox package manager](https://flox.dev/)
```sh
flox install shellcheck
```
Alternatively, you can download pre-compiled binaries for the latest release here: Alternatively, you can download pre-compiled binaries for the latest release here:
* [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked) * [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
* [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked) * [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
* [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked) * [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)
* [macOS, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.aarch64.tar.xz)
* [macOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz) * [macOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz)
* [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip) * [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip)

View file

@ -53,12 +53,12 @@ library
bytestring >= 0.10.6 && < 0.13, bytestring >= 0.10.6 && < 0.13,
containers >= 0.5.6 && < 0.8, containers >= 0.5.6 && < 0.8,
deepseq >= 1.4.1 && < 1.6, deepseq >= 1.4.1 && < 1.6,
Diff >= 0.4.0 && < 1.1, Diff >= 0.4.0 && < 0.6,
fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9), fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9),
filepath >= 1.4.0 && < 1.6, filepath >= 1.4.0 && < 1.5,
mtl >= 2.2.2 && < 2.4, mtl >= 2.2.2 && < 2.4,
parsec >= 3.1.14 && < 3.2, parsec >= 3.1.14 && < 3.2,
QuickCheck >= 2.14.2 && < 2.16, QuickCheck >= 2.14.2 && < 2.15,
regex-tdfa >= 1.2.0 && < 1.4, regex-tdfa >= 1.2.0 && < 1.4,
transformers >= 0.4.2 && < 0.7, transformers >= 0.4.2 && < 0.7,

View file

@ -11,7 +11,3 @@ This makes it simple to build any release without exotic hardware or software.
An image can be built and tagged using `build_builder`, An image can be built and tagged using `build_builder`,
and run on a source tarball using `run_builder`. and run on a source tarball using `run_builder`.
Tip: Are you developing an image that relies on QEmu usermode emulation?
It's easy to accidentally depend on binfmt\_misc on the host OS.
Do a `echo 0 | sudo tee /proc/sys/fs/binfmt_misc/status` before testing.

View file

@ -4,6 +4,7 @@ set -xe
tar xzv --strip-components=1 tar xzv --strip-components=1
chmod +x striptests && ./striptests chmod +x striptests && ./striptests
mkdir "$TARGETNAME" mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS ) ( IFS=';'; cabal build $CABALOPTS )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME" ls -l "$TARGETNAME"

View file

@ -4,6 +4,7 @@ set -xe
tar xzv --strip-components=1 tar xzv --strip-components=1
chmod +x striptests && ./striptests chmod +x striptests && ./striptests
mkdir "$TARGETNAME" mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS ) ( IFS=';'; cabal build $CABALOPTS )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME" ls -l "$TARGETNAME"

View file

@ -12,15 +12,11 @@ RUN apt-get update && apt-get install -y llvm gcc-$TARGET
# The rest are from 22.10 # The rest are from 22.10
RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list
# Kinetic does not receive updates anymore, switch to last available
RUN sed -e 's/archive.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list
RUN sed -e 's/security.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list
RUN apt-get update && apt-get install -y ghc alex happy automake autoconf build-essential curl qemu-user-static RUN apt-get update && apt-get install -y ghc alex happy automake autoconf build-essential curl qemu-user-static
# Build GHC # Build GHC
WORKDIR /ghc WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/9.2.8/ghc-9.2.8-src.tar.xz" | tar xJ --strip-components=1 RUN curl -L "https://downloads.haskell.org/~ghc/9.2.5/ghc-9.2.5-src.tar.xz" | tar xJ --strip-components=1
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" 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 cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
RUN make install RUN make install
@ -28,7 +24,7 @@ RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-in
# Due to an apparent cabal bug, we specify our options directly to cabal # 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 # It won't reuse caches if ghc-options are specified in ~/.cabal/config
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;-c;hashable -arch-native" ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
# Prebuild the dependencies # Prebuild the dependencies
RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck

View file

@ -4,6 +4,7 @@ set -xe
tar xzv --strip-components=1 tar xzv --strip-components=1
chmod +x striptests && ./striptests chmod +x striptests && ./striptests
mkdir "$TARGETNAME" mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS --enable-executable-static ) ( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME" ls -l "$TARGETNAME"

View file

@ -1,7 +1,25 @@
# This Docker file uses a custom QEmu fork with patches to follow execve # I've again spent days trying to get a working armv6hf compiler going.
# to build all of ShellCheck emulated. # 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:24.04 FROM ubuntu:20.04
ENV TARGETNAME linux.armv6hf ENV TARGETNAME linux.armv6hf
@ -9,34 +27,34 @@ ENV TARGETNAME linux.armv6hf
USER root USER root
ENV DEBIAN_FRONTEND noninteractive ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update RUN apt-get update
RUN apt-get install -y --no-install-recommends build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev python3-setuptools ca-certificates debootstrap RUN apt-get install -y build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev
WORKDIR /qemu WORKDIR /build
RUN git clone --depth 1 https://github.com/koalaman/qemu . RUN git clone --depth 1 https://github.com/koalaman/qemu
RUN ./configure --static --disable-werror && cd build && ninja qemu-arm RUN cd qemu && ./configure --static && cd build && ninja qemu-arm
RUN cp qemu/build/qemu-arm /build/qemu-arm-static
ENV QEMU_EXECVE 1 ENV QEMU_EXECVE 1
# Convenience utility
COPY scutil /bin/scutil
COPY scutil /chroot/bin/scutil
RUN chmod +x /bin/scutil /chroot/bin/scutil
# Set up an armv6 userspace # Set up an armv6 userspace
WORKDIR / WORKDIR /
RUN debootstrap --arch armhf --variant=minbase --foreign bookworm /chroot http://mirrordirector.raspbian.org/raspbian RUN apt-get install -y debootstrap qemu-user-static
RUN cp /qemu/build/qemu-arm /chroot/bin/qemu # We expect this to fail if the host doesn't have binfmt qemu support
RUN scutil emu /debootstrap/debootstrap --second-stage 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 # Install deps in the chroot
RUN scutil emu apt-get update RUN pirun apt-get update
RUN scutil emu apt-get install -y --no-install-recommends ghc cabal-install RUN pirun apt-get install -y ghc cabal-install
RUN scutil emu cabal update
# Finally we can build the current dependencies. This takes hours. # 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" ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--gcc-options;-Os -Wl,--gc-sections -ffunction-sections -fdata-sections"
# Generated with `cabal freeze --constraint 'hashable -arch-native'` RUN pirun cabal update
COPY cabal.project.freeze /chroot/etc RUN IFS=";" && pirun cabal install --dependencies-only $CABALOPTS ShellCheck
RUN IFS=";" && scutil install_from_freeze /chroot/etc/cabal.project.freeze emu cabal install $CABALOPTS RUN IFS=';' && pirun cabal install $CABALOPTS --lib fgl
# Copy the build script # Copy the build script
COPY build /chroot/bin WORKDIR /pi/scratch
ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"] COPY build /pi/usr/bin
ENTRYPOINT ["/bin/pirun", "/usr/bin/build"]

View file

@ -1,9 +1,8 @@
#!/bin/sh #!/bin/sh
set -xe set -xe
mkdir /scratch && cd /scratch cd /scratch
{ {
tar xzv --strip-components=1 tar xzv --strip-components=1
cp /etc/cabal.project.freeze .
chmod +x striptests && ./striptests chmod +x striptests && ./striptests
mkdir "$TARGETNAME" mkdir "$TARGETNAME"
# This script does not cabal update because compiling anything new is slow # This script does not cabal update because compiling anything new is slow

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,4 @@
FROM alpine:3.16 FROM alpine:latest
# alpine:3.16 (GHC 9.0.1): 5.8 megabytes
# alpine:3.17 (GHC 9.0.2): 15.0 megabytes
# alpine:3.18 (GHC 9.4.4): 29.0 megabytes
# alpine:3.19 (GHC 9.4.7): 29.0 megabytes
ENV TARGETNAME linux.x86_64 ENV TARGETNAME linux.x86_64

View file

@ -12,7 +12,7 @@ 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 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 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://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-mingw32.zip" | busybox unzip -
RUN curl -L "https://curl.se/windows/dl-8.7.1_7/curl-8.7.1_7-win64-mingw.zip" | busybox unzip - && mv curl-*-win64-mingw/bin/* . RUN curl -L "https://curl.se/windows/dl-7.84.0/curl-7.84.0-win64-mingw.zip" | busybox unzip - && mv curl-7.84.0-win64-mingw/bin/* .
ENV WINEPATH /haskell/bin ENV WINEPATH /haskell/bin
# It's unknown whether Cabal on Windows suffers from the same issue # It's unknown whether Cabal on Windows suffers from the same issue

View file

@ -8,6 +8,7 @@ set -xe
tar xzv --strip-components=1 tar xzv --strip-components=1
chmod +x striptests && ./striptests chmod +x striptests && ./striptests
mkdir "$TARGETNAME" mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS ) ( IFS=';'; cabal build $CABALOPTS )
find dist*/ -name shellcheck.exe -type f -ls -exec mv {} "$TARGETNAME/" \; find dist*/ -name shellcheck.exe -type f -ls -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME" ls -l "$TARGETNAME"

View file

@ -78,7 +78,7 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
: Don't try to look for .shellcheckrc configuration files. : Don't try to look for .shellcheckrc configuration files.
**--rcfile** *RCFILE* --rcfile\ RCFILE
: Prefer the specified configuration file over searching for one : Prefer the specified configuration file over searching for one
in the default locations. in the default locations.
@ -317,7 +317,7 @@ Here is an example `.shellcheckrc`:
disable=SC2236 disable=SC2236
If no `.shellcheckrc` is found in any of the parent directories, ShellCheck If no `.shellcheckrc` is found in any of the parent directories, ShellCheck
will look in `~/.shellcheckrc` followed by the `$XDG_CONFIG_HOME` will look in `~/.shellcheckrc` followed by the XDG config directory
(usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on (usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on
Windows. Only the first file found will be used. Windows. Only the first file found will be used.
@ -403,4 +403,4 @@ see https://gnu.org/licenses/gpl.html
# SEE ALSO # SEE ALSO
sh(1) bash(1) dash(1) ksh(1) sh(1) bash(1)

View file

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

View file

@ -446,12 +446,6 @@ 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
-- Is this token a string literal number?
isLiteralNumber t = fromMaybe False $ do
s <- getLiteralString t
guard $ all isDigit s
return True
-- Escape user data for messages. -- Escape user data for messages.
-- Messages generally avoid repeating user data, but sometimes it's helpful. -- Messages generally avoid repeating user data, but sometimes it's helpful.
e4m = escapeForMessage e4m = escapeForMessage

View file

@ -103,7 +103,8 @@ nodeChecksToTreeCheck checkList =
nodeChecks :: [Parameters -> Token -> Writer [TokenComment] ()] nodeChecks :: [Parameters -> Token -> Writer [TokenComment] ()]
nodeChecks = [ nodeChecks = [
checkPipePitfalls checkUuoc
,checkPipePitfalls
,checkForInQuoted ,checkForInQuoted
,checkForInLs ,checkForInLs
,checkShorthandIf ,checkShorthandIf
@ -123,7 +124,6 @@ nodeChecks = [
,checkCaseAgainstGlob ,checkCaseAgainstGlob
,checkCommarrays ,checkCommarrays
,checkOrNeq ,checkOrNeq
,checkAndEq
,checkEchoWc ,checkEchoWc
,checkConstantIfs ,checkConstantIfs
,checkPipedAssignment ,checkPipedAssignment
@ -204,8 +204,6 @@ nodeChecks = [
,checkUnnecessaryArithmeticExpansionIndex ,checkUnnecessaryArithmeticExpansionIndex
,checkUnnecessaryParens ,checkUnnecessaryParens
,checkPlusEqualsNumber ,checkPlusEqualsNumber
,checkExpansionWithRedirection
,checkUnaryTestA
] ]
optionalChecks = map fst optionalTreeChecks optionalChecks = map fst optionalTreeChecks
@ -274,13 +272,6 @@ optionalTreeChecks = [
cdPositive = "rm -r \"$(get_chroot_dir)/home\"", cdPositive = "rm -r \"$(get_chroot_dir)/home\"",
cdNegative = "set -e; dir=\"$(get_chroot_dir)\"; rm -r \"$dir/home\"" cdNegative = "set -e; dir=\"$(get_chroot_dir)\"; rm -r \"$dir/home\""
}, checkExtraMaskedReturns) }, checkExtraMaskedReturns)
,(newCheckDescription {
cdName = "useless-use-of-cat",
cdDescription = "Check for Useless Use Of Cat (UUOC)",
cdPositive = "cat foo | grep bar",
cdNegative = "grep bar foo"
}, nodeChecksToTreeCheck [checkUuoc])
] ]
optionalCheckMap :: Map.Map String (Parameters -> Token -> [TokenComment]) optionalCheckMap :: Map.Map String (Parameters -> Token -> [TokenComment])
@ -499,12 +490,15 @@ checkWrongArithmeticAssignment params (T_SimpleCommand id [T_Assignment _ _ _ _
sequence_ $ do sequence_ $ do
str <- getNormalString val str <- getNormalString val
var:op:_ <- matchRegex regex str var:op:_ <- matchRegex regex str
guard $ S.member var references Map.lookup var references
return . warn (getId val) 2100 $ return . warn (getId val) 2100 $
"Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))" "Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))"
where where
regex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)([+*-]).+$" regex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)([+*-]).+$"
references = S.fromList [name | Assignment (_, _, name, _) <- variableFlow params] references = foldl (flip ($)) Map.empty (map insertRef $ variableFlow params)
insertRef (Assignment (_, _, name, _)) =
Map.insert name ()
insertRef _ = Prelude.id
getNormalString (T_NormalWord _ words) = do getNormalString (T_NormalWord _ words) = do
parts <- mapM getLiterals words parts <- mapM getLiterals words
@ -882,16 +876,13 @@ prop_checkShorthandIf5 = verifyNot checkShorthandIf "foo && rm || printf b"
prop_checkShorthandIf6 = verifyNot checkShorthandIf "if foo && bar || baz; then true; fi" prop_checkShorthandIf6 = verifyNot checkShorthandIf "if foo && bar || baz; then true; fi"
prop_checkShorthandIf7 = verifyNot checkShorthandIf "while foo && bar || baz; do true; done" prop_checkShorthandIf7 = verifyNot checkShorthandIf "while foo && bar || baz; do true; done"
prop_checkShorthandIf8 = verify checkShorthandIf "if true; then foo && bar || baz; fi" prop_checkShorthandIf8 = verify checkShorthandIf "if true; then foo && bar || baz; fi"
prop_checkShorthandIf9 = verifyNot checkShorthandIf "foo && [ -x /file ] || bar" checkShorthandIf params x@(T_OrIf _ (T_AndIf id _ _) (T_Pipeline _ _ t))
prop_checkShorthandIf10 = verifyNot checkShorthandIf "foo && bar || true" | not (isOk t || inCondition) =
prop_checkShorthandIf11 = verify checkShorthandIf "foo && bar || false"
checkShorthandIf params x@(T_OrIf _ (T_AndIf id _ b) (T_Pipeline _ _ t))
| not (isOk t || inCondition) && not (isTestCommand b) =
info id 2015 "Note that A && B || C is not if-then-else. C may run when A is true." info id 2015 "Note that A && B || C is not if-then-else. C may run when A is true."
where where
isOk [t] = isAssignment t || fromMaybe False (do isOk [t] = isAssignment t || fromMaybe False (do
name <- getCommandBasename t name <- getCommandBasename t
return $ name `elem` ["echo", "exit", "return", "printf", "true", ":"]) return $ name `elem` ["echo", "exit", "return", "printf"])
isOk _ = False isOk _ = False
inCondition = isCondition $ getPath (parentMap params) x inCondition = isCondition $ getPath (parentMap params) x
checkShorthandIf _ _ = return () checkShorthandIf _ _ = return ()
@ -981,32 +972,32 @@ prop_checkArrayWithoutIndex9 = verifyTree checkArrayWithoutIndex "read -r -a arr
prop_checkArrayWithoutIndex10 = verifyTree checkArrayWithoutIndex "read -ra arr <<< 'foo bar'; echo \"$arr\"" prop_checkArrayWithoutIndex10 = verifyTree checkArrayWithoutIndex "read -ra arr <<< 'foo bar'; echo \"$arr\""
prop_checkArrayWithoutIndex11 = verifyNotTree checkArrayWithoutIndex "read -rpfoobar r; r=42" prop_checkArrayWithoutIndex11 = verifyNotTree checkArrayWithoutIndex "read -rpfoobar r; r=42"
checkArrayWithoutIndex params _ = checkArrayWithoutIndex params _ =
doVariableFlowAnalysis readF writeF defaultSet (variableFlow params) doVariableFlowAnalysis readF writeF defaultMap (variableFlow params)
where where
defaultSet = S.fromList arrayVariables defaultMap = Map.fromList $ map (\x -> (x,())) arrayVariables
readF _ (T_DollarBraced id _ token) _ = do readF _ (T_DollarBraced id _ token) _ = do
s <- get map <- get
return . maybeToList $ do return . maybeToList $ do
name <- getLiteralString token name <- getLiteralString token
guard $ S.member name s assigned <- Map.lookup name map
return $ makeComment WarningC id 2128 return $ makeComment WarningC id 2128
"Expanding an array without an index only gives the first element." "Expanding an array without an index only gives the first element."
readF _ _ _ = return [] readF _ _ _ = return []
writeF _ (T_Assignment id mode name [] _) _ (DataString _) = do writeF _ (T_Assignment id mode name [] _) _ (DataString _) = do
isArray <- gets (S.member name) isArray <- gets (Map.member name)
return $ if not isArray then [] else return $ if not isArray then [] else
case mode of case mode of
Assign -> [makeComment WarningC id 2178 "Variable was used as an array but is now assigned a string."] Assign -> [makeComment WarningC id 2178 "Variable was used as an array but is now assigned a string."]
Append -> [makeComment WarningC id 2179 "Use array+=(\"item\") to append items to an array."] Append -> [makeComment WarningC id 2179 "Use array+=(\"item\") to append items to an array."]
writeF _ t name (DataArray _) = do writeF _ t name (DataArray _) = do
modify (S.insert name) modify (Map.insert name ())
return [] return []
writeF _ expr name _ = do writeF _ expr name _ = do
if isIndexed expr if isIndexed expr
then modify (S.insert name) then modify (Map.insert name ())
else modify (S.delete name) else modify (Map.delete name)
return [] return []
isIndexed expr = isIndexed expr =
@ -1107,7 +1098,6 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
,"sudo" -- covering "sudo sh" and such ,"sudo" -- covering "sudo sh" and such
,"docker" -- like above ,"docker" -- like above
,"podman" ,"podman"
,"oc"
,"dpkg-query" ,"dpkg-query"
,"jq" -- could also check that user provides --arg ,"jq" -- could also check that user provides --arg
,"rename" ,"rename"
@ -1533,7 +1523,6 @@ prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = *
prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]" prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]"
prop_checkComparisonAgainstGlob5 = verify checkComparisonAgainstGlob "[[ $cow != $bar ]]" prop_checkComparisonAgainstGlob5 = verify checkComparisonAgainstGlob "[[ $cow != $bar ]]"
prop_checkComparisonAgainstGlob6 = verify checkComparisonAgainstGlob "[ $f != /* ]" prop_checkComparisonAgainstGlob6 = verify checkComparisonAgainstGlob "[ $f != /* ]"
prop_checkComparisonAgainstGlob7 = verify checkComparisonAgainstGlob "#!/bin/busybox sh\n[[ $f == *foo* ]]"
checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _ _])) checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _ _]))
| op `elem` ["=", "==", "!="] = | op `elem` ["=", "==", "!="] =
warn id 2053 $ "Quote the right-hand side of " ++ op ++ " in [[ ]] to prevent glob matching." warn id 2053 $ "Quote the right-hand side of " ++ op ++ " in [[ ]] to prevent glob matching."
@ -1541,14 +1530,10 @@ checkComparisonAgainstGlob params (TC_Binary _ SingleBracket op _ word)
| op `elem` ["=", "==", "!="] && isGlob word = | op `elem` ["=", "==", "!="] && isGlob word =
err (getId word) 2081 msg err (getId word) 2081 msg
where where
msg = if (shellType params) `elem` [Bash, Ksh] -- Busybox does not support glob matching msg = if isBashLike params
then "[ .. ] can't match globs. Use [[ .. ]] or case statement." then "[ .. ] can't match globs. Use [[ .. ]] or case statement."
else "[ .. ] can't match globs. Use a case statement." else "[ .. ] can't match globs. Use a case statement."
checkComparisonAgainstGlob params (TC_Binary _ DoubleBracket op _ word)
| shellType params == BusyboxSh && op `elem` ["=", "==", "!="] && isGlob word =
err (getId word) 2330 "BusyBox [[ .. ]] does not support glob matching. Use a case statement."
checkComparisonAgainstGlob _ _ = return () checkComparisonAgainstGlob _ _ = return ()
prop_checkCaseAgainstGlob1 = verify checkCaseAgainstGlob "case foo in lol$n) foo;; esac" prop_checkCaseAgainstGlob1 = verify checkCaseAgainstGlob "case foo in lol$n) foo;; esac"
@ -1635,64 +1620,6 @@ checkOrNeq _ (T_OrIf id lhs rhs) = sequence_ $ do
checkOrNeq _ _ = return () checkOrNeq _ _ = return ()
prop_checkAndEq1 = verifyNot checkAndEq "cow=0; foo=0; if [[ $lol -eq cow && $lol -eq foo ]]; then echo foo; fi"
prop_checkAndEq2 = verifyNot checkAndEq "lol=0 foo=0; (( a==lol && a==foo ))"
prop_checkAndEq3 = verify checkAndEq "[ \"$a\" = lol && \"$a\" = foo ]"
prop_checkAndEq4 = verifyNot checkAndEq "[ a = $cow && b = $foo ]"
prop_checkAndEq5 = verifyNot checkAndEq "[[ $a = /home && $a = */public_html/* ]]"
prop_checkAndEq6 = verify checkAndEq "[ $a = a ] && [ $a = b ]"
prop_checkAndEq7 = verify checkAndEq "[ $a = a ] && [ $a = b ] || true"
prop_checkAndEq8 = verifyNot checkAndEq "[[ $a == x && $a == x ]]"
prop_checkAndEq9 = verifyNot checkAndEq "[ 0 -eq $FOO ] && [ 0 -eq $BAR ]"
prop_checkAndEq10 = verify checkAndEq "(( a == 1 && a == 2 ))"
prop_checkAndEq11 = verify checkAndEq "[ $x -eq 1 ] && [ $x -eq 2 ]"
prop_checkAndEq12 = verify checkAndEq "[ 1 -eq $x ] && [ $x -eq 2 ]"
prop_checkAndEq13 = verifyNot checkAndEq "[ 1 -eq $x ] && [ $x -eq 1 ]"
prop_checkAndEq14 = verifyNot checkAndEq "[ $a = $b ] && [ $a = $c ]"
checkAndEqOperands "-eq" rhs1 rhs2 = isLiteralNumber rhs1 && isLiteralNumber rhs2
checkAndEqOperands op rhs1 rhs2 | op == "=" || op == "==" = isLiteral rhs1 && isLiteral rhs2
checkAndEqOperands _ _ _ = False
-- For test-level "and": [ x = y -a x = z ]
checkAndEq _ (TC_And id typ op (TC_Binary _ _ op1 lhs1 rhs1 ) (TC_Binary _ _ op2 lhs2 rhs2))
| op1 == op2 && lhs1 == lhs2 && rhs1 /= rhs2 && checkAndEqOperands op1 rhs1 rhs2 =
warn id 2333 $ "You probably wanted " ++ (if typ == SingleBracket then "-o" else "||") ++ " here, otherwise it's always false."
-- For arithmetic context "and"
checkAndEq _ (TA_Binary id "&&" (TA_Binary _ "==" lhs1 rhs1) (TA_Binary _ "==" lhs2 rhs2))
| lhs1 == lhs2 && isLiteralNumber rhs1 && isLiteralNumber rhs2 =
warn id 2334 "You probably wanted || here, otherwise it's always false."
-- For command level "and": [ x = y ] && [ x = z ]
checkAndEq _ (T_AndIf id lhs rhs) = sequence_ $ do
(lhs1, op1, rhs1) <- getExpr lhs
(lhs2, op2, rhs2) <- getExpr rhs
guard $ op1 == op2
guard $ lhs1 == lhs2 && rhs1 /= rhs2
guard $ checkAndEqOperands op1 rhs1 rhs2
return $ warn id 2333 "You probably wanted || here, otherwise it's always false."
where
getExpr x =
case x of
T_AndIf _ lhs _ -> getExpr lhs -- Fetches x and y in `T_AndIf x (T_AndIf y z)`
T_Pipeline _ _ [x] -> getExpr x
T_Redirecting _ _ c -> getExpr c
T_Condition _ _ c -> getExpr c
TC_Binary _ _ op lhs rhs -> orient (lhs, op, rhs)
_ -> Nothing
-- Swap items so that the constant side is rhs (or Nothing if both/neither is constant)
orient (lhs, op, rhs) =
case (isConstant lhs, isConstant rhs) of
(True, False) -> return (rhs, op, lhs)
(False, True) -> return (lhs, op, rhs)
_ -> Nothing
checkAndEq _ _ = return ()
prop_checkValidCondOps1 = verify checkValidCondOps "[[ a -xz b ]]" prop_checkValidCondOps1 = verify checkValidCondOps "[[ a -xz b ]]"
prop_checkValidCondOps2 = verify checkValidCondOps "[ -M a ]" prop_checkValidCondOps2 = verify checkValidCondOps "[ -M a ]"
prop_checkValidCondOps2a = verifyNot checkValidCondOps "[ 3 \\> 2 ]" prop_checkValidCondOps2a = verifyNot checkValidCondOps "[ 3 \\> 2 ]"
@ -1958,9 +1885,7 @@ prop_checkSpuriousExec8 = verifyNot checkSpuriousExec "exec {origout}>&1- >tmp.l
prop_checkSpuriousExec9 = verify checkSpuriousExec "for file in rc.d/*; do exec \"$file\"; done" prop_checkSpuriousExec9 = verify checkSpuriousExec "for file in rc.d/*; do exec \"$file\"; done"
prop_checkSpuriousExec10 = verifyNot checkSpuriousExec "exec file; r=$?; printf >&2 'failed\n'; return $r" prop_checkSpuriousExec10 = verifyNot checkSpuriousExec "exec file; r=$?; printf >&2 'failed\n'; return $r"
prop_checkSpuriousExec11 = verifyNot checkSpuriousExec "exec file; :" prop_checkSpuriousExec11 = verifyNot checkSpuriousExec "exec file; :"
prop_checkSpuriousExec12 = verifyNot checkSpuriousExec "#!/bin/bash\nshopt -s execfail; exec foo; exec bar; echo 'Error'; exit 1;" checkSpuriousExec _ = doLists
prop_checkSpuriousExec13 = verify checkSpuriousExec "#!/bin/dash\nshopt -s execfail; exec foo; exec bar; echo 'Error'; exit 1;"
checkSpuriousExec params t = when (not $ hasExecfail params) $ doLists t
where where
doLists (T_Script _ _ cmds) = doList cmds False doLists (T_Script _ _ cmds) = doList cmds False
doLists (T_BraceGroup _ cmds) = doList cmds False doLists (T_BraceGroup _ cmds) = doList cmds False
@ -2448,9 +2373,15 @@ prop_checkUnused51 = verifyTree checkUnusedAssignments "x[y[z=1]]=1; echo ${x[@]
checkUnusedAssignments params t = execWriter (mapM_ warnFor unused) checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
where where
flow = variableFlow params flow = variableFlow params
references = Map.union (Map.fromList [(stripSuffix name, ()) | Reference (base, token, name) <- flow]) defaultMap references = foldl (flip ($)) defaultMap (map insertRef flow)
insertRef (Reference (base, token, name)) =
Map.insert (stripSuffix name) ()
insertRef _ = id
assignments = Map.fromList [(name, token) | Assignment (_, token, name, _) <- flow, isVariableName name] assignments = foldl (flip ($)) Map.empty (map insertAssignment flow)
insertAssignment (Assignment (_, token, name, _)) | isVariableName name =
Map.insert name token
insertAssignment _ = id
unused = Map.assocs $ Map.difference assignments references unused = Map.assocs $ Map.difference assignments references
@ -2514,7 +2445,6 @@ prop_checkUnassignedReferences_minusZDefault = verifyNotTree checkUnassignedRefe
prop_checkUnassignedReferences50 = verifyNotTree checkUnassignedReferences "echo ${foo:+bar}" prop_checkUnassignedReferences50 = verifyNotTree checkUnassignedReferences "echo ${foo:+bar}"
prop_checkUnassignedReferences51 = verifyNotTree checkUnassignedReferences "echo ${foo:+$foo}" prop_checkUnassignedReferences51 = verifyNotTree checkUnassignedReferences "echo ${foo:+$foo}"
prop_checkUnassignedReferences52 = verifyNotTree checkUnassignedReferences "wait -p pid; echo $pid" prop_checkUnassignedReferences52 = verifyNotTree checkUnassignedReferences "wait -p pid; echo $pid"
prop_checkUnassignedReferences53 = verifyTree checkUnassignedReferences "x=($foo)"
checkUnassignedReferences = checkUnassignedReferences' False checkUnassignedReferences = checkUnassignedReferences' False
checkUnassignedReferences' includeGlobals params t = warnings checkUnassignedReferences' includeGlobals params t = warnings
@ -2570,12 +2500,14 @@ checkUnassignedReferences' includeGlobals params t = warnings
warnings = execWriter . sequence $ mapMaybe warningFor unassigned warnings = execWriter . sequence $ mapMaybe warningFor unassigned
-- ${foo[bar baz]} may not be referencing bar/baz. Just skip these. -- Due to parsing, foo=( [bar]=baz ) parses 'bar' as a reference even for assoc arrays.
-- Similarly, ${foo[bar baz]} may not be referencing bar/baz. Just skip these.
-- We can also have ${foo:+$foo} should be treated like [[ -n $foo ]] && echo $foo -- We can also have ${foo:+$foo} should be treated like [[ -n $foo ]] && echo $foo
isException var t = any shouldExclude $ getPath (parentMap params) t isException var t = any shouldExclude $ getPath (parentMap params) t
where where
shouldExclude t = shouldExclude t =
case t of case t of
T_Array {} -> True
(T_DollarBraced _ _ l) -> (T_DollarBraced _ _ l) ->
let str = concat $ oversimplify l let str = concat $ oversimplify l
ref = getBracedReference str ref = getBracedReference str
@ -2886,10 +2818,6 @@ prop_checkUnpassedInFunctions11 = verifyNotTree checkUnpassedInFunctions "foo()
prop_checkUnpassedInFunctions12 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${!var*}; }; foo;" prop_checkUnpassedInFunctions12 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${!var*}; }; foo;"
prop_checkUnpassedInFunctions13 = verifyNotTree checkUnpassedInFunctions "# shellcheck disable=SC2120\nfoo() { echo $1; }\nfoo\n" prop_checkUnpassedInFunctions13 = verifyNotTree checkUnpassedInFunctions "# shellcheck disable=SC2120\nfoo() { echo $1; }\nfoo\n"
prop_checkUnpassedInFunctions14 = verifyTree checkUnpassedInFunctions "foo() { echo $#; }; foo" prop_checkUnpassedInFunctions14 = verifyTree checkUnpassedInFunctions "foo() { echo $#; }; foo"
prop_checkUnpassedInFunctions15 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${1-x}; }; foo"
prop_checkUnpassedInFunctions16 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${1:-x}; }; foo"
prop_checkUnpassedInFunctions17 = verifyNotTree checkUnpassedInFunctions "foo() { mycommand ${1+--verbose}; }; foo"
prop_checkUnpassedInFunctions18 = verifyNotTree checkUnpassedInFunctions "foo() { if mycheck; then foo ${1?Missing}; fi; }; foo"
checkUnpassedInFunctions params root = checkUnpassedInFunctions params root =
execWriter $ mapM_ warnForGroup referenceGroups execWriter $ mapM_ warnForGroup referenceGroups
where where
@ -2906,10 +2834,9 @@ checkUnpassedInFunctions params root =
case x of case x of
Assignment (_, _, str, _) -> isPositional str Assignment (_, _, str, _) -> isPositional str
_ -> False _ -> False
isPositionalReference function x = isPositionalReference function x =
case x of case x of
Reference (_, t, str) -> isPositional str && t `isDirectChildOf` function && not (hasDefaultValue t) Reference (_, t, str) -> isPositional str && t `isDirectChildOf` function
_ -> False _ -> False
isDirectChildOf child parent = fromMaybe False $ do isDirectChildOf child parent = fromMaybe False $ do
@ -2923,7 +2850,6 @@ checkUnpassedInFunctions params root =
referenceList :: [(String, Bool, Token)] referenceList :: [(String, Bool, Token)]
referenceList = execWriter $ referenceList = execWriter $
doAnalysis (sequence_ . checkCommand) root doAnalysis (sequence_ . checkCommand) root
checkCommand :: Token -> Maybe (Writer [(String, Bool, Token)] ()) checkCommand :: Token -> Maybe (Writer [(String, Bool, Token)] ())
checkCommand t@(T_SimpleCommand _ _ (cmd:args)) = do checkCommand t@(T_SimpleCommand _ _ (cmd:args)) = do
str <- getLiteralString cmd str <- getLiteralString cmd
@ -2934,22 +2860,6 @@ checkUnpassedInFunctions params root =
isPositional str = str == "*" || str == "@" || str == "#" isPositional str = str == "*" || str == "@" || str == "#"
|| (all isDigit str && str /= "0" && str /= "") || (all isDigit str && str /= "0" && str /= "")
-- True if t is a variable that specifies a default value,
-- such as ${1-x} or ${1:-x}.
hasDefaultValue t =
case t of
T_DollarBraced _ True l ->
let str = concat $ oversimplify l
in isDefaultValueModifier $ getBracedModifier str
_ -> False
isDefaultValueModifier str =
case str of
':':c:_ -> c `elem` handlesDefault
c:_ -> c `elem` handlesDefault
_ -> False
where handlesDefault = "-+?"
isArgumentless (_, b, _) = b isArgumentless (_, b, _) = b
referenceGroups = Map.elems $ foldr updateWith Map.empty referenceList referenceGroups = Map.elems $ foldr updateWith Map.empty referenceList
updateWith x@(name, _, _) = Map.insertWith (++) name [x] updateWith x@(name, _, _) = Map.insertWith (++) name [x]
@ -3387,7 +3297,7 @@ checkReturnAgainstZero params token =
next@(TA_Unary _ "!" _):_ -> isOnlyTestInCommand next next@(TA_Unary _ "!" _):_ -> isOnlyTestInCommand next
next@(TC_Group {}):_ -> isOnlyTestInCommand next next@(TC_Group {}):_ -> isOnlyTestInCommand next
next@(TA_Sequence _ [_]):_ -> isOnlyTestInCommand next next@(TA_Sequence _ [_]):_ -> isOnlyTestInCommand next
next@(TA_Parenthesis _ _):_ -> isOnlyTestInCommand next next@(TA_Parentesis _ _):_ -> isOnlyTestInCommand next
_ -> False _ -> False
-- TODO: Do better $? tracking and filter on whether -- TODO: Do better $? tracking and filter on whether
@ -3682,8 +3592,6 @@ prop_checkPipeToNowhere17 = verify checkPipeToNowhere "echo World | cat << 'EOF'
prop_checkPipeToNowhere18 = verifyNot checkPipeToNowhere "ls 1>&3 3>&1 3>&- | wc -l" prop_checkPipeToNowhere18 = verifyNot checkPipeToNowhere "ls 1>&3 3>&1 3>&- | wc -l"
prop_checkPipeToNowhere19 = verifyNot checkPipeToNowhere "find . -print0 | du --files0-from=/dev/stdin" prop_checkPipeToNowhere19 = verifyNot checkPipeToNowhere "find . -print0 | du --files0-from=/dev/stdin"
prop_checkPipeToNowhere20 = verifyNot checkPipeToNowhere "find . | du --exclude-from=/dev/fd/0" prop_checkPipeToNowhere20 = verifyNot checkPipeToNowhere "find . | du --exclude-from=/dev/fd/0"
prop_checkPipeToNowhere21 = verifyNot checkPipeToNowhere "yes | cp -ri foo/* bar"
prop_checkPipeToNowhere22 = verifyNot checkPipeToNowhere "yes | rm --interactive *"
data PipeType = StdoutPipe | StdoutStderrPipe | NoPipe deriving (Eq) data PipeType = StdoutPipe | StdoutStderrPipe | NoPipe deriving (Eq)
checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity () checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity ()
@ -3749,7 +3657,6 @@ checkPipeToNowhere params t =
commandSpecificException name cmd = commandSpecificException name cmd =
case name of case name of
"du" -> any ((`elem` ["exclude-from", "files0-from"]) . snd) $ getAllFlags cmd "du" -> any ((`elem` ["exclude-from", "files0-from"]) . snd) $ getAllFlags cmd
_ | name `elem` interactiveFlagCmds -> hasInteractiveFlag cmd
_ -> False _ -> False
warnAboutDupes (n, list@(_:_:_)) = warnAboutDupes (n, list@(_:_:_)) =
@ -3773,7 +3680,7 @@ checkPipeToNowhere params t =
name <- getCommandBasename cmd name <- getCommandBasename cmd
guard $ name `elem` nonReadingCommands guard $ name `elem` nonReadingCommands
guard . not $ hasAdditionalConsumers cmd guard . not $ hasAdditionalConsumers cmd
guard . not $ name `elem` interactiveFlagCmds && hasInteractiveFlag cmd guard . not $ name `elem` ["cp", "mv", "rm"] && cmd `hasFlag` "i"
let suggestion = let suggestion =
if name == "echo" if name == "echo"
then "Did you want 'cat' instead?" then "Did you want 'cat' instead?"
@ -3788,9 +3695,6 @@ checkPipeToNowhere params t =
treeContains pred t = isNothing $ treeContains pred t = isNothing $
doAnalysis (guard . not . pred) t doAnalysis (guard . not . pred) t
interactiveFlagCmds = [ "cp", "mv", "rm" ]
hasInteractiveFlag cmd = cmd `hasFlag` "i" || cmd `hasFlag` "interactive"
mayConsume t = mayConsume t =
case t of case t of
T_ProcSub _ "<" _ -> True T_ProcSub _ "<" _ -> True
@ -3857,32 +3761,32 @@ prop_checkUseBeforeDefinition1 = verifyTree checkUseBeforeDefinition "f; f() { t
prop_checkUseBeforeDefinition2 = verifyNotTree checkUseBeforeDefinition "f() { true; }; f" prop_checkUseBeforeDefinition2 = verifyNotTree checkUseBeforeDefinition "f() { true; }; f"
prop_checkUseBeforeDefinition3 = verifyNotTree checkUseBeforeDefinition "if ! mycmd --version; then mycmd() { true; }; fi" prop_checkUseBeforeDefinition3 = verifyNotTree checkUseBeforeDefinition "if ! mycmd --version; then mycmd() { true; }; fi"
prop_checkUseBeforeDefinition4 = verifyNotTree checkUseBeforeDefinition "mycmd || mycmd() { f; }" prop_checkUseBeforeDefinition4 = verifyNotTree checkUseBeforeDefinition "mycmd || mycmd() { f; }"
prop_checkUseBeforeDefinition5 = verifyTree checkUseBeforeDefinition "false || mycmd; mycmd() { f; }" checkUseBeforeDefinition _ t =
prop_checkUseBeforeDefinition6 = verifyNotTree checkUseBeforeDefinition "f() { one; }; f; f() { two; }; f" execWriter $ evalStateT (mapM_ examine $ revCommands) Map.empty
checkUseBeforeDefinition :: Parameters -> Token -> [TokenComment]
checkUseBeforeDefinition params t = fromMaybe [] $ do
cfga <- cfgAnalysis params
let funcs = execState (doAnalysis findFunction t) Map.empty
-- Green cut: no point enumerating commands if there are no functions.
guard . not $ Map.null funcs
return $ execWriter $ doAnalysis (findInvocation cfga funcs) t
where where
findFunction t = examine t = case t of
case t of T_Pipeline _ _ [T_Redirecting _ _ (T_Function _ _ _ name _)] ->
T_Function id _ _ name _ -> modify (Map.insertWith (++) name [id]) modify $ Map.insert name t
_ -> return () T_Annotation _ _ w -> examine w
T_Pipeline _ _ cmds -> do
m <- get
unless (Map.null m) $
mapM_ (checkUsage m) $ concatMap recursiveSequences cmds
_ -> return ()
findInvocation cfga funcs t = checkUsage map cmd = sequence_ $ do
case t of name <- getCommandName cmd
T_SimpleCommand id _ (cmd:_) -> sequence_ $ do def <- Map.lookup name map
name <- getLiteralString cmd return $
invocations <- Map.lookup name funcs err (getId cmd) 2218
-- Is the function definitely being defined later? "This function is only defined later. Move the definition up."
guard $ any (\c -> CF.doesPostDominate cfga c id) invocations
-- Was one already defined, so it's actually a re-definition? revCommands = reverse $ concat $ getCommandSequences t
guard . not $ any (\c -> CF.doesPostDominate cfga id c) invocations recursiveSequences x =
return $ err id 2218 "This function is only defined later. Move the definition up." let list = concat $ getCommandSequences x in
_ -> return () if null list
then [x]
else concatMap recursiveSequences list
prop_checkForLoopGlobVariables1 = verify checkForLoopGlobVariables "for i in $var/*.txt; do true; done" prop_checkForLoopGlobVariables1 = verify checkForLoopGlobVariables "for i in $var/*.txt; do true; done"
prop_checkForLoopGlobVariables2 = verifyNot checkForLoopGlobVariables "for i in \"$var\"/*.txt; do true; done" prop_checkForLoopGlobVariables2 = verifyNot checkForLoopGlobVariables "for i in \"$var\"/*.txt; do true; done"
@ -4058,10 +3962,13 @@ prop_checkTranslatedStringVariable4 = verifyNot checkTranslatedStringVariable "v
prop_checkTranslatedStringVariable5 = verifyNot checkTranslatedStringVariable "foo=var; bar=val2; $\"foo bar\"" prop_checkTranslatedStringVariable5 = verifyNot checkTranslatedStringVariable "foo=var; bar=val2; $\"foo bar\""
checkTranslatedStringVariable params (T_DollarDoubleQuoted id [T_Literal _ s]) checkTranslatedStringVariable params (T_DollarDoubleQuoted id [T_Literal _ s])
| all isVariableChar s | all isVariableChar s
&& S.member s assignments && Map.member s assignments
= warnWithFix id 2256 "This translated string is the name of a variable. Flip leading $ and \" if this should be a quoted substitution." (fix id) = warnWithFix id 2256 "This translated string is the name of a variable. Flip leading $ and \" if this should be a quoted substitution." (fix id)
where where
assignments = S.fromList [name | Assignment (_, _, name, _) <- variableFlow params, isVariableName name] assignments = foldl (flip ($)) Map.empty (map insertAssignment $ variableFlow params)
insertAssignment (Assignment (_, token, name, _)) | isVariableName name =
Map.insert name token
insertAssignment _ = Prelude.id
fix id = fixWith [replaceStart id params 2 "\"$"] fix id = fixWith [replaceStart id params 2 "\"$"]
checkTranslatedStringVariable _ _ = return () checkTranslatedStringVariable _ _ = return ()
@ -4091,7 +3998,6 @@ prop_checkUselessBang6 = verify checkUselessBang "set -e; { ! true; }"
prop_checkUselessBang7 = verifyNot checkUselessBang "set -e; x() { ! [ x ]; }" prop_checkUselessBang7 = verifyNot checkUselessBang "set -e; x() { ! [ x ]; }"
prop_checkUselessBang8 = verifyNot checkUselessBang "set -e; if { ! true; }; then true; fi" prop_checkUselessBang8 = verifyNot checkUselessBang "set -e; if { ! true; }; then true; fi"
prop_checkUselessBang9 = verifyNot checkUselessBang "set -e; while ! true; do true; done" prop_checkUselessBang9 = verifyNot checkUselessBang "set -e; while ! true; do true; done"
prop_checkUselessBang10 = verify checkUselessBang "set -e\nshellcheck disable=SC0000\n! true\nrest"
checkUselessBang params t = when (hasSetE params) $ mapM_ check (getNonReturningCommands t) checkUselessBang params t = when (hasSetE params) $ mapM_ check (getNonReturningCommands t)
where where
check t = check t =
@ -4100,7 +4006,6 @@ checkUselessBang params t = when (hasSetE params) $ mapM_ check (getNonReturning
addComment $ makeCommentWithFix InfoC id 2251 addComment $ makeCommentWithFix InfoC id 2251
"This ! is not on a condition and skips errexit. Use `&& exit 1` instead, or make sure $? is checked." "This ! is not on a condition and skips errexit. Use `&& exit 1` instead, or make sure $? is checked."
(fixWith [replaceStart id params 1 "", replaceEnd (getId cmd) params 0 " && exit 1"]) (fixWith [replaceStart id params 1 "", replaceEnd (getId cmd) params 0 " && exit 1"])
T_Annotation _ _ t -> check t
_ -> return () _ -> return ()
-- Get all the subcommands that aren't likely to be the return value -- Get all the subcommands that aren't likely to be the return value
@ -4291,7 +4196,7 @@ checkBadTestAndOr params t =
in in
mapM_ checkTest commandWithSeps mapM_ checkTest commandWithSeps
checkTest (before, cmd, after) = checkTest (before, cmd, after) =
when (isTestCommand cmd) $ do when (isTest cmd) $ do
checkPipe before checkPipe before
checkPipe after checkPipe after
@ -4307,10 +4212,17 @@ checkBadTestAndOr params t =
T_AndIf _ _ rhs -> checkAnds id rhs T_AndIf _ _ rhs -> checkAnds id rhs
T_OrIf _ _ rhs -> checkAnds id rhs T_OrIf _ _ rhs -> checkAnds id rhs
T_Pipeline _ _ list | not (null list) -> checkAnds id (last list) T_Pipeline _ _ list | not (null list) -> checkAnds id (last list)
cmd -> when (isTestCommand cmd) $ cmd -> when (isTest cmd) $
errWithFix id 2265 "Use && for logical AND. Single & will background and return true." $ errWithFix id 2265 "Use && for logical AND. Single & will background and return true." $
(fixWith [replaceEnd id params 0 "&"]) (fixWith [replaceEnd id params 0 "&"])
isTest t =
case t of
T_Condition {} -> True
T_SimpleCommand {} -> t `isCommand` "test"
T_Redirecting _ _ t -> isTest t
T_Annotation _ _ t -> isTest t
_ -> False
prop_checkComparisonWithLeadingX1 = verify checkComparisonWithLeadingX "[ x$foo = xlol ]" prop_checkComparisonWithLeadingX1 = verify checkComparisonWithLeadingX "[ x$foo = xlol ]"
prop_checkComparisonWithLeadingX2 = verify checkComparisonWithLeadingX "test x$foo = xlol" prop_checkComparisonWithLeadingX2 = verify checkComparisonWithLeadingX "test x$foo = xlol"
@ -4626,13 +4538,13 @@ prop_checkRequireDoubleBracket2 = verifyTree checkRequireDoubleBracket "[ foo -o
prop_checkRequireDoubleBracket3 = verifyNotTree checkRequireDoubleBracket "#!/bin/sh\n[ -x foo ]" prop_checkRequireDoubleBracket3 = verifyNotTree checkRequireDoubleBracket "#!/bin/sh\n[ -x foo ]"
prop_checkRequireDoubleBracket4 = verifyNotTree checkRequireDoubleBracket "[[ -x foo ]]" prop_checkRequireDoubleBracket4 = verifyNotTree checkRequireDoubleBracket "[[ -x foo ]]"
checkRequireDoubleBracket params = checkRequireDoubleBracket params =
if (shellType params) `elem` [Bash, Ksh, BusyboxSh] if isBashLike params
then nodeChecksToTreeCheck [check] params then nodeChecksToTreeCheck [check] params
else const [] else const []
where where
check _ t = case t of check _ t = case t of
T_Condition id SingleBracket _ -> T_Condition id SingleBracket _ ->
styleWithFix id 2292 "Prefer [[ ]] over [ ] for tests in Bash/Ksh/Busybox." (fixFor t) styleWithFix id 2292 "Prefer [[ ]] over [ ] for tests in Bash/Ksh." (fixFor t)
_ -> return () _ -> return ()
fixFor t = fixWith $ fixFor t = fixWith $
@ -4983,33 +4895,16 @@ checkBatsTestDoesNotUseNegation params t =
prop_checkCommandIsUnreachable1 = verify checkCommandIsUnreachable "foo; bar; exit; baz" prop_checkCommandIsUnreachable1 = verify checkCommandIsUnreachable "foo; bar; exit; baz"
prop_checkCommandIsUnreachable2 = verify checkCommandIsUnreachable "die() { exit; }; foo; bar; die; baz" prop_checkCommandIsUnreachable2 = verify checkCommandIsUnreachable "die() { exit; }; foo; bar; die; baz"
prop_checkCommandIsUnreachable3 = verifyNot checkCommandIsUnreachable "foo; bar || exit; baz" prop_checkCommandIsUnreachable3 = verifyNot checkCommandIsUnreachable "foo; bar || exit; baz"
prop_checkCommandIsUnreachable4 = verifyNot checkCommandIsUnreachable "f() { foo; }; # Maybe sourced"
prop_checkCommandIsUnreachable5 = verify checkCommandIsUnreachable "f() { foo; }; exit # Not sourced"
checkCommandIsUnreachable params t = checkCommandIsUnreachable params t =
case t of case t of
T_Pipeline {} -> sequence_ $ do T_Pipeline {} -> sequence_ $ do
cfga <- cfgAnalysis params cfga <- cfgAnalysis params
state <- CF.getIncomingState cfga (getId t) state <- CF.getIncomingState cfga id
guard . not $ CF.stateIsReachable state guard . not $ CF.stateIsReachable state
guard . not $ isSourced params t guard . not $ isSourced params t
guard . not $ any (\t -> isUnreachable t || isUnreachableFunction t) $ NE.drop 1 $ getPath (parentMap params) t return $ info id 2317 "Command appears to be unreachable. Check usage (or ignore if invoked indirectly)."
return $ info (getId t) 2317 "Command appears to be unreachable. Check usage (or ignore if invoked indirectly)."
T_Function id _ _ _ _ ->
when (isUnreachableFunction t
&& (not . any isUnreachableFunction . NE.drop 1 $ getPath (parentMap params) t)
&& (not $ isSourced params t)) $
info id 2329 "This function is never invoked. Check usage (or ignored if invoked indirectly)."
_ -> return () _ -> return ()
where where id = getId t
isUnreachableFunction :: Token -> Bool
isUnreachableFunction f =
case f of
T_Function id _ _ _ t -> isUnreachable t
_ -> False
isUnreachable t = fromMaybe False $ do
cfga <- cfgAnalysis params
state <- CF.getIncomingState cfga (getId t)
return . not $ CF.stateIsReachable state
prop_checkOverwrittenExitCode1 = verify checkOverwrittenExitCode "x; [ $? -eq 1 ] || [ $? -eq 2 ]" prop_checkOverwrittenExitCode1 = verify checkOverwrittenExitCode "x; [ $? -eq 1 ] || [ $? -eq 2 ]"
@ -5089,14 +4984,14 @@ checkUnnecessaryParens params t =
T_ForArithmetic _ x y z _ -> mapM_ (checkLeading "for (((x); (y); (z))) is the same as for ((x; y; z))") [x,y,z] T_ForArithmetic _ x y z _ -> mapM_ (checkLeading "for (((x); (y); (z))) is the same as for ((x; y; z))") [x,y,z]
T_Assignment _ _ _ [t] _ -> checkLeading "a[(x)] is the same as a[x]" t T_Assignment _ _ _ [t] _ -> checkLeading "a[(x)] is the same as a[x]" t
T_Arithmetic _ t -> checkLeading "(( (x) )) is the same as (( x ))" t T_Arithmetic _ t -> checkLeading "(( (x) )) is the same as (( x ))" t
TA_Parenthesis _ (TA_Sequence _ [ TA_Parenthesis id _ ]) -> TA_Parentesis _ (TA_Sequence _ [ TA_Parentesis id _ ]) ->
styleWithFix id 2322 "In arithmetic contexts, ((x)) is the same as (x). Prefer only one layer of parentheses." $ fix id styleWithFix id 2322 "In arithmetic contexts, ((x)) is the same as (x). Prefer only one layer of parentheses." $ fix id
_ -> return () _ -> return ()
where where
checkLeading str t = checkLeading str t =
case t of case t of
TA_Sequence _ [TA_Parenthesis id _ ] -> styleWithFix id 2323 (str ++ ". Prefer not wrapping in additional parentheses.") $ fix id TA_Sequence _ [TA_Parentesis id _ ] -> styleWithFix id 2323 (str ++ ". Prefer not wrapping in additional parentheses.") $ fix id
_ -> return () _ -> return ()
fix id = fix id =
@ -5122,8 +5017,7 @@ checkPlusEqualsNumber params t =
state <- CF.getIncomingState cfga id state <- CF.getIncomingState cfga id
guard $ isNumber state word guard $ isNumber state word
guard . not $ fromMaybe False $ CF.variableMayBeDeclaredInteger state var guard . not $ fromMaybe False $ CF.variableMayBeDeclaredInteger state var
-- Recommend "typeset" because ksh does not have "declare". return $ warn id 2324 "var+=1 will append, not increment. Use (( var += 1 )), declare -i var, or quote number to silence."
return $ warn id 2324 "var+=1 will append, not increment. Use (( var += 1 )), typeset -i var, or quote number to silence."
_ -> return () _ -> return ()
where where
@ -5145,52 +5039,5 @@ checkPlusEqualsNumber params t =
isUnquotedNumber || isNumericalVariableName || isNumericalVariableExpansion isUnquotedNumber || isNumericalVariableName || isNumericalVariableExpansion
prop_checkExpansionWithRedirection1 = verify checkExpansionWithRedirection "var=$(foo > bar)"
prop_checkExpansionWithRedirection2 = verify checkExpansionWithRedirection "var=`foo 1> bar`"
prop_checkExpansionWithRedirection3 = verify checkExpansionWithRedirection "var=${ foo >> bar; }"
prop_checkExpansionWithRedirection4 = verify checkExpansionWithRedirection "var=$(foo | bar > baz)"
prop_checkExpansionWithRedirection5 = verifyNot checkExpansionWithRedirection "stderr=$(foo 2>&1 > /dev/null)"
prop_checkExpansionWithRedirection6 = verifyNot checkExpansionWithRedirection "var=$(foo; bar > baz)"
prop_checkExpansionWithRedirection7 = verifyNot checkExpansionWithRedirection "var=$(foo > bar; baz)"
prop_checkExpansionWithRedirection8 = verifyNot checkExpansionWithRedirection "var=$(cat <&3)"
checkExpansionWithRedirection params t =
case t of
T_DollarExpansion id [cmd] -> check id cmd
T_Backticked id [cmd] -> check id cmd
T_DollarBraceCommandExpansion id [cmd] -> check id cmd
_ -> return ()
where
check id pipe =
case pipe of
(T_Pipeline _ _ t@(_:_)) -> checkCmd id (last t)
_ -> return ()
checkCmd captureId (T_Redirecting _ redirs _) = foldr (walk captureId) (return ()) redirs
walk captureId t acc =
case t of
T_FdRedirect _ _ (T_IoDuplicate _ _ "1") -> return ()
T_FdRedirect id "1" (T_IoDuplicate _ _ _) -> return ()
T_FdRedirect id "" (T_IoDuplicate _ op _) | op `elem` [T_GREATAND (Id 0), T_Greater (Id 0)] -> emit id captureId True
T_FdRedirect id str (T_IoFile _ op file) | str `elem` ["", "1"] && op `elem` [ T_DGREAT (Id 0), T_Greater (Id 0) ] ->
emit id captureId $ getLiteralString file /= Just "/dev/null"
_ -> acc
emit redirectId captureId suggestTee = do
warn captureId 2327 "This command substitution will be empty because the command's output gets redirected away."
err redirectId 2328 $ "This redirection takes output away from the command substitution" ++ if suggestTee then " (use tee to duplicate)." else "."
prop_checkUnaryTestA1 = verify checkUnaryTestA "[ -a foo ]"
prop_checkUnaryTestA2 = verify checkUnaryTestA "[ ! -a foo ]"
prop_checkUnaryTestA3 = verifyNot checkUnaryTestA "[ foo -a bar ]"
checkUnaryTestA params t =
case t of
TC_Unary id _ "-a" _ ->
styleWithFix id 2331 "For file existence, prefer standard -e over legacy -a." $
fixWith [replaceStart id params 2 "-e"]
_ -> return ()
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View file

@ -89,8 +89,6 @@ data Parameters = Parameters {
hasSetE :: Bool, hasSetE :: Bool,
-- Whether this script has 'set -o pipefail' anywhere. -- Whether this script has 'set -o pipefail' anywhere.
hasPipefail :: Bool, hasPipefail :: Bool,
-- Whether this script has 'shopt -s execfail' anywhere.
hasExecfail :: Bool,
-- A linear (bad) analysis of data flow -- A linear (bad) analysis of data flow
variableFlow :: [StackData], variableFlow :: [StackData],
-- A map from Id to Token -- A map from Id to Token
@ -228,10 +226,6 @@ makeParameters spec = params
BusyboxSh -> isOptionSet "pipefail" root BusyboxSh -> isOptionSet "pipefail" root
Sh -> True Sh -> True
Ksh -> isOptionSet "pipefail" root, Ksh -> isOptionSet "pipefail" root,
hasExecfail =
case shellType params of
Bash -> isOptionSet "execfail" root
_ -> False,
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec), shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
idMap = getTokenMap root, idMap = getTokenMap root,
parentMap = getParentTree root, parentMap = getParentTree root,
@ -541,9 +535,7 @@ getModifiedVariables t =
T_BatsTest {} -> [ T_BatsTest {} -> [
(t, t, "lines", DataArray SourceExternal), (t, t, "lines", DataArray SourceExternal),
(t, t, "status", DataString SourceInteger), (t, t, "status", DataString SourceInteger),
(t, t, "output", DataString SourceExternal), (t, t, "output", DataString SourceExternal)
(t, t, "stderr", DataString SourceExternal),
(t, t, "stderr_lines", DataArray SourceExternal)
] ]
-- Count [[ -v foo ]] as an "assignment". -- Count [[ -v foo ]] as an "assignment".
@ -565,12 +557,8 @@ getModifiedVariables 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_CoProc _ Nothing _ -> T_CoProc _ name _ ->
[(t, t, "COPROC", DataArray SourceInteger)] [(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
T_CoProc _ (Just token) _ -> do
name <- maybeToList $ getLiteralString token
[(t, t, name, DataArray SourceInteger)]
--Points to 'for' rather than variable --Points to 'for' rather than variable
T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)] T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)]
@ -914,6 +902,16 @@ supportsArrays Bash = True
supportsArrays Ksh = True supportsArrays Ksh = True
supportsArrays _ = False supportsArrays _ = False
-- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh)
isBashLike :: Parameters -> Bool
isBashLike params =
case shellType params of
Bash -> True
Ksh -> True
Dash -> False
BusyboxSh -> False
Sh -> False
isTrueAssignmentSource c = isTrueAssignmentSource c =
case c of case c of
DataString SourceChecked -> False DataString SourceChecked -> False
@ -931,14 +929,6 @@ modifiesVariable params token name =
Assignment (_, _, n, source) -> isTrueAssignmentSource source && n == name Assignment (_, _, n, source) -> isTrueAssignmentSource source && n == name
_ -> False _ -> False
isTestCommand t =
case t of
T_Condition {} -> True
T_SimpleCommand {} -> t `isCommand` "test"
T_Redirecting _ _ t -> isTestCommand t
T_Annotation _ _ t -> isTestCommand t
T_Pipeline _ _ [t] -> isTestCommand t
_ -> False
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View file

@ -295,19 +295,19 @@ removeUnnecessaryStructuralNodes (nodes, edges, mapping, association) =
regularEdges = filter isRegularEdge edges regularEdges = filter isRegularEdge edges
inDegree = counter $ map (\(from,to,_) -> from) regularEdges inDegree = counter $ map (\(from,to,_) -> from) regularEdges
outDegree = counter $ map (\(from,to,_) -> to) regularEdges outDegree = counter $ map (\(from,to,_) -> to) regularEdges
structuralNodes = S.fromList [node | (node, CFStructuralNode) <- nodes] structuralNodes = S.fromList $ map fst $ filter isStructural nodes
candidateNodes = S.filter isLinear structuralNodes candidateNodes = S.filter isLinear structuralNodes
edgesToCollapse = S.fromList $ filter filterEdges regularEdges edgesToCollapse = S.fromList $ filter filterEdges regularEdges
remapping :: M.Map Node Node remapping :: M.Map Node Node
remapping = M.fromList $ map orderEdge $ S.toList edgesToCollapse remapping = foldl' (\m (new, old) -> M.insert old new m) M.empty $ map orderEdge $ S.toList edgesToCollapse
recursiveRemapping = M.mapWithKey (\c _ -> recursiveLookup remapping c) remapping recursiveRemapping = M.fromList $ map (\c -> (c, recursiveLookup remapping c)) $ M.keys remapping
filterEdges (a,b,_) = filterEdges (a,b,_) =
a `S.member` candidateNodes && b `S.member` candidateNodes a `S.member` candidateNodes && b `S.member` candidateNodes
orderEdge (a,b,_) = if a < b then (b,a) else (a,b) orderEdge (a,b,_) = if a < b then (a,b) else (b,a)
counter = M.fromListWith (+) . map (\key -> (key, 1)) counter = foldl' (\map key -> M.insertWith (+) key 1 map) M.empty
isRegularEdge (_, _, CFEFlow) = True isRegularEdge (_, _, CFEFlow) = True
isRegularEdge _ = False isRegularEdge _ = False
@ -317,6 +317,11 @@ removeUnnecessaryStructuralNodes (nodes, edges, mapping, association) =
Nothing -> node Nothing -> node
Just x -> recursiveLookup map x Just x -> recursiveLookup map x
isStructural (node, label) =
case label of
CFStructuralNode -> True
_ -> False
isLinear node = isLinear node =
M.findWithDefault 0 node inDegree == 1 M.findWithDefault 0 node inDegree == 1
&& M.findWithDefault 0 node outDegree == 1 && M.findWithDefault 0 node outDegree == 1
@ -490,7 +495,7 @@ build t = do
TA_Binary _ _ a b -> sequentially [a,b] TA_Binary _ _ a b -> sequentially [a,b]
TA_Expansion _ list -> sequentially list TA_Expansion _ list -> sequentially list
TA_Sequence _ list -> sequentially list TA_Sequence _ list -> sequentially list
TA_Parenthesis _ t -> build t TA_Parentesis _ t -> build t
TA_Trinary _ cond a b -> do TA_Trinary _ cond a b -> do
condition <- build cond condition <- build cond
@ -668,18 +673,10 @@ build t = do
status <- newNodeRange $ CFSetExitCode id status <- newNodeRange $ CFSetExitCode id
linkRange cond status linkRange cond status
T_CoProc id maybeNameToken t -> do T_CoProc id maybeName t -> do
-- If unspecified, "COPROC". If not a constant string, Nothing. let name = fromMaybe "COPROC" maybeName
let maybeName = case maybeNameToken of
Just x -> getLiteralString x
Nothing -> Just "COPROC"
let parentNode = case maybeName of
Just str -> applySingle $ IdTagged id $ CFWriteVariable str CFValueArray
Nothing -> CFStructuralNode
start <- newStructuralNode start <- newStructuralNode
parent <- newNodeRange parentNode parent <- newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable name CFValueArray
child <- subshell id "coproc" $ build t child <- subshell id "coproc" $ build t
end <- newNodeRange $ CFSetExitCode id end <- newNodeRange $ CFSetExitCode id

View file

@ -133,7 +133,7 @@ internalToExternal s =
literalValue = Nothing literalValue = Nothing
} }
} }
flatVars = M.unions $ map mapStorage [sPrefixValues s, sLocalValues s, sGlobalValues s] flatVars = M.unionsWith (\_ last -> last) $ map mapStorage [sGlobalValues s, sLocalValues s, sPrefixValues s]
-- Conveniently get the state before a token id -- Conveniently get the state before a token id
getIncomingState :: CFGAnalysis -> Id -> Maybe ProgramState getIncomingState :: CFGAnalysis -> Id -> Maybe ProgramState
@ -672,7 +672,7 @@ vmPatch base diff =
_ | vmIsQuickEqual base diff -> diff _ | vmIsQuickEqual base diff -> diff
_ -> VersionedMap { _ -> VersionedMap {
mapVersion = -1, mapVersion = -1,
mapStorage = M.union (mapStorage diff) (mapStorage base) mapStorage = M.unionWith (flip const) (mapStorage base) (mapStorage diff)
} }
-- Set a variable. This includes properties. Applies it to the appropriate scope. -- Set a variable. This includes properties. Applies it to the appropriate scope.
@ -1286,7 +1286,7 @@ dataflow ctx entry = do
else do else do
let (next, rest) = S.deleteFindMin ps let (next, rest) = S.deleteFindMin ps
nexts <- process states next nexts <- process states next
writeSTRef pending $ S.union (S.fromList nexts) rest writeSTRef pending $ foldl (flip S.insert) rest nexts
f (n-1) pending states f (n-1) pending states
process states node = do process states node = do
@ -1350,7 +1350,7 @@ analyzeControlFlow params t =
-- All nodes we've touched -- All nodes we've touched
invocations <- readSTRef $ cInvocations ctx invocations <- readSTRef $ cInvocations ctx
let invokedNodes = M.fromSet (const ()) $ S.unions $ map (M.keysSet . snd) $ M.elems invocations let invokedNodes = M.fromDistinctAscList $ map (\c -> (c, ())) $ S.toList $ M.keysSet $ groupByNode $ M.map snd invocations
-- Invoke all functions that were declared but not invoked -- Invoke all functions that were declared but not invoked
-- This is so that we still get warnings for dead code -- This is so that we still get warnings for dead code
@ -1373,7 +1373,7 @@ analyzeControlFlow params t =
-- Fill in the map with unreachable states for anything we didn't get to -- Fill in the map with unreachable states for anything we didn't get to
let baseStates = M.fromDistinctAscList $ map (\c -> (c, (unreachableState, unreachableState))) $ uncurry enumFromTo $ nodeRange $ cfGraph cfg let baseStates = M.fromDistinctAscList $ map (\c -> (c, (unreachableState, unreachableState))) $ uncurry enumFromTo $ nodeRange $ cfGraph cfg
let allStates = M.union invokedStates baseStates let allStates = M.unionWith (flip const) baseStates invokedStates
-- Convert to external states -- Convert to external states
let nodeToData = M.map (\(a,b) -> (internalToExternal a, internalToExternal b)) allStates let nodeToData = M.map (\(a,b) -> (internalToExternal a, internalToExternal b)) allStates

View file

@ -1431,8 +1431,9 @@ prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration
checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check
where where
check t = do check t = do
maybeCfga <- asks cfgAnalysis cfga <- asks cfgAnalysis
mapM_ (\cfga -> foldM_ (perArg cfga) M.empty $ arguments t) maybeCfga when (isJust cfga) $
foldM_ (perArg $ fromJust cfga) M.empty $ arguments t
perArg cfga leftArgs t = perArg cfga leftArgs t =
case t of case t of

View file

@ -78,7 +78,7 @@ controlFlowEffectChecks = [
runNodeChecks :: [ControlFlowNodeCheck] -> ControlFlowCheck runNodeChecks :: [ControlFlowNodeCheck] -> ControlFlowCheck
runNodeChecks perNode = do runNodeChecks perNode = do
cfg <- asks cfgAnalysis cfg <- asks cfgAnalysis
mapM_ runOnAll cfg sequence_ $ runOnAll <$> cfg
where where
getData datas n@(node, label) = do getData datas n@(node, label) = do
(pre, post) <- M.lookup node datas (pre, post) <- M.lookup node datas

View file

@ -63,7 +63,6 @@ checks = [
,checkPS1Assignments ,checkPS1Assignments
,checkMultipleBangs ,checkMultipleBangs
,checkBangAfterPipe ,checkBangAfterPipe
,checkNegatedUnaryOps
] ]
testChecker (ForShell _ t) = testChecker (ForShell _ t) =
@ -87,7 +86,7 @@ checkForDecimals = ForShell [Sh, Dash, BusyboxSh, Bash] f
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)" prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
prop_checkBashisms2 = verifyNot checkBashisms "[ foo -nt bar ]" prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"
prop_checkBashisms3 = verify checkBashisms "echo $((i++))" prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)" prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
prop_checkBashisms5 = verify checkBashisms "source file" prop_checkBashisms5 = verify checkBashisms "source file"
@ -213,16 +212,6 @@ prop_checkBashisms118 = verify checkBashisms "#!/bin/busybox sh\nxyz=1\n${!x*}"
prop_checkBashisms119 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x^^[t]}" -- SC3059 prop_checkBashisms119 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x^^[t]}" -- SC3059
prop_checkBashisms120 = verify checkBashisms "#!/bin/sh\n[ x == y ]" prop_checkBashisms120 = verify checkBashisms "#!/bin/sh\n[ x == y ]"
prop_checkBashisms121 = verifyNot checkBashisms "#!/bin/sh\n# shellcheck shell=busybox\n[ x == y ]" prop_checkBashisms121 = verifyNot checkBashisms "#!/bin/sh\n# shellcheck shell=busybox\n[ x == y ]"
prop_checkBashisms122 = verify checkBashisms "#!/bin/dash\n$'a'"
prop_checkBashisms123 = verifyNot checkBashisms "#!/bin/busybox sh\n$'a'"
prop_checkBashisms124 = verify checkBashisms "#!/bin/dash\ntype -p test"
prop_checkBashisms125 = verifyNot checkBashisms "#!/bin/busybox sh\ntype -p test"
prop_checkBashisms126 = verifyNot checkBashisms "#!/bin/busybox sh\nread -p foo -r bar"
prop_checkBashisms127 = verifyNot checkBashisms "#!/bin/busybox sh\necho -ne foo"
prop_checkBashisms128 = verify checkBashisms "#!/bin/dash\ntype -p test"
prop_checkBashisms129 = verify checkBashisms "#!/bin/sh\n[ -k /tmp ]"
prop_checkBashisms130 = verifyNot checkBashisms "#!/bin/dash\ntest -k /tmp"
prop_checkBashisms131 = verify checkBashisms "#!/bin/sh\n[ -o errexit ]"
checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
params <- ask params <- ask
kludge params t kludge params t
@ -240,8 +229,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is" bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is"
bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is" bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is"
bashism (T_DollarSingleQuoted id _) = bashism (T_DollarSingleQuoted id _) = warnMsg id 3003 "$'..' is"
unless isBusyboxSh $ warnMsg id 3003 "$'..' is"
bashism (T_DollarDoubleQuoted id _) = warnMsg id 3004 "$\"..\" is" bashism (T_DollarDoubleQuoted id _) = warnMsg id 3004 "$\"..\" is"
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id 3005 "arithmetic for loops are" bashism (T_ForArithmetic id _ _ _ _) = warnMsg id 3005 "arithmetic for loops are"
bashism (T_Arithmetic id _) = warnMsg id 3006 "standalone ((..)) is" bashism (T_Arithmetic id _) = warnMsg id 3006 "standalone ((..)) is"
@ -251,16 +239,34 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
bashism (T_Condition id DoubleBracket _) = bashism (T_Condition id DoubleBracket _) =
unless isBusyboxSh $ warnMsg id 3010 "[[ ]] is" unless isBusyboxSh $ warnMsg id 3010 "[[ ]] is"
bashism (T_HereString id _) = warnMsg id 3011 "here-strings are" bashism (T_HereString id _) = warnMsg id 3011 "here-strings are"
bashism (TC_Binary id SingleBracket op _ _)
bashism (TC_Binary id _ op _ _) = | op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
checkTestOp bashismBinaryTestFlags op id unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is"
bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs]) = bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs])
checkTestOp bashismBinaryTestFlags op id | op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
bashism (TC_Unary id _ op _) = unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is"
checkTestOp bashismUnaryTestFlags op id bashism (TC_Binary id SingleBracket op _ _)
bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just op, _]) = | op `elem` [ "-ot", "-nt", "-ef" ] =
checkTestOp bashismUnaryTestFlags op id unless isDash $ warnMsg id 3013 $ op ++ " is"
bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs])
| op `elem` [ "-ot", "-nt", "-ef" ] =
unless isDash $ warnMsg id 3013 $ op ++ " is"
bashism (TC_Binary id SingleBracket "==" _ _) =
unless isBusyboxSh $ warnMsg id 3014 "== in place of = is"
bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just "==", rhs]) =
unless isBusyboxSh $ warnMsg id 3014 "== in place of = is"
bashism (TC_Binary id SingleBracket "=~" _ _) =
warnMsg id 3015 "=~ regex matching is"
bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just "=~", rhs]) =
warnMsg id 3015 "=~ regex matching is"
bashism (TC_Unary id SingleBracket "-v" _) =
warnMsg id 3016 "unary -v (in place of [ -n \"${var+x}\" ]) is"
bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just "-v", _]) =
warnMsg id 3016 "unary -v (in place of [ -n \"${var+x}\" ]) is"
bashism (TC_Unary id _ "-a" _) =
warnMsg id 3017 "unary -a in place of -e is"
bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just "-a", _]) =
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 3018 $ filter (/= '|') op ++ " is" warnMsg id 3018 $ filter (/= '|') op ++ " is"
@ -315,11 +321,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "echo" && argString `matches` flagRegex = | t `isCommand` "echo" && argString `matches` flagRegex =
if isBusyboxSh if isDash
then
unless (argString `matches` busyboxFlagRegex) $
warnMsg (getId arg) 3036 "echo flags besides -n and -e"
else if isDash
then then
when (argString /= "-n") $ when (argString /= "-n") $
warnMsg (getId arg) 3036 "echo flags besides -n" warnMsg (getId arg) 3036 "echo flags besides -n"
@ -328,7 +330,6 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
where where
argString = concat $ oversimplify arg argString = concat $ oversimplify arg
flagRegex = mkRegex "^-[eEsn]+$" flagRegex = mkRegex "^-[eEsn]+$"
busyboxFlagRegex = mkRegex "^-[en]+$"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) = | getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
@ -442,10 +443,10 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
("hash", Just $ if isDash then ["r", "v"] else ["r"]), ("hash", Just $ if isDash then ["r", "v"] else ["r"]),
("jobs", Just ["l", "p"]), ("jobs", Just ["l", "p"]),
("printf", Just []), ("printf", Just []),
("read", Just $ if isDash || isBusyboxSh then ["r", "p"] else ["r"]), ("read", Just $ if isDash then ["r", "p"] else ["r"]),
("readonly", Just ["p"]), ("readonly", Just ["p"]),
("trap", Just []), ("trap", Just []),
("type", Just $ if isBusyboxSh then ["p"] else []), ("type", Just []),
("ulimit", if isDash then Nothing else Just ["f"]), ("ulimit", if isDash then Nothing else Just ["f"]),
("umask", Just ["S"]), ("umask", Just ["S"]),
("unset", Just ["f", "v"]), ("unset", Just ["f", "v"]),
@ -497,50 +498,6 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
Assignment (_, _, name, _) -> name == var Assignment (_, _, name, _) -> name == var
_ -> False _ -> False
checkTestOp table op id = sequence_ $ do
(code, shells, msg) <- Map.lookup op table
guard . not $ shellType params `elem` shells
return $ warnMsg id code (msg op)
buildTestFlagMap list = Map.fromList $ concatMap (\(x,y) -> map (\c -> (c,y)) x) list
bashismBinaryTestFlags = buildTestFlagMap [
-- ([list of applicable flags],
-- (error code, exempt shells, message builder :: String -> String)),
--
-- Distinct error codes allow the wiki to give more helpful, targeted
-- information.
(["<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="],
(3012, [Dash, BusyboxSh], \op -> "lexicographical " ++ op ++ " is")),
(["=="],
(3014, [BusyboxSh], \op -> op ++ " in place of = is")),
(["=~"],
(3015, [], \op -> op ++ " regex matching is")),
([], (0,[],const ""))
]
bashismUnaryTestFlags = buildTestFlagMap [
(["-v"],
(3016, [], \op -> "test " ++ op ++ " (in place of [ -n \"${var+x}\" ]) is")),
(["-a"],
(3017, [], \op -> "unary " ++ op ++ " in place of -e is")),
(["-o"],
(3062, [], \op -> "test " ++ op ++ " to check options is")),
(["-R"],
(3063, [], \op -> "test " ++ op ++ " and namerefs in general are")),
(["-N"],
(3064, [], \op -> "test " ++ op ++ " is")),
(["-k"],
(3065, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")),
(["-G"],
(3066, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")),
(["-O"],
(3067, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")),
([], (0,[],const ""))
]
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')" prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
prop_checkEchoSed1b = verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")" prop_checkEchoSed1b = verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")"
prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')" prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
@ -680,22 +637,5 @@ checkBangAfterPipe = ForShell [Dash, BusyboxSh, Sh, Bash] f
err id 2326 "! is not allowed in the middle of pipelines. Use command group as in cmd | { ! cmd; } if necessary." err id 2326 "! is not allowed in the middle of pipelines. Use command group as in cmd | { ! cmd; } if necessary."
_ -> return () _ -> return ()
prop_checkNegatedUnaryOps1 = verify checkNegatedUnaryOps "[ ! -o braceexpand ]"
prop_checkNegatedUnaryOps2 = verifyNot checkNegatedUnaryOps "[ -o braceexpand ]"
prop_checkNegatedUnaryOps3 = verifyNot checkNegatedUnaryOps "[[ ! -o braceexpand ]]"
prop_checkNegatedUnaryOps4 = verifyNot checkNegatedUnaryOps "! [ -o braceexpand ]"
prop_checkNegatedUnaryOps5 = verify checkNegatedUnaryOps "[ ! -a file ]"
checkNegatedUnaryOps = ForShell [Bash] f
where
f token = case token of
TC_Unary id SingleBracket "!" (TC_Unary _ _ op _) | op `elem` ["-a", "-o"] ->
err id 2332 $ msg op
_ -> return ()
msg "-o" = "[ ! -o opt ] is always true because -o becomes logical OR. Use [[ ]] or ! [ -o opt ]."
msg "-a" = "[ ! -a file ] is always true because -a becomes logical AND. Use -e instead."
msg _ = pleaseReport "unhandled negated unary message"
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View file

@ -49,7 +49,6 @@ internalVariables = [
"LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH", "LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH",
"POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1", "POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1",
"PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR", "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR",
"BASH_MONOSECONDS", "BASH_TRAPSIG", "GLOBSORT",
"auto_resume", "histchars", "auto_resume", "histchars",
-- Other -- Other
@ -63,9 +62,6 @@ internalVariables = [
, "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP", , "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP",
"FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION", "FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION",
"flags_error", "flags_return" "flags_error", "flags_return"
-- Bats
,"stderr", "stderr_lines"
] ]
specialIntegerVariables = [ specialIntegerVariables = [
@ -79,7 +75,7 @@ variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
"EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM", "EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM",
"READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS", "READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS",
"SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE", "SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE",
"HISTSIZE", "LINES", "BASH_MONOSECONDS", "BASH_TRAPSIG" "HISTSIZE", "LINES"
-- shflags -- shflags
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE" , "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
@ -168,7 +164,6 @@ shellForExecutable name =
"ksh" -> return Ksh "ksh" -> return Ksh
"ksh88" -> return Ksh "ksh88" -> return Ksh
"ksh93" -> return Ksh "ksh93" -> return Ksh
"oksh" -> return Ksh
_ -> Nothing _ -> Nothing
flagsForRead = "sreu:n:N:i:p:a:t:" flagsForRead = "sreu:n:N:i:p:a:t:"

View file

@ -169,7 +169,7 @@ showFixedString color comments lineNum fileLines =
-- and/or other unrelated lines. -- and/or other unrelated lines.
let (excerptFix, excerpt) = sliceFile mergedFix fileLines let (excerptFix, excerpt) = sliceFile mergedFix fileLines
-- in the spirit of error prone -- in the spirit of error prone
putStrLn $ color "message" "Did you mean:" putStrLn $ color "message" "Did you mean: "
putStrLn $ unlines $ applyFix excerptFix excerpt putStrLn $ unlines $ applyFix excerptFix excerpt
cuteIndent :: PositionedComment -> String cuteIndent :: PositionedComment -> String

View file

@ -141,9 +141,15 @@ carriageReturn = do
parseProblemAt pos ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ." parseProblemAt pos ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ."
return '\r' return '\r'
almostSpace = do almostSpace =
parseNote ErrorC 1018 $ "This is a unicode space. Delete and retype it." choice [
oneOf "\xA0\x2002\x2003\x2004\x2005\x2006\x2007\x2008\x2009\x200B\x202F" check '\xA0' "unicode non-breaking space",
check '\x200B' "unicode zerowidth space"
]
where
check c name = do
parseNote ErrorC 1018 $ "This is a " ++ name ++ ". Delete and retype it."
char c
return ' ' return ' '
--------- Message/position annotation on top of user state --------- Message/position annotation on top of user state
@ -821,7 +827,7 @@ readArithmeticContents =
char ')' char ')'
id <- endSpan start id <- endSpan start
spacing spacing
return $ TA_Parenthesis id s return $ TA_Parentesis id s
readArithTerm = readGroup <|> readVariable <|> readExpansion readArithTerm = readGroup <|> readVariable <|> readExpansion
@ -2795,29 +2801,17 @@ readFunctionDefinition = called "function" $ do
prop_readCoProc1 = isOk readCoProc "coproc foo { echo bar; }" prop_readCoProc1 = isOk readCoProc "coproc foo { echo bar; }"
prop_readCoProc2 = isOk readCoProc "coproc { echo bar; }" prop_readCoProc2 = isOk readCoProc "coproc { echo bar; }"
prop_readCoProc3 = isOk readCoProc "coproc echo bar" prop_readCoProc3 = isOk readCoProc "coproc echo bar"
prop_readCoProc4 = isOk readCoProc "coproc a=b echo bar"
prop_readCoProc5 = isOk readCoProc "coproc 'foo' { echo bar; }"
prop_readCoProc6 = isOk readCoProc "coproc \"foo$$\" { echo bar; }"
prop_readCoProc7 = isOk readCoProc "coproc 'foo' ( echo bar )"
prop_readCoProc8 = isOk readCoProc "coproc \"foo$$\" while true; do true; done"
readCoProc = called "coproc" $ do readCoProc = called "coproc" $ do
start <- startSpan start <- startSpan
try $ do try $ do
string "coproc" string "coproc"
spacing1 whitespace
choice [ try $ readCompoundCoProc start, readSimpleCoProc start ] choice [ try $ readCompoundCoProc start, readSimpleCoProc start ]
where where
readCompoundCoProc start = do readCompoundCoProc start = do
notFollowedBy2 readAssignmentWord var <- optionMaybe $
(var, body) <- choice [ readVariableName `thenSkip` whitespace
try $ do body <- readBody readCompoundCommand
body <- readBody readCompoundCommand
return (Nothing, body),
try $ do
var <- readNormalWord `thenSkip` spacing
body <- readBody readCompoundCommand
return (Just var, body)
]
id <- endSpan start id <- endSpan start
return $ T_CoProc id var body return $ T_CoProc id var body
readSimpleCoProc start = do readSimpleCoProc start = do
@ -3387,8 +3381,7 @@ readScriptFile sourced = do
"busybox sh", "busybox sh",
"bash", "bash",
"bats", "bats",
"ksh", "ksh"
"oksh"
] ]
badShells = [ badShells = [
"awk", "awk",
@ -3397,7 +3390,6 @@ readScriptFile sourced = do
"fish", "fish",
"perl", "perl",
"python", "python",
"python3",
"ruby", "ruby",
"tcsh", "tcsh",
"zsh" "zsh"
@ -3450,22 +3442,13 @@ isOk p s = parsesCleanly p s == Just True -- The string parses with no wa
isWarning p s = parsesCleanly p s == Just False -- The string parses with warnings isWarning p s = parsesCleanly p s == Just False -- The string parses with warnings
isNotOk p s = parsesCleanly p s == Nothing -- The string does not parse isNotOk p s = parsesCleanly p s == Nothing -- The string does not parse
-- If the parser matches the string, return Right [ParseNotes+ParseProblems] parsesCleanly parser string = runIdentity $ do
-- If it does not match the string, return Left [ParseProblems] (res, sys) <- runParser testEnvironment
getParseOutput parser string = runIdentity $ do (parser >> eof >> getState) "-" string
(res, systemState) <- runParser testEnvironment case (res, sys) of
(parser >> eof >> getState) "-" string (Right userState, systemState) ->
return $ case res of return $ Just . null $ parseNotes userState ++ parseProblems systemState
Right userState -> (Left _, _) -> return Nothing
Right $ parseNotes userState ++ parseProblems systemState
Left _ -> Left $ parseProblems systemState
-- If the parser matches the string, return Just whether it was clean (without emitting suggestions)
-- Otherwise, Nothing
parsesCleanly parser string =
case getParseOutput parser string of
Right list -> Just $ null list
Left _ -> Nothing
parseWithNotes parser = do parseWithNotes parser = do
item <- parser item <- parser

View file

@ -12,17 +12,6 @@ then
fail "There are uncommitted changes" fail "There are uncommitted changes"
fi fi
version=${current#v}
if ! grep "Version:" ShellCheck.cabal | grep -qFw "$version"
then
fail "The cabal file does not match tag version $version"
fi
if ! grep -qF "## $current" CHANGELOG.md
then
fail "CHANGELOG.md does not contain '## $current'"
fi
current=$(git tag --points-at) current=$(git tag --points-at)
if [[ -z "$current" ]] if [[ -z "$current" ]]
then then
@ -45,30 +34,33 @@ then
fail "You are not on master" fail "You are not on master"
fi fi
version=${current#v}
if ! grep "Version:" ShellCheck.cabal | grep -qFw "$version"
then
fail "The cabal file does not match tag version $version"
fi
if ! grep -qF "## $current" CHANGELOG.md
then
fail "CHANGELOG.md does not contain '## $current'"
fi
if [[ $(git log -1 --pretty=%B) != "Stable version "* ]] if [[ $(git log -1 --pretty=%B) != "Stable version "* ]]
then then
fail "Expected git log message to be 'Stable version ...'" fail "Expected git log message to be 'Stable version ...'"
fi fi
if [[ $(git log -1 --pretty=%B) != *"CHANGELOG"* ]]
then
fail "Expected git log message to contain CHANGELOG"
fi
i=1 j=1 i=1 j=1
cat << EOF cat << EOF
Manual Checklist Manual Checklist
$((i++)). Make sure none of the automated checks above failed $((i++)). Make sure none of the automated checks above failed
$((i++)). Run \`build/build_builder build/*/\` to update all builder images.
$((j++)). \`build/run_builder dist-newstyle/sdist/ShellCheck-*.tar.gz build/*/\` to verify that they work.
$((j++)). \`for f in \$(cat build/*/tag); do docker push "\$f"; done\` to upload them.
$((i++)). Run test/distrotest to ensure that most distros can build OOTB.
$((i++)). Make sure GitHub Build currently passes: https://github.com/koalaman/shellcheck/actions $((i++)). Make sure GitHub Build currently passes: https://github.com/koalaman/shellcheck/actions
$((i++)). Make sure SnapCraft build currently works: https://build.snapcraft.io/user/koalaman $((i++)). Make sure SnapCraft build currently works: https://build.snapcraft.io/user/koalaman
$((i++)). Run test/distrotest to ensure that most distros can build OOTB.
$((i++)). Format and read over the manual for bad formatting and outdated info. $((i++)). Format and read over the manual for bad formatting and outdated info.
$((i++)). Make sure the Hackage package builds locally. $((i++)). Make sure the Hackage package builds.
Release Steps Release Steps

View file

@ -17,13 +17,13 @@ and is still highly experimental.
Make sure you're plugged in and have screen/tmux in place, Make sure you're plugged in and have screen/tmux in place,
then re-run with $0 --run to continue. then re-run with $0 --run to continue.
Also note that dist*/ and .stack-work/ will be deleted. Also note that dist* will be deleted.
EOF EOF
exit 0 exit 0
} }
echo "Deleting 'dist', 'dist-newstyle', and '.stack-work'..." echo "Deleting 'dist' and 'dist-newstyle'..."
rm -rf dist dist-newstyle .stack-work rm -rf dist dist-newstyle
execs=$(find . -name shellcheck) execs=$(find . -name shellcheck)
@ -74,12 +74,11 @@ fedora:latest dnf install -y cabal-install ghc-template-haskell-devel fi
archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
# Ubuntu LTS # Ubuntu LTS
ubuntu:24.04 apt-get update && apt-get install -y cabal-install
ubuntu:22.04 apt-get update && apt-get install -y cabal-install ubuntu:22.04 apt-get update && apt-get install -y cabal-install
ubuntu:20.04 apt-get update && apt-get install -y cabal-install ubuntu:20.04 apt-get update && apt-get install -y cabal-install
# Stack on Ubuntu LTS # Stack on Ubuntu LTS
ubuntu:24.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest ubuntu:22.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
EOF EOF
exit "$final" exit "$final"

View file

@ -15,7 +15,7 @@ die() { echo "$*" >&2; exit 1; }
command -v stack || command -v stack ||
die "stack is missing" die "stack is missing"
stack setup --allow-different-user || die "Failed to setup with default resolver" stack setup || die "Failed to setup with default resolver"
stack build --test || die "Failed to build/test with default resolver" stack build --test || die "Failed to build/test with default resolver"
# Nice to haves, but not necessary # Nice to haves, but not necessary