Compare commits

..

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

53 changed files with 625 additions and 1619 deletions

View file

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

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, 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/
@ -126,16 +103,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: Deploy environment: Deploy
steps: steps:
- name: Install Dependencies
run: |
sudo apt-get update
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:

1
.gitignore vendored
View file

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

View file

@ -3,10 +3,28 @@
# binaries previously built and deployed to GitHub. # binaries previously built and deployed to GitHub.
function multi_arch_docker::install_docker_buildx() { function multi_arch_docker::install_docker_buildx() {
# Install up-to-date version of docker, with buildx support.
local -r docker_apt_repo='https://download.docker.com/linux/ubuntu'
curl -fsSL "${docker_apt_repo}/gpg" | sudo apt-key add -
local -r os="$(lsb_release -cs)"
sudo add-apt-repository "deb [arch=amd64] $docker_apt_repo $os stable"
sudo apt-get update
sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce
# Enable docker daemon experimental support (for 'pull --platform').
local -r config='/etc/docker/daemon.json'
if [[ -e "$config" ]]; then
sudo sed -i -e 's/{/{ "experimental": true, /' "$config"
else
echo '{ "experimental": true }' | sudo tee "$config"
fi
sudo systemctl restart docker
# Install QEMU multi-architecture support for docker buildx. # Install QEMU multi-architecture support for docker buildx.
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
# Instantiate docker buildx builder with multi-architecture support. # Instantiate docker buildx builder with multi-architecture support.
export DOCKER_CLI_EXPERIMENTAL=enabled
docker buildx create --name mybuilder docker buildx create --name mybuilder
docker buildx use mybuilder docker buildx use mybuilder
# Start up buildx and verify that all is OK. # Start up buildx and verify that all is OK.
@ -80,7 +98,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

14
.snapsquid.conf Normal file
View file

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

View file

@ -1,47 +1,3 @@
## Git
### Added
- SC2327/SC2328: Warn about capturing the output of redirected commands.
- SC2329: Warn when (non-escaping) functions are never invoked.
- SC2330: Warn about unsupported glob matches with [[ .. ]] in BusyBox.
- SC2331: Suggest using standard -e instead of unary -a in tests.
- SC2332: Warn about `[ ! -o opt ]` being unconditionally true in Bash.
- SC3062: Warn about bashism `[ -o opt ]`.
- Precompiled binaries for Linux riscv64 (linux.riscv64)
### Changed
- SC2002 about Useless Use Of Cat is now disabled by default. It can be
re-enabled with `--enable=useless-use-of-cat` or equivalent directive.
- SC2015 about `A && B || C` no longer triggers when B is a test command.
- SC3012: Do not warn about `\<` and `\>` in test/[] as specified in POSIX.1-2024
### Fixed
- SC2218 about function use-before-define is now more accurate.
- SC2317 about unreachable commands is now less spammy for nested ones.
- SC2292, optional suggestion for [[ ]], now triggers for Busybox.
### Removed
- SC3013: removed since the operators `-ot/-nt/-ef` are specified in POSIX.1-2024
## v0.10.0 - 2024-03-07
### Added
- Precompiled binaries for macOS ARM64 (darwin.aarch64)
- Added support for busybox sh
- Added flag --rcfile to specify an rc file by name.
- Added `extended-analysis=true` directive to enable/disable dataflow analysis
(with a corresponding --extended-analysis flag).
- SC2324: Warn when x+=1 appends instead of increments
- SC2325: Warn about multiple `!`s in dash/sh.
- SC2326: Warn about `foo | ! bar` in bash/dash/sh.
- SC3012: Warn about lexicographic-compare bashism in test like in [ ]
- SC3013: Warn bashism `test _ -op/-nt/-ef _` like in [ ]
- SC3014: Warn bashism `test _ == _` like in [ ]
- SC3015: Warn bashism `test _ =~ _` like in [ ]
- SC3016: Warn bashism `test -v _` like in [ ]
- SC3017: Warn bashism `test -a _` like in [ ]
### Fixed
- source statements with here docs now work correctly
- "(Array.!): undefined array element" error should no longer occur
## v0.9.0 - 2022-12-12 ## v0.9.0 - 2022-12-12
### Added ### Added
- SC2316: Warn about 'local readonly foo' and similar (thanks, patrickxia!) - SC2316: Warn about 'local readonly foo' and similar (thanks, patrickxia!)

View file

@ -77,7 +77,7 @@ You can see ShellCheck suggestions directly in a variety of editors.
* Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck). * Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck).
* Pulsar Edit (former Atom), through [linter-shellcheck-pulsar](https://github.com/pulsar-cooperative/linter-shellcheck-pulsar). * Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
* VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck). * VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck).
@ -110,11 +110,8 @@ Services and platforms that have ShellCheck pre-installed and ready to use:
* [Codacy](https://www.codacy.com/) * [Codacy](https://www.codacy.com/)
* [Code Climate](https://codeclimate.com/) * [Code Climate](https://codeclimate.com/)
* [Code Factor](https://www.codefactor.io/) * [Code Factor](https://www.codefactor.io/)
* [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)
* [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)),
@ -196,12 +193,6 @@ On Windows (via [chocolatey](https://chocolatey.org/packages/shellcheck)):
C:\> choco install shellcheck C:\> choco install shellcheck
``` ```
Or Windows (via [winget](https://github.com/microsoft/winget-pkgs)):
```cmd
C:\> winget install --id koalaman.shellcheck
```
Or Windows (via [scoop](http://scoop.sh)): Or Windows (via [scoop](http://scoop.sh)):
```cmd ```cmd
@ -230,26 +221,17 @@ Using the [nix package manager](https://nixos.org/nix):
nix-env -iA nixpkgs.shellcheck nix-env -iA nixpkgs.shellcheck
``` ```
Using the [Flox package manager](https://flox.dev/)
```sh
flox install shellcheck
```
Alternatively, you can download pre-compiled binaries for the latest release here: Alternatively, you can download pre-compiled binaries for the latest release here:
* [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked) * [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
* [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked) * [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
* [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked) * [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)
* [macOS, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.aarch64.tar.xz)
* [macOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz) * [macOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz)
* [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip) * [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip)
or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
(including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds). (including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds).
There are currently no official binaries for Apple Silicon, but third party builds are available via
[ShellCheck for Visual Studio Code](https://github.com/vscode-shellcheck/shellcheck-binaries/releases).
Distro packages already come with a `man` page. If you are building from source, it can be installed with: Distro packages already come with a `man` page. If you are building from source, it can be installed with:
```console ```console
@ -317,6 +299,10 @@ Verify that `cabal` is installed and update its dependency list with
$ cabal install $ cabal install
Or if you intend to run the tests:
$ cabal install --enable-tests
This will compile ShellCheck and install it to your `~/.cabal/bin` directory. This will compile ShellCheck and install it to your `~/.cabal/bin` directory.
Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`): Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
@ -562,3 +548,4 @@ Happy ShellChecking!
* The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221). * The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221).
* ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)! * ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)!

View file

@ -1,5 +1,5 @@
Name: ShellCheck Name: ShellCheck
Version: 0.10.0 Version: 0.9.0
Synopsis: Shell script analysis tool Synopsis: Shell script analysis tool
License: GPL-3 License: GPL-3
License-file: LICENSE License-file: LICENSE
@ -46,21 +46,21 @@ library
semigroups semigroups
build-depends: build-depends:
-- The lower bounds are based on GHC 7.10.3 -- The lower bounds are based on GHC 7.10.3
-- The upper bounds are based on GHC 9.8.1 -- The upper bounds are based on GHC 9.4.3
aeson >= 1.4.0 && < 2.3, aeson >= 1.4.0 && < 2.2,
array >= 0.5.1 && < 0.6, array >= 0.5.1 && < 0.6,
base >= 4.8.0.0 && < 5, base >= 4.8.0.0 && < 5,
bytestring >= 0.10.6 && < 0.13, bytestring >= 0.10.6 && < 0.12,
containers >= 0.5.6 && < 0.8, containers >= 0.5.6 && < 0.7,
deepseq >= 1.4.1 && < 1.6, deepseq >= 1.4.1 && < 1.5,
Diff >= 0.4.0 && < 1.1, Diff >= 0.4.0 && < 0.5,
fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9), fgl >= 5.7.0 && < 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.3,
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.6,
-- getXdgDirectory from 1.2.3.0 -- getXdgDirectory from 1.2.3.0
directory >= 1.2.3 && < 1.4, directory >= 1.2.3 && < 1.4,

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

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

View file

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

View file

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

View file

@ -6,18 +6,15 @@ ENV TARGETNAME darwin.x86_64
# Build dependencies # Build dependencies
USER root USER root
ENV DEBIAN_FRONTEND noninteractive ENV DEBIAN_FRONTEND noninteractive
RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list RUN apt-get update && apt-get install -y ghc automake autoconf llvm curl
RUN apt-get update
RUN apt-get dist-upgrade -y
RUN apt-get install -y ghc automake autoconf llvm curl alex happy
# Build GHC # Build GHC
WORKDIR /ghc WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/9.2.5/ghc-9.2.5-src.tar.xz" | tar xJ --strip-components=1 RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1
RUN ./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
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/local/bin
# Due to an apparent cabal bug, we specify our options directly to cabal # 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

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

@ -6,29 +6,19 @@ ENV TARGETNAME linux.aarch64
# Build dependencies # Build dependencies
USER root USER root
ENV DEBIAN_FRONTEND noninteractive ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y ghc automake autoconf build-essential llvm curl qemu-user-static gcc-$TARGET
# These deps are from 20.04, because GHC's compiler/llvm support moves slowly
RUN apt-get update && apt-get install -y llvm gcc-$TARGET
# The rest are from 22.10
RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list
# Kinetic does not receive updates anymore, switch to last available
RUN sed -e 's/archive.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list
RUN sed -e 's/security.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list
RUN apt-get update && apt-get install -y ghc alex happy automake autoconf build-essential curl qemu-user-static
# Build GHC # 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/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" RUN ./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
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/local/bin
# Due to an apparent cabal bug, we specify our options directly to cabal # 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;--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

@ -56,13 +56,6 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
options are cumulative, but all the codes can be specified at once, options are cumulative, but all the codes can be specified at once,
comma-separated as a single argument. comma-separated as a single argument.
**--extended-analysis=true/false**
: Enable/disable Dataflow Analysis to identify more issues (default true). If
ShellCheck uses too much CPU/RAM when checking scripts with several
thousand lines of code, extended analysis can be disabled with this flag
or a directive. This flag overrides directives and rc files.
**-f** *FORMAT*, **--format=***FORMAT* **-f** *FORMAT*, **--format=***FORMAT*
: Specify the output format of shellcheck, which prints its results in the : Specify the output format of shellcheck, which prints its results in the
@ -78,11 +71,6 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
: Don't try to look for .shellcheckrc configuration files. : Don't try to look for .shellcheckrc configuration files.
**--rcfile** *RCFILE*
: Prefer the specified configuration file over searching for one
in the default locations.
**-o**\ *NAME1*[,*NAME2*...],\ **--enable=***NAME1*[,*NAME2*...] **-o**\ *NAME1*[,*NAME2*...],\ **--enable=***NAME1*[,*NAME2*...]
: Enable optional checks. The special name *all* enables all of them. : Enable optional checks. The special name *all* enables all of them.
@ -97,8 +85,7 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
**-s**\ *shell*,\ **--shell=***shell* **-s**\ *shell*,\ **--shell=***shell*
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash*, *ksh*, : Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
and *busybox*.
The default is to deduce the shell from the file's `shell` directive, The default is to deduce the shell from the file's `shell` directive,
shebang, or `.bash/.bats/.dash/.ksh` extension, in that order. *sh* refers to shebang, or `.bash/.bats/.dash/.ksh` extension, in that order. *sh* refers to
POSIX `sh` (not the system's), and will warn of portability issues. POSIX `sh` (not the system's), and will warn of portability issues.
@ -256,12 +243,6 @@ Valid keys are:
: Enable an optional check by name, as listed with **--list-optional**. : Enable an optional check by name, as listed with **--list-optional**.
Only file-wide `enable` directives are considered. Only file-wide `enable` directives are considered.
**extended-analysis**
: Set to true/false to enable/disable dataflow analysis. Specifying
`# shellcheck extended-analysis=false` in particularly large (2000+ line)
auto-generated scripts will reduce ShellCheck's resource usage at the
expense of certain checks. Extended analysis is enabled by default.
**external-sources** **external-sources**
: Set to `true` in `.shellcheckrc` to always allow ShellCheck to open : Set to `true` in `.shellcheckrc` to always allow ShellCheck to open
arbitrary files from 'source' statements (the way most tools do). arbitrary files from 'source' statements (the way most tools do).
@ -317,7 +298,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.
@ -397,10 +378,10 @@ long list of wonderful contributors.
# COPYRIGHT # COPYRIGHT
Copyright 2012-2024, Vidar Holen and contributors. Copyright 2012-2022, Vidar Holen and contributors.
Licensed under the GNU General Public License version 3 or later, Licensed under the GNU General Public License version 3 or later,
see https://gnu.org/licenses/gpl.html see https://gnu.org/licenses/gpl.html
# SEE ALSO # SEE ALSO
sh(1) bash(1) dash(1) ksh(1) sh(1) bash(1)

View file

@ -76,8 +76,7 @@ data Options = Options {
externalSources :: Bool, externalSources :: Bool,
sourcePaths :: [FilePath], sourcePaths :: [FilePath],
formatterOptions :: FormatterOptions, formatterOptions :: FormatterOptions,
minSeverity :: Severity, minSeverity :: Severity
rcfile :: Maybe FilePath
} }
defaultOptions = Options { defaultOptions = Options {
@ -87,8 +86,7 @@ defaultOptions = Options {
formatterOptions = newFormatterOptions { formatterOptions = newFormatterOptions {
foColorOption = ColorAuto foColorOption = ColorAuto
}, },
minSeverity = StyleC, minSeverity = StyleC
rcfile = Nothing
} }
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..." usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
@ -102,8 +100,6 @@ options = [
(ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings", (ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings",
Option "e" ["exclude"] Option "e" ["exclude"]
(ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings", (ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings",
Option "" ["extended-analysis"]
(ReqArg (Flag "extended-analysis") "bool") "Perform dataflow analysis (default true)",
Option "f" ["format"] Option "f" ["format"]
(ReqArg (Flag "format") "FORMAT") $ (ReqArg (Flag "format") "FORMAT") $
"Output format (" ++ formatList ++ ")", "Output format (" ++ formatList ++ ")",
@ -111,9 +107,6 @@ options = [
(NoArg $ Flag "list-optional" "true") "List checks disabled by default", (NoArg $ Flag "list-optional" "true") "List checks disabled by default",
Option "" ["norc"] Option "" ["norc"]
(NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc files", (NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc files",
Option "" ["rcfile"]
(ReqArg (Flag "rcfile") "RCFILE")
"Prefer the specified configuration file over searching for one",
Option "o" ["enable"] Option "o" ["enable"]
(ReqArg (Flag "enable") "check1,check2..") (ReqArg (Flag "enable") "check1,check2..")
"List of optional checks to enable (or 'all')", "List of optional checks to enable (or 'all')",
@ -122,7 +115,7 @@ options = [
"Specify path when looking for sourced files (\"SCRIPTDIR\" for script's dir)", "Specify path when looking for sourced files (\"SCRIPTDIR\" for script's dir)",
Option "s" ["shell"] Option "s" ["shell"]
(ReqArg (Flag "shell") "SHELLNAME") (ReqArg (Flag "shell") "SHELLNAME")
"Specify dialect (sh, bash, dash, ksh, busybox)", "Specify dialect (sh, bash, dash, ksh)",
Option "S" ["severity"] Option "S" ["severity"]
(ReqArg (Flag "severity") "SEVERITY") (ReqArg (Flag "severity") "SEVERITY")
"Minimum severity of errors to consider (error, warning, info, style)", "Minimum severity of errors to consider (error, warning, info, style)",
@ -259,9 +252,9 @@ runFormatter sys format options files = do
else SomeProblems else SomeProblems
parseEnum name value list = parseEnum name value list =
case lookup value list of case filter ((== value) . fst) list of
Just value -> return value [(name, value)] -> return value
Nothing -> do [] -> do
printErr $ "Unknown value for --" ++ name ++ ". " ++ printErr $ "Unknown value for --" ++ name ++ ". " ++
"Valid options are: " ++ (intercalate ", " $ map fst list) "Valid options are: " ++ (intercalate ", " $ map fst list)
throwError SupportFailure throwError SupportFailure
@ -374,11 +367,6 @@ parseOption flag options =
} }
} }
Flag "rcfile" str -> do
return options {
rcfile = Just str
}
Flag "enable" value -> Flag "enable" value ->
let cs = checkSpec options in return options { let cs = checkSpec options in return options {
checkSpec = cs { checkSpec = cs {
@ -386,14 +374,6 @@ parseOption flag options =
} }
} }
Flag "extended-analysis" str -> do
value <- parseBool str
return options {
checkSpec = (checkSpec options) {
csExtendedAnalysis = Just value
}
}
-- This flag is handled specially in 'process' -- This flag is handled specially in 'process'
Flag "format" _ -> return options Flag "format" _ -> return options
@ -411,20 +391,12 @@ parseOption flag options =
throwError SyntaxFailure throwError SyntaxFailure
return (Prelude.read num :: Integer) return (Prelude.read num :: Integer)
parseBool str = do
case str of
"true" -> return True
"false" -> return False
_ -> do
printErr $ "Invalid boolean, expected true/false: " ++ str
throwError SyntaxFailure
ioInterface :: Options -> [FilePath] -> IO (SystemInterface IO) ioInterface :: Options -> [FilePath] -> IO (SystemInterface IO)
ioInterface options files = do ioInterface options files = do
inputs <- mapM normalize files inputs <- mapM normalize files
cache <- newIORef emptyCache cache <- newIORef emptyCache
configCache <- newIORef ("", Nothing) configCache <- newIORef ("", Nothing)
return (newSystemInterface :: SystemInterface IO) { return SystemInterface {
siReadFile = get cache inputs, siReadFile = get cache inputs,
siFindSource = findSourceFile inputs (sourcePaths options), siFindSource = findSourceFile inputs (sourcePaths options),
siGetConfig = getConfig configCache siGetConfig = getConfig configCache
@ -469,23 +441,8 @@ ioInterface options files = do
fallback :: FilePath -> IOException -> IO FilePath fallback :: FilePath -> IOException -> IO FilePath
fallback path _ = return path fallback path _ = return path
-- Returns the name and contents of .shellcheckrc for the given file -- Returns the name and contents of .shellcheckrc for the given file
getConfig cache filename = getConfig cache filename = do
case rcfile options of
Just file -> do
-- We have a specified rcfile. Ignore normal rcfile resolution.
(path, result) <- readIORef cache
if path == "/"
then return result
else do
result <- readConfig file
when (isNothing result) $
hPutStrLn stderr $ "Warning: unable to read --rcfile " ++ file
writeIORef cache ("/", result)
return result
Nothing -> do
path <- normalize filename path <- normalize filename
let dir = takeDirectory path let dir = takeDirectory path
(previousPath, result) <- readIORef cache (previousPath, result) <- readIORef cache
@ -533,7 +490,7 @@ ioInterface options files = do
where where
handler :: FilePath -> IOException -> IO (String, Bool) handler :: FilePath -> IOException -> IO (String, Bool)
handler file err = do handler file err = do
hPutStrLn stderr $ file ++ ": " ++ show err putStrLn $ file ++ ": " ++ show err
return ("", True) return ("", True)
andM a b arg = do andM a b arg = do

View file

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

View file

@ -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
@ -152,7 +152,6 @@ data Annotation =
| ShellOverride String | ShellOverride String
| SourcePath String | SourcePath String
| ExternalSources Bool | ExternalSources Bool
| ExtendedAnalysis Bool
deriving (Show, Eq) deriving (Show, Eq)
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq) data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
@ -206,7 +205,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 +258,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

@ -31,7 +31,6 @@ import Data.Functor
import Data.Functor.Identity import Data.Functor.Identity
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map import qualified Data.Map as Map
import Numeric (showHex) import Numeric (showHex)
@ -158,10 +157,9 @@ isFlag token =
_ -> False _ -> False
-- Is this token a flag where the - is unquoted? -- Is this token a flag where the - is unquoted?
isUnquotedFlag token = isUnquotedFlag token = fromMaybe False $ do
case getLeadingUnquotedString token of str <- getLeadingUnquotedString token
Just ('-':_) -> True return $ "-" `isPrefixOf` str
_ -> False
-- getGnuOpts "erd:u:" will parse a list of arguments tokens like `read` -- getGnuOpts "erd:u:" will parse a list of arguments tokens like `read`
-- -re -d : -u 3 bar -- -re -d : -u 3 bar
@ -446,12 +444,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
@ -766,8 +758,8 @@ prop_executableFromShebang6 = executableFromShebang "/usr/bin/env --split-string
prop_executableFromShebang7 = executableFromShebang "/usr/bin/env --split-string bash -x" == "bash" prop_executableFromShebang7 = executableFromShebang "/usr/bin/env --split-string bash -x" == "bash"
prop_executableFromShebang8 = executableFromShebang "/usr/bin/env --split-string foo=bar bash -x" == "bash" prop_executableFromShebang8 = executableFromShebang "/usr/bin/env --split-string foo=bar bash -x" == "bash"
prop_executableFromShebang9 = executableFromShebang "/usr/bin/env foo=bar dash" == "dash" prop_executableFromShebang9 = executableFromShebang "/usr/bin/env foo=bar dash" == "dash"
prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "busybox sh" prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "ash"
prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "busybox ash" prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "ash"
-- Get the shell executable from a string like '/usr/bin/env bash' -- Get the shell executable from a string like '/usr/bin/env bash'
executableFromShebang :: String -> String executableFromShebang :: String -> String
@ -784,8 +776,7 @@ executableFromShebang = shellFor
[x] -> basename x [x] -> basename x
(first:second:args) | basename first == "busybox" -> (first:second:args) | basename first == "busybox" ->
case basename second of case basename second of
"sh" -> "busybox sh" "sh" -> "ash" -- busybox sh is ash
"ash" -> "busybox ash"
x -> x x -> x
(first:args) | basename first == "env" -> (first:args) | basename first == "env" ->
fromEnvArgs args fromEnvArgs args
@ -865,7 +856,8 @@ getBracedModifier s = headOrDefault "" $ do
-- Get the variables from indices like ["x", "y"] in ${var[x+y+1]} -- Get the variables from indices like ["x", "y"] in ${var[x+y+1]}
prop_getIndexReferences1 = getIndexReferences "var[x+y+1]" == ["x", "y"] prop_getIndexReferences1 = getIndexReferences "var[x+y+1]" == ["x", "y"]
getIndexReferences s = fromMaybe [] $ do getIndexReferences s = fromMaybe [] $ do
index:_ <- matchRegex re s match <- matchRegex re s
index <- match !!! 0
return $ matchAllStrings variableNameRegex index return $ matchAllStrings variableNameRegex index
where where
re = mkRegex "(\\[.*\\])" re = mkRegex "(\\[.*\\])"
@ -876,7 +868,8 @@ prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"] prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
getOffsetReferences mods = fromMaybe [] $ do getOffsetReferences mods = fromMaybe [] $ do
-- if mods start with [, then drop until ] -- if mods start with [, then drop until ]
_:offsets:_ <- matchRegex re mods match <- matchRegex re mods
offsets <- match !!! 1
return $ matchAllStrings variableNameRegex offsets return $ matchAllStrings variableNameRegex offsets
where where
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)" re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
@ -893,17 +886,11 @@ isUnmodifiedParameterExpansion t =
in getBracedReference str == str in getBracedReference str == str
_ -> False _ -> False
-- Return the referenced variable if (and only if) it's an unmodified parameter expansion.
getUnmodifiedParameterExpansion t =
case t of
T_DollarBraced _ _ list -> do
let str = concat $ oversimplify list
guard $ getBracedReference str == str
return str
_ -> Nothing
--- A list of the element and all its parents up to the root node. --- A list of the element and all its parents up to the root node.
getPath tree = NE.unfoldr $ \t -> (t, Map.lookup (getId t) tree) getPath tree t = t :
case Map.lookup (getId t) tree of
Nothing -> []
Just parent -> getPath tree parent
isClosingFileOp op = isClosingFileOp op =
case op of case op of
@ -916,11 +903,5 @@ getEnableDirectives root =
T_Annotation _ list _ -> [s | EnableComment s <- list] T_Annotation _ list _ -> [s | EnableComment s <- list]
_ -> [] _ -> []
getExtendedAnalysisDirective :: Token -> Maybe Bool
getExtendedAnalysisDirective root =
case root of
T_Annotation _ list _ -> listToMaybe $ [s | ExtendedAnalysis s <- list]
_ -> Nothing
return [] return []
runTests = $quickCheckAll runTests = $quickCheckAll

File diff suppressed because it is too large Load diff

View file

@ -41,7 +41,6 @@ import Data.Char
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import Data.Semigroup import Data.Semigroup
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map import qualified Data.Map as Map
import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.All (forAllProperties)
@ -89,8 +88,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
@ -106,7 +103,7 @@ data Parameters = Parameters {
-- map from token id to start and end position -- map from token id to start and end position
tokenPositions :: Map.Map Id (Position, Position), tokenPositions :: Map.Map Id (Position, Position),
-- Result from Control Flow Graph analysis (including data flow analysis) -- Result from Control Flow Graph analysis (including data flow analysis)
cfgAnalysis :: Maybe CF.CFGAnalysis cfgAnalysis :: CF.CFGAnalysis
} deriving (Show) } deriving (Show)
-- TODO: Cache results of common AST ops here -- TODO: Cache results of common AST ops here
@ -199,10 +196,8 @@ makeCommentWithFix severity id code str fix =
} }
in force withFix in force withFix
-- makeParameters :: CheckSpec -> Parameters
makeParameters spec = params makeParameters spec = params
where where
extendedAnalysis = fromMaybe True $ msum [asExtendedAnalysis spec, getExtendedAnalysisDirective root]
params = Parameters { params = Parameters {
rootNode = root, rootNode = root,
shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec, shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec,
@ -211,35 +206,26 @@ makeParameters spec = params
case shellType params of case shellType params of
Bash -> isOptionSet "lastpipe" root Bash -> isOptionSet "lastpipe" root
Dash -> False Dash -> False
BusyboxSh -> False
Sh -> False Sh -> False
Ksh -> True, Ksh -> True,
hasInheritErrexit = hasInheritErrexit =
case shellType params of case shellType params of
Bash -> isOptionSet "inherit_errexit" root Bash -> isOptionSet "inherit_errexit" root
Dash -> True Dash -> True
BusyboxSh -> True
Sh -> True Sh -> True
Ksh -> False, Ksh -> False,
hasPipefail = hasPipefail =
case shellType params of case shellType params of
Bash -> isOptionSet "pipefail" root Bash -> isOptionSet "pipefail" root
Dash -> True Dash -> True
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,
variableFlow = getVariableFlow params root, variableFlow = getVariableFlow params root,
tokenPositions = asTokenPositions spec, tokenPositions = asTokenPositions spec,
cfgAnalysis = do cfgAnalysis = CF.analyzeControlFlow cfParams root
guard extendedAnalysis
return $ CF.analyzeControlFlow cfParams root
} }
cfParams = CF.CFGParameters { cfParams = CF.CFGParameters {
CF.cfLastpipe = hasLastpipe params, CF.cfLastpipe = hasLastpipe params,
@ -298,8 +284,8 @@ prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh
prop_determineShell9 = determineShellTest "#!/bin/env -S dash -x" == Dash prop_determineShell9 = determineShellTest "#!/bin/env -S dash -x" == Dash
prop_determineShell10 = determineShellTest "#!/bin/env --split-string= dash -x" == Dash prop_determineShell10 = determineShellTest "#!/bin/env --split-string= dash -x" == Dash
prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == BusyboxSh -- busybox sh is a specific shell, not posix sh prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == Dash -- busybox sh is a specific shell, not posix sh
prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == BusyboxSh prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == Dash
determineShellTest = determineShellTest' Nothing determineShellTest = determineShellTest' Nothing
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
@ -347,12 +333,12 @@ isQuoteFree = isQuoteFreeNode False
isQuoteFreeNode strict shell tree t = isQuoteFreeNode strict shell tree t =
isQuoteFreeElement t || isQuoteFreeElement t ||
(fromMaybe False $ msum $ map isQuoteFreeContext $ NE.tail $ getPath tree t) (fromMaybe False $ msum $ map isQuoteFreeContext $ drop 1 $ getPath tree t)
where where
-- Is this node self-quoting in itself? -- Is this node self-quoting in itself?
isQuoteFreeElement t = isQuoteFreeElement t =
case t of case t of
T_Assignment id _ _ _ _ -> assignmentIsQuoting id T_Assignment {} -> assignmentIsQuoting t
T_FdRedirect {} -> True T_FdRedirect {} -> True
_ -> False _ -> False
@ -364,7 +350,7 @@ isQuoteFreeNode strict shell tree t =
TC_Binary _ DoubleBracket _ _ _ -> return True TC_Binary _ DoubleBracket _ _ _ -> return True
TA_Sequence {} -> return True TA_Sequence {} -> return True
T_Arithmetic {} -> return True T_Arithmetic {} -> return True
T_Assignment id _ _ _ _ -> return $ assignmentIsQuoting id T_Assignment {} -> return $ assignmentIsQuoting t
T_Redirecting {} -> return False T_Redirecting {} -> return False
T_DoubleQuoted _ _ -> return True T_DoubleQuoted _ _ -> return True
T_DollarDoubleQuoted _ _ -> return True T_DollarDoubleQuoted _ _ -> return True
@ -379,11 +365,11 @@ isQuoteFreeNode strict shell tree t =
-- Check whether this assignment is self-quoting due to being a recognized -- Check whether this assignment is self-quoting due to being a recognized
-- assignment passed to a Declaration Utility. This will soon be required -- assignment passed to a Declaration Utility. This will soon be required
-- by POSIX: https://austingroupbugs.net/view.php?id=351 -- by POSIX: https://austingroupbugs.net/view.php?id=351
assignmentIsQuoting id = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand id) assignmentIsQuoting t = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand t)
shellParsesParamsAsAssignments = shell /= Sh shellParsesParamsAsAssignments = shell /= Sh
-- Is this assignment a parameter to a command like export/typeset/etc? -- Is this assignment a parameter to a command like export/typeset/etc?
isAssignmentParamToCommand id = isAssignmentParamToCommand (T_Assignment id _ _ _ _) =
case Map.lookup id tree of case Map.lookup id tree of
Just (T_SimpleCommand _ _ (_:args)) -> id `elem` (map getId args) Just (T_SimpleCommand _ _ (_:args)) -> id `elem` (map getId args)
_ -> False _ -> False
@ -409,7 +395,7 @@ isParamTo tree cmd =
-- Get the parent command (T_Redirecting) of a Token, if any. -- Get the parent command (T_Redirecting) of a Token, if any.
getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token
getClosestCommand tree t = getClosestCommand tree t =
findFirst findCommand $ NE.toList $ getPath tree t findFirst findCommand $ getPath tree t
where where
findCommand t = findCommand t =
case t of case t of
@ -423,7 +409,7 @@ getClosestCommandM t = do
return $ getClosestCommand (parentMap params) t return $ getClosestCommand (parentMap params) t
-- Is the token used as a command name (the first word in a T_SimpleCommand)? -- Is the token used as a command name (the first word in a T_SimpleCommand)?
usedAsCommandName tree token = go (getId token) (NE.tail $ getPath tree token) usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
where where
go currentId (T_NormalWord id [word]:rest) go currentId (T_NormalWord id [word]:rest)
| currentId == getId word = go id rest | currentId == getId word = go id rest
@ -440,9 +426,7 @@ getPathM t = do
return $ getPath (parentMap params) t return $ getPath (parentMap params) t
isParentOf tree parent child = isParentOf tree parent child =
any (\t -> parentId == getId t) (getPath tree child) elem (getId parent) . map getId $ getPath tree child
where
parentId = getId parent
parents params = getPath (parentMap params) parents params = getPath (parentMap params)
@ -541,9 +525,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 +547,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)]
@ -832,7 +810,7 @@ getReferencedVariables parents t =
return (context, token, getBracedReference str) return (context, token, getBracedReference str)
isArithmeticAssignment t = case getPath parents t of isArithmeticAssignment t = case getPath parents t of
this NE.:| TA_Assignment _ "=" lhs _ :_ -> lhs == t this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
_ -> False _ -> False
isDereferencingBinaryOp = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"]) isDereferencingBinaryOp = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
@ -914,6 +892,15 @@ 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
Sh -> False
isTrueAssignmentSource c = isTrueAssignmentSource c =
case c of case c of
DataString SourceChecked -> False DataString SourceChecked -> False
@ -931,14 +918,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

@ -51,7 +51,6 @@ import Control.Monad.Identity
import Data.Array.Unboxed import Data.Array.Unboxed
import Data.Array.ST import Data.Array.ST
import Data.List hiding (map) import Data.List hiding (map)
import qualified Data.List.NonEmpty as NE
import Data.Maybe import Data.Maybe
import qualified Data.Map as M import qualified Data.Map as M
import qualified Data.Set as S import qualified Data.Set as S
@ -112,8 +111,8 @@ data CFEdge =
-- Actions we track -- Actions we track
data CFEffect = data CFEffect =
CFSetProps (Maybe Scope) String (S.Set CFVariableProp) CFSetProps Scope String (S.Set CFVariableProp)
| CFUnsetProps (Maybe Scope) String (S.Set CFVariableProp) | CFUnsetProps Scope String (S.Set CFVariableProp)
| CFReadVariable String | CFReadVariable String
| CFWriteVariable String CFValue | CFWriteVariable String CFValue
| CFWriteGlobal String CFValue | CFWriteGlobal String CFValue
@ -193,7 +192,7 @@ buildGraph params root =
base base
idToRange = M.fromList mapping idToRange = M.fromList mapping
isRealEdge (from, to, edge) = case edge of CFEFlow -> True; CFEExit -> True; _ -> False isRealEdge (from, to, edge) = case edge of CFEFlow -> True; _ -> False
onlyRealEdges = filter isRealEdge edges onlyRealEdges = filter isRealEdge edges
(_, mainExit) = fromJust $ M.lookup (getId root) idToRange (_, mainExit) = fromJust $ M.lookup (getId root) idToRange
@ -295,19 +294,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 +316,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 +494,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
@ -574,7 +578,7 @@ build t = do
T_Array _ list -> sequentially list T_Array _ list -> sequentially list
T_Assignment {} -> buildAssignment Nothing t T_Assignment {} -> buildAssignment DefaultScope t
T_Backgrounded id body -> do T_Backgrounded id body -> do
start <- newStructuralNode start <- newStructuralNode
@ -610,15 +614,15 @@ build t = do
T_CaseExpression id t [] -> build t T_CaseExpression id t [] -> build t
T_CaseExpression id t list@(hd:tl) -> do T_CaseExpression id t list -> do
start <- newStructuralNode start <- newStructuralNode
token <- build t token <- build t
branches <- mapM buildBranch (hd NE.:| tl) branches <- mapM buildBranch list
end <- newStructuralNode end <- newStructuralNode
let neighbors = zip (NE.toList branches) $ NE.tail branches let neighbors = zip branches $ tail branches
let (_, firstCond, _) = NE.head branches let (_, firstCond, _) = head branches
let (_, lastCond, lastBody) = NE.last branches let (_, lastCond, lastBody) = last branches
linkRange start token linkRange start token
linkRange token firstCond linkRange token firstCond
@ -668,18 +672,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
@ -861,8 +857,8 @@ build t = do
status <- newNodeRange (CFSetExitCode id) status <- newNodeRange (CFSetExitCode id)
linkRange assignments status linkRange assignments status
T_SimpleCommand id vars (cmd:args) -> T_SimpleCommand id vars list@(cmd:_) ->
handleCommand t vars (cmd NE.:| args) $ getUnquotedLiteral cmd handleCommand t vars list $ getUnquotedLiteral cmd
T_SingleQuoted _ _ -> none T_SingleQuoted _ _ -> none
@ -891,9 +887,7 @@ build t = do
T_Less _ -> none T_Less _ -> none
T_ParamSubSpecialChar _ _ -> none T_ParamSubSpecialChar _ _ -> none
x -> do x -> error ("Unimplemented: " ++ show x)
error ("Unimplemented: " ++ show x) -- STRIP
none
-- Still in `where` clause -- Still in `where` clause
forInHelper id name words body = do forInHelper id name words body = do
@ -929,8 +923,8 @@ handleCommand cmd vars args literalCmd = do
-- TODO: Handle assignments in declaring commands -- TODO: Handle assignments in declaring commands
case literalCmd of case literalCmd of
Just "exit" -> regularExpansion vars (NE.toList args) $ handleExit Just "exit" -> regularExpansion vars args $ handleExit
Just "return" -> regularExpansion vars (NE.toList args) $ handleReturn Just "return" -> regularExpansion vars args $ handleReturn
Just "unset" -> regularExpansionWithStatus vars args $ handleUnset args Just "unset" -> regularExpansionWithStatus vars args $ handleUnset args
Just "declare" -> handleDeclare args Just "declare" -> handleDeclare args
@ -953,14 +947,14 @@ handleCommand cmd vars args literalCmd = do
-- This will mostly behave like 'command' but ok -- This will mostly behave like 'command' but ok
Just "builtin" -> Just "builtin" ->
case args of case args of
_ NE.:| [] -> regular [_] -> regular
(_ NE.:| newcmd:newargs) -> (_:newargs@(newcmd:_)) ->
handleCommand newcmd vars (newcmd NE.:| newargs) $ getLiteralString newcmd handleCommand newcmd vars newargs $ getLiteralString newcmd
Just "command" -> Just "command" ->
case args of case args of
_ NE.:| [] -> regular [_] -> regular
(_ NE.:| newcmd:newargs) -> (_:newargs@(newcmd:_)) ->
handleOthers (getId newcmd) vars (newcmd NE.:| newargs) $ getLiteralString newcmd handleOthers (getId newcmd) vars newargs $ getLiteralString newcmd
_ -> regular _ -> regular
where where
@ -988,7 +982,7 @@ handleCommand cmd vars args literalCmd = do
unreachable <- newNode CFUnreachable unreachable <- newNode CFUnreachable
return $ Range ret unreachable return $ Range ret unreachable
handleUnset (cmd NE.:| args) = do handleUnset (cmd:args) = do
case () of case () of
_ | "n" `elem` flagNames -> unsetWith CFUndefineNameref _ | "n" `elem` flagNames -> unsetWith CFUndefineNameref
_ | "v" `elem` flagNames -> unsetWith CFUndefineVariable _ | "v" `elem` flagNames -> unsetWith CFUndefineVariable
@ -1000,14 +994,14 @@ handleCommand cmd vars args literalCmd = do
(names, flags) = partition (null . fst) pairs (names, flags) = partition (null . fst) pairs
flagNames = map fst flags flagNames = map fst flags
literalNames :: [(Token, String)] -- Literal names to unset, e.g. [(myfuncToken, "myfunc")] literalNames :: [(Token, String)] -- Literal names to unset, e.g. [(myfuncToken, "myfunc")]
literalNames = mapMaybe (\(_, t) -> (,) t <$> getLiteralString t) names literalNames = mapMaybe (\(_, t) -> getLiteralString t >>= (return . (,) t)) names
-- Apply a constructor like CFUndefineVariable to each literalName, and tag with its id -- Apply a constructor like CFUndefineVariable to each literalName, and tag with its id
unsetWith c = newNodeRange $ CFApplyEffects $ map (\(token, name) -> IdTagged (getId token) $ c name) literalNames unsetWith c = newNodeRange $ CFApplyEffects $ map (\(token, name) -> IdTagged (getId token) $ c name) literalNames
variableAssignRegex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)=" variableAssignRegex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)="
handleDeclare (cmd NE.:| args) = do handleDeclare (cmd:args) = do
isFunc <- asks cfIsFunction isFunc <- asks cfIsFunction
-- This is a bit of a kludge: we don't have great support for things like -- This is a bit of a kludge: we don't have great support for things like
-- 'declare -i x=$x' so do one round with declare x=$x, followed by declare -i x -- 'declare -i x=$x' so do one round with declare x=$x, followed by declare -i x
@ -1034,9 +1028,9 @@ handleCommand cmd vars args literalCmd = do
scope isFunc = scope isFunc =
case () of case () of
_ | global -> Just GlobalScope _ | global -> GlobalScope
_ | isFunc -> Just LocalScope _ | isFunc -> LocalScope
_ -> Nothing _ -> DefaultScope
addedProps = S.fromList $ concat $ [ addedProps = S.fromList $ concat $ [
[ CFVPArray | array ], [ CFVPArray | array ],
@ -1064,7 +1058,7 @@ handleCommand cmd vars args literalCmd = do
let let
id = getId t id = getId t
pre = [t] pre = [t]
literal = getLiteralStringDef "\0" t literal = fromJust $ getLiteralStringExt (const $ Just "\0") t
isKnown = '\0' `notElem` literal isKnown = '\0' `notElem` literal
match = fmap head $ variableAssignRegex `matchRegex` literal match = fmap head $ variableAssignRegex `matchRegex` literal
name = fromMaybe literal match name = fromMaybe literal match
@ -1096,7 +1090,7 @@ handleCommand cmd vars args literalCmd = do
in in
concatMap (drop 1) plusses concatMap (drop 1) plusses
handlePrintf (cmd NE.:| args) = handlePrintf (cmd:args) =
newNodeRange $ CFApplyEffects $ maybeToList findVar newNodeRange $ CFApplyEffects $ maybeToList findVar
where where
findVar = do findVar = do
@ -1105,7 +1099,7 @@ handleCommand cmd vars args literalCmd = do
name <- getLiteralString arg name <- getLiteralString arg
return $ IdTagged (getId arg) $ CFWriteVariable name CFValueString return $ IdTagged (getId arg) $ CFWriteVariable name CFValueString
handleWait (cmd NE.:| args) = handleWait (cmd:args) =
newNodeRange $ CFApplyEffects $ maybeToList findVar newNodeRange $ CFApplyEffects $ maybeToList findVar
where where
findVar = do findVar = do
@ -1114,7 +1108,7 @@ handleCommand cmd vars args literalCmd = do
name <- getLiteralString arg name <- getLiteralString arg
return $ IdTagged (getId arg) $ CFWriteVariable name CFValueInteger return $ IdTagged (getId arg) $ CFWriteVariable name CFValueInteger
handleMapfile (cmd NE.:| args) = handleMapfile (cmd:args) =
newNodeRange $ CFApplyEffects [findVar] newNodeRange $ CFApplyEffects [findVar]
where where
findVar = findVar =
@ -1134,7 +1128,7 @@ handleCommand cmd vars args literalCmd = do
guard $ isVariableName name guard $ isVariableName name
return (getId c, name) return (getId c, name)
handleRead (cmd NE.:| args) = newNodeRange $ CFApplyEffects main handleRead (cmd:args) = newNodeRange $ CFApplyEffects main
where where
main = fromMaybe fallback $ do main = fromMaybe fallback $ do
flags <- getGnuOpts flagsForRead args flags <- getGnuOpts flagsForRead args
@ -1164,7 +1158,7 @@ handleCommand cmd vars args literalCmd = do
in in
map (\(id, name) -> IdTagged id $ CFWriteVariable name value) namesOrDefault map (\(id, name) -> IdTagged id $ CFWriteVariable name value) namesOrDefault
handleDEFINE (cmd NE.:| args) = handleDEFINE (cmd:args) =
newNodeRange $ CFApplyEffects $ maybeToList findVar newNodeRange $ CFApplyEffects $ maybeToList findVar
where where
findVar = do findVar = do
@ -1174,14 +1168,14 @@ handleCommand cmd vars args literalCmd = do
return $ IdTagged (getId name) $ CFWriteVariable str CFValueString return $ IdTagged (getId name) $ CFWriteVariable str CFValueString
handleOthers id vars args cmd = handleOthers id vars args cmd =
regularExpansion vars (NE.toList args) $ do regularExpansion vars args $ do
exe <- newNodeRange $ CFExecuteCommand cmd exe <- newNodeRange $ CFExecuteCommand cmd
status <- newNodeRange $ CFSetExitCode id status <- newNodeRange $ CFSetExitCode id
linkRange exe status linkRange exe status
regularExpansion vars args p = do regularExpansion vars args p = do
args <- sequentially args args <- sequentially args
assignments <- mapM (buildAssignment (Just PrefixScope)) vars assignments <- mapM (buildAssignment PrefixScope) vars
exe <- p exe <- p
dropAssignments <- dropAssignments <-
if null vars if null vars
@ -1193,15 +1187,15 @@ handleCommand cmd vars args literalCmd = do
linkRanges $ [args] ++ assignments ++ [exe] ++ dropAssignments linkRanges $ [args] ++ assignments ++ [exe] ++ dropAssignments
regularExpansionWithStatus vars args@(cmd NE.:| _) p = do regularExpansionWithStatus vars args@(cmd:_) p = do
initial <- regularExpansion vars (NE.toList args) p initial <- regularExpansion vars args p
status <- newNodeRange $ CFSetExitCode (getId cmd) status <- newNodeRange $ CFSetExitCode (getId cmd)
linkRange initial status linkRange initial status
none = newStructuralNode none = newStructuralNode
data Scope = GlobalScope | LocalScope | PrefixScope data Scope = DefaultScope | GlobalScope | LocalScope | PrefixScope
deriving (Eq, Ord, Show, Generic, NFData) deriving (Eq, Ord, Show, Generic, NFData)
buildAssignment scope t = do buildAssignment scope t = do
@ -1215,10 +1209,10 @@ buildAssignment scope t = do
let valueType = if null indices then f id value else CFValueArray let valueType = if null indices then f id value else CFValueArray
let scoper = let scoper =
case scope of case scope of
Just PrefixScope -> CFWritePrefix PrefixScope -> CFWritePrefix
Just LocalScope -> CFWriteLocal LocalScope -> CFWriteLocal
Just GlobalScope -> CFWriteGlobal GlobalScope -> CFWriteGlobal
Nothing -> CFWriteVariable DefaultScope -> CFWriteVariable
write <- newNodeRange $ applySingle $ IdTagged id $ scoper var valueType write <- newNodeRange $ applySingle $ IdTagged id $ scoper var valueType
linkRanges [expand, index, read, write] linkRanges [expand, index, read, write]
where where
@ -1307,10 +1301,7 @@ findPostDominators mainexit graph = asArray
reversed = grev withExitEdges reversed = grev withExitEdges
postDoms = dom reversed mainexit postDoms = dom reversed mainexit
(_, maxNode) = nodeRange graph (_, maxNode) = nodeRange graph
-- Holes in the array cause "Exception: (Array.!): undefined array element" while asArray = array (0, maxNode) postDoms
-- inspecting/debugging, so fill the array first and then update.
initializedArray = listArray (0, maxNode) $ repeat []
asArray = initializedArray // postDoms
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View file

@ -59,8 +59,6 @@ module ShellCheck.CFGAnalysis (
,getIncomingState ,getIncomingState
,getOutgoingState ,getOutgoingState
,doesPostDominate ,doesPostDominate
,variableMayBeDeclaredInteger
,variableMayBeAssignedInteger
,ShellCheck.CFGAnalysis.runTests -- STRIP ,ShellCheck.CFGAnalysis.runTests -- STRIP
) where ) where
@ -133,7 +131,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
@ -155,20 +153,6 @@ doesPostDominate analysis target base = fromMaybe False $ do
(targetStart, _) <- M.lookup target $ tokenToRange analysis (targetStart, _) <- M.lookup target $ tokenToRange analysis
return $ targetStart `elem` (postDominators analysis ! baseEnd) return $ targetStart `elem` (postDominators analysis ! baseEnd)
-- See if any execution path results in the variable containing a state
variableMayHaveState :: ProgramState -> String -> CFVariableProp -> Maybe Bool
variableMayHaveState state var property = do
value <- M.lookup var $ variablesInScope state
return $ any (S.member property) $ variableProperties value
-- See if any execution path declares the variable an integer (declare -i).
variableMayBeDeclaredInteger state var = variableMayHaveState state var CFVPInteger
-- See if any execution path suggests the variable may contain an integer value
variableMayBeAssignedInteger state var = do
value <- M.lookup var $ variablesInScope state
return $ (numericalStatus $ variableValue value) >= NumericalStatusMaybe
getDataForNode analysis node = M.lookup node $ nodeToData analysis getDataForNode analysis node = M.lookup node $ nodeToData analysis
-- The current state of data flow at a point in the program, potentially as a diff -- The current state of data flow at a point in the program, potentially as a diff
@ -299,6 +283,7 @@ depsToState set = foldl insert newInternalState $ S.toList set
PrefixScope -> (sPrefixValues, insertPrefix) PrefixScope -> (sPrefixValues, insertPrefix)
LocalScope -> (sLocalValues, insertLocal) LocalScope -> (sLocalValues, insertLocal)
GlobalScope -> (sGlobalValues, insertGlobal) GlobalScope -> (sGlobalValues, insertGlobal)
DefaultScope -> error $ pleaseReport "Unresolved scope in dependency"
alreadyExists = isJust $ vmLookup name $ mapToCheck state alreadyExists = isJust $ vmLookup name $ mapToCheck state
in in
@ -672,7 +657,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.
@ -829,7 +814,7 @@ lookupStack' functionOnly get dep def ctx key = do
f (s:rest) = do f (s:rest) = do
-- Go up the stack until we find the value, and add -- Go up the stack until we find the value, and add
-- a dependency on each state (including where it was found) -- a dependency on each state (including where it was found)
res <- maybe (f rest) return (get (stackState s) key) res <- fromMaybe (f rest) (return <$> get (stackState s) key)
modifySTRef (dependencies s) $ S.insert $ dep key res modifySTRef (dependencies s) $ S.insert $ dep key res
return res return res
@ -1119,34 +1104,34 @@ transferEffect ctx effect =
CFSetProps scope name props -> CFSetProps scope name props ->
case scope of case scope of
Nothing -> do DefaultScope -> do
state <- readVariable ctx name state <- readVariable ctx name
writeVariable ctx name $ addProperties props state writeVariable ctx name $ addProperties props state
Just GlobalScope -> do GlobalScope -> do
state <- readGlobal ctx name state <- readGlobal ctx name
writeGlobal ctx name $ addProperties props state writeGlobal ctx name $ addProperties props state
Just LocalScope -> do LocalScope -> do
out <- readSTRef (cOutput ctx) out <- readSTRef (cOutput ctx)
state <- readLocal ctx name state <- readLocal ctx name
writeLocal ctx name $ addProperties props state writeLocal ctx name $ addProperties props state
Just PrefixScope -> do PrefixScope -> do
-- Prefix values become local -- Prefix values become local
state <- readLocal ctx name state <- readLocal ctx name
writeLocal ctx name $ addProperties props state writeLocal ctx name $ addProperties props state
CFUnsetProps scope name props -> CFUnsetProps scope name props ->
case scope of case scope of
Nothing -> do DefaultScope -> do
state <- readVariable ctx name state <- readVariable ctx name
writeVariable ctx name $ removeProperties props state writeVariable ctx name $ removeProperties props state
Just GlobalScope -> do GlobalScope -> do
state <- readGlobal ctx name state <- readGlobal ctx name
writeGlobal ctx name $ removeProperties props state writeGlobal ctx name $ removeProperties props state
Just LocalScope -> do LocalScope -> do
out <- readSTRef (cOutput ctx) out <- readSTRef (cOutput ctx)
state <- readLocal ctx name state <- readLocal ctx name
writeLocal ctx name $ removeProperties props state writeLocal ctx name $ removeProperties props state
Just PrefixScope -> do PrefixScope -> do
-- Prefix values become local -- Prefix values become local
state <- readLocal ctx name state <- readLocal ctx name
writeLocal ctx name $ removeProperties props state writeLocal ctx name $ removeProperties props state
@ -1286,7 +1271,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 +1335,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 +1358,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

@ -25,7 +25,6 @@ import ShellCheck.ASTLib
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Parser import ShellCheck.Parser
import Debug.Trace -- DO NOT SUBMIT
import Data.Either import Data.Either
import Data.Functor import Data.Functor
import Data.List import Data.List
@ -87,7 +86,6 @@ checkScript sys spec = do
asCheckSourced = csCheckSourced spec, asCheckSourced = csCheckSourced spec,
asExecutionMode = Executed, asExecutionMode = Executed,
asTokenPositions = tokenPositions, asTokenPositions = tokenPositions,
asExtendedAnalysis = csExtendedAnalysis spec,
asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec
} where as = newAnalysisSpec root } where as = newAnalysisSpec root
let analysisMessages = let analysisMessages =
@ -510,55 +508,5 @@ prop_rcCanSuppressEarlyProblems2 = null result
csScript = "!/bin/bash\necho 'hello world'" csScript = "!/bin/bash\necho 'hello world'"
} }
prop_sourceWithHereDocWorks = null result
where
result = checkWithIncludes [("bar", "true\n")] "source bar << eof\nlol\neof"
prop_hereDocsAreParsedWithoutTrailingLinefeed = 1044 `elem` result
where
result = check "cat << eof"
prop_hereDocsWillHaveParsedIndices = null result
where
result = check "#!/bin/bash\nmy_array=(a b)\ncat <<EOF >> ./test\n $(( 1 + my_array[1] ))\nEOF"
prop_rcCanSuppressDfa = null result
where
result = checkWithRc "extended-analysis=false" emptyCheckSpec {
csScript = "#!/bin/sh\nexit; foo;"
}
prop_fileCanSuppressDfa = null $ traceShowId result
where
result = checkWithRc "" emptyCheckSpec {
csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;"
}
prop_fileWinsWhenSuppressingDfa1 = null result
where
result = checkWithRc "extended-analysis=true" emptyCheckSpec {
csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;"
}
prop_fileWinsWhenSuppressingDfa2 = result == [2317]
where
result = checkWithRc "extended-analysis=false" emptyCheckSpec {
csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;"
}
prop_flagWinsWhenSuppressingDfa1 = result == [2317]
where
result = checkWithRc "extended-analysis=false" emptyCheckSpec {
csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;",
csExtendedAnalysis = Just True
}
prop_flagWinsWhenSuppressingDfa2 = null result
where
result = checkWithRc "extended-analysis=true" emptyCheckSpec {
csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;",
csExtendedAnalysis = Just False
}
return [] return []
runTests = $quickCheckAll runTests = $quickCheckAll

View file

@ -20,7 +20,6 @@
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE PatternGuards #-}
-- This module contains checks that examine specific commands by name. -- This module contains checks that examine specific commands by name.
module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where
@ -43,7 +42,6 @@ import Data.Functor.Identity
import qualified Data.Graph.Inductive.Graph as G import qualified Data.Graph.Inductive.Graph as G
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import qualified Data.Set as S import qualified Data.Set as S
import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.All (forAllProperties)
@ -183,13 +181,14 @@ checkCommand :: M.Map CommandName (Token -> Analysis) -> Token -> Analysis
checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do
name <- getLiteralString cmd name <- getLiteralString cmd
return $ return $
if | '/' `elem` name -> if '/' `elem` name
then
M.findWithDefault nullCheck (Basename $ basename name) map t M.findWithDefault nullCheck (Basename $ basename name) map t
| name == "builtin", (h:_) <- rest -> else if name == "builtin" && not (null rest) then
let t' = T_SimpleCommand id cmdPrefix rest let t' = T_SimpleCommand id cmdPrefix rest
selectedBuiltin = onlyLiteralString h selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest
in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t' in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
| otherwise -> do else do
M.findWithDefault nullCheck (Exactly name) map t M.findWithDefault nullCheck (Exactly name) map t
M.findWithDefault nullCheck (Basename name) map t M.findWithDefault nullCheck (Basename name) map t
@ -300,7 +299,7 @@ checkExpr = CommandCheck (Basename "expr") f where
"'expr' expects 3+ arguments but sees 1. Make sure each operator/operand is a separate argument, and escape <>&|." "'expr' expects 3+ arguments but sees 1. Make sure each operator/operand is a separate argument, and escape <>&|."
[first, second] | [first, second] |
onlyLiteralString first /= "length" (fromMaybe "" $ getLiteralString first) /= "length"
&& not (willSplit first || willSplit second) -> do && not (willSplit first || willSplit second) -> do
checkOp first checkOp first
warn (getId t) 2307 warn (getId t) 2307
@ -931,7 +930,7 @@ prop_checkTimedCommand2 = verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar
prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1" prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1"
checkTimedCommand = CommandCheck (Exactly "time") f where checkTimedCommand = CommandCheck (Exactly "time") f where
f (T_SimpleCommand _ _ (c:args@(_:_))) = f (T_SimpleCommand _ _ (c:args@(_:_))) =
whenShell [Sh, Dash, BusyboxSh] $ do whenShell [Sh, Dash] $ do
let cmd = last args -- "time" is parsed with a command as argument let cmd = last args -- "time" is parsed with a command as argument
when (isPiped cmd) $ when (isPiped cmd) $
warn (getId c) 2176 "'time' is undefined for pipelines. time single stage or bash -c instead." warn (getId c) 2176 "'time' is undefined for pipelines. time single stage or bash -c instead."
@ -955,7 +954,7 @@ checkTimedCommand = CommandCheck (Exactly "time") f where
prop_checkLocalScope1 = verify checkLocalScope "local foo=3" prop_checkLocalScope1 = verify checkLocalScope "local foo=3"
prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }" prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }"
checkLocalScope = CommandCheck (Exactly "local") $ \t -> checkLocalScope = CommandCheck (Exactly "local") $ \t ->
whenShell [Bash, Dash, BusyboxSh] $ do -- Ksh allows it, Sh doesn't support local whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local
path <- getPathM t path <- getPathM t
unless (any isFunctionLike path) $ unless (any isFunctionLike path) $
err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions." err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
@ -1006,8 +1005,8 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
sequence_ $ do sequence_ $ do
options <- getLiteralString arg1 options <- getLiteralString arg1
getoptsVar <- getLiteralString name getoptsVar <- getLiteralString name
(T_WhileExpression _ _ body) <- findFirst whileLoop (NE.toList path) (T_WhileExpression _ _ body) <- findFirst whileLoop path
T_CaseExpression id var list <- mapMaybe findCase body !!! 0 caseCmd@(T_CaseExpression _ var _) <- mapMaybe findCase body !!! 0
-- Make sure getopts name and case variable matches -- Make sure getopts name and case variable matches
[T_DollarBraced _ _ bracedWord] <- return $ getWordParts var [T_DollarBraced _ _ bracedWord] <- return $ getWordParts var
@ -1017,11 +1016,11 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
-- Make sure the variable isn't modified -- Make sure the variable isn't modified
guard . not $ modifiesVariable params (T_BraceGroup (Id 0) body) getoptsVar guard . not $ modifiesVariable params (T_BraceGroup (Id 0) body) getoptsVar
return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) id list return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) caseCmd
f _ = return () f _ = return ()
check :: Id -> [String] -> Id -> [(CaseType, [Token], [Token])] -> Analysis check :: Id -> [String] -> Token -> Analysis
check optId opts id list = do check optId opts (T_CaseExpression id _ list) = do
unless (Nothing `M.member` handledMap) $ do unless (Nothing `M.member` handledMap) $ do
mapM_ (warnUnhandled optId id) $ catMaybes $ M.keys notHandled mapM_ (warnUnhandled optId id) $ catMaybes $ M.keys notHandled
@ -1237,7 +1236,8 @@ checkSudoArgs = CommandCheck (Basename "sudo") f
where where
f t = sequence_ $ do f t = sequence_ $ do
opts <- parseOpts $ arguments t opts <- parseOpts $ arguments t
(_,(commandArg, _)) <- find (null . fst) opts let nonFlags = [x | ("",(x, _)) <- opts]
commandArg <- nonFlags !!! 0
command <- getLiteralString commandArg command <- getLiteralString commandArg
guard $ command `elem` builtins guard $ command `elem` builtins
return $ warn (getId t) 2232 $ "Can't use sudo with builtins like " ++ command ++ ". Did you want sudo sh -c .. instead?" return $ warn (getId t) 2232 $ "Can't use sudo with builtins like " ++ command ++ ". Did you want sudo sh -c .. instead?"
@ -1430,27 +1430,26 @@ prop_checkBackreferencingDeclaration6 = verify (checkBackreferencingDeclaration
prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration "declare") "declare x=var $k=$x" prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration "declare") "declare x=var $k=$x"
checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check
where where
check t = do check t = foldM_ perArg M.empty $ arguments t
maybeCfga <- asks cfgAnalysis
mapM_ (\cfga -> foldM_ (perArg cfga) M.empty $ arguments t) maybeCfga
perArg cfga leftArgs t = perArg leftArgs t =
case t of case t of
T_Assignment id _ name idx t -> do T_Assignment id _ name idx t -> do
warnIfBackreferencing cfga leftArgs $ t:idx warnIfBackreferencing leftArgs $ t:idx
return $ M.insert name id leftArgs return $ M.insert name id leftArgs
t -> do t -> do
warnIfBackreferencing cfga leftArgs [t] warnIfBackreferencing leftArgs [t]
return leftArgs return leftArgs
warnIfBackreferencing cfga backrefs l = do warnIfBackreferencing backrefs l = do
references <- findReferences cfga l references <- findReferences l
let reused = M.intersection backrefs references let reused = M.intersection backrefs references
mapM msg $ M.toList reused mapM msg $ M.toList reused
msg (name, id) = warn id 2318 $ "This assignment is used again in this '" ++ cmd ++ "', but won't have taken effect. Use two '" ++ cmd ++ "'s." msg (name, id) = warn id 2318 $ "This assignment is used again in this '" ++ cmd ++ "', but won't have taken effect. Use two '" ++ cmd ++ "'s."
findReferences cfga list = do findReferences list = do
cfga <- asks cfgAnalysis
let graph = CF.graph cfga let graph = CF.graph cfga
let nodesMap = CF.tokenToNodes cfga let nodesMap = CF.tokenToNodes cfga
let nodes = S.unions $ map (\id -> M.findWithDefault S.empty id nodesMap) $ map getId $ list let nodes = S.unions $ map (\id -> M.findWithDefault S.empty id nodesMap) $ map getId $ list

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 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

@ -19,7 +19,6 @@
-} -}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ViewPatterns #-}
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
import ShellCheck.AST import ShellCheck.AST
@ -61,9 +60,6 @@ checks = [
,checkBraceExpansionVars ,checkBraceExpansionVars
,checkMultiDimensionalArrays ,checkMultiDimensionalArrays
,checkPS1Assignments ,checkPS1Assignments
,checkMultipleBangs
,checkBangAfterPipe
,checkNegatedUnaryOps
] ]
testChecker (ForShell _ t) = testChecker (ForShell _ t) =
@ -77,24 +73,22 @@ verifyNot c s = producesComments (testChecker c) s == Just False
prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))" prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar" prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar" prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
checkForDecimals = ForShell [Sh, Dash, BusyboxSh, Bash] f checkForDecimals = ForShell [Sh, Dash, Bash] f
where where
f t@(TA_Expansion id _) = sequence_ $ do f t@(TA_Expansion id _) = sequence_ $ do
first:rest <- getLiteralString t str <- getLiteralString t
guard $ isDigit first && '.' `elem` rest first <- str !!! 0
guard $ isDigit first && '.' `elem` str
return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk." return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
f _ = return () f _ = return ()
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)" prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
prop_checkBashisms2 = verifyNot checkBashisms "[ foo -nt bar ]" prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"
prop_checkBashisms3 = verify checkBashisms "echo $((i++))" prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)" prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
prop_checkBashisms5 = verify checkBashisms "source file" prop_checkBashisms5 = verify checkBashisms "source file"
prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]" prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
prop_checkBashisms6b = verify checkBashisms "test \"$a\" == 42"
prop_checkBashisms6c = verify checkBashisms "[ foo =~ bar ]"
prop_checkBashisms6d = verify checkBashisms "test foo =~ bar"
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}" prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}" prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}" prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
@ -110,7 +104,6 @@ prop_checkBashisms18 = verify checkBashisms "foo &> /dev/null"
prop_checkBashisms19 = verify checkBashisms "foo > file*.txt" prop_checkBashisms19 = verify checkBashisms "foo > file*.txt"
prop_checkBashisms20 = verify checkBashisms "read -ra foo" prop_checkBashisms20 = verify checkBashisms "read -ra foo"
prop_checkBashisms21 = verify checkBashisms "[ -a foo ]" prop_checkBashisms21 = verify checkBashisms "[ -a foo ]"
prop_checkBashisms21b = verify checkBashisms "test -a foo"
prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]" prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]"
prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT" prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT"
prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM" prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM"
@ -191,82 +184,49 @@ prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}" prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}"
prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}" prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}"
prop_checkBashisms99 = verify checkBashisms "#!/bin/dash\necho [^f]oo" prop_checkBashisms99 = verify checkBashisms "#!/bin/dash\necho [^f]oo"
prop_checkBashisms100 = verify checkBashisms "read -r" checkBashisms = ForShell [Sh, Dash] $ \t -> do
prop_checkBashisms101 = verify checkBashisms "read"
prop_checkBashisms102 = verifyNot checkBashisms "read -r foo"
prop_checkBashisms103 = verifyNot checkBashisms "read foo"
prop_checkBashisms104 = verifyNot checkBashisms "read ''"
prop_checkBashisms105 = verifyNot checkBashisms "#!/bin/busybox sh\nset -o pipefail"
prop_checkBashisms106 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[[ \"$x\" = \"$x\" ]]"
prop_checkBashisms107 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[ \"$x\" == \"$x\" ]"
prop_checkBashisms108 = verifyNot checkBashisms "#!/bin/busybox sh\necho magic &> /dev/null"
prop_checkBashisms109 = verifyNot checkBashisms "#!/bin/busybox sh\ntrap stop EXIT SIGTERM"
prop_checkBashisms110 = verifyNot checkBashisms "#!/bin/busybox sh\nsource /dev/null"
prop_checkBashisms111 = verify checkBashisms "#!/bin/dash\nx='test'\n${x:0:3}" -- SC3057
prop_checkBashisms112 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x:0:3}" -- SC3057
prop_checkBashisms113 = verify checkBashisms "#!/bin/dash\nx='test'\n${x/st/xt}" -- SC3060
prop_checkBashisms114 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x/st/xt}" -- SC3060
prop_checkBashisms115 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x}" -- SC3053
prop_checkBashisms116 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x[1]}" -- SC3054
prop_checkBashisms117 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x[@]}" -- SC3055
prop_checkBashisms118 = verify checkBashisms "#!/bin/busybox sh\nxyz=1\n${!x*}" -- SC3056
prop_checkBashisms119 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x^^[t]}" -- SC3059
prop_checkBashisms120 = verify checkBashisms "#!/bin/sh\n[ x == y ]"
prop_checkBashisms121 = verifyNot checkBashisms "#!/bin/sh\n# shellcheck shell=busybox\n[ x == y ]"
prop_checkBashisms122 = verify checkBashisms "#!/bin/dash\n$'a'"
prop_checkBashisms123 = verifyNot checkBashisms "#!/bin/busybox sh\n$'a'"
prop_checkBashisms124 = verify checkBashisms "#!/bin/dash\ntype -p test"
prop_checkBashisms125 = verifyNot checkBashisms "#!/bin/busybox sh\ntype -p test"
prop_checkBashisms126 = verifyNot checkBashisms "#!/bin/busybox sh\nread -p foo -r bar"
prop_checkBashisms127 = verifyNot checkBashisms "#!/bin/busybox sh\necho -ne foo"
prop_checkBashisms128 = verify checkBashisms "#!/bin/dash\ntype -p test"
prop_checkBashisms129 = verify checkBashisms "#!/bin/sh\n[ -k /tmp ]"
prop_checkBashisms130 = verifyNot checkBashisms "#!/bin/dash\ntest -k /tmp"
prop_checkBashisms131 = verify checkBashisms "#!/bin/sh\n[ -o errexit ]"
checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
params <- ask params <- ask
kludge params t kludge params t
where where
-- This code was copy-pasted from Analytics where params was a variable -- This code was copy-pasted from Analytics where params was a variable
kludge params = bashism kludge params = bashism
where where
isBusyboxSh = shellType params == BusyboxSh isDash = shellType params == Dash
isDash = shellType params == Dash || isBusyboxSh
warnMsg id code s = warnMsg id code s =
if isDash if isDash
then err id code $ "In dash, " ++ s ++ " not supported." then err id code $ "In dash, " ++ s ++ " not supported."
else warn id code $ "In POSIX sh, " ++ s ++ " undefined." else warn id code $ "In POSIX sh, " ++ s ++ " undefined."
asStr = getLiteralString
bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is" bashism (T_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"
bashism (T_DollarBracket id _) = warnMsg id 3007 "$[..] in place of $((..)) is" bashism (T_DollarBracket id _) = warnMsg id 3007 "$[..] in place of $((..)) is"
bashism (T_SelectIn id _ _ _) = warnMsg id 3008 "select loops are" bashism (T_SelectIn id _ _ _) = warnMsg id 3008 "select loops are"
bashism (T_BraceExpansion id _) = warnMsg id 3009 "brace expansion is" bashism (T_BraceExpansion id _) = warnMsg id 3009 "brace expansion is"
bashism (T_Condition id DoubleBracket _) = bashism (T_Condition id DoubleBracket _) = 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 (TC_Binary id SingleBracket op _ _)
checkTestOp bashismBinaryTestFlags op id | op `elem` [ "-ot", "-nt", "-ef" ] =
bashism (TC_Unary id _ op _) = unless isDash $ warnMsg id 3013 $ op ++ " is"
checkTestOp bashismUnaryTestFlags op id bashism (TC_Binary id SingleBracket "==" _ _) =
bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just op, _]) = warnMsg id 3014 "== in place of = is"
checkTestOp bashismUnaryTestFlags op id bashism (TC_Binary id SingleBracket "=~" _ _) =
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 (TC_Unary id _ "-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"
bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are" bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id 3020 "&> is"
unless isBusyboxSh $ warnMsg id 3020 "&> is"
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) file)) = bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) file)) =
unless (all isDigit $ onlyLiteralString file) $ warnMsg id 3021 ">& filename (as opposed to >& fd) is" unless (all isDigit $ onlyLiteralString file) $ warnMsg id 3021 ">& filename (as opposed to >& fd) is"
bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are" bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are"
@ -286,8 +246,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
warnMsg id 3028 $ str ++ " is" warnMsg id 3028 $ str ++ " is"
bashism t@(T_DollarBraced id _ token) = do bashism t@(T_DollarBraced id _ token) = do
unless isBusyboxSh $ mapM_ check simpleExpansions mapM_ check expansion
mapM_ check advancedExpansions
when (isBashVariable var) $ when (isBashVariable var) $
warnMsg id 3028 $ var ++ " is" warnMsg id 3028 $ var ++ " is"
where where
@ -315,11 +274,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 +283,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) =
@ -402,8 +356,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
(\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is" return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is"
when (name == "source" && not isBusyboxSh) $ when (name == "source") $ warnMsg id 3046 "'source' in place of '.' is"
warnMsg id 3046 "'source' in place of '.' is"
when (name == "trap") $ when (name == "trap") $
let let
check token = sequence_ $ do check token = sequence_ $ do
@ -412,7 +365,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
return $ do return $ do
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $ when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is" warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is"
when (not isBusyboxSh && "SIG" `isPrefixOf` upper) $ when ("SIG" `isPrefixOf` upper) $
warnMsg (getId token) 3048 warnMsg (getId token) 3048
"prefixing signal names with 'SIG' is" "prefixing signal names with 'SIG' is"
when (not isDash && upper /= str) $ when (not isDash && upper /= str) $
@ -426,9 +379,6 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
let literal = onlyLiteralString format let literal = onlyLiteralString format
guard $ "%q" `isInfixOf` literal guard $ "%q" `isInfixOf` literal
return $ warnMsg (getId format) 3050 "printf %q is" return $ warnMsg (getId format) 3050 "printf %q is"
when (name == "read" && all isFlag rest) $
warnMsg (getId cmd) 3061 "read without a variable is"
where where
unsupportedCommands = [ unsupportedCommands = [
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown", "let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
@ -442,19 +392,17 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
("hash", Just $ if isDash then ["r", "v"] else ["r"]), ("hash", Just $ if isDash then ["r", "v"] else ["r"]),
("jobs", Just ["l", "p"]), ("jobs", Just ["l", "p"]),
("printf", Just []), ("printf", Just []),
("read", Just $ if isDash || isBusyboxSh then ["r", "p"] else ["r"]), ("read", Just $ if isDash then ["r", "p"] else ["r"]),
("readonly", Just ["p"]), ("readonly", Just ["p"]),
("trap", Just []), ("trap", Just []),
("type", Just $ if isBusyboxSh then ["p"] else []), ("type", Just []),
("ulimit", if isDash then Nothing else Just ["f"]), ("ulimit", if isDash then Nothing else Just ["f"]),
("umask", Just ["S"]), ("umask", Just ["S"]),
("unset", Just ["f", "v"]), ("unset", Just ["f", "v"]),
("wait", Just []) ("wait", Just [])
] ]
bashism t@(T_SourceCommand id src _) bashism t@(T_SourceCommand id src _)
| getCommandName src == Just "source" = | getCommandName src == Just "source" = warnMsg id 3051 "'source' in place of '.' is"
unless isBusyboxSh $
warnMsg id 3051 "'source' in place of '.' is"
bashism (TA_Expansion _ (T_Literal id str : _)) bashism (TA_Expansion _ (T_Literal id str : _))
| str `matches` radix = warnMsg id 3052 "arithmetic base conversion is" | str `matches` radix = warnMsg id 3052 "arithmetic base conversion is"
where where
@ -462,16 +410,14 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
bashism _ = return () bashism _ = return ()
varChars="_0-9a-zA-Z" varChars="_0-9a-zA-Z"
advancedExpansions = let re = mkRegex in [ expansion = let re = mkRegex in [
(re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"), (re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"),
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"), (re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"),
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"), (re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"),
(re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"), (re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is")
]
simpleExpansions = let re = mkRegex in [
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", 3057, "string indexing is"), (re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", 3057, "string indexing is"),
(re $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"), (re $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is") (re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is")
] ]
bashVars = [ bashVars = [
@ -497,50 +443,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,')"
@ -656,46 +558,5 @@ checkPS1Assignments = ForShell [Bash] f
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033" escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
prop_checkMultipleBangs1 = verify checkMultipleBangs "! ! true"
prop_checkMultipleBangs2 = verifyNot checkMultipleBangs "! true"
checkMultipleBangs = ForShell [Dash, BusyboxSh, Sh] f
where
f token = case token of
T_Banged id (T_Banged _ _) ->
err id 2325 "Multiple ! in front of pipelines are a bash/ksh extension. Use only 0 or 1."
_ -> return ()
prop_checkBangAfterPipe1 = verify checkBangAfterPipe "true | ! true"
prop_checkBangAfterPipe2 = verifyNot checkBangAfterPipe "true | ( ! true )"
prop_checkBangAfterPipe3 = verifyNot checkBangAfterPipe "! ! true | true"
checkBangAfterPipe = ForShell [Dash, BusyboxSh, Sh, Bash] f
where
f token = case token of
T_Pipeline _ _ cmds -> mapM_ check cmds
_ -> return ()
check token = case token of
T_Banged id _ ->
err id 2326 "! is not allowed in the middle of pipelines. Use command group as in cmd | { ! cmd; } if necessary."
_ -> return ()
prop_checkNegatedUnaryOps1 = verify checkNegatedUnaryOps "[ ! -o braceexpand ]"
prop_checkNegatedUnaryOps2 = verifyNot checkNegatedUnaryOps "[ -o braceexpand ]"
prop_checkNegatedUnaryOps3 = verifyNot checkNegatedUnaryOps "[[ ! -o braceexpand ]]"
prop_checkNegatedUnaryOps4 = verifyNot checkNegatedUnaryOps "! [ -o braceexpand ]"
prop_checkNegatedUnaryOps5 = verify checkNegatedUnaryOps "[ ! -a file ]"
checkNegatedUnaryOps = ForShell [Bash] f
where
f token = case token of
TC_Unary id SingleBracket "!" (TC_Unary _ _ op _) | op `elem` ["-a", "-o"] ->
err id 2332 $ msg op
_ -> return ()
msg "-o" = "[ ! -o opt ] is always true because -o becomes logical OR. Use [[ ]] or ! [ -o opt ]."
msg "-a" = "[ ! -a file ] is always true because -a becomes logical AND. Use -e instead."
msg _ = pleaseReport "unhandled negated unary message"
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View file

@ -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"
@ -160,15 +156,11 @@ shellForExecutable name =
"sh" -> return Sh "sh" -> return Sh
"bash" -> return Bash "bash" -> return Bash
"bats" -> return Bash "bats" -> return Bash
"busybox" -> return BusyboxSh -- Used for directives and --shell=busybox
"busybox sh" -> return BusyboxSh
"busybox ash" -> return BusyboxSh
"dash" -> return Dash "dash" -> return Dash
"ash" -> return Dash -- There's also a warning for this. "ash" -> return Dash -- There's also a warning for this.
"ksh" -> return Ksh "ksh" -> return Ksh
"ksh88" -> return Ksh "ksh88" -> return Ksh
"ksh93" -> return Ksh "ksh93" -> return Ksh
"oksh" -> return Ksh
_ -> Nothing _ -> Nothing
flagsForRead = "sreu:n:N:i:p:a:t:" flagsForRead = "sreu:n:N:i:p:a:t:"

View file

@ -24,8 +24,8 @@ import ShellCheck.Formatter.Format
import Data.Char import Data.Char
import Data.List import Data.List
import GHC.Exts
import System.IO import System.IO
import qualified Data.List.NonEmpty as NE
format :: IO Formatter format :: IO Formatter
format = return Formatter { format = return Formatter {
@ -45,12 +45,12 @@ outputResults cr sys =
else mapM_ outputGroup fileGroups else mapM_ outputGroup fileGroups
where where
comments = crComments cr comments = crComments cr
fileGroups = NE.groupWith sourceFile comments fileGroups = groupWith sourceFile comments
outputGroup group = do outputGroup group = do
let filename = sourceFile (NE.head group) let filename = sourceFile (head group)
result <- siReadFile sys (Just True) filename result <- siReadFile sys (Just True) filename
let contents = either (const "") id result let contents = either (const "") id result
outputFile filename contents (NE.toList group) outputFile filename contents group
outputFile filename contents warnings = do outputFile filename contents warnings = do
let comments = makeNonVirtual warnings contents let comments = makeNonVirtual warnings contents

View file

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

View file

@ -27,9 +27,9 @@ import Control.DeepSeq
import Data.Aeson import Data.Aeson
import Data.IORef import Data.IORef
import Data.Monoid import Data.Monoid
import GHC.Exts
import System.IO import System.IO
import qualified Data.ByteString.Lazy.Char8 as BL import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.List.NonEmpty as NE
format :: IO Formatter format :: IO Formatter
format = do format = do
@ -114,10 +114,10 @@ outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
collectResult ref cr sys = mapM_ f groups collectResult ref cr sys = mapM_ f groups
where where
comments = crComments cr comments = crComments cr
groups = NE.groupWith sourceFile comments groups = groupWith sourceFile comments
f :: NE.NonEmpty PositionedComment -> IO () f :: [PositionedComment] -> IO ()
f group = do f group = do
let filename = sourceFile (NE.head group) let filename = sourceFile (head group)
result <- siReadFile sys (Just True) filename result <- siReadFile sys (Just True) filename
let contents = either (const "") id result let contents = either (const "") id result
let comments' = makeNonVirtual comments contents let comments' = makeNonVirtual comments contents

View file

@ -31,9 +31,9 @@ import Data.Ord
import Data.IORef import Data.IORef
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import GHC.Exts
import System.IO import System.IO
import System.Info import System.Info
import qualified Data.List.NonEmpty as NE
wikiLink = "https://www.shellcheck.net/wiki/" wikiLink = "https://www.shellcheck.net/wiki/"
@ -117,19 +117,19 @@ outputResult options ref result sys = do
color <- getColorFunc $ foColorOption options color <- getColorFunc $ foColorOption options
let comments = crComments result let comments = crComments result
appendComments ref comments (fromIntegral $ foWikiLinkCount options) appendComments ref comments (fromIntegral $ foWikiLinkCount options)
let fileGroups = NE.groupWith sourceFile comments let fileGroups = groupWith sourceFile comments
mapM_ (outputForFile color sys) fileGroups mapM_ (outputForFile color sys) fileGroups
outputForFile color sys comments = do outputForFile color sys comments = do
let fileName = sourceFile (NE.head comments) let fileName = sourceFile (head comments)
result <- siReadFile sys (Just True) fileName result <- siReadFile sys (Just True) fileName
let contents = either (const "") id result let contents = either (const "") id result
let fileLinesList = lines contents let fileLinesList = lines contents
let lineCount = length fileLinesList let lineCount = length fileLinesList
let fileLines = listArray (1, lineCount) fileLinesList let fileLines = listArray (1, lineCount) fileLinesList
let groups = NE.groupWith lineNo comments let groups = groupWith lineNo comments
forM_ groups $ \commentsForLine -> do forM_ groups $ \commentsForLine -> do
let lineNum = fromIntegral $ lineNo (NE.head commentsForLine) let lineNum = fromIntegral $ lineNo (head commentsForLine)
let line = if lineNum < 1 || lineNum > lineCount let line = if lineNum < 1 || lineNum > lineCount
then "" then ""
else fileLines ! fromIntegral lineNum else fileLines ! fromIntegral lineNum
@ -139,7 +139,7 @@ outputForFile color sys comments = do
putStrLn (color "source" line) putStrLn (color "source" line)
forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c
putStrLn "" putStrLn ""
showFixedString color (toList commentsForLine) (fromIntegral lineNum) fileLines showFixedString color commentsForLine (fromIntegral lineNum) fileLines
-- Pick out only the lines necessary to show a fix in action -- Pick out only the lines necessary to show a fix in action
sliceFile :: Fix -> Array Int String -> (Fix, Array Int String) sliceFile :: Fix -> Array Int String -> (Fix, Array Int String)

View file

@ -1,5 +1,5 @@
{- {-
Copyright 2012-2024 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@ -21,14 +21,14 @@
module ShellCheck.Interface module ShellCheck.Interface
( (
SystemInterface(..) SystemInterface(..)
, CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csExtendedAnalysis, csOptionalChecks) , CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csOptionalChecks)
, CheckResult(crFilename, crComments) , CheckResult(crFilename, crComments)
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride) , ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
, ParseResult(prComments, prTokenPositions, prRoot) , ParseResult(prComments, prTokenPositions, prRoot)
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asExtendedAnalysis, asOptionalChecks) , AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asOptionalChecks)
, AnalysisResult(arComments) , AnalysisResult(arComments)
, FormatterOptions(foColorOption, foWikiLinkCount) , FormatterOptions(foColorOption, foWikiLinkCount)
, Shell(Ksh, Sh, Bash, Dash, BusyboxSh) , Shell(Ksh, Sh, Bash, Dash)
, ExecutionMode(Executed, Sourced) , ExecutionMode(Executed, Sourced)
, ErrorMessage , ErrorMessage
, Code , Code
@ -39,12 +39,11 @@ module ShellCheck.Interface
, ColorOption(ColorAuto, ColorAlways, ColorNever) , ColorOption(ColorAuto, ColorAlways, ColorNever)
, TokenComment(tcId, tcComment, tcFix) , TokenComment(tcId, tcComment, tcFix)
, emptyCheckResult , emptyCheckResult
, newAnalysisResult
, newAnalysisSpec
, newFormatterOptions
, newParseResult , newParseResult
, newAnalysisSpec
, newAnalysisResult
, newFormatterOptions
, newPosition , newPosition
, newSystemInterface
, newTokenComment , newTokenComment
, mockedSystemInterface , mockedSystemInterface
, mockRcFile , mockRcFile
@ -100,7 +99,6 @@ data CheckSpec = CheckSpec {
csIncludedWarnings :: Maybe [Integer], csIncludedWarnings :: Maybe [Integer],
csShellTypeOverride :: Maybe Shell, csShellTypeOverride :: Maybe Shell,
csMinSeverity :: Severity, csMinSeverity :: Severity,
csExtendedAnalysis :: Maybe Bool,
csOptionalChecks :: [String] csOptionalChecks :: [String]
} deriving (Show, Eq) } deriving (Show, Eq)
@ -125,7 +123,6 @@ emptyCheckSpec = CheckSpec {
csIncludedWarnings = Nothing, csIncludedWarnings = Nothing,
csShellTypeOverride = Nothing, csShellTypeOverride = Nothing,
csMinSeverity = StyleC, csMinSeverity = StyleC,
csExtendedAnalysis = Nothing,
csOptionalChecks = [] csOptionalChecks = []
} }
@ -138,14 +135,6 @@ newParseSpec = ParseSpec {
psShellTypeOverride = Nothing psShellTypeOverride = Nothing
} }
newSystemInterface :: Monad m => SystemInterface m
newSystemInterface =
SystemInterface {
siReadFile = \_ _ -> return $ Left "Not implemented",
siFindSource = \_ _ _ name -> return name,
siGetConfig = \_ -> return Nothing
}
-- Parser input and output -- Parser input and output
data ParseSpec = ParseSpec { data ParseSpec = ParseSpec {
psFilename :: String, psFilename :: String,
@ -176,7 +165,6 @@ data AnalysisSpec = AnalysisSpec {
asExecutionMode :: ExecutionMode, asExecutionMode :: ExecutionMode,
asCheckSourced :: Bool, asCheckSourced :: Bool,
asOptionalChecks :: [String], asOptionalChecks :: [String],
asExtendedAnalysis :: Maybe Bool,
asTokenPositions :: Map.Map Id (Position, Position) asTokenPositions :: Map.Map Id (Position, Position)
} }
@ -187,7 +175,6 @@ newAnalysisSpec token = AnalysisSpec {
asExecutionMode = Executed, asExecutionMode = Executed,
asCheckSourced = False, asCheckSourced = False,
asOptionalChecks = [], asOptionalChecks = [],
asExtendedAnalysis = Nothing,
asTokenPositions = Map.empty asTokenPositions = Map.empty
} }
@ -225,7 +212,7 @@ newCheckDescription = CheckDescription {
} }
-- Supporting data types -- Supporting data types
data Shell = Ksh | Sh | Bash | Dash | BusyboxSh deriving (Show, Eq) data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
data ExecutionMode = Executed | Sourced deriving (Show, Eq) data ExecutionMode = Executed | Sourced deriving (Show, Eq)
type ErrorMessage = String type ErrorMessage = String
@ -324,7 +311,7 @@ data ColorOption =
-- For testing -- For testing
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
mockedSystemInterface files = (newSystemInterface :: SystemInterface Identity) { mockedSystemInterface files = SystemInterface {
siReadFile = rf, siReadFile = rf,
siFindSource = fs, siFindSource = fs,
siGetConfig = const $ return Nothing siGetConfig = const $ return Nothing
@ -339,3 +326,4 @@ mockedSystemInterface files = (newSystemInterface :: SystemInterface Identity) {
mockRcFile rcfile mock = mock { mockRcFile rcfile mock = mock {
siGetConfig = const . return $ Just (".shellcheckrc", rcfile) siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
} }

View file

@ -46,7 +46,6 @@ import Text.Parsec.Error
import Text.Parsec.Pos import Text.Parsec.Pos
import qualified Control.Monad.Reader as Mr import qualified Control.Monad.Reader as Mr
import qualified Control.Monad.State as Ms import qualified Control.Monad.State as Ms
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
import Test.QuickCheck.All (quickCheckAll) import Test.QuickCheck.All (quickCheckAll)
@ -141,9 +140,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
@ -155,7 +160,7 @@ data Context =
deriving (Show) deriving (Show)
data HereDocContext = data HereDocContext =
HereDocPending Id Dashed Quoted String [Context] -- on linefeed, read this T_HereDoc HereDocPending Token [Context] -- on linefeed, read this T_HereDoc
deriving (Show) deriving (Show)
data UserState = UserState { data UserState = UserState {
@ -233,12 +238,12 @@ addToHereDocMap id list = do
hereDocMap = Map.insert id list map hereDocMap = Map.insert id list map
} }
addPendingHereDoc id d q str = do addPendingHereDoc t = do
state <- getState state <- getState
context <- getCurrentContexts context <- getCurrentContexts
let docs = pendingHereDocs state let docs = pendingHereDocs state
putState $ state { putState $ state {
pendingHereDocs = HereDocPending id d q str context : docs pendingHereDocs = HereDocPending t context : docs
} }
popPendingHereDocs = do popPendingHereDocs = do
@ -821,7 +826,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
@ -1052,16 +1057,6 @@ readAnnotationWithoutPrefix sandboxed = do
"This shell type is unknown. Use e.g. sh or bash." "This shell type is unknown. Use e.g. sh or bash."
return [ShellOverride shell] return [ShellOverride shell]
"extended-analysis" -> do
pos <- getPosition
value <- plainOrQuoted $ many1 letter
case value of
"true" -> return [ExtendedAnalysis True]
"false" -> return [ExtendedAnalysis False]
_ -> do
parseNoteAt pos ErrorC 1146 "Unknown extended-analysis value. Expected true/false."
return []
"external-sources" -> do "external-sources" -> do
pos <- getPosition pos <- getPosition
value <- plainOrQuoted $ many1 letter value <- plainOrQuoted $ many1 letter
@ -1199,7 +1194,7 @@ readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|>
readDollarBracedLiteral = do readDollarBracedLiteral = do
start <- startSpan start <- startSpan
vars <- (readBraceEscaped <|> ((\x -> [x]) <$> anyChar)) `reluctantlyTill1` bracedQuotable vars <- (readBraceEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` bracedQuotable
id <- endSpan start id <- endSpan start
return $ T_Literal id $ concat vars return $ T_Literal id $ concat vars
@ -1561,7 +1556,7 @@ readGenericLiteral endChars = do
return $ concat strings return $ concat strings
readGenericLiteral1 endExp = do readGenericLiteral1 endExp = do
strings <- (readGenericEscaped <|> ((\x -> [x]) <$> anyChar)) `reluctantlyTill1` endExp strings <- (readGenericEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` endExp
return $ concat strings return $ concat strings
readGenericEscaped = do readGenericEscaped = do
@ -1840,7 +1835,7 @@ readHereDoc = called "here document" $ do
-- add empty tokens for now, read the rest in readPendingHereDocs -- add empty tokens for now, read the rest in readPendingHereDocs
let doc = T_HereDoc hid dashed quoted endToken [] let doc = T_HereDoc hid dashed quoted endToken []
addPendingHereDoc hid dashed quoted endToken addPendingHereDoc doc
return doc return doc
where where
unquote :: String -> (Quoted, String) unquote :: String -> (Quoted, String)
@ -1861,7 +1856,7 @@ readPendingHereDocs = do
docs <- popPendingHereDocs docs <- popPendingHereDocs
mapM_ readDoc docs mapM_ readDoc docs
where where
readDoc (HereDocPending id dashed quoted endToken ctx) = readDoc (HereDocPending (T_HereDoc id dashed quoted endToken _) ctx) =
swapContext ctx $ swapContext ctx $
do do
docStartPos <- getPosition docStartPos <- getPosition
@ -2288,31 +2283,22 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
subRead name script = subRead name script =
withContext (ContextSource name) $ withContext (ContextSource name) $
inSeparateContext $ do inSeparateContext $
oldState <- getState subParse (initialPos name) (readScriptFile True) script
setState $ oldState { pendingHereDocs = [] }
result <- subParse (initialPos name) (readScriptFile True) script
newState <- getState
setState $ newState { pendingHereDocs = pendingHereDocs oldState }
return result
readSource t = return t readSource t = return t
prop_readPipeline = isOk readPipeline "! cat /etc/issue | grep -i ubuntu" prop_readPipeline = isOk readPipeline "! cat /etc/issue | grep -i ubuntu"
prop_readPipeline2 = isWarning readPipeline "!cat /etc/issue | grep -i ubuntu" prop_readPipeline2 = isWarning readPipeline "!cat /etc/issue | grep -i ubuntu"
prop_readPipeline3 = isOk readPipeline "for f; do :; done|cat" prop_readPipeline3 = isOk readPipeline "for f; do :; done|cat"
prop_readPipeline4 = isOk readPipeline "! ! true"
prop_readPipeline5 = isOk readPipeline "true | ! true"
readPipeline = do readPipeline = do
unexpecting "keyword/token" readKeyword unexpecting "keyword/token" readKeyword
readBanged readPipeSequence do
readBanged parser = do
pos <- getPosition
(T_Bang id) <- g_Bang (T_Bang id) <- g_Bang
next <- readBanged parser pipe <- readPipeSequence
return $ T_Banged id next return $ T_Banged id pipe
<|> parser <|>
readPipeSequence
prop_readAndOr = isOk readAndOr "grep -i lol foo || exit 1" prop_readAndOr = isOk readAndOr "grep -i lol foo || exit 1"
prop_readAndOr1 = isOk readAndOr "# shellcheck disable=1\nfoo" prop_readAndOr1 = isOk readAndOr "# shellcheck disable=1\nfoo"
@ -2368,14 +2354,14 @@ readTerm = do
readPipeSequence = do readPipeSequence = do
start <- startSpan start <- startSpan
(cmds, pipes) <- sepBy1WithSeparators (readBanged readCommand) (cmds, pipes) <- sepBy1WithSeparators readCommand
(readPipe `thenSkip` (spacing >> readLineBreak)) (readPipe `thenSkip` (spacing >> readLineBreak))
id <- endSpan start id <- endSpan start
spacing spacing
return $ T_Pipeline id pipes cmds return $ T_Pipeline id pipes cmds
where where
sepBy1WithSeparators p s = do sepBy1WithSeparators p s = do
let elems = (\x -> ([x], [])) <$> p let elems = p >>= \x -> return ([x], [])
let seps = do let seps = do
separator <- s separator <- s
return $ \(a,b) (c,d) -> (a++c, b ++ d ++ [separator]) return $ \(a,b) (c,d) -> (a++c, b ++ d ++ [separator])
@ -2398,10 +2384,6 @@ readCommand = choice [
] ]
readCmdName = do readCmdName = do
-- If the command name is `!` then
optional . lookAhead . try $ do
char '!'
whitespace
-- Ignore alias suppression -- Ignore alias suppression
optional . try $ do optional . try $ do
char '\\' char '\\'
@ -2795,29 +2777,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
@ -2921,8 +2891,8 @@ readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord)
kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos) kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos)
kludgeAwayQuotes s p = kludgeAwayQuotes s p =
case s of case s of
first:second:rest -> first:rest@(_:_) ->
let (last NE.:| backwards) = NE.reverse (second NE.:| rest) let (last:backwards) = reverse rest
middle = reverse backwards middle = reverse backwards
in in
if first `elem` "'\"" && first == last if first `elem` "'\"" && first == last
@ -3352,12 +3322,10 @@ readScriptFile sourced = do
then do then do
commands <- readCompoundListOrEmpty commands <- readCompoundListOrEmpty
id <- endSpan start id <- endSpan start
readPendingHereDocs
verifyEof verifyEof
let script = T_Annotation annotationId annotations $ let script = T_Annotation annotationId annotations $
T_Script id shebang commands T_Script id shebang commands
userstate <- getState reparseIndices script
reparseIndices $ reattachHereDocs script (hereDocMap userstate)
else do else do
many anyChar many anyChar
id <- endSpan start id <- endSpan start
@ -3367,8 +3335,8 @@ readScriptFile sourced = do
verifyShebang pos s = do verifyShebang pos s = do
case isValidShell s of case isValidShell s of
Just True -> return () Just True -> return ()
Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/dash/ksh/'busybox sh' scripts. Sorry!" Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/dash/ksh scripts. Sorry!"
Nothing -> parseProblemAt pos ErrorC 1008 "This shebang was unrecognized. ShellCheck only supports sh/bash/dash/ksh/'busybox sh'. Add a 'shell' directive to specify." Nothing -> parseProblemAt pos ErrorC 1008 "This shebang was unrecognized. ShellCheck only supports sh/bash/dash/ksh. Add a 'shell' directive to specify."
isValidShell s = isValidShell s =
let good = null s || any (`isPrefixOf` s) goodShells let good = null s || any (`isPrefixOf` s) goodShells
@ -3384,20 +3352,16 @@ readScriptFile sourced = do
"sh", "sh",
"ash", "ash",
"dash", "dash",
"busybox sh",
"bash", "bash",
"bats", "bats",
"ksh", "ksh"
"oksh"
] ]
badShells = [ badShells = [
"awk", "awk",
"csh", "csh",
"expect", "expect",
"fish",
"perl", "perl",
"python", "python",
"python3",
"ruby", "ruby",
"tcsh", "tcsh",
"zsh" "zsh"
@ -3450,22 +3414,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
(res, systemState) <- runParser testEnvironment
(parser >> eof >> getState) "-" string (parser >> eof >> getState) "-" string
return $ case res of case (res, sys) of
Right userState -> (Right userState, systemState) ->
Right $ parseNotes userState ++ parseProblems systemState return $ Just . null $ parseNotes userState ++ parseProblems systemState
Left _ -> Left $ parseProblems systemState (Left _, _) -> return Nothing
-- 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
@ -3483,7 +3438,8 @@ makeErrorFor parsecError =
pos = errorPos parsecError pos = errorPos parsecError
getStringFromParsec errors = getStringFromParsec errors =
headOrDefault "" (mapMaybe f $ reverse errors) ++ case map f errors of
r -> unwords (take 1 $ catMaybes $ reverse r) ++
" Fix any mentioned problems and try again." " Fix any mentioned problems and try again."
where where
f err = f err =
@ -3515,7 +3471,8 @@ parseShell env name contents = do
return newParseResult { return newParseResult {
prComments = map toPositionedComment $ nub $ parseNotes userstate ++ parseProblems state, prComments = map toPositionedComment $ nub $ parseNotes userstate ++ parseProblems state,
prTokenPositions = Map.map startEndPosToPos (positionMap userstate), prTokenPositions = Map.map startEndPosToPos (positionMap userstate),
prRoot = Just script prRoot = Just $
reattachHereDocs script (hereDocMap userstate)
} }
Left err -> do Left err -> do
let context = contextStack state let context = contextStack state
@ -3533,11 +3490,13 @@ parseShell env name contents = do
-- A final pass for ignoring parse errors after failed parsing -- A final pass for ignoring parse errors after failed parsing
isIgnored stack note = any (contextItemDisablesCode False (codeForParseNote note)) stack isIgnored stack note = any (contextItemDisablesCode False (codeForParseNote note)) stack
notesForContext list = zipWith ($) [first, second] [(pos, str) | ContextName pos str <- list] notesForContext list = zipWith ($) [first, second] $ filter isName list
where where
first (pos, str) = ParseNote pos pos ErrorC 1073 $ isName (ContextName _ _) = True
isName _ = False
first (ContextName pos str) = ParseNote pos pos ErrorC 1073 $
"Couldn't parse this " ++ str ++ ". Fix to allow more checks." "Couldn't parse this " ++ str ++ ". Fix to allow more checks."
second (pos, str) = ParseNote pos pos InfoC 1009 $ second (ContextName pos str) = ParseNote pos pos InfoC 1009 $
"The mentioned syntax error was in this " ++ str ++ "." "The mentioned syntax error was in this " ++ str ++ "."
-- Go over all T_UnparsedIndex and reparse them as either arithmetic or text -- Go over all T_UnparsedIndex and reparse them as either arithmetic or text

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,13 @@ 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
ubuntu:18.04 apt-get update && apt-get install -y cabal-install
ubuntu:16.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

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

View file

@ -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