11 Commits

Author SHA1 Message Date
Vidar Holen
c9b8ad3439 Drop attoparsec/text dependencies 2023-10-08 18:16:09 -07:00
Vidar Holen
e59fbfebda Re-add other Portage functionality 2023-10-08 16:09:58 -07:00
Vidar Holen
ce3414eeea Move from Parameters to SystemInterface for Portage variables 2023-08-27 17:53:14 -07:00
Vidar Holen
feebbbb096 Merge branch 'kangie' into ebuild 2023-08-27 15:20:00 -07:00
Vidar Holen
87ef5ae18a Merge branch 'portage' of https://github.com/Kangie/shellcheck into kangie 2023-08-27 15:18:32 -07:00
Vidar Holen
0138a6fafc Example plumbing for Portage variables 2023-08-13 17:49:36 -07:00
hololeap
dfa920c5d2 Switch to attoparsec for gentoo scan
Signed-off-by: hololeap <hololeap@users.noreply.github.com>
2023-08-05 17:38:01 -06:00
hololeap
fc9b63fb5e Remove PortageAutoInternalVariables and python
The Gentoo eclass list is now populated using pure Haskell. The old
python generators and generated module are no longer needed.

Signed-off-by: hololeap <hololeap@users.noreply.github.com>
2023-08-05 15:31:15 -06:00
hololeap
272ef819b9 Scan for Gentoo eclass variables
Creates a Map of eclass names to eclass variables by scanning the
system for repositories and their respective eclasses. Runs `portageq`
to determine repository names and locations. Emits a warning if an
IOException is caught when attempting to run `portageq`.

This Map is passed via CheckSpec to AnalysisSpec and finally to
Parameters, where it is read by `checkUnusedAssignments` in order to
determine which variables can be safely ignored by this check.

Signed-off-by: hololeap <hololeap@users.noreply.github.com>
2023-08-05 15:31:15 -06:00
hololeap
08ae7ef836 New IO interface to scan for Gentoo eclass vars
Uses the `portageq` command to scan for repositories, which in turn are
scanned for eclasses, which are then scanned for eclass variables.

The variables are scanned using a heuristic which looks for

    "# @ECLASS_VARIABLE: "

at the start of each line, which means only properly documented
variables will be found.

Signed-off-by: hololeap <hololeap@users.noreply.github.com>
2023-08-04 17:19:05 -06:00
Matt Jolly
e3d8483e49 Rebase of chromiumos fork
https://chromium.googlesource.com/chromiumos/third_party/shellcheck/
2023-08-04 15:56:48 -06:00
71 changed files with 1174 additions and 1742 deletions

View File

@@ -1,6 +1,6 @@
#### For bugs
- Rule Id (if any, e.g. SC1000):
- My shellcheck version (`shellcheck --version` or "online"):
- Rule Id (if any, e.g. SC1000):
- My shellcheck version (`shellcheck --version` or "online"):
- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
- [ ] I tried on https://www.shellcheck.net/ and verified that this is still a problem on the latest commit

View File

@@ -1,28 +0,0 @@
---
name: Bug report
about: Create a new bug report
title: ''
labels: ''
assignees: ''
---
#### For bugs with existing features
- Rule Id (if any, e.g. SC1000):
- My shellcheck version (`shellcheck --version` or "online"):
- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
- [ ] I tried on https://www.shellcheck.net/ and verified that this is still a problem on the latest commit
#### Here's a snippet or screenshot that shows the problem:
```sh
#!/bin/sh
your script here
```
#### Here's what shellcheck currently says:
#### Here's what I wanted or expected to see:

View File

@@ -1,25 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
#### For new checks and feature suggestions
- [ ] https://www.shellcheck.net/ (i.e. the latest commit) currently gives no useful warnings about this
- [ ] I searched through https://github.com/koalaman/shellcheck/issues and didn't find anything related
#### Here's a snippet or screenshot that shows a potential problem:
```sh
#!/bin/sh
your script here
```
#### Here's what shellcheck currently says:
#### Here's what I wanted to see:

View File

@@ -15,7 +15,7 @@ jobs:
sudo apt-get install cabal-install
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
fetch-depth: 0
@@ -37,58 +37,35 @@ jobs:
mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz
- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: source
path: source/
run_tests:
name: Run tests
needs: package_source
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
- name: Install dependencies
run: |
sudo apt-get update && sudo apt-get install ghc cabal-install
cabal update
- name: Unpack source
run: |
cd source
tar xvf source.tar.gz --strip-components=1
- name: Build and run tests
run: |
cd source
cabal test
build_source:
name: Build
name: Build Source Code
needs: package_source
strategy:
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
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
- name: Build source
run: |
mkdir -p bin
mkdir -p bin/${{matrix.build}}
( cd bin && ../builders/run_builder ../source/source.tar.gz ../builders/${{matrix.build}} )
( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} )
- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: ${{matrix.build}}.bin
name: bin
path: bin/
package_binary:
@@ -97,25 +74,25 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
- name: Work around GitHub permissions bug
run: chmod +x *.bin/*/shellcheck*
run: chmod +x bin/*/shellcheck*
- name: Package binaries
run: |
export TAGS="$(cat source/tags)"
mkdir -p deploy
cp -r *.bin/* deploy
cp -r bin/* deploy
cd deploy
../.prepare_deploy
rm -rf */ README* LICENSE*
- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: deploy
path: deploy/
@@ -126,16 +103,11 @@ jobs:
runs-on: ubuntu-latest
environment: Deploy
steps:
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install hub
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
- name: Upload to GitHub
env:

2
.gitignore vendored
View File

@@ -12,7 +12,6 @@ cabal-dev
.cabal-sandbox/
cabal.sandbox.config
cabal.config
cabal.project.freeze
.stack-work
### Snap ###
@@ -21,4 +20,3 @@ cabal.project.freeze
/parts/
/prime/
*.snap
/dist-newstyle/

View File

@@ -80,7 +80,6 @@ function multi_arch_docker::main() {
export DOCKER_PLATFORMS='linux/amd64'
DOCKER_PLATFORMS+=' linux/arm64'
DOCKER_PLATFORMS+=' linux/arm/v6'
DOCKER_PLATFORMS+=' linux/riscv64'
multi_arch_docker::install_docker_buildx
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,54 +1,13 @@
## v0.11.0 - 2025-08-03
## 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 ]`.
- Optional `avoid-negated-conditions`: suggest replacing `[ ! a -eq b ]`
with `[ a -ne b ]`, and similar for -ge/-lt/=/!=/etc (SC2335).
- 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.
- SC2236/SC2237 about replacing `[ ! -n .. ]` with `[ -z ]` and vice versa
is now optional under `avoid-negated-conditions`.
- 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
- Diff output now uses / as path separator on Windows
### 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.
- Updates for Bash 5.3, including `${| cmd; }` and `source -p`
### 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
### Changed
## v0.9.0 - 2022-12-12

View File

@@ -5,15 +5,17 @@ ARG tag
# Put the right binary for each architecture into place for the
# multi-architecture docker image.
ARG url_base="https://github.com/koalaman/shellcheck/releases/download/"
RUN set -x; \
arch="$(uname -m)"; \
echo "arch is $arch"; \
if [ "${arch}" = 'armv7l' ]; then \
arch='armv6hf'; \
fi; \
url_base='https://github.com/koalaman/shellcheck/releases/download/'; \
tar_file="${tag}/shellcheck-${tag}.linux.${arch}.tar.xz"; \
wget "${url_base}${tar_file}" -O - | tar -C /bin --strip-components=1 -xJf - "shellcheck-${tag}/shellcheck" && \
wget "${url_base}${tar_file}" -O - | tar xJf -; \
mv "shellcheck-${tag}/shellcheck" /bin/; \
rm -rf "shellcheck-${tag}"; \
ls -laF /bin/shellcheck
# ShellCheck image

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).
* 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).
@@ -110,11 +110,9 @@ Services and platforms that have ShellCheck pre-installed and ready to use:
* [Codacy](https://www.codacy.com/)
* [Code Climate](https://codeclimate.com/)
* [Code Factor](https://www.codefactor.io/)
* [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)
* [Github](https://github.com/features/actions) (only Linux)
* [Trunk Code Quality](https://trunk.io/code-quality) (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/)
* [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)
Most other services, including [GitLab](https://about.gitlab.com/), let you install
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
@@ -196,12 +194,6 @@ On Windows (via [chocolatey](https://chocolatey.org/packages/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)):
```cmd
@@ -230,17 +222,11 @@ Using the [nix package manager](https://nixos.org/nix):
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:
* [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
* [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
* [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)
* [macOS, 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)
* [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip)
@@ -317,6 +303,10 @@ Verify that `cabal` is installed and update its dependency list with
$ 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.
Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
@@ -562,3 +552,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).
* 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
Version: 0.11.0
Version: 0.9.0
Synopsis: Shell script analysis tool
License: GPL-3
License-file: LICENSE
@@ -46,19 +46,19 @@ library
semigroups
build-depends:
-- The lower bounds are based on GHC 7.10.3
-- The upper bounds are based on GHC 9.12.1
aeson >= 1.4.0 && < 2.3,
-- The upper bounds are based on GHC 9.6.1
aeson >= 1.4.0 && < 2.2,
array >= 0.5.1 && < 0.6,
base >= 4.8.0.0 && < 5,
bytestring >= 0.10.6 && < 0.13,
containers >= 0.5.6 && < 0.9,
deepseq >= 1.4.1 && < 1.6,
Diff >= 0.4.0 && < 1.1,
bytestring >= 0.10.6 && < 0.12,
containers >= 0.5.6 && < 0.7,
deepseq >= 1.4.1 && < 1.5,
Diff >= 0.4.0 && < 0.5,
fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9),
filepath >= 1.4.0 && < 1.6,
filepath >= 1.4.0 && < 1.5,
mtl >= 2.2.2 && < 2.4,
parsec >= 3.1.14 && < 3.2,
QuickCheck >= 2.14.2 && < 2.17,
QuickCheck >= 2.14.2 && < 2.15,
regex-tdfa >= 1.2.0 && < 1.4,
transformers >= 0.4.2 && < 0.7,
@@ -93,6 +93,7 @@ library
ShellCheck.Formatter.Quiet
ShellCheck.Interface
ShellCheck.Parser
ShellCheck.PortageVariables
ShellCheck.Prelude
ShellCheck.Regex
other-modules:

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`,
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,19 +1,19 @@
FROM liushuyu/osxcross@sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76
ENV TARGET=x86_64-apple-darwin18
ENV TARGETNAME=darwin.x86_64
ENV TARGET x86_64-apple-darwin18
ENV TARGETNAME darwin.x86_64
# Build dependencies
USER root
ENV DEBIAN_FRONTEND=noninteractive
RUN sed -e 's/focal/kinetic/g' -e 's/archive\|security/old-releases/' -i /etc/apt/sources.list
ENV DEBIAN_FRONTEND noninteractive
RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list
RUN apt-get update
RUN apt-get dist-upgrade -y
RUN apt-get install -y ghc automake autoconf llvm curl alex happy
# Build GHC
WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/9.2.8/ghc-9.2.8-src.tar.xz" | tar xJ --strip-components=1
RUN curl -L "https://downloads.haskell.org/~ghc/9.2.5/ghc-9.2.5-src.tar.xz" | tar xJ --strip-components=1
RUN ./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
@@ -21,7 +21,7 @@ RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-in
# Due to an apparent cabal bug, we specify our options directly to cabal
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
ENV CABALOPTS="--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
ENV CABALOPTS "--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
# Prebuild the dependencies
RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck

View File

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

View File

@@ -0,0 +1,36 @@
FROM ubuntu:20.04
ENV TARGET aarch64-linux-gnu
ENV TARGETNAME linux.aarch64
# Build dependencies
USER root
ENV DEBIAN_FRONTEND noninteractive
# These deps are from 20.04, because GHC's compiler/llvm support moves slowly
RUN apt-get update && apt-get install -y llvm gcc-$TARGET
# The rest are from 22.10
RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list
RUN apt-get update && apt-get install -y ghc alex happy automake autoconf build-essential curl qemu-user-static
# Build GHC
WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/9.2.5/ghc-9.2.5-src.tar.xz" | tar xJ --strip-components=1
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
RUN make install
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin
# Due to an apparent cabal bug, we specify our options directly to cabal
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
# Prebuild the dependencies
RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

View File

@@ -4,11 +4,12 @@ set -xe
tar xzv --strip-components=1
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
"$TARGET-strip" -s "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
"qemu-${TARGET%%-*}-static" "$TARGETNAME/shellcheck" --version
qemu-aarch64-static "$TARGETNAME/shellcheck" --version
} >&2
tar czv "$TARGETNAME"

View File

@@ -0,0 +1,60 @@
# I've again spent days trying to get a working armv6hf compiler going.
# God only knows how many recompilations of GCC, GHC, libraries, and
# ShellCheck itself, has gone into it.
#
# I tried Debian's toolchain. I tried my custom one built according to
# RPi `gcc -v`. I tried GHC9, glibc, musl, registerised vs not, but
# nothing has yielded an armv6hf binary that does not immediately
# segfault on qemu-arm-static or the RPi itself.
#
# I then tried the same but with armv7hf. Same story.
#
# Emulating the entire userspace with balenalib again? Very strange build
# failures where programs would fail to execute with > ~100 arguments.
#
# Finally, creating our own appears to work when using a custom QEmu
# patched to follow execve calls.
#
# PS: $100 bounty for getting a RPi1 compatible static build going
# with cross-compilation, similar to what the aarch64 build does.
#
FROM ubuntu:20.04
ENV TARGETNAME linux.armv6hf
# Build QEmu with execve follow support
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
RUN apt-get install -y build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev
WORKDIR /build
RUN git clone --depth 1 https://github.com/koalaman/qemu
RUN cd qemu && ./configure --static && cd build && ninja qemu-arm
RUN cp qemu/build/qemu-arm /build/qemu-arm-static
ENV QEMU_EXECVE 1
# Set up an armv6 userspace
WORKDIR /
RUN apt-get install -y debootstrap qemu-user-static
# We expect this to fail if the host doesn't have binfmt qemu support
RUN qemu-debootstrap --arch armhf bullseye pi http://mirrordirector.raspbian.org/raspbian || [ -e /pi/etc/issue ]
RUN cp /build/qemu-arm-static /pi/usr/bin/qemu-arm-static
RUN printf > /bin/pirun '%s\n' '#!/bin/sh' 'chroot /pi /usr/bin/qemu-arm-static /usr/bin/env "$@"' && chmod +x /bin/pirun
# If the debootstrap process didn't finish, continue it
RUN [ ! -e /pi/debootstrap ] || pirun '/debootstrap/debootstrap' --second-stage
# Install deps in the chroot
RUN pirun apt-get update
RUN pirun apt-get install -y ghc cabal-install
# Finally we can build the current dependencies. This takes hours.
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--gcc-options;-Os -Wl,--gc-sections -ffunction-sections -fdata-sections"
RUN pirun cabal update
RUN IFS=";" && pirun cabal install --dependencies-only $CABALOPTS ShellCheck
RUN IFS=';' && pirun cabal install $CABALOPTS --lib fgl
# Copy the build script
WORKDIR /pi/scratch
COPY build /pi/usr/bin
ENTRYPOINT ["/bin/pirun", "/usr/bin/build"]

View File

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

View File

@@ -0,0 +1,20 @@
FROM alpine:latest
ENV TARGETNAME linux.x86_64
# Install GHC and cabal
USER root
RUN apk add ghc cabal g++ libffi-dev curl bash
# Use ld.bfd instead of ld.gold due to
# x86_64-linux-gnu/libpthread.a(pthread_cond_init.o)(.note.stapsdt+0x14): error:
# relocation refers to local symbol "" [2], which is defined in a discarded section
ENV CABALOPTS "--ghc-options;-optl-Wl,-fuse-ld=bfd -split-sections -optc-Os -optc-Wl,--gc-sections"
# Other archs pre-build dependencies here, but this one doesn't to detect ecosystem movement
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

View File

@@ -0,0 +1,27 @@
FROM ubuntu:20.04
ENV TARGETNAME windows.x86_64
# We don't need wine32, even though it complains
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y curl busybox wine winbind
# Fetch Windows version, will be available under z:\haskell
WORKDIR /haskell
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-x86_64-unknown-mingw32.tar.xz" | tar xJ --strip-components=1
WORKDIR /haskell/bin
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-mingw32.zip" | busybox unzip -
RUN curl -L "https://curl.se/windows/dl-7.84.0/curl-7.84.0-win64-mingw.zip" | busybox unzip - && mv curl-7.84.0-win64-mingw/bin/* .
ENV WINEPATH /haskell/bin
# It's unknown whether Cabal on Windows suffers from the same issue
# that necessitated this but I don't care enough to find out
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections"
# Precompile some deps to speed up later builds
RUN wine /haskell/bin/cabal.exe update && IFS=';' && wine /haskell/bin/cabal.exe install --lib --dependencies-only $CABALOPTS ShellCheck
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,35 +0,0 @@
FROM ubuntu:25.04
ENV TARGET=aarch64-linux-gnu
ENV TARGETNAME=linux.aarch64
# Build dependencies
USER root
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y llvm-20 "gcc-$TARGET" "g++-$TARGET" ghc alex happy automake autoconf build-essential curl qemu-user-static
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.16.0.0/cabal-install-3.16.0.0-x86_64-linux-alpine3_20.tar.xz" | tar xJv -C /usr/local/bin && cabal update
# Build GHC
WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/9.12.2/ghc-9.12.2-src.tar.xz" | tar xJ --strip-components=1
RUN ./boot.source && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
# GHC fails to build if it can't encode non-ascii
ENV LC_CTYPE=C.utf8
# We have to do a binary-dist instead of a direct install, otherwise the targest won't have
# cross compilation prefixes in /usr/local/lib/aarch64-linux-gnu-ghc-*/lib/settings
RUN ./hadrian/build --flavour=quickest --bignum=native -V -j --prefix=/usr/local install
# Hadrian just outputs "gcc" as the name of gcc, without accounting for $TARGET. Manually fix up the paths:
RUN sed -e 's/"\(gcc\|g++\|ld\)"/"'"$TARGET"'-\1"/g' -i /usr/local/lib/$TARGET-ghc-*/lib/settings
# Due to an apparent cabal bug, we specify our options directly to cabal
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
ENV CABALOPTS="--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections -optc-fPIC;--with-compiler=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;-c;hashable -arch-native"
# Prebuild the dependencies
RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

View File

@@ -1,42 +0,0 @@
# This Docker file uses a custom QEmu fork with patches to follow execve
# to build all of ShellCheck emulated.
FROM ubuntu:25.04
ENV TARGETNAME linux.armv6hf
# Build QEmu with execve follow support
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
RUN apt-get install -y --no-install-recommends build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev python3-setuptools ca-certificates debootstrap
WORKDIR /qemu
RUN git clone --depth 1 https://github.com/koalaman/qemu .
RUN ./configure --static --disable-werror && cd build && ninja qemu-arm
ENV QEMU_EXECVE 1
# Convenience utility
COPY scutil /bin/scutil
COPY scutil /chroot/bin/scutil
RUN chmod +x /bin/scutil /chroot/bin/scutil
# Set up an armv6 userspace
WORKDIR /
RUN debootstrap --arch armhf --variant=minbase --foreign bookworm /chroot http://mirrordirector.raspbian.org/raspbian
RUN cp /qemu/build/qemu-arm /chroot/bin/qemu
RUN scutil emu /debootstrap/debootstrap --second-stage
# Install deps in the chroot
RUN scutil emu apt-get update
RUN scutil emu apt-get install -y --no-install-recommends ghc cabal-install
RUN scutil emu cabal update
# Finally we can build the current dependencies. This takes hours.
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--gcc-options;-Os -Wl,--gc-sections -ffunction-sections -fdata-sections"
# Generated with `cabal freeze --constraint 'hashable -arch-native'`
COPY cabal.project.freeze /chroot/etc
RUN IFS=";" && scutil install_from_freeze /chroot/etc/cabal.project.freeze emu cabal install $CABALOPTS
# Copy the build script
COPY build /chroot/bin
ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"]

View File

@@ -1,105 +0,0 @@
active-repositories: hackage.haskell.org:merge
constraints: any.Diff ==1.0.2,
any.OneTuple ==0.4.2,
any.QuickCheck ==2.16.0.0,
QuickCheck -old-random +templatehaskell,
any.StateVar ==1.2.2,
any.aeson ==2.2.3.0,
aeson +ordered-keymap,
any.ansi-terminal ==1.1.3,
ansi-terminal -example,
any.ansi-terminal-types ==1.1.3,
any.array ==0.5.4.0,
any.assoc ==1.1.1,
assoc -tagged,
any.base ==4.15.1.0,
any.base-orphans ==0.9.3,
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.colour ==2.3.6,
any.comonad ==5.0.9,
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.4,
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.3.0,
fgl +containers042,
any.filepath ==1.4.2.1,
any.foldable1-classes-compat ==0.1.2,
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.7.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.4,
integer-logarithms -check-bounds +integer-gmp,
any.mtl ==2.2.2,
any.network-uri ==2.6.4.2,
any.optparse-applicative ==0.19.0.0,
optparse-applicative +process,
any.parsec ==3.1.14.0,
any.pretty ==1.1.3.6,
any.prettyprinter ==1.7.1,
prettyprinter -buildreadme +text,
any.prettyprinter-ansi-terminal ==1.1.3,
any.primitive ==0.9.1.0,
any.process ==1.6.13.2,
any.random ==1.3.1,
any.regex-base ==0.94.0.3,
any.regex-tdfa ==1.3.2.4,
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.3.1,
splitmix -optimised-mixer,
any.stm ==2.5.0.0,
any.strict ==0.5.1,
any.tagged ==0.8.9,
tagged +deepseq +transformers,
any.tasty ==1.5.3,
tasty +unix,
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.1.0,
any.th-compat ==0.1.6,
any.these ==1.2.1,
any.time ==1.9.3,
any.time-compat ==1.9.8,
any.transformers ==0.5.6.2,
any.transformers-compat ==0.7.2,
transformers-compat -five +five-three -four +generic-deriving +mtl -three -two,
any.unbounded-delays ==0.1.1.1,
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.2.0,
vector +boundschecks -internalchecks -unsafechecks -wall,
any.vector-stream ==0.1.0.1,
any.witherable ==0.5
index-state: hackage.haskell.org 2025-07-22T18:12:16Z

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 -e ghc |
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,35 +0,0 @@
FROM ubuntu:25.04
ENV TARGETNAME=linux.riscv64
ENV TARGET=riscv64-linux-gnu
# Build dependencies
USER root
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y llvm-20 "gcc-$TARGET" "g++-$TARGET" ghc alex happy automake autoconf build-essential curl qemu-user-static
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.16.0.0/cabal-install-3.16.0.0-x86_64-linux-alpine3_20.tar.xz" | tar xJv -C /usr/local/bin && cabal update
# Build GHC
WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/9.12.2/ghc-9.12.2-src.tar.xz" | tar xJ --strip-components=1
RUN ./boot.source && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
# GHC fails to build if it can't encode non-ascii
ENV LC_CTYPE=C.utf8
# We have to do a binary-dist instead of a direct install, otherwise the targest won't have
# cross compilation prefixes in /usr/local/lib/aarch64-linux-gnu-ghc-*/lib/settings
RUN ./hadrian/build --flavour=quickest --bignum=native -V -j --prefix=/usr/local install
# Hadrian just outputs "gcc" as the name of gcc, without accounting for $TARGET. Manually fix up the paths:
RUN sed -e 's/"\(gcc\|g++\|ld\)"/"'"$TARGET"'-\1"/g' -i /usr/local/lib/$TARGET-ghc-*/lib/settings
# Due to an apparent cabal bug, we specify our options directly to cabal
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
ENV CABALOPTS="--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections -optc-fPIC;--with-compiler=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;-c;hashable -arch-native"
# Prebuild the dependencies
RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

View File

@@ -1,14 +0,0 @@
#!/bin/sh
set -xe
{
tar xzv --strip-components=1
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
"$TARGET-strip" -s "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
"qemu-${TARGET%%-*}-static" "$TARGETNAME/shellcheck" --version
} >&2
tar czv "$TARGETNAME"

View File

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

View File

@@ -1,30 +0,0 @@
FROM alpine:3.22
# alpine:3.16 (GHC 9.0.1): 5.8 megabytes (certs expired)
# alpine:3.17 (GHC 9.0.2): 15.0 megabytes (certs expired)
# alpine:3.18 (GHC 9.4.4): 29.0 megabytes (certs expired)
# alpine:3.19 (GHC 9.4.7): 29.0 megabytes (certs expired)
# alpine:3.20 (GHC 9.8.2): 16.0 megabytes
# alpine:3.21 (GHC 9.8.2): 16.0 megabytes
# alpine:3.22 (GHC 9.8.2): 16.0 megabytes
ENV TARGETNAME=linux.x86_64
# Install GHC and cabal
USER root
RUN apk add ghc cabal g++ libffi-dev curl bash gmp gmp-static
# Cabal has failed to cache if options are not specified on the command line,
# so do that explicitly.
ENV CABALOPTS="--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections"
# Verify that we have the certificates in place to successfully update cabal
RUN cabal update && rm -rf ~/.cabal
# Other archs pre-build dependencies here, but this one doesn't to detect ecosystem movement
RUN true
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

View File

@@ -1,34 +0,0 @@
FROM ubuntu:25.04
ENV TARGETNAME=windows.x86_64
# We don't need wine32, even though it complains
USER root
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y curl busybox wine winbind xz-utils
# Fetch Windows version, will be available under z:\haskell
WORKDIR /haskell
# 9.12.2 produces a 37M binary
# 9.0.2 produces a 28M binary
# 8.10.4 produces a 16M binary
# We don't want to be stuck on old versions forever though, so just go with the latest version
RUN curl -L "https://downloads.haskell.org/~ghc/9.12.2/ghc-9.12.2-x86_64-unknown-mingw32.tar.xz" | tar xJ --strip-components=1
# Fetch dependencies
WORKDIR /haskell/bin
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.16.0.0/cabal-install-3.16.0.0-x86_64-windows.zip" | busybox unzip -
RUN curl -L "https://curl.se/windows/dl-8.15.0_2/curl-8.15.0_2-win64-mingw.zip" | busybox unzip - && mv curl-*-win64-mingw/bin/* .
RUN wine /haskell/bin/cabal.exe update
ENV WINEPATH=/haskell/bin:/haskell/mingw/bin
# None of these actually seem to have an effect on GHC on Windows anymore,
# but we'll leave them in place anyways.
ENV CABALOPTS="--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections"
# Precompile some deps to speed up later builds
RUN IFS=';' && wine /haskell/bin/cabal.exe install --lib --dependencies-only $CABALOPTS ShellCheck
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

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,
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*
: 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.
**--rcfile** *RCFILE*
: Prefer the specified configuration file over searching for one
in the default locations.
**-o**\ *NAME1*[,*NAME2*...],\ **--enable=***NAME1*[,*NAME2*...]
: Enable optional checks. The special name *all* enables all of them.
@@ -97,11 +85,11 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
**-s**\ *shell*,\ **--shell=***shell*
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash*, *ksh*,
and *busybox*.
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
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
POSIX `sh` (not the system's), and will warn of portability issues.
shebang, or `.bash/.bats/.dash/.ksh/.ebuild/.eclass` extension, in that
order. *sh* refers to POSIX `sh` (not the system's), and will warn of
portability issues.
**-S**\ *SEVERITY*,\ **--severity=***severity*
@@ -256,12 +244,6 @@ Valid keys are:
: Enable an optional check by name, as listed with **--list-optional**.
Only file-wide `enable` directives are considered.
**extended-analysis**
: Set to true/false to enable/disable dataflow analysis. Specifying
`# shellcheck extended-analysis=false` in particularly large (2000+ line)
auto-generated scripts will reduce ShellCheck's resource usage at the
expense of certain checks. Extended analysis is enabled by default.
**external-sources**
: Set to `true` in `.shellcheckrc` to always allow ShellCheck to open
arbitrary files from 'source' statements (the way most tools do).
@@ -317,7 +299,7 @@ Here is an example `.shellcheckrc`:
disable=SC2236
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
Windows. Only the first file found will be used.
@@ -397,10 +379,10 @@ long list of wonderful contributors.
# COPYRIGHT
Copyright 2012-2025, Vidar Holen and contributors.
Copyright 2012-2022, Vidar Holen and contributors.
Licensed under the GNU General Public License version 3 or later,
see https://gnu.org/licenses/gpl.html
# SEE ALSO
sh(1), bash(1), dash(1), ksh(1)
sh(1) bash(1)

View File

@@ -21,6 +21,7 @@ import qualified ShellCheck.Analyzer
import ShellCheck.Checker
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.PortageVariables
import ShellCheck.Regex
import qualified ShellCheck.Formatter.CheckStyle
@@ -76,8 +77,7 @@ data Options = Options {
externalSources :: Bool,
sourcePaths :: [FilePath],
formatterOptions :: FormatterOptions,
minSeverity :: Severity,
rcfile :: Maybe FilePath
minSeverity :: Severity
}
defaultOptions = Options {
@@ -87,8 +87,7 @@ defaultOptions = Options {
formatterOptions = newFormatterOptions {
foColorOption = ColorAuto
},
minSeverity = StyleC,
rcfile = Nothing
minSeverity = StyleC
}
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
@@ -102,8 +101,6 @@ options = [
(ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings",
Option "e" ["exclude"]
(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"]
(ReqArg (Flag "format") "FORMAT") $
"Output format (" ++ formatList ++ ")",
@@ -111,9 +108,6 @@ options = [
(NoArg $ Flag "list-optional" "true") "List checks disabled by default",
Option "" ["norc"]
(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"]
(ReqArg (Flag "enable") "check1,check2..")
"List of optional checks to enable (or 'all')",
@@ -122,7 +116,7 @@ options = [
"Specify path when looking for sourced files (\"SCRIPTDIR\" for script's dir)",
Option "s" ["shell"]
(ReqArg (Flag "shell") "SHELLNAME")
"Specify dialect (sh, bash, dash, ksh, busybox)",
"Specify dialect (sh, bash, dash, ksh)",
Option "S" ["severity"]
(ReqArg (Flag "severity") "SEVERITY")
"Minimum severity of errors to consider (error, warning, info, style)",
@@ -259,9 +253,9 @@ runFormatter sys format options files = do
else SomeProblems
parseEnum name value list =
case lookup value list of
Just value -> return value
Nothing -> do
case filter ((== value) . fst) list of
[(name, value)] -> return value
[] -> do
printErr $ "Unknown value for --" ++ name ++ ". " ++
"Valid options are: " ++ (intercalate ", " $ map fst list)
throwError SupportFailure
@@ -374,11 +368,6 @@ parseOption flag options =
}
}
Flag "rcfile" str -> do
return options {
rcfile = Just str
}
Flag "enable" value ->
let cs = checkSpec options in return options {
checkSpec = cs {
@@ -386,14 +375,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'
Flag "format" _ -> return options
@@ -411,23 +392,17 @@ parseOption flag options =
throwError SyntaxFailure
return (Prelude.read num :: Integer)
parseBool str = do
case str of
"true" -> return True
"false" -> return False
_ -> do
printErr $ "Invalid boolean, expected true/false: " ++ str
throwError SyntaxFailure
ioInterface :: Options -> [FilePath] -> IO (SystemInterface IO)
ioInterface options files = do
inputs <- mapM normalize files
cache <- newIORef emptyCache
configCache <- newIORef ("", Nothing)
portageVars <- newIORef Nothing
return (newSystemInterface :: SystemInterface IO) {
siReadFile = get cache inputs,
siFindSource = findSourceFile inputs (sourcePaths options),
siGetConfig = getConfig configCache
siGetConfig = getConfig configCache,
siGetPortageVariables = getOrLoadPortage portageVars
}
where
emptyCache :: Map.Map FilePath String
@@ -469,33 +444,18 @@ ioInterface options files = do
fallback :: FilePath -> IOException -> IO FilePath
fallback path _ = return path
-- Returns the name and contents of .shellcheckrc for the given file
getConfig cache filename =
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
let dir = takeDirectory path
(previousPath, result) <- readIORef cache
if dir == previousPath
then return result
else do
paths <- getConfigPaths dir
result <- findConfig paths
writeIORef cache (dir, result)
return result
getConfig cache filename = do
path <- normalize filename
let dir = takeDirectory path
(previousPath, result) <- readIORef cache
if dir == previousPath
then return result
else do
paths <- getConfigPaths dir
result <- findConfig paths
writeIORef cache (dir, result)
return result
findConfig paths =
case paths of
@@ -533,7 +493,7 @@ ioInterface options files = do
where
handler :: FilePath -> IOException -> IO (String, Bool)
handler file err = do
hPutStrLn stderr $ file ++ ": " ++ show err
putStrLn $ file ++ ": " ++ show err
return ("", True)
andM a b arg = do
@@ -566,6 +526,21 @@ ioInterface options files = do
("SCRIPTDIR":rest) -> joinPath (scriptdir:rest)
_ -> str
getOrLoadPortage cache = do
x <- readIORef cache
case x of
Just m -> do
return m
Nothing -> do
vars <- readPortageVariables `catch` handler
writeIORef cache $ Just vars
return vars
where
handler :: IOException -> IO (Map.Map String [String])
handler e = do
hPutStrLn stderr $ "Error finding portage repos, eclass definitions will be ignored: " ++ show e
return $ Map.empty
inputFile file = do
(handle, shouldCache) <-
if file == "-"

View File

@@ -23,7 +23,7 @@ description: |
# snap connect shellcheck:removable-media
version: git
base: core24
base: core18
grade: stable
confinement: strict
@@ -40,18 +40,17 @@ parts:
source: .
build-packages:
- cabal-install
- squid
override-build: |
# Give ourselves enough memory to build
fallocate -l 2G /tmp/swap
chmod 0600 /tmp/swap
mkswap /tmp/swap
if ! swapon /tmp/swap; then
echo "Could not enable swap file, continuing anyway"
rm /tmp/swap
fi
cabal update
# See comments in .snapsquid.conf
[ "$http_proxy" ] && {
squid3 -f .snapsquid.conf
export http_proxy="http://localhost:8888"
sleep 3
}
cabal sandbox init
cabal update || cat /var/log/squid/*
cabal install -j
install -d "${CRAFT_PART_INSTALL}/usr/bin"
install --strip ~/.cabal/bin/shellcheck "${CRAFT_PART_INSTALL}/usr/bin"
install -d $SNAPCRAFT_PART_INSTALL/usr/bin
install .cabal-sandbox/bin/shellcheck $SNAPCRAFT_PART_INSTALL/usr/bin

View File

@@ -31,7 +31,6 @@ newtype Id = Id Int deriving (Show, Eq, Ord, Generic, NFData)
data Quoted = Quoted | Unquoted deriving (Show, Eq)
data Dashed = Dashed | Undashed deriving (Show, Eq)
data Piped = Piped | Unpiped deriving (Show, Eq)
data AssignmentMode = Assign | Append deriving (Show, Eq)
newtype FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
newtype FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
@@ -85,7 +84,7 @@ data InnerToken t =
| Inner_T_DollarDoubleQuoted [t]
| Inner_T_DollarExpansion [t]
| Inner_T_DollarSingleQuoted String
| Inner_T_DollarBraceCommandExpansion Piped [t]
| Inner_T_DollarBraceCommandExpansion [t]
| Inner_T_Done
| Inner_T_DoubleQuoted [t]
| Inner_T_EOF
@@ -139,7 +138,7 @@ data InnerToken t =
| Inner_T_WhileExpression [t] [t]
| Inner_T_Annotation [Annotation] t
| Inner_T_Pipe String
| Inner_T_CoProc (Maybe Token) t
| Inner_T_CoProc (Maybe String) t
| Inner_T_CoProcBody t
| Inner_T_Include t
| Inner_T_SourceCommand t t
@@ -153,7 +152,6 @@ data Annotation =
| ShellOverride String
| SourcePath String
| ExternalSources Bool
| ExtendedAnalysis Bool
deriving (Show, Eq)
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
@@ -207,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_Array id t = OuterToken id (Inner_T_Array t)
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 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)
@@ -229,7 +227,7 @@ pattern T_CoProc id var body = OuterToken id (Inner_T_CoProc var body)
pattern TC_Or id typ str t1 t2 = OuterToken id (Inner_TC_Or typ str t1 t2)
pattern TC_Unary id typ op token = OuterToken id (Inner_TC_Unary typ op token)
pattern T_DollarArithmetic id c = OuterToken id (Inner_T_DollarArithmetic c)
pattern T_DollarBraceCommandExpansion id pipe list = OuterToken id (Inner_T_DollarBraceCommandExpansion pipe list)
pattern T_DollarBraceCommandExpansion id list = OuterToken id (Inner_T_DollarBraceCommandExpansion list)
pattern T_DollarBraced id braced op = OuterToken id (Inner_T_DollarBraced braced op)
pattern T_DollarBracket id c = OuterToken id (Inner_T_DollarBracket c)
pattern T_DollarDoubleQuoted id list = OuterToken id (Inner_T_DollarDoubleQuoted list)
@@ -260,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_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
OuterToken _ a == OuterToken _ b = a == b

View File

@@ -31,14 +31,11 @@ import Data.Functor
import Data.Functor.Identity
import Data.List
import Data.Maybe
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map
import Numeric (showHex)
import Test.QuickCheck
arguments (T_SimpleCommand _ _ (cmd:args)) = args
-- Is this a type of loop?
isLoop t = case t of
T_WhileExpression {} -> True
@@ -158,10 +155,9 @@ isFlag token =
_ -> False
-- Is this token a flag where the - is unquoted?
isUnquotedFlag token =
case getLeadingUnquotedString token of
Just ('-':_) -> True
_ -> False
isUnquotedFlag token = fromMaybe False $ do
str <- getLeadingUnquotedString token
return $ "-" `isPrefixOf` str
-- getGnuOpts "erd:u:" will parse a list of arguments tokens like `read`
-- -re -d : -u 3 bar
@@ -446,12 +442,6 @@ getLiteralStringExt more = g
-- Is this token a string literal?
isLiteral t = isJust $ getLiteralString t
-- Is this token a string literal number?
isLiteralNumber t = fromMaybe False $ do
s <- getLiteralString t
guard $ all isDigit s
return True
-- Escape user data for messages.
-- Messages generally avoid repeating user data, but sometimes it's helpful.
e4m = escapeForMessage
@@ -554,17 +544,16 @@ getCommandNameAndToken direct t = fromMaybe (Nothing, t) $ do
return t
_ -> fail ""
-- If a command substitution is a single command, get its name.
-- $(date +%s) = Just "date"
getCommandNameFromExpansion :: Token -> Maybe String
getCommandNameFromExpansion t =
-- If a command substitution is a single SimpleCommand, return it.
getSimpleCommandFromExpansion :: Token -> Maybe Token
getSimpleCommandFromExpansion t =
case t of
T_DollarExpansion _ [c] -> extract c
T_Backticked _ [c] -> extract c
T_DollarBraceCommandExpansion _ _ [c] -> extract c
T_DollarBraceCommandExpansion _ [c] -> extract c
_ -> Nothing
where
extract (T_Pipeline _ _ [cmd]) = getCommandName cmd
extract (T_Pipeline _ _ [c]) = getCommand c
extract _ = Nothing
-- Get the basename of a token representing a command
@@ -572,6 +561,10 @@ getCommandBasename = fmap basename . getCommandName
basename = reverse . takeWhile (/= '/') . reverse
-- Get the arguments to a command
arguments (T_SimpleCommand _ _ (cmd:args)) = args
arguments t = maybe [] arguments (getCommand t)
isAssignment t =
case t of
T_Redirecting _ _ w -> isAssignment w
@@ -616,7 +609,7 @@ getCommandSequences t =
T_Annotation _ _ t -> getCommandSequences t
T_DollarExpansion _ cmds -> [cmds]
T_DollarBraceCommandExpansion _ _ cmds -> [cmds]
T_DollarBraceCommandExpansion _ cmds -> [cmds]
T_Backticked _ cmds -> [cmds]
_ -> []
@@ -766,8 +759,8 @@ prop_executableFromShebang6 = executableFromShebang "/usr/bin/env --split-string
prop_executableFromShebang7 = executableFromShebang "/usr/bin/env --split-string bash -x" == "bash"
prop_executableFromShebang8 = executableFromShebang "/usr/bin/env --split-string foo=bar bash -x" == "bash"
prop_executableFromShebang9 = executableFromShebang "/usr/bin/env foo=bar dash" == "dash"
prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "busybox sh"
prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "busybox ash"
prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "ash"
prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "ash"
-- Get the shell executable from a string like '/usr/bin/env bash'
executableFromShebang :: String -> String
@@ -784,8 +777,7 @@ executableFromShebang = shellFor
[x] -> basename x
(first:second:args) | basename first == "busybox" ->
case basename second of
"sh" -> "busybox sh"
"ash" -> "busybox ash"
"sh" -> "ash" -- busybox sh is ash
x -> x
(first:args) | basename first == "env" ->
fromEnvArgs args
@@ -865,7 +857,8 @@ getBracedModifier s = headOrDefault "" $ do
-- Get the variables from indices like ["x", "y"] in ${var[x+y+1]}
prop_getIndexReferences1 = getIndexReferences "var[x+y+1]" == ["x", "y"]
getIndexReferences s = fromMaybe [] $ do
index:_ <- matchRegex re s
match <- matchRegex re s
index <- match !!! 0
return $ matchAllStrings variableNameRegex index
where
re = mkRegex "(\\[.*\\])"
@@ -876,7 +869,8 @@ prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
getOffsetReferences mods = fromMaybe [] $ do
-- if mods start with [, then drop until ]
_:offsets:_ <- matchRegex re mods
match <- matchRegex re mods
offsets <- match !!! 1
return $ matchAllStrings variableNameRegex offsets
where
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
@@ -903,7 +897,10 @@ getUnmodifiedParameterExpansion t =
_ -> Nothing
--- A list of the element and all its parents up to the root node.
getPath tree = NE.unfoldr $ \t -> (t, Map.lookup (getId t) tree)
getPath tree t = t :
case Map.lookup (getId t) tree of
Nothing -> []
Just parent -> getPath tree parent
isClosingFileOp op =
case op of
@@ -916,11 +913,20 @@ getEnableDirectives root =
T_Annotation _ list _ -> [s | EnableComment s <- list]
_ -> []
getExtendedAnalysisDirective :: Token -> Maybe Bool
getExtendedAnalysisDirective root =
case root of
T_Annotation _ list _ -> listToMaybe $ [s | ExtendedAnalysis s <- list]
commandExpansionShouldBeSplit t = do
cmd <- getSimpleCommandFromExpansion t
name <- getCommandName cmd
case () of
-- Should probably be split
_ | name `elem` ["seq", "pgrep"] -> return True
-- Portage macros that return a single word or nothing
_ | name `elem` ["usev", "use_with", "use_enable"] -> return True
-- Portage macros that are fine as long as the arguments have no spaces
_ | name `elem` ["usex", "meson_use", "meson_feature"] -> do
return . not $ any (' ' `elem`) $ map (getLiteralStringDef " ") $ arguments cmd
_ -> Nothing
return []
runTests = $quickCheckAll

File diff suppressed because it is too large Load Diff

View File

@@ -31,14 +31,14 @@ import qualified ShellCheck.Checks.ShellSupport
-- TODO: Clean up the cruft this is layered on
analyzeScript :: AnalysisSpec -> AnalysisResult
analyzeScript spec = newAnalysisResult {
arComments =
filterByAnnotation spec params . nub $
runChecker params (checkers spec params)
}
where
params = makeParameters spec
analyzeScript :: Monad m => SystemInterface m -> AnalysisSpec -> m AnalysisResult
analyzeScript sys spec = do
params <- makeParameters sys spec
return $ newAnalysisResult {
arComments =
filterByAnnotation spec params . nub $
runChecker params (checkers spec params)
}
checkers spec params = mconcat $ map ($ params) [
ShellCheck.Analytics.checker spec,

View File

@@ -41,7 +41,6 @@ import Data.Char
import Data.List
import Data.Maybe
import Data.Semigroup
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map
import Test.QuickCheck.All (forAllProperties)
@@ -89,8 +88,8 @@ data Parameters = Parameters {
hasSetE :: Bool,
-- Whether this script has 'set -o pipefail' anywhere.
hasPipefail :: Bool,
-- Whether this script has 'shopt -s execfail' anywhere.
hasExecfail :: Bool,
-- Whether this script is an Ebuild file.
isPortage :: Bool,
-- A linear (bad) analysis of data flow
variableFlow :: [StackData],
-- A map from Id to Token
@@ -106,9 +105,12 @@ data Parameters = Parameters {
-- map from token id to start and end position
tokenPositions :: Map.Map Id (Position, Position),
-- Result from Control Flow Graph analysis (including data flow analysis)
cfgAnalysis :: Maybe CF.CFGAnalysis
cfgAnalysis :: CF.CFGAnalysis,
-- A set of additional variables known to be set (TODO: make this more general?)
additionalKnownVariables :: [String]
} deriving (Show)
-- TODO: Cache results of common AST ops here
data Cache = Cache {}
@@ -155,7 +157,7 @@ producesComments c s = do
let pr = pScript s
prRoot pr
let spec = defaultSpec pr
let params = makeParameters spec
let params = runIdentity $ makeParameters (mockedSystemInterface []) spec
return . not . null $ filterByAnnotation spec params $ runChecker params c
makeComment :: Severity -> Id -> Code -> String -> TokenComment
@@ -199,52 +201,58 @@ makeCommentWithFix severity id code str fix =
}
in force withFix
-- makeParameters :: CheckSpec -> Parameters
makeParameters spec = params
makeParameters :: Monad m => SystemInterface m -> AnalysisSpec -> m Parameters
makeParameters sys spec = do
extraVars <-
if asIsPortage spec
then do
vars <- siGetPortageVariables sys
let classes = getInheritedEclasses root
return $ concatMap (\c -> Map.findWithDefault [] c vars) classes
else
return []
return $ makeParams extraVars
where
extendedAnalysis = fromMaybe True $ msum [asExtendedAnalysis spec, getExtendedAnalysisDirective root]
params = Parameters {
rootNode = root,
shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec,
hasSetE = containsSetE root,
hasLastpipe =
case shellType params of
Bash -> isOptionSet "lastpipe" root
Dash -> False
BusyboxSh -> False
Sh -> False
Ksh -> True,
hasInheritErrexit =
case shellType params of
Bash -> isOptionSet "inherit_errexit" root
Dash -> True
BusyboxSh -> True
Sh -> True
Ksh -> False,
hasPipefail =
case shellType params of
Bash -> isOptionSet "pipefail" root
Dash -> True
BusyboxSh -> isOptionSet "pipefail" root
Sh -> True
Ksh -> isOptionSet "pipefail" root,
hasExecfail =
case shellType params of
Bash -> isOptionSet "execfail" root
_ -> False,
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
idMap = getTokenMap root,
parentMap = getParentTree root,
variableFlow = getVariableFlow params root,
tokenPositions = asTokenPositions spec,
cfgAnalysis = do
guard extendedAnalysis
return $ CF.analyzeControlFlow cfParams root
}
cfParams = CF.CFGParameters {
CF.cfLastpipe = hasLastpipe params,
CF.cfPipefail = hasPipefail params
}
shell = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec
makeParams extraVars = params
where
params = Parameters {
rootNode = root,
shellType = shell,
hasSetE = containsSetE root,
hasLastpipe =
case shellType params of
Bash -> isOptionSet "lastpipe" root
Dash -> False
Sh -> False
Ksh -> True,
hasInheritErrexit =
case shellType params of
Bash -> isOptionSet "inherit_errexit" root
Dash -> True
Sh -> True
Ksh -> False,
hasPipefail =
case shellType params of
Bash -> isOptionSet "pipefail" root
Dash -> True
Sh -> True
Ksh -> isOptionSet "pipefail" root,
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
isPortage = asIsPortage spec,
idMap = getTokenMap root,
parentMap = getParentTree root,
variableFlow = getVariableFlow params root,
tokenPositions = asTokenPositions spec,
cfgAnalysis = CF.analyzeControlFlow cfParams root,
additionalKnownVariables = extraVars
}
cfParams = CF.CFGParameters {
CF.cfLastpipe = hasLastpipe params,
CF.cfPipefail = hasPipefail params,
CF.cfAdditionalInitialVariables = additionalKnownVariables params
}
root = asScript spec
@@ -298,8 +306,8 @@ prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh
prop_determineShell9 = determineShellTest "#!/bin/env -S dash -x" == Dash
prop_determineShell10 = determineShellTest "#!/bin/env --split-string= dash -x" == Dash
prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == BusyboxSh -- busybox sh is a specific shell, not posix sh
prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == BusyboxSh
prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == Dash -- busybox sh is a specific shell, not posix sh
prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == Dash
determineShellTest = determineShellTest' Nothing
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
@@ -347,14 +355,14 @@ isQuoteFree = isQuoteFreeNode False
isQuoteFreeNode strict shell tree t =
isQuoteFreeElement t ||
(fromMaybe False $ msum $ map isQuoteFreeContext $ NE.tail $ getPath tree t)
(fromMaybe False $ msum $ map isQuoteFreeContext $ drop 1 $ getPath tree t)
where
-- Is this node self-quoting in itself?
isQuoteFreeElement t =
case t of
T_Assignment id _ _ _ _ -> assignmentIsQuoting id
T_FdRedirect {} -> True
_ -> False
T_Assignment {} -> assignmentIsQuoting t
T_FdRedirect {} -> True
_ -> False
-- Are any subnodes inherently self-quoting?
isQuoteFreeContext t =
@@ -364,7 +372,7 @@ isQuoteFreeNode strict shell tree t =
TC_Binary _ DoubleBracket _ _ _ -> return True
TA_Sequence {} -> return True
T_Arithmetic {} -> return True
T_Assignment id _ _ _ _ -> return $ assignmentIsQuoting id
T_Assignment {} -> return $ assignmentIsQuoting t
T_Redirecting {} -> return False
T_DoubleQuoted _ _ -> return True
T_DollarDoubleQuoted _ _ -> return True
@@ -379,11 +387,11 @@ isQuoteFreeNode strict shell tree t =
-- Check whether this assignment is self-quoting due to being a recognized
-- assignment passed to a Declaration Utility. This will soon be required
-- by POSIX: https://austingroupbugs.net/view.php?id=351
assignmentIsQuoting id = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand id)
assignmentIsQuoting t = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand t)
shellParsesParamsAsAssignments = shell /= Sh
-- Is this assignment a parameter to a command like export/typeset/etc?
isAssignmentParamToCommand id =
isAssignmentParamToCommand (T_Assignment id _ _ _ _) =
case Map.lookup id tree of
Just (T_SimpleCommand _ _ (_:args)) -> id `elem` (map getId args)
_ -> False
@@ -409,7 +417,7 @@ isParamTo tree cmd =
-- Get the parent command (T_Redirecting) of a Token, if any.
getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token
getClosestCommand tree t =
findFirst findCommand $ NE.toList $ getPath tree t
findFirst findCommand $ getPath tree t
where
findCommand t =
case t of
@@ -423,7 +431,7 @@ getClosestCommandM t = do
return $ getClosestCommand (parentMap params) t
-- Is the token used as a command name (the first word in a T_SimpleCommand)?
usedAsCommandName tree token = go (getId token) (NE.tail $ getPath tree token)
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
where
go currentId (T_NormalWord id [word]:rest)
| currentId == getId word = go id rest
@@ -440,9 +448,7 @@ getPathM t = do
return $ getPath (parentMap params) t
isParentOf tree parent child =
any (\t -> parentId == getId t) (getPath tree child)
where
parentId = getId parent
elem (getId parent) . map getId $ getPath tree child
parents params = getPath (parentMap params)
@@ -541,9 +547,7 @@ getModifiedVariables t =
T_BatsTest {} -> [
(t, t, "lines", DataArray SourceExternal),
(t, t, "status", DataString SourceInteger),
(t, t, "output", DataString SourceExternal),
(t, t, "stderr", DataString SourceExternal),
(t, t, "stderr_lines", DataArray SourceExternal)
(t, t, "output", DataString SourceExternal)
]
-- Count [[ -v foo ]] as an "assignment".
@@ -565,12 +569,8 @@ getModifiedVariables t =
T_FdRedirect _ ('{':var) op -> -- {foo}>&2 modifies foo
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
T_CoProc _ Nothing _ ->
[(t, t, "COPROC", DataArray SourceInteger)]
T_CoProc _ (Just token) _ -> do
name <- maybeToList $ getLiteralString token
[(t, t, name, DataArray SourceInteger)]
T_CoProc _ name _ ->
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
--Points to 'for' rather than variable
T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)]
@@ -604,6 +604,17 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
head:_ -> map (\x -> (base, head, x)) $ getVariablesFromLiteralToken head
_ -> []
"alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token]
-- tc-export makes a list of toolchain variables available, similar to export.
-- Usage tc-export CC CXX
"tc-export" -> concatMap getReference rest
-- tc-export_build_env exports the listed variables plus a bunch of BUILD_XX variables.
-- Usage tc-export_build_env BUILD_CC
"tc-export_build_env" ->
concatMap getReference rest
++ [ (base, base, v) | v <- portageBuildEnvVariables ]
_ -> []
where
forDeclare =
@@ -675,6 +686,16 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
"DEFINE_integer" -> maybeToList $ getFlagVariable rest
"DEFINE_string" -> maybeToList $ getFlagVariable rest
"tc-export" -> concatMap getModifierParamString rest
-- tc-export_build_env exports the listed variables plus a bunch of BUILD_XX variables.
-- Usage tc-export_build_env BUILD_CC
"tc-export_build_env" ->
concatMap getModifierParamString rest
++ [ (base, base, var, DataString $ SourceExternal) |
var <- ["BUILD_" ++ x, x ++ "_FOR_BUILD" ],
x <- portageBuildEnvVariables ]
_ -> []
where
flags = map snd $ getAllFlags base
@@ -832,7 +853,7 @@ getReferencedVariables parents t =
return (context, token, getBracedReference str)
isArithmeticAssignment t = case getPath parents t of
this NE.:| TA_Assignment _ "=" lhs _ :_ -> lhs == t
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
_ -> False
isDereferencingBinaryOp = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
@@ -914,6 +935,15 @@ supportsArrays Bash = True
supportsArrays Ksh = True
supportsArrays _ = False
-- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh)
isBashLike :: Parameters -> Bool
isBashLike params =
case shellType params of
Bash -> True
Ksh -> True
Dash -> False
Sh -> False
isTrueAssignmentSource c =
case c of
DataString SourceChecked -> False
@@ -931,14 +961,13 @@ modifiesVariable params token name =
Assignment (_, _, n, source) -> isTrueAssignmentSource source && n == name
_ -> 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
-- Ebuild files inherit eclasses using 'inherit myclass1 myclass2'
getInheritedEclasses :: Token -> [String]
getInheritedEclasses root = execWriter $ doAnalysis findInheritedEclasses root
where
findInheritedEclasses cmd
| cmd `isCommand` "inherit" = tell $ catMaybes $ getLiteralString <$> (arguments cmd)
findInheritedEclasses _ = return ()
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -51,7 +51,6 @@ import Control.Monad.Identity
import Data.Array.Unboxed
import Data.Array.ST
import Data.List hiding (map)
import qualified Data.List.NonEmpty as NE
import Data.Maybe
import qualified Data.Map as M
import qualified Data.Set as S
@@ -112,8 +111,8 @@ data CFEdge =
-- Actions we track
data CFEffect =
CFSetProps (Maybe Scope) String (S.Set CFVariableProp)
| CFUnsetProps (Maybe Scope) String (S.Set CFVariableProp)
CFSetProps Scope String (S.Set CFVariableProp)
| CFUnsetProps Scope String (S.Set CFVariableProp)
| CFReadVariable String
| CFWriteVariable String CFValue
| CFWriteGlobal String CFValue
@@ -168,7 +167,9 @@ data CFGParameters = CFGParameters {
-- Whether the last element in a pipeline runs in the current shell
cfLastpipe :: Bool,
-- Whether all elements in a pipeline count towards the exit status
cfPipefail :: Bool
cfPipefail :: Bool,
-- Additional variables to consider defined
cfAdditionalInitialVariables :: [String]
}
data CFGResult = CFGResult {
@@ -295,19 +296,19 @@ removeUnnecessaryStructuralNodes (nodes, edges, mapping, association) =
regularEdges = filter isRegularEdge edges
inDegree = counter $ map (\(from,to,_) -> from) 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
edgesToCollapse = S.fromList $ filter filterEdges regularEdges
remapping :: M.Map Node Node
remapping = M.fromList $ map orderEdge $ S.toList edgesToCollapse
recursiveRemapping = M.mapWithKey (\c _ -> recursiveLookup remapping c) remapping
remapping = foldl' (\m (new, old) -> M.insert old new m) M.empty $ map orderEdge $ S.toList edgesToCollapse
recursiveRemapping = M.fromList $ map (\c -> (c, recursiveLookup remapping c)) $ M.keys remapping
filterEdges (a,b,_) =
a `S.member` candidateNodes && b `S.member` candidateNodes
orderEdge (a,b,_) = if a < b then (b,a) else (a,b)
counter = M.fromListWith (+) . map (\key -> (key, 1))
orderEdge (a,b,_) = if a < b then (a,b) else (b,a)
counter = foldl' (\map key -> M.insertWith (+) key 1 map) M.empty
isRegularEdge (_, _, CFEFlow) = True
isRegularEdge _ = False
@@ -317,6 +318,11 @@ removeUnnecessaryStructuralNodes (nodes, edges, mapping, association) =
Nothing -> node
Just x -> recursiveLookup map x
isStructural (node, label) =
case label of
CFStructuralNode -> True
_ -> False
isLinear node =
M.findWithDefault 0 node inDegree == 1
&& M.findWithDefault 0 node outDegree == 1
@@ -490,7 +496,7 @@ build t = do
TA_Binary _ _ a b -> sequentially [a,b]
TA_Expansion _ list -> sequentially list
TA_Sequence _ list -> sequentially list
TA_Parenthesis _ t -> build t
TA_Parentesis _ t -> build t
TA_Trinary _ cond a b -> do
condition <- build cond
@@ -574,7 +580,7 @@ build t = do
T_Array _ list -> sequentially list
T_Assignment {} -> buildAssignment Nothing t
T_Assignment {} -> buildAssignment DefaultScope t
T_Backgrounded id body -> do
start <- newStructuralNode
@@ -610,15 +616,15 @@ build t = do
T_CaseExpression id t [] -> build t
T_CaseExpression id t list@(hd:tl) -> do
T_CaseExpression id t list -> do
start <- newStructuralNode
token <- build t
branches <- mapM buildBranch (hd NE.:| tl)
branches <- mapM buildBranch list
end <- newStructuralNode
let neighbors = zip (NE.toList branches) $ NE.tail branches
let (_, firstCond, _) = NE.head branches
let (_, lastCond, lastBody) = NE.last branches
let neighbors = zip branches $ tail branches
let (_, firstCond, _) = head branches
let (_, lastCond, lastBody) = last branches
linkRange start token
linkRange token firstCond
@@ -668,18 +674,10 @@ build t = do
status <- newNodeRange $ CFSetExitCode id
linkRange cond status
T_CoProc id maybeNameToken t -> do
-- If unspecified, "COPROC". If not a constant string, Nothing.
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
T_CoProc id maybeName t -> do
let name = fromMaybe "COPROC" maybeName
start <- newStructuralNode
parent <- newNodeRange parentNode
parent <- newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable name CFValueArray
child <- subshell id "coproc" $ build t
end <- newNodeRange $ CFSetExitCode id
@@ -716,9 +714,6 @@ build t = do
linkRange totalRead result
else return totalRead
T_DollarBraceCommandExpansion id _ body ->
sequentially body
T_DoubleQuoted _ list -> sequentially list
T_DollarExpansion id body ->
@@ -864,8 +859,8 @@ build t = do
status <- newNodeRange (CFSetExitCode id)
linkRange assignments status
T_SimpleCommand id vars (cmd:args) ->
handleCommand t vars (cmd NE.:| args) $ getUnquotedLiteral cmd
T_SimpleCommand id vars list@(cmd:_) ->
handleCommand t vars list $ getUnquotedLiteral cmd
T_SingleQuoted _ _ -> none
@@ -894,9 +889,7 @@ build t = do
T_Less _ -> none
T_ParamSubSpecialChar _ _ -> none
x -> do
error ("Unimplemented: " ++ show x) -- STRIP
none
x -> error ("Unimplemented: " ++ show x)
-- Still in `where` clause
forInHelper id name words body = do
@@ -932,8 +925,8 @@ handleCommand cmd vars args literalCmd = do
-- TODO: Handle assignments in declaring commands
case literalCmd of
Just "exit" -> regularExpansion vars (NE.toList args) $ handleExit
Just "return" -> regularExpansion vars (NE.toList args) $ handleReturn
Just "exit" -> regularExpansion vars args $ handleExit
Just "return" -> regularExpansion vars args $ handleReturn
Just "unset" -> regularExpansionWithStatus vars args $ handleUnset args
Just "declare" -> handleDeclare args
@@ -956,14 +949,14 @@ handleCommand cmd vars args literalCmd = do
-- This will mostly behave like 'command' but ok
Just "builtin" ->
case args of
_ NE.:| [] -> regular
(_ NE.:| newcmd:newargs) ->
handleCommand newcmd vars (newcmd NE.:| newargs) $ getLiteralString newcmd
[_] -> regular
(_:newargs@(newcmd:_)) ->
handleCommand newcmd vars newargs $ getLiteralString newcmd
Just "command" ->
case args of
_ NE.:| [] -> regular
(_ NE.:| newcmd:newargs) ->
handleOthers (getId newcmd) vars (newcmd NE.:| newargs) $ getLiteralString newcmd
[_] -> regular
(_:newargs@(newcmd:_)) ->
handleOthers (getId newcmd) vars newargs $ getLiteralString newcmd
_ -> regular
where
@@ -991,7 +984,7 @@ handleCommand cmd vars args literalCmd = do
unreachable <- newNode CFUnreachable
return $ Range ret unreachable
handleUnset (cmd NE.:| args) = do
handleUnset (cmd:args) = do
case () of
_ | "n" `elem` flagNames -> unsetWith CFUndefineNameref
_ | "v" `elem` flagNames -> unsetWith CFUndefineVariable
@@ -1003,14 +996,14 @@ handleCommand cmd vars args literalCmd = do
(names, flags) = partition (null . fst) pairs
flagNames = map fst flags
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
unsetWith c = newNodeRange $ CFApplyEffects $ map (\(token, name) -> IdTagged (getId token) $ c name) literalNames
variableAssignRegex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)="
handleDeclare (cmd NE.:| args) = do
handleDeclare (cmd:args) = do
isFunc <- asks cfIsFunction
-- 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
@@ -1037,9 +1030,9 @@ handleCommand cmd vars args literalCmd = do
scope isFunc =
case () of
_ | global -> Just GlobalScope
_ | isFunc -> Just LocalScope
_ -> Nothing
_ | global -> GlobalScope
_ | isFunc -> LocalScope
_ -> DefaultScope
addedProps = S.fromList $ concat $ [
[ CFVPArray | array ],
@@ -1067,7 +1060,7 @@ handleCommand cmd vars args literalCmd = do
let
id = getId t
pre = [t]
literal = getLiteralStringDef "\0" t
literal = fromJust $ getLiteralStringExt (const $ Just "\0") t
isKnown = '\0' `notElem` literal
match = fmap head $ variableAssignRegex `matchRegex` literal
name = fromMaybe literal match
@@ -1099,7 +1092,7 @@ handleCommand cmd vars args literalCmd = do
in
concatMap (drop 1) plusses
handlePrintf (cmd NE.:| args) =
handlePrintf (cmd:args) =
newNodeRange $ CFApplyEffects $ maybeToList findVar
where
findVar = do
@@ -1108,7 +1101,7 @@ handleCommand cmd vars args literalCmd = do
name <- getLiteralString arg
return $ IdTagged (getId arg) $ CFWriteVariable name CFValueString
handleWait (cmd NE.:| args) =
handleWait (cmd:args) =
newNodeRange $ CFApplyEffects $ maybeToList findVar
where
findVar = do
@@ -1117,7 +1110,7 @@ handleCommand cmd vars args literalCmd = do
name <- getLiteralString arg
return $ IdTagged (getId arg) $ CFWriteVariable name CFValueInteger
handleMapfile (cmd NE.:| args) =
handleMapfile (cmd:args) =
newNodeRange $ CFApplyEffects [findVar]
where
findVar =
@@ -1137,7 +1130,7 @@ handleCommand cmd vars args literalCmd = do
guard $ isVariableName name
return (getId c, name)
handleRead (cmd NE.:| args) = newNodeRange $ CFApplyEffects main
handleRead (cmd:args) = newNodeRange $ CFApplyEffects main
where
main = fromMaybe fallback $ do
flags <- getGnuOpts flagsForRead args
@@ -1167,7 +1160,7 @@ handleCommand cmd vars args literalCmd = do
in
map (\(id, name) -> IdTagged id $ CFWriteVariable name value) namesOrDefault
handleDEFINE (cmd NE.:| args) =
handleDEFINE (cmd:args) =
newNodeRange $ CFApplyEffects $ maybeToList findVar
where
findVar = do
@@ -1177,14 +1170,14 @@ handleCommand cmd vars args literalCmd = do
return $ IdTagged (getId name) $ CFWriteVariable str CFValueString
handleOthers id vars args cmd =
regularExpansion vars (NE.toList args) $ do
regularExpansion vars args $ do
exe <- newNodeRange $ CFExecuteCommand cmd
status <- newNodeRange $ CFSetExitCode id
linkRange exe status
regularExpansion vars args p = do
args <- sequentially args
assignments <- mapM (buildAssignment (Just PrefixScope)) vars
assignments <- mapM (buildAssignment PrefixScope) vars
exe <- p
dropAssignments <-
if null vars
@@ -1196,15 +1189,15 @@ handleCommand cmd vars args literalCmd = do
linkRanges $ [args] ++ assignments ++ [exe] ++ dropAssignments
regularExpansionWithStatus vars args@(cmd NE.:| _) p = do
initial <- regularExpansion vars (NE.toList args) p
regularExpansionWithStatus vars args@(cmd:_) p = do
initial <- regularExpansion vars args p
status <- newNodeRange $ CFSetExitCode (getId cmd)
linkRange initial status
none = newStructuralNode
data Scope = GlobalScope | LocalScope | PrefixScope
data Scope = DefaultScope | GlobalScope | LocalScope | PrefixScope
deriving (Eq, Ord, Show, Generic, NFData)
buildAssignment scope t = do
@@ -1218,10 +1211,10 @@ buildAssignment scope t = do
let valueType = if null indices then f id value else CFValueArray
let scoper =
case scope of
Just PrefixScope -> CFWritePrefix
Just LocalScope -> CFWriteLocal
Just GlobalScope -> CFWriteGlobal
Nothing -> CFWriteVariable
PrefixScope -> CFWritePrefix
LocalScope -> CFWriteLocal
GlobalScope -> CFWriteGlobal
DefaultScope -> CFWriteVariable
write <- newNodeRange $ applySingle $ IdTagged id $ scoper var valueType
linkRanges [expand, index, read, write]
where

View File

@@ -133,7 +133,7 @@ internalToExternal s =
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
getIncomingState :: CFGAnalysis -> Id -> Maybe ProgramState
@@ -197,12 +197,13 @@ unreachableState = modified newInternalState {
}
-- The default state we assume we get from the environment
createEnvironmentState :: InternalState
createEnvironmentState = do
createEnvironmentState :: CFGParameters -> InternalState
createEnvironmentState params = do
foldl' (flip ($)) newInternalState $ concat [
addVars Data.internalVariables unknownVariableState,
addVars Data.variablesWithoutSpaces spacelessVariableState,
addVars Data.specialIntegerVariables integerVariableState
addVars Data.specialIntegerVariables integerVariableState,
addVars (cfAdditionalInitialVariables params) unknownVariableState
]
where
addVars names val = map (\name -> insertGlobal name val) names
@@ -299,6 +300,7 @@ depsToState set = foldl insert newInternalState $ S.toList set
PrefixScope -> (sPrefixValues, insertPrefix)
LocalScope -> (sLocalValues, insertLocal)
GlobalScope -> (sGlobalValues, insertGlobal)
DefaultScope -> error $ pleaseReport "Unresolved scope in dependency"
alreadyExists = isJust $ vmLookup name $ mapToCheck state
in
@@ -672,7 +674,7 @@ vmPatch base diff =
_ | vmIsQuickEqual base diff -> diff
_ -> VersionedMap {
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.
@@ -829,7 +831,7 @@ lookupStack' functionOnly get dep def ctx key = do
f (s:rest) = do
-- Go up the stack until we find the value, and add
-- 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
return res
@@ -1119,34 +1121,34 @@ transferEffect ctx effect =
CFSetProps scope name props ->
case scope of
Nothing -> do
DefaultScope -> do
state <- readVariable ctx name
writeVariable ctx name $ addProperties props state
Just GlobalScope -> do
GlobalScope -> do
state <- readGlobal ctx name
writeGlobal ctx name $ addProperties props state
Just LocalScope -> do
LocalScope -> do
out <- readSTRef (cOutput ctx)
state <- readLocal ctx name
writeLocal ctx name $ addProperties props state
Just PrefixScope -> do
PrefixScope -> do
-- Prefix values become local
state <- readLocal ctx name
writeLocal ctx name $ addProperties props state
CFUnsetProps scope name props ->
case scope of
Nothing -> do
DefaultScope -> do
state <- readVariable ctx name
writeVariable ctx name $ removeProperties props state
Just GlobalScope -> do
GlobalScope -> do
state <- readGlobal ctx name
writeGlobal ctx name $ removeProperties props state
Just LocalScope -> do
LocalScope -> do
out <- readSTRef (cOutput ctx)
state <- readLocal ctx name
writeLocal ctx name $ removeProperties props state
Just PrefixScope -> do
PrefixScope -> do
-- Prefix values become local
state <- readLocal ctx name
writeLocal ctx name $ removeProperties props state
@@ -1286,7 +1288,7 @@ dataflow ctx entry = do
else do
let (next, rest) = S.deleteFindMin ps
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
process states node = do
@@ -1343,14 +1345,14 @@ analyzeControlFlow params t =
runST $ f cfg entry exit
where
f cfg entry exit = do
let env = createEnvironmentState
let env = createEnvironmentState params
ctx <- newCtx $ cfGraph cfg
-- Do a dataflow analysis starting on the root node
exitState <- runRoot ctx env entry exit
-- All nodes we've touched
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
-- This is so that we still get warnings for dead code
@@ -1373,7 +1375,7 @@ analyzeControlFlow params t =
-- 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 allStates = M.union invokedStates baseStates
let allStates = M.unionWith (flip const) baseStates invokedStates
-- Convert to external states
let nodeToData = M.map (\(a,b) -> (internalToExternal a, internalToExternal b)) allStates

View File

@@ -25,7 +25,7 @@ import ShellCheck.ASTLib
import ShellCheck.Interface
import ShellCheck.Parser
import Debug.Trace -- DO NOT SUBMIT
import Data.Char
import Data.Either
import Data.Functor
import Data.List
@@ -55,6 +55,8 @@ shellFromFilename filename = listToMaybe candidates
shellExtensions = [(".ksh", Ksh)
,(".bash", Bash)
,(".bats", Bash)
,(".ebuild", Bash)
,(".eclass", Bash)
,(".dash", Dash)]
-- The `.sh` is too generic to determine the shell:
-- We fallback to Bash in this case and emit SC2148 if there is no shebang
@@ -85,19 +87,24 @@ checkScript sys spec = do
asShellType = csShellTypeOverride spec,
asFallbackShell = shellFromFilename $ csFilename spec,
asCheckSourced = csCheckSourced spec,
asIsPortage = isPortage $ csFilename spec,
asExecutionMode = Executed,
asTokenPositions = tokenPositions,
asExtendedAnalysis = csExtendedAnalysis spec,
asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec
} where as = newAnalysisSpec root
let analysisMessages =
maybe []
(arComments . analyzeScript . analysisSpec)
$ prRoot result
let getAnalysisMessages =
case prRoot result of
Just root -> arComments <$> (analyzeScript sys $ analysisSpec root)
Nothing -> return []
let translator = tokenToPosition tokenPositions
analysisMessages <- getAnalysisMessages
return . nub . sortMessages . filter shouldInclude $
(parseMessages ++ map translator analysisMessages)
isPortage filename =
let f = map toLower filename in
".ebuild" `isSuffixOf` f || ".eclass" `isSuffixOf` f
shouldInclude pc =
severity <= csMinSeverity spec &&
case csIncludedWarnings spec of
@@ -221,9 +228,6 @@ prop_worksWhenSourcing =
prop_worksWhenSourcingWithDashDash =
null $ checkWithIncludes [("lib", "bar=1")] "source -- lib; echo \"$bar\""
prop_worksWhenSourcingWithDashP =
null $ checkWithIncludes [("lib", "bar=1")] "source -p \"$MYPATH\" lib; echo \"$bar\""
prop_worksWhenDotting =
null $ checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\""
@@ -521,47 +525,6 @@ 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 []
runTests = $quickCheckAll

View File

@@ -20,7 +20,6 @@
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE PatternGuards #-}
-- This module contains checks that examine specific commands by name.
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 Data.List
import Data.Maybe
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Strict as M
import qualified Data.Set as S
import Test.QuickCheck.All (forAllProperties)
@@ -183,15 +181,16 @@ checkCommand :: M.Map CommandName (Token -> Analysis) -> Token -> Analysis
checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do
name <- getLiteralString cmd
return $
if | '/' `elem` name ->
M.findWithDefault nullCheck (Basename $ basename name) map t
| name == "builtin", (h:_) <- rest ->
let t' = T_SimpleCommand id cmdPrefix rest
selectedBuiltin = onlyLiteralString h
in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
| otherwise -> do
M.findWithDefault nullCheck (Exactly name) map t
M.findWithDefault nullCheck (Basename name) map t
if '/' `elem` name
then
M.findWithDefault nullCheck (Basename $ basename name) map t
else if name == "builtin" && not (null rest) then
let t' = T_SimpleCommand id cmdPrefix rest
selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest
in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
else do
M.findWithDefault nullCheck (Exactly name) map t
M.findWithDefault nullCheck (Basename name) map t
where
basename = reverse . takeWhile (/= '/') . reverse
@@ -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 <>&|."
[first, second] |
onlyLiteralString first /= "length"
(fromMaybe "" $ getLiteralString first) /= "length"
&& not (willSplit first || willSplit second) -> do
checkOp first
warn (getId t) 2307
@@ -725,9 +724,6 @@ prop_checkGetPrintfFormats4 = getPrintfFormats "%d%%%(%s)T" == "dT"
prop_checkGetPrintfFormats5 = getPrintfFormats "%bPassed: %d, %bFailed: %d%b, Skipped: %d, %bErrored: %d%b\\n" == "bdbdbdbdb"
prop_checkGetPrintfFormats6 = getPrintfFormats "%s%s" == "ss"
prop_checkGetPrintfFormats7 = getPrintfFormats "%s\n%s" == "ss"
prop_checkGetPrintfFormats8 = getPrintfFormats "%ld" == "d"
prop_checkGetPrintfFormats9 = getPrintfFormats "%lld" == "d"
prop_checkGetPrintfFormats10 = getPrintfFormats "%Q" == "Q"
getPrintfFormats = getFormats
where
-- Get the arguments in the string as a string of type characters,
@@ -746,17 +742,17 @@ getPrintfFormats = getFormats
regexBasedGetFormats rest =
case matchRegex re rest of
Just [width, precision, len, typ, rest, _] ->
Just [width, precision, typ, rest, _] ->
(if width == "*" then "*" else "") ++
(if precision == "*" then "*" else "") ++
typ ++ getFormats rest
Nothing -> take 1 rest ++ getFormats rest
where
-- constructed based on specifications in "man printf"
re = mkRegex "^#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)(hh|h|l|ll|q|L|j|z|Z|t)?([diouxXfFeEgGaAcsbqQSC])((\n|.)*)"
-- \____ _____/\___ ____/ \____ ____/\__________ ___________/\___________ ___________/\___ ___/
-- V V V V V V
-- flags field width precision length modifier format character rest
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)([diouxXfFeEgGaAcsbq])((\n|.)*)"
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \______ /
-- V V V V V
-- flags field width precision format character rest
-- field width and precision can be specified with an '*' instead of a digit,
-- in which case printf will accept one more argument for each '*' used
@@ -934,7 +930,7 @@ prop_checkTimedCommand2 = verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar
prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1"
checkTimedCommand = CommandCheck (Exactly "time") f where
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
when (isPiped cmd) $
warn (getId c) 2176 "'time' is undefined for pipelines. time single stage or bash -c instead."
@@ -958,7 +954,7 @@ checkTimedCommand = CommandCheck (Exactly "time") f where
prop_checkLocalScope1 = verify checkLocalScope "local foo=3"
prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }"
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
unless (any isFunctionLike path) $
err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
@@ -1009,8 +1005,8 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
sequence_ $ do
options <- getLiteralString arg1
getoptsVar <- getLiteralString name
(T_WhileExpression _ _ body) <- findFirst whileLoop (NE.toList path)
T_CaseExpression id var list <- mapMaybe findCase body !!! 0
(T_WhileExpression _ _ body) <- findFirst whileLoop path
caseCmd@(T_CaseExpression _ var _) <- mapMaybe findCase body !!! 0
-- Make sure getopts name and case variable matches
[T_DollarBraced _ _ bracedWord] <- return $ getWordParts var
@@ -1020,11 +1016,11 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
-- Make sure the variable isn't modified
guard . not $ modifiesVariable params (T_BraceGroup (Id 0) body) getoptsVar
return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) id list
return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) caseCmd
f _ = return ()
check :: Id -> [String] -> Id -> [(CaseType, [Token], [Token])] -> Analysis
check optId opts id list = do
check :: Id -> [String] -> Token -> Analysis
check optId opts (T_CaseExpression id _ list) = do
unless (Nothing `M.member` handledMap) $ do
mapM_ (warnUnhandled optId id) $ catMaybes $ M.keys notHandled
@@ -1240,11 +1236,12 @@ checkSudoArgs = CommandCheck (Basename "sudo") f
where
f t = sequence_ $ do
opts <- parseOpts $ arguments t
(_,(commandArg, _)) <- find (null . fst) opts
let nonFlags = [x | ("",(x, _)) <- opts]
commandArg <- nonFlags !!! 0
command <- getLiteralString commandArg
guard $ command `elem` builtins
return $ warn (getId t) 2232 $ "Can't use sudo with builtins like " ++ command ++ ". Did you want sudo sh -c .. instead?"
builtins = [ "cd", "command", "declare", "eval", "exec", "exit", "export", "hash", "history", "local", "popd", "pushd", "read", "readonly", "return", "set", "source", "trap", "type", "typeset", "ulimit", "umask", "unset", "wait" ]
builtins = [ "cd", "eval", "export", "history", "read", "source", "wait" ]
-- This mess is why ShellCheck prefers not to know.
parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:"
@@ -1433,27 +1430,26 @@ prop_checkBackreferencingDeclaration6 = verify (checkBackreferencingDeclaration
prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration "declare") "declare x=var $k=$x"
checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check
where
check t = do
maybeCfga <- asks cfgAnalysis
mapM_ (\cfga -> foldM_ (perArg cfga) M.empty $ arguments t) maybeCfga
check t = foldM_ perArg M.empty $ arguments t
perArg cfga leftArgs t =
perArg leftArgs t =
case t of
T_Assignment id _ name idx t -> do
warnIfBackreferencing cfga leftArgs $ t:idx
warnIfBackreferencing leftArgs $ t:idx
return $ M.insert name id leftArgs
t -> do
warnIfBackreferencing cfga leftArgs [t]
warnIfBackreferencing leftArgs [t]
return leftArgs
warnIfBackreferencing cfga backrefs l = do
references <- findReferences cfga l
warnIfBackreferencing backrefs l = do
references <- findReferences l
let reused = M.intersection backrefs references
mapM msg $ M.toList reused
msg (name, id) = warn id 2318 $ "This assignment is used again in this '" ++ cmd ++ "', but won't have taken effect. Use two '" ++ cmd ++ "'s."
findReferences cfga list = do
findReferences list = do
cfga <- asks cfgAnalysis
let graph = CF.graph cfga
let nodesMap = CF.tokenToNodes cfga
let nodes = S.unions $ map (\id -> M.findWithDefault S.empty id nodesMap) $ map getId $ list

View File

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

View File

@@ -1,7 +1,7 @@
{-
This empty file is provided for ease of patching in site specific checks.
However, there are no guarantees regarding compatibility between versions.
-}
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Checks.Custom (checker, ShellCheck.Checks.Custom.runTests) where

View File

@@ -19,7 +19,6 @@
-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ViewPatterns #-}
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
import ShellCheck.AST
@@ -63,7 +62,6 @@ checks = [
,checkPS1Assignments
,checkMultipleBangs
,checkBangAfterPipe
,checkNegatedUnaryOps
]
testChecker (ForShell _ t) =
@@ -77,24 +75,22 @@ verifyNot c s = producesComments (testChecker c) s == Just False
prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
prop_checkForDecimals2 = verify checkForDecimals "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
f t@(TA_Expansion id _) = sequence_ $ do
first:rest <- getLiteralString t
guard $ isDigit first && '.' `elem` rest
str <- getLiteralString t
first <- str !!! 0
guard $ isDigit first && '.' `elem` str
return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
f _ = return ()
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_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
prop_checkBashisms5 = verify checkBashisms "source file"
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_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
@@ -110,7 +106,6 @@ prop_checkBashisms18 = verify checkBashisms "foo &> /dev/null"
prop_checkBashisms19 = verify checkBashisms "foo > file*.txt"
prop_checkBashisms20 = verify checkBashisms "read -ra foo"
prop_checkBashisms21 = verify checkBashisms "[ -a foo ]"
prop_checkBashisms21b = verify checkBashisms "test -a foo"
prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]"
prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT"
prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM"
@@ -196,77 +191,49 @@ 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
checkBashisms = ForShell [Sh, Dash] $ \t -> do
params <- ask
kludge params t
where
-- This code was copy-pasted from Analytics where params was a variable
kludge params = bashism
where
isBusyboxSh = shellType params == BusyboxSh
isDash = shellType params == Dash || isBusyboxSh
isDash = shellType params == Dash
warnMsg id code s =
if isDash
then err id code $ "In dash, " ++ s ++ " not supported."
else warn id code $ "In POSIX sh, " ++ s ++ " undefined."
asStr = getLiteralString
bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is"
bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is"
bashism (T_DollarSingleQuoted id _) =
unless isBusyboxSh $ warnMsg id 3003 "$'..' is"
bashism (T_DollarSingleQuoted id _) = warnMsg id 3003 "$'..' is"
bashism (T_DollarDoubleQuoted id _) = warnMsg id 3004 "$\"..\" is"
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id 3005 "arithmetic for loops are"
bashism (T_Arithmetic id _) = warnMsg id 3006 "standalone ((..)) is"
bashism (T_DollarBracket id _) = warnMsg id 3007 "$[..] in place of $((..)) is"
bashism (T_SelectIn id _ _ _) = warnMsg id 3008 "select loops are"
bashism (T_BraceExpansion id _) = warnMsg id 3009 "brace expansion is"
bashism (T_Condition id DoubleBracket _) =
unless isBusyboxSh $ warnMsg id 3010 "[[ ]] is"
bashism (T_Condition id DoubleBracket _) = warnMsg id 3010 "[[ ]] is"
bashism (T_HereString id _) = warnMsg id 3011 "here-strings are"
bashism (TC_Binary id _ op _ _) =
checkTestOp bashismBinaryTestFlags op id
bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs]) =
checkTestOp bashismBinaryTestFlags op id
bashism (TC_Unary id _ op _) =
checkTestOp bashismUnaryTestFlags op id
bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just op, _]) =
checkTestOp bashismUnaryTestFlags op id
bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is"
bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "-ot", "-nt", "-ef" ] =
unless isDash $ warnMsg id 3013 $ op ++ " is"
bashism (TC_Binary id SingleBracket "==" _ _) =
warnMsg id 3014 "== in place of = is"
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 _)
| op `elem` [ "|++", "|--", "++|", "--|"] =
warnMsg id 3018 $ filter (/= '|') op ++ " is"
bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) =
unless isBusyboxSh $ warnMsg id 3020 "&> is"
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id 3020 "&> is"
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) file)) =
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"
@@ -286,8 +253,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
warnMsg id 3028 $ str ++ " is"
bashism t@(T_DollarBraced id _ token) = do
unless isBusyboxSh $ mapM_ check simpleExpansions
mapM_ check advancedExpansions
mapM_ check expansion
when (isBashVariable var) $
warnMsg id 3028 $ var ++ " is"
where
@@ -315,11 +281,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "echo" && argString `matches` flagRegex =
if isBusyboxSh
then
unless (argString `matches` busyboxFlagRegex) $
warnMsg (getId arg) 3036 "echo flags besides -n and -e"
else if isDash
if isDash
then
when (argString /= "-n") $
warnMsg (getId arg) 3036 "echo flags besides -n"
@@ -328,7 +290,6 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
where
argString = concat $ oversimplify arg
flagRegex = mkRegex "^-[eEsn]+$"
busyboxFlagRegex = mkRegex "^-[en]+$"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
@@ -402,8 +363,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
(\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is"
when (name == "source" && not isBusyboxSh) $
warnMsg id 3046 "'source' in place of '.' is"
when (name == "source") $ warnMsg id 3046 "'source' in place of '.' is"
when (name == "trap") $
let
check token = sequence_ $ do
@@ -412,7 +372,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
return $ do
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is"
when (not isBusyboxSh && "SIG" `isPrefixOf` upper) $
when ("SIG" `isPrefixOf` upper) $
warnMsg (getId token) 3048
"prefixing signal names with 'SIG' is"
when (not isDash && upper /= str) $
@@ -442,19 +402,17 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
("hash", Just $ if isDash then ["r", "v"] else ["r"]),
("jobs", Just ["l", "p"]),
("printf", Just []),
("read", Just $ if isDash || isBusyboxSh then ["r", "p"] else ["r"]),
("read", Just $ if isDash then ["r", "p"] else ["r"]),
("readonly", Just ["p"]),
("trap", Just []),
("type", Just $ if isBusyboxSh then ["p"] else []),
("type", Just []),
("ulimit", if isDash then Nothing else Just ["f"]),
("umask", Just ["S"]),
("unset", Just ["f", "v"]),
("wait", Just [])
]
bashism t@(T_SourceCommand id src _)
| getCommandName src == Just "source" =
unless isBusyboxSh $
warnMsg id 3051 "'source' in place of '.' is"
| getCommandName src == Just "source" = warnMsg id 3051 "'source' in place of '.' is"
bashism (TA_Expansion _ (T_Literal id str : _))
| str `matches` radix = warnMsg id 3052 "arithmetic base conversion is"
where
@@ -462,16 +420,14 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
bashism _ = return ()
varChars="_0-9a-zA-Z"
advancedExpansions = let re = mkRegex in [
expansion = let re = mkRegex in [
(re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"),
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"),
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"),
(re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is")
]
simpleExpansions = let re = mkRegex in [
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", 3057, "string indexing is"),
(re $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is")
]
bashVars = [
@@ -497,50 +453,6 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
Assignment (_, _, name, _) -> name == var
_ -> 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_checkEchoSed1b = verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")"
prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
@@ -658,7 +570,7 @@ checkPS1Assignments = ForShell [Bash] f
prop_checkMultipleBangs1 = verify checkMultipleBangs "! ! true"
prop_checkMultipleBangs2 = verifyNot checkMultipleBangs "! true"
checkMultipleBangs = ForShell [Dash, BusyboxSh, Sh] f
checkMultipleBangs = ForShell [Dash, Sh] f
where
f token = case token of
T_Banged id (T_Banged _ _) ->
@@ -669,7 +581,7 @@ checkMultipleBangs = ForShell [Dash, BusyboxSh, Sh] f
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
checkBangAfterPipe = ForShell [Dash, Sh, Bash] f
where
f token = case token of
T_Pipeline _ _ cmds -> mapM_ check cmds
@@ -680,22 +592,5 @@ checkBangAfterPipe = ForShell [Dash, BusyboxSh, Sh, Bash] f
err id 2326 "! is not allowed in the middle of pipelines. Use command group as in cmd | { ! cmd; } if necessary."
_ -> 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 []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -49,7 +49,6 @@ internalVariables = [
"LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH",
"POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1",
"PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR",
"BASH_MONOSECONDS", "BASH_TRAPSIG", "GLOBSORT",
"auto_resume", "histchars",
-- Other
@@ -63,11 +62,92 @@ internalVariables = [
, "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP",
"FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION",
"flags_error", "flags_return"
] ++ portageManualInternalVariables
-- Bats
,"stderr", "stderr_lines"
portageManualInternalVariables = [
-- toolchain settings
"CFLAGS", "CXXFLAGS", "CPPFLAGS", "LDFLAGS", "FFLAGS", "FCFLAGS",
"CBUILD", "CHOST", "MAKEOPTS",
-- TODO: Delete these if we can handle `tc-export CC` implicit export.
"CC", "CPP", "CXX",
-- portage internals
"EBUILD_PHASE", "EBUILD_SH_ARGS", "EMERGE_FROM", "FILESDIR",
"MERGE_TYPE", "PM_EBUILD_HOOK_DIR", "PORTAGE_ACTUAL_DISTDIR",
"PORTAGE_ARCHLIST", "PORTAGE_BASHRC", "PORTAGE_BINPKG_FILE",
"PORTAGE_BINPKG_TAR_OPTS", "PORTAGE_BINPKG_TMPFILE", "PORTAGE_BIN_PATH",
"PORTAGE_BUILDDIR", "PORTAGE_BUILD_GROUP", "PORTAGE_BUILD_USER",
"PORTAGE_BUNZIP2_COMMAND", "PORTAGE_BZIP2_COMMAND", "PORTAGE_COLORMAP",
"PORTAGE_CONFIGROOT", "PORTAGE_DEBUG", "PORTAGE_DEPCACHEDIR",
"PORTAGE_EBUILD_EXIT_FILE", "PORTAGE_ECLASS_LOCATIONS", "PORTAGE_GID",
"PORTAGE_GRPNAME", "PORTAGE_INST_GID", "PORTAGE_INST_UID",
"PORTAGE_INTERNAL_CALLER", "PORTAGE_IPC_DAEMON", "PORTAGE_IUSE",
"PORTAGE_LOG_FILE", "PORTAGE_MUTABLE_FILTERED_VARS",
"PORTAGE_OVERRIDE_EPREFIX", "PORTAGE_PYM_PATH", "PORTAGE_PYTHON",
"PORTAGE_PYTHONPATH", "PORTAGE_READONLY_METADATA", "PORTAGE_READONLY_VARS",
"PORTAGE_REPO_NAME", "PORTAGE_REPOSITORIES", "PORTAGE_RESTRICT",
"PORTAGE_SAVED_READONLY_VARS", "PORTAGE_SIGPIPE_STATUS", "PORTAGE_TMPDIR",
"PORTAGE_UPDATE_ENV", "PORTAGE_USERNAME", "PORTAGE_VERBOSE",
"PORTAGE_WORKDIR_MODE", "PORTAGE_XATTR_EXCLUDE", "REPLACING_VERSIONS",
"REPLACED_BY_VERSION", "__PORTAGE_HELPER", "__PORTAGE_TEST_HARDLINK_LOCKS",
-- generic ebuilds
"A", "ARCH", "BDEPEND", "BOARD_USE", "BROOT", "CATEGORY", "D",
"DEFINED_PHASES", "DEPEND", "DESCRIPTION", "DISTDIR", "DOCS", "EAPI",
"ECLASS", "ED", "EPREFIX", "EROOT", "ESYSROOT", "EXTRA_ECONF",
"EXTRA_EINSTALL", "EXTRA_MAKE", "FEATURES", "FILESDIR", "HOME", "HOMEPAGE",
"HTML_DOCS", "INHERITED", "IUSE", "KEYWORDS", "LICENSE", "P", "PATCHES",
"PDEPEND", "PF", "PKG_INSTALL_MASK", "PKGUSE", "PN", "PR", "PROPERTIES",
"PROVIDES_EXCLUDE", "PV", "PVR", "QA_AM_MAINTAINER_MODE",
"QA_CONFIGURE_OPTIONS", "QA_DESKTOP_FILE", "QA_DT_NEEDED", "QA_EXECSTACK",
"QA_FLAGS_IGNORED", "QA_MULTILIB_PATHS", "QA_PREBUILT", "QA_PRESTRIPPED",
"QA_SONAME", "QA_SONAME_NO_SYMLINK", "QA_TEXTRELS", "QA_WX_LOAD", "RDEPEND",
"REPOSITORY", "REQUIRED_USE", "REQUIRES_EXCLUDE", "RESTRICT", "ROOT", "S",
"SLOT", "SRC_TEST", "SRC_URI", "STRIP_MASK", "SUBSLOT", "SYSROOT", "T",
"WORKDIR",
-- autotest.eclass declared incorrectly
"AUTOTEST_CLIENT_TESTS", "AUTOTEST_CLIENT_SITE_TESTS",
"AUTOTEST_SERVER_TESTS", "AUTOTEST_SERVER_SITE_TESTS", "AUTOTEST_CONFIG",
"AUTOTEST_DEPS", "AUTOTEST_PROFILERS", "AUTOTEST_CONFIG_LIST",
"AUTOTEST_DEPS_LIST", "AUTOTEST_PROFILERS_LIST",
-- cros-board.eclass declared incorrectly
"CROS_BOARDS",
-- Undeclared cros-kernel2 vars
"AFDO_PROFILE_VERSION",
-- haskell-cabal.eclass declared incorrectly
"CABAL_FEATURES",
-- Undeclared haskell-cabal.eclass vars
"CABAL_CORE_LIB_GHC_PV",
-- Undeclared readme.gentoo.eclass vars
"DOC_CONTENTS",
-- Backwards compatibility perl-module.eclass vars
"MODULE_AUTHOR", "MODULE_VERSION",
-- Undeclared perl-module.eclass vars
"mydoc",
-- python-utils-r1.eclass declared incorrectly
"RESTRICT_PYTHON_ABIS", "PYTHON_MODNAME",
-- ABI variables
"ABI", "DEFAULT_ABI",
-- AFDO variables
"AFDO_LOCATION",
-- Linguas
"LINGUAS"
]
specialIntegerVariables = [
"$", "?", "!", "#"
]
@@ -79,7 +159,7 @@ variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
"EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM",
"READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS",
"SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE",
"HISTSIZE", "LINES", "BASH_MONOSECONDS", "BASH_TRAPSIG"
"HISTSIZE", "LINES"
-- shflags
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
@@ -94,7 +174,9 @@ unbracedVariables = specialVariables ++ [
arrayVariables = [
"BASH_ALIASES", "BASH_ARGC", "BASH_ARGV", "BASH_CMDS", "BASH_LINENO",
"BASH_REMATCH", "BASH_SOURCE", "BASH_VERSINFO", "COMP_WORDS", "COPROC",
"DIRSTACK", "FUNCNAME", "GROUPS", "MAPFILE", "PIPESTATUS", "COMPREPLY"
"DIRSTACK", "FUNCNAME", "GROUPS", "MAPFILE", "PIPESTATUS", "COMPREPLY",
-- For Portage
"PATCHES"
]
commonCommands = [
@@ -154,21 +236,22 @@ unaryTestOps = [
"-o", "-v", "-R"
]
-- Variables inspected by Portage tc-export_build_env
portageBuildEnvVariables = [
"CFLAGS", "CXXFLAGS", "CPPFLAGS", "LDFLAGS"
]
shellForExecutable :: String -> Maybe Shell
shellForExecutable name =
case name of
"sh" -> return Sh
"bash" -> 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
"ash" -> return Dash -- There's also a warning for this.
"ksh" -> return Ksh
"ksh88" -> return Ksh
"ksh93" -> return Ksh
"oksh" -> return Ksh
_ -> Nothing
flagsForRead = "sreu:n:N:i:p:a:t:"

View File

@@ -117,7 +117,8 @@ dummySystemInterface = mockedSystemInterface [
cfgParams :: CFGParameters
cfgParams = CFGParameters {
cfLastpipe = False,
cfPipefail = False
cfPipefail = False,
cfAdditionalInitialVariables = []
}
-- An example script to play with

View File

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

View File

@@ -191,17 +191,11 @@ splitLast x =
let (last, rest) = splitAt 1 $ reverse x
in (reverse rest, last)
-- git patch does not like `\` on Windows
normalizePath path =
case path of
c:rest -> (if c == pathSeparator then '/' else c) : normalizePath rest
[] -> []
formatDoc color (DiffDoc name lf regions) =
let (most, last) = splitLast regions
in
(color bold $ "--- " ++ (normalizePath $ "a" </> name)) ++ "\n" ++
(color bold $ "+++ " ++ (normalizePath $ "b" </> name)) ++ "\n" ++
(color bold $ "--- " ++ ("a" </> name)) ++ "\n" ++
(color bold $ "+++ " ++ ("b" </> name)) ++ "\n" ++
concatMap (formatRegion color LinefeedOk) most ++
concatMap (formatRegion color lf) last

View File

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

View File

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

View File

@@ -31,9 +31,9 @@ import Data.Ord
import Data.IORef
import Data.List
import Data.Maybe
import GHC.Exts
import System.IO
import System.Info
import qualified Data.List.NonEmpty as NE
wikiLink = "https://www.shellcheck.net/wiki/"
@@ -117,19 +117,19 @@ outputResult options ref result sys = do
color <- getColorFunc $ foColorOption options
let comments = crComments result
appendComments ref comments (fromIntegral $ foWikiLinkCount options)
let fileGroups = NE.groupWith sourceFile comments
let fileGroups = groupWith sourceFile comments
mapM_ (outputForFile color sys) fileGroups
outputForFile color sys comments = do
let fileName = sourceFile (NE.head comments)
let fileName = sourceFile (head comments)
result <- siReadFile sys (Just True) fileName
let contents = either (const "") id result
let fileLinesList = lines contents
let lineCount = length fileLinesList
let fileLines = listArray (1, lineCount) fileLinesList
let groups = NE.groupWith lineNo comments
let groups = groupWith lineNo comments
forM_ groups $ \commentsForLine -> do
let lineNum = fromIntegral $ lineNo (NE.head commentsForLine)
let lineNum = fromIntegral $ lineNo (head commentsForLine)
let line = if lineNum < 1 || lineNum > lineCount
then ""
else fileLines ! fromIntegral lineNum
@@ -139,7 +139,7 @@ outputForFile color sys comments = do
putStrLn (color "source" line)
forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c
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
sliceFile :: Fix -> Array Int String -> (Fix, Array Int String)
@@ -169,7 +169,7 @@ showFixedString color comments lineNum fileLines =
-- and/or other unrelated lines.
let (excerptFix, excerpt) = sliceFile mergedFix fileLines
-- in the spirit of error prone
putStrLn $ color "message" "Did you mean:"
putStrLn $ color "message" "Did you mean: "
putStrLn $ unlines $ applyFix excerptFix excerpt
cuteIndent :: PositionedComment -> String

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2024 Vidar Holen
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -21,14 +21,14 @@
module ShellCheck.Interface
(
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)
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
, ParseResult(prComments, prTokenPositions, prRoot)
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asExtendedAnalysis, asOptionalChecks)
, AnalysisSpec(..)
, AnalysisResult(arComments)
, FormatterOptions(foColorOption, foWikiLinkCount)
, Shell(Ksh, Sh, Bash, Dash, BusyboxSh)
, Shell(Ksh, Sh, Bash, Dash)
, ExecutionMode(Executed, Sourced)
, ErrorMessage
, Code
@@ -87,7 +87,9 @@ data SystemInterface m = SystemInterface {
-- find the sourced file
siFindSource :: String -> Maybe Bool -> [String] -> String -> m FilePath,
-- | Get the configuration file (name, contents) for a filename
siGetConfig :: String -> m (Maybe (FilePath, String))
siGetConfig :: String -> m (Maybe (FilePath, String)),
-- | Look up Portage Eclass variables
siGetPortageVariables :: m (Map.Map String [String])
}
-- ShellCheck input and output
@@ -100,7 +102,6 @@ data CheckSpec = CheckSpec {
csIncludedWarnings :: Maybe [Integer],
csShellTypeOverride :: Maybe Shell,
csMinSeverity :: Severity,
csExtendedAnalysis :: Maybe Bool,
csOptionalChecks :: [String]
} deriving (Show, Eq)
@@ -125,7 +126,6 @@ emptyCheckSpec = CheckSpec {
csIncludedWarnings = Nothing,
csShellTypeOverride = Nothing,
csMinSeverity = StyleC,
csExtendedAnalysis = Nothing,
csOptionalChecks = []
}
@@ -143,7 +143,8 @@ newSystemInterface =
SystemInterface {
siReadFile = \_ _ -> return $ Left "Not implemented",
siFindSource = \_ _ _ name -> return name,
siGetConfig = \_ -> return Nothing
siGetConfig = \_ -> return Nothing,
siGetPortageVariables = return Map.empty
}
-- Parser input and output
@@ -175,8 +176,8 @@ data AnalysisSpec = AnalysisSpec {
asFallbackShell :: Maybe Shell,
asExecutionMode :: ExecutionMode,
asCheckSourced :: Bool,
asIsPortage :: Bool,
asOptionalChecks :: [String],
asExtendedAnalysis :: Maybe Bool,
asTokenPositions :: Map.Map Id (Position, Position)
}
@@ -186,8 +187,8 @@ newAnalysisSpec token = AnalysisSpec {
asFallbackShell = Nothing,
asExecutionMode = Executed,
asCheckSourced = False,
asIsPortage = False,
asOptionalChecks = [],
asExtendedAnalysis = Nothing,
asTokenPositions = Map.empty
}
@@ -225,7 +226,7 @@ newCheckDescription = CheckDescription {
}
-- 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)
type ErrorMessage = String
@@ -339,3 +340,4 @@ mockedSystemInterface files = (newSystemInterface :: SystemInterface Identity) {
mockRcFile rcfile mock = mock {
siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
}

View File

@@ -46,9 +46,7 @@ import Text.Parsec.Error
import Text.Parsec.Pos
import qualified Control.Monad.Reader as Mr
import qualified Control.Monad.State as Ms
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Strict as Map
import Debug.Trace
import Test.QuickCheck.All (quickCheckAll)
@@ -67,14 +65,10 @@ singleQuote = char '\''
doubleQuote = char '"'
variableStart = upper <|> lower <|> oneOf "_"
variableChars = upper <|> lower <|> digit <|> oneOf "_"
-- Chars to allow function names to start with
functionStartChars = variableChars <|> oneOf ":+?-./^@,"
-- Chars to allow inside function names
functionChars = variableChars <|> oneOf "#:+?-./^@,"
-- Chars to allow function names to start with, using the 'function' keyword
extendedFunctionStartChars = functionStartChars <|> oneOf "[]*=!"
-- Chars to allow in function names
functionChars = variableChars <|> oneOf ":+?-./^@,"
-- Chars to allow in functions using the 'function' keyword
extendedFunctionChars = extendedFunctionStartChars <|> oneOf "[]*=!"
extendedFunctionChars = functionChars <|> oneOf "[]*=!"
specialVariable = oneOf (concat specialVariables)
paramSubSpecialChars = oneOf "/:+-=%"
quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars
@@ -146,9 +140,15 @@ carriageReturn = do
parseProblemAt pos ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ."
return '\r'
almostSpace = do
parseNote ErrorC 1018 $ "This is a unicode space. Delete and retype it."
oneOf "\xA0\x2002\x2003\x2004\x2005\x2006\x2007\x2008\x2009\x200B\x202F"
almostSpace =
choice [
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 ' '
--------- Message/position annotation on top of user state
@@ -160,7 +160,7 @@ data Context =
deriving (Show)
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)
data UserState = UserState {
@@ -238,12 +238,12 @@ addToHereDocMap id list = do
hereDocMap = Map.insert id list map
}
addPendingHereDoc id d q str = do
addPendingHereDoc t = do
state <- getState
context <- getCurrentContexts
let docs = pendingHereDocs state
putState $ state {
pendingHereDocs = HereDocPending id d q str context : docs
pendingHereDocs = HereDocPending t context : docs
}
popPendingHereDocs = do
@@ -826,7 +826,7 @@ readArithmeticContents =
char ')'
id <- endSpan start
spacing
return $ TA_Parenthesis id s
return $ TA_Parentesis id s
readArithTerm = readGroup <|> readVariable <|> readExpansion
@@ -1057,16 +1057,6 @@ readAnnotationWithoutPrefix sandboxed = do
"This shell type is unknown. Use e.g. sh or bash."
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
pos <- getPosition
value <- plainOrQuoted $ many1 letter
@@ -1204,7 +1194,7 @@ readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|>
readDollarBracedLiteral = do
start <- startSpan
vars <- (readBraceEscaped <|> ((\x -> [x]) <$> anyChar)) `reluctantlyTill1` bracedQuotable
vars <- (readBraceEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` bracedQuotable
id <- endSpan start
return $ T_Literal id $ concat vars
@@ -1566,7 +1556,7 @@ readGenericLiteral endChars = do
return $ concat strings
readGenericLiteral1 endExp = do
strings <- (readGenericEscaped <|> ((\x -> [x]) <$> anyChar)) `reluctantlyTill1` endExp
strings <- (readGenericEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` endExp
return $ concat strings
readGenericEscaped = do
@@ -1700,17 +1690,16 @@ readAmbiguous prefix expected alternative warner = do
prop_readDollarBraceCommandExpansion1 = isOk readDollarBraceCommandExpansion "${ ls; }"
prop_readDollarBraceCommandExpansion2 = isOk readDollarBraceCommandExpansion "${\nls\n}"
prop_readDollarBraceCommandExpansion3 = isOk readDollarBraceCommandExpansion "${| REPLY=42; }"
readDollarBraceCommandExpansion = called "ksh-style ${ ..; } command expansion" $ do
readDollarBraceCommandExpansion = called "ksh ${ ..; } command expansion" $ do
start <- startSpan
c <- try $ do
string "${"
char '|' <|> whitespace
try $ do
string "${"
whitespace
allspacing
term <- readTerm
char '}' <|> fail "Expected } to end the ksh-style ${ ..; } command expansion"
char '}' <|> fail "Expected } to end the ksh ${ ..; } command expansion"
id <- endSpan start
return $ T_DollarBraceCommandExpansion id (if c == '|' then Piped else Unpiped) term
return $ T_DollarBraceCommandExpansion id term
prop_readDollarBraced1 = isOk readDollarBraced "${foo//bar/baz}"
prop_readDollarBraced2 = isOk readDollarBraced "${foo/'{cow}'}"
@@ -1846,7 +1835,7 @@ readHereDoc = called "here document" $ do
-- add empty tokens for now, read the rest in readPendingHereDocs
let doc = T_HereDoc hid dashed quoted endToken []
addPendingHereDoc hid dashed quoted endToken
addPendingHereDoc doc
return doc
where
unquote :: String -> (Quoted, String)
@@ -1867,7 +1856,7 @@ readPendingHereDocs = do
docs <- popPendingHereDocs
mapM_ readDoc docs
where
readDoc (HereDocPending id dashed quoted endToken ctx) =
readDoc (HereDocPending (T_HereDoc id dashed quoted endToken _) ctx) =
swapContext ctx $
do
docStartPos <- getPosition
@@ -2211,18 +2200,17 @@ readSimpleCommand = called "simple command" $ do
readSource :: Monad m => Token -> SCParser m Token
readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:args'))) = do
let file = getFile args'
readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = do
let file = getFile file' rest'
override <- getSourceOverride
let literalFile = do
name <- override `mplus` (getLiteralString =<< file) `mplus` (stripDynamicPrefix =<< file)
name <- override `mplus` getLiteralString file `mplus` stripDynamicPrefix file
-- Hack to avoid 'source ~/foo' trying to read from literal tilde
guard . not $ "~/" `isPrefixOf` name
return name
let fileId = fromMaybe (getId cmd) (getId <$> file)
case literalFile of
Nothing -> do
parseNoteAtId fileId WarningC 1090
parseNoteAtId (getId file) WarningC 1090
"ShellCheck can't follow non-constant source. Use a directive to specify location."
return t
Just filename -> do
@@ -2230,7 +2218,7 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:args'))) = do
if not proceed
then do
-- FIXME: This actually gets squashed without -a
parseNoteAtId fileId InfoC 1093
parseNoteAtId (getId file) InfoC 1093
"This file appears to be recursively sourced. Ignoring."
return t
else do
@@ -2248,7 +2236,7 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:args'))) = do
return (contents, resolved)
case input of
Left err -> do
parseNoteAtId fileId InfoC 1091 $
parseNoteAtId (getId file) InfoC 1091 $
"Not following: " ++ err
return t
Right script -> do
@@ -2260,19 +2248,18 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:args'))) = do
return $ T_SourceCommand id1 t (T_Include id2 src)
let failed = do
parseNoteAtId fileId WarningC 1094
parseNoteAtId (getId file) WarningC 1094
"Parsing of sourced file failed. Ignoring it."
return t
included <|> failed
where
getFile :: [Token] -> Maybe Token
getFile (first:rest) =
case getLiteralString first of
Just "--" -> rest !!! 0
Just "-p" -> rest !!! 1
_ -> return first
getFile _ = Nothing
getFile :: Token -> [Token] -> Token
getFile file (next:rest) =
case getLiteralString file of
Just "--" -> next
x -> file
getFile file _ = file
getSourcePath t =
case t of
@@ -2383,7 +2370,7 @@ readPipeSequence = do
return $ T_Pipeline id pipes cmds
where
sepBy1WithSeparators p s = do
let elems = (\x -> ([x], [])) <$> p
let elems = p >>= \x -> return ([x], [])
let seps = do
separator <- s
return $ \(a,b) (c,d) -> (a++c, b ++ d ++ [separator])
@@ -2759,8 +2746,6 @@ prop_readFunctionDefinition10 = isOk readFunctionDefinition "function foo () { t
prop_readFunctionDefinition11 = isWarning readFunctionDefinition "function foo{\ntrue\n}"
prop_readFunctionDefinition12 = isOk readFunctionDefinition "function []!() { true; }"
prop_readFunctionDefinition13 = isOk readFunctionDefinition "@require(){ true; }"
prop_readFunctionDefinition14 = isOk readFunctionDefinition "foo#bar(){ :; }"
prop_readFunctionDefinition15 = isNotOk readFunctionDefinition "#bar(){ :; }"
readFunctionDefinition = called "function" $ do
start <- startSpan
functionSignature <- try readFunctionSignature
@@ -2778,7 +2763,7 @@ readFunctionDefinition = called "function" $ do
string "function"
whitespace
spacing
name <- (:) <$> extendedFunctionStartChars <*> many extendedFunctionChars
name <- many1 extendedFunctionChars
spaces <- spacing
hasParens <- wasIncluded readParens
when (not hasParens && null spaces) $
@@ -2787,7 +2772,7 @@ readFunctionDefinition = called "function" $ do
return $ \id -> T_Function id (FunctionKeyword True) (FunctionParentheses hasParens) name
readWithoutFunction = try $ do
name <- (:) <$> functionStartChars <*> many functionChars
name <- many1 functionChars
guard $ name /= "time" -- Interferes with time ( foo )
spacing
readParens
@@ -2805,29 +2790,17 @@ readFunctionDefinition = called "function" $ do
prop_readCoProc1 = isOk readCoProc "coproc foo { echo bar; }"
prop_readCoProc2 = 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
start <- startSpan
try $ do
string "coproc"
spacing1
whitespace
choice [ try $ readCompoundCoProc start, readSimpleCoProc start ]
where
readCompoundCoProc start = do
notFollowedBy2 readAssignmentWord
(var, body) <- choice [
try $ do
body <- readBody readCompoundCommand
return (Nothing, body),
try $ do
var <- readNormalWord `thenSkip` spacing
body <- readBody readCompoundCommand
return (Just var, body)
]
var <- optionMaybe $
readVariableName `thenSkip` whitespace
body <- readBody readCompoundCommand
id <- endSpan start
return $ T_CoProc id var body
readSimpleCoProc start = do
@@ -2931,8 +2904,8 @@ readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord)
kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos)
kludgeAwayQuotes s p =
case s of
first:second:rest ->
let (last NE.:| backwards) = NE.reverse (second NE.:| rest)
first:rest@(_:_) ->
let (last:backwards) = reverse rest
middle = reverse backwards
in
if first `elem` "'\"" && first == last
@@ -3366,8 +3339,7 @@ readScriptFile sourced = do
verifyEof
let script = T_Annotation annotationId annotations $
T_Script id shebang commands
userstate <- getState
reparseIndices $ reattachHereDocs script (hereDocMap userstate)
reparseIndices script
else do
many anyChar
id <- endSpan start
@@ -3377,8 +3349,8 @@ readScriptFile sourced = do
verifyShebang pos s = do
case isValidShell s of
Just True -> return ()
Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/dash/ksh/'busybox sh' 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."
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. Add a 'shell' directive to specify."
isValidShell s =
let good = null s || any (`isPrefixOf` s) goodShells
@@ -3394,11 +3366,9 @@ readScriptFile sourced = do
"sh",
"ash",
"dash",
"busybox sh",
"bash",
"bats",
"ksh",
"oksh"
"ksh"
]
badShells = [
"awk",
@@ -3407,7 +3377,6 @@ readScriptFile sourced = do
"fish",
"perl",
"python",
"python3",
"ruby",
"tcsh",
"zsh"
@@ -3460,22 +3429,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
isNotOk p s = parsesCleanly p s == Nothing -- The string does not parse
-- If the parser matches the string, return Right [ParseNotes+ParseProblems]
-- If it does not match the string, return Left [ParseProblems]
getParseOutput parser string = runIdentity $ do
(res, systemState) <- runParser testEnvironment
(parser >> eof >> getState) "-" string
return $ case res of
Right userState ->
Right $ parseNotes userState ++ parseProblems systemState
Left _ -> Left $ parseProblems systemState
-- If the parser matches the string, return Just whether it was clean (without emitting suggestions)
-- Otherwise, Nothing
parsesCleanly parser string =
case getParseOutput parser string of
Right list -> Just $ null list
Left _ -> Nothing
parsesCleanly parser string = runIdentity $ do
(res, sys) <- runParser testEnvironment
(parser >> eof >> getState) "-" string
case (res, sys) of
(Right userState, systemState) ->
return $ Just . null $ parseNotes userState ++ parseProblems systemState
(Left _, _) -> return Nothing
parseWithNotes parser = do
item <- parser
@@ -3493,8 +3453,9 @@ makeErrorFor parsecError =
pos = errorPos parsecError
getStringFromParsec errors =
headOrDefault "" (mapMaybe f $ reverse errors) ++
" Fix any mentioned problems and try again."
case map f errors of
r -> unwords (take 1 $ catMaybes $ reverse r) ++
" Fix any mentioned problems and try again."
where
f err =
case err of
@@ -3525,7 +3486,8 @@ parseShell env name contents = do
return newParseResult {
prComments = map toPositionedComment $ nub $ parseNotes userstate ++ parseProblems state,
prTokenPositions = Map.map startEndPosToPos (positionMap userstate),
prRoot = Just script
prRoot = Just $
reattachHereDocs script (hereDocMap userstate)
}
Left err -> do
let context = contextStack state
@@ -3543,11 +3505,13 @@ parseShell env name contents = do
-- A final pass for ignoring parse errors after failed parsing
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
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."
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 ++ "."
-- Go over all T_UnparsedIndex and reparse them as either arithmetic or text

View File

@@ -0,0 +1,138 @@
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module ShellCheck.PortageVariables (
readPortageVariables
) where
import ShellCheck.Regex
import Control.Exception
import Control.Monad
import Data.Maybe
import System.Directory (listDirectory)
import System.Exit (ExitCode(..))
import System.FilePath
import System.IO
import System.Process
import qualified Data.ByteString as B
import qualified Data.Map as M
type RepoName = String
type RepoPath = String
type EclassName = String
type EclassVar = String
-- | This is used for looking up what eclass variables are inherited,
-- keyed by the name of the eclass.
type EclassMap = M.Map EclassName [EclassVar]
data Repository = Repository
{ repositoryName :: RepoName
, repositoryLocation :: RepoPath
, repositoryEclasses :: [Eclass]
} deriving (Show, Eq, Ord)
data Eclass = Eclass
{ eclassName :: EclassName
, eclassVars :: [EclassVar]
} deriving (Show, Eq, Ord)
readPortageVariables :: IO (M.Map String [String])
readPortageVariables = portageVariables <$> scanRepos
-- | Map from eclass names to a list of eclass variables
portageVariables :: [Repository] -> EclassMap
portageVariables = foldMap $ foldMap go . repositoryEclasses
where
go e = M.singleton (eclassName e) (eclassVars e)
-- | Run @portageq@ to gather a list of repo names and paths, then scan each
-- one for eclasses and ultimately eclass metadata.
scanRepos :: IO [Repository]
scanRepos = do
let cmd = "portageq"
let args = ["repos_config", "/"]
out <- runOrDie cmd args
forM (reposParser $ lines out) $ \(n,p) -> Repository n p <$> getEclasses p
-- | Get the name of the repo and its path from blocks outputted by
-- @portageq@. If the path doesn't exist, this will return @Nothing@.
reposParser :: [String] -> [(RepoName, RepoPath)]
reposParser = f ""
where
segmentRegex = mkRegex "^\\[(.*)\\].*"
locationRegex = mkRegex "^[[:space:]]*location[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$"
f name [] = []
f name (line:rest) =
case (matchRegex segmentRegex line, matchRegex locationRegex line) of
(Just [next], _) -> f next rest
(_, Just [location]) -> (name, location) : f name rest
_ -> f name rest
-- | Scan the repo path for @*.eclass@ files in @eclass/@, then run
-- 'eclassParser' on each of them to produce @[Eclass]@.
--
-- If the @eclass/@ directory doesn't exist, the scan is skipped for that
-- repo.
getEclasses :: RepoPath -> IO [Eclass]
getEclasses repoLoc = do
let eclassDir = repoLoc </> "eclass"
files <- handle catcher $ listDirectory eclassDir
let names = filter (\(_, e) -> e == ".eclass") $ map splitExtension files
forM (names :: [(String, String)]) $ \(name, ext) -> do
contents <- withFile (eclassDir </> name <.> ext) ReadMode readFully
return $ Eclass name $ eclassParser (lines contents)
where
catcher :: IOException -> IO [String]
catcher e = do
hPutStrLn stderr $ "Unable to find .eclass files: " ++ show e
return []
-- | Scan a @.eclass@ file for any @@@ECLASS_VARIABLE:@ comments, generating
-- a list of eclass variables.
eclassParser :: [String] -> [String]
eclassParser lines = mapMaybe match lines
where
varRegex = mkRegex "^[[:space:]]*#[[:space:]]*@ECLASS_VARIABLE:[[:space:]]*([^[:space:]]*)[[:space:]]*$"
match str = head <$> matchRegex varRegex str
-- | Run the command and return the full stdout string (stdin is ignored).
--
-- If the command exits with a non-zero exit code, this will throw an
-- error including the captured contents of stdout and stderr.
runOrDie :: FilePath -> [String] -> IO String
runOrDie cmd args = bracket acquire release $ \(_,o,e,p) -> do
ot <- readFully (fromJust o)
et <- readFully (fromJust e)
ec <- waitForProcess p
case ec of
ExitSuccess -> pure ot
ExitFailure i -> fail $ unlines $ map unwords
$ [ [ show cmd ]
++ map show args
++ [ "failed with exit code", show i]
, [ "stdout:" ], [ ot ]
, [ "stderr:" ], [ et ]
]
where
acquire = createProcess (proc cmd args)
{ std_in = NoStream
, std_out = CreatePipe
, std_err = CreatePipe
}
release (i,o,e,p) = do
_ <- waitForProcess p
forM_ [i,o,e] $ mapM_ hClose
readFully :: Handle -> IO String
readFully handle = do
hSetBinaryMode handle True
str <- hGetContents handle
length str `seq` return str

View File

@@ -12,12 +12,6 @@ command -v cabal ||
cabal update ||
die "can't update"
if [ -e "cabal.project.freeze" ]
then
echo "Renaming cabal.project.freeze to .bak to avoid it interferring" >&2
mv "cabal.project.freeze" "cabal.project.freeze.bak" || die "Couldn't rename"
fi
if [ -e /etc/arch-release ]
then
# Arch has an unconventional packaging setup

View File

@@ -12,22 +12,6 @@ then
fail "There are uncommitted changes"
fi
if [[ $(git log -1 --pretty=%B) != *"CHANGELOG"* ]]
then
fail "Expected git log message to contain CHANGELOG"
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)
if [[ -z "$current" ]]
then
@@ -50,6 +34,17 @@ then
fail "You are not on master"
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 "* ]]
then
fail "Expected git log message to be 'Stable version ...'"
@@ -61,14 +56,11 @@ cat << EOF
Manual Checklist
$((i++)). Make sure none of the automated checks above failed
$((i++)). Run \`builders/build_builder build/*/\` to update all builder images.
$((j++)). \`builders/run_builder dist-newstyle/sdist/ShellCheck-*.tar.gz builders/*/\` to verify that they work.
$((j++)). \`for f in \$(cat build/*/tag); do docker push "\$f"; done\` to upload them.
$((i++)). Run test/distrotest to ensure that most distros can build OOTB.
$((i++)). Make sure GitHub Build currently passes: https://github.com/koalaman/shellcheck/actions
$((i++)). Make sure SnapCraft build currently works: https://snapcraft.io/shellcheck/builds
$((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++)). Make sure the Hackage package builds locally.
$((i++)). Make sure the Hackage package builds.
Release Steps
@@ -80,6 +72,5 @@ $((j++)). Verify release:
b. Check that the docker images have version tags: https://hub.docker.com/u/koalaman
$((j++)). If no disaster, upload to Hackage: http://hackage.haskell.org/upload
$((j++)). Push a new commit that updates CHANGELOG.md
$((j++)). Run 'autoupdate' from https://github.com/koalaman/shellcheck-precommit
EOF
exit "$failed"

View File

@@ -8,15 +8,6 @@ die() { echo "$*" >&4; exit 1; }
[ -e "ShellCheck.cabal" ] || die "ShellCheck.cabal not in this dir"
if ( snap list | grep -q docker ) > /dev/null 2>&1
then
# Snap docker can't mount /tmp in containers
echo "You appear to be using Docker from snap. Creating ~/tmp for temp files." >&2
echo >&2
export TMPDIR="$HOME/tmp"
mkdir -p "$TMPDIR"
fi
[ "$1" = "--run" ] || {
cat << EOF
This script pulls multiple distros via Docker and compiles
@@ -26,13 +17,13 @@ and is still highly experimental.
Make sure you're plugged in and have screen/tmux in place,
then re-run with $0 --run to continue.
Also note that dist*/ and .stack-work/ will be deleted.
Also note that dist* will be deleted.
EOF
exit 0
}
echo "Deleting 'dist', 'dist-newstyle', and '.stack-work'..."
rm -rf dist dist-newstyle .stack-work
echo "Deleting 'dist' and 'dist-newstyle'..."
rm -rf dist dist-newstyle
execs=$(find . -name shellcheck)
@@ -83,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
# 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: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
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
exit "$final"

View File

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

View File

@@ -15,7 +15,7 @@ die() { echo "$*" >&2; exit 1; }
command -v stack ||
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"
# Nice to haves, but not necessary