mirror of
https://github.com/koalaman/shellcheck
synced 2025-07-06 04:51:37 -07:00
Compare commits
204 commits
Author | SHA1 | Date | |
---|---|---|---|
|
20d11c1c33 | ||
|
47d358c1d4 | ||
|
ad58768563 | ||
|
62a8ecf9bf | ||
|
0b5410d759 | ||
|
975cfeee50 | ||
|
b381658dbc | ||
|
950578ae0e | ||
|
f78714e0f6 | ||
|
de07ec1c56 | ||
|
85066dd805 | ||
|
140274b810 | ||
|
dc41f0cc5b | ||
|
fbb8386797 | ||
|
efb5a5a274 | ||
|
553a80f77a | ||
|
7fc992d0dc | ||
|
c553288085 | ||
|
1be41dd652 | ||
|
2eddec86d3 | ||
|
c41f3a4b8a | ||
|
574c6d18fb | ||
|
e4853af5b0 | ||
|
72af76f443 | ||
|
8ff0c5be7a | ||
|
4f628cbe2a | ||
|
bc60607f9e | ||
|
3a9ddae06b | ||
|
cbf0b33463 | ||
|
fe315a25c4 | ||
|
d3001f337a | ||
|
7deb7e853b | ||
|
26b949b9b0 | ||
|
5adfea21ee | ||
|
0ecaf2b5f1 | ||
|
195b70db8c | ||
|
3c75d82db5 | ||
|
7f3f014d49 | ||
|
944d87915a | ||
|
47bff1d5fd | ||
|
0ee46a0f33 | ||
|
792466bc22 | ||
|
097018754b | ||
|
f2932ebcdc | ||
|
5e3e98bcb0 | ||
|
68bc17b8ea | ||
|
5c2be767ab | ||
|
79e43c4550 | ||
|
ca65071d77 | ||
|
8a1b24c7af | ||
|
88e441453b | ||
|
1487e57a46 | ||
|
68e6f02267 | ||
|
c7611dfcc6 | ||
|
15f132e167 | ||
|
4e69767b03 | ||
|
8bf8cf5cc7 | ||
|
17ebc3dda0 | ||
|
4cd76283da | ||
|
cd6fdee99b | ||
|
c831616f3a | ||
|
38c5ba7c79 | ||
|
2696c6472d | ||
|
d590a35ff8 | ||
|
6d2f3d8628 | ||
|
4c85274921 | ||
|
6593096ba0 | ||
|
98b8dc0720 | ||
|
95c0cc2e4b | ||
|
e5fdec970a | ||
|
8746c6e7f2 | ||
|
61b7e66f80 | ||
|
b408f54620 | ||
|
3946cbd4a0 | ||
|
c4b7b79b8b | ||
|
23e76de4f2 | ||
|
15de97e33f | ||
|
78d1ee0222 | ||
|
ac8fb00504 | ||
|
a13cb85f49 | ||
|
a7a906e2cb | ||
|
d705716dc4 | ||
|
76ff702e93 | ||
|
4f81dbe839 | ||
|
796c6bd848 | ||
|
69fe4e1306 | ||
|
2c5155e43d | ||
|
04a86245a1 | ||
|
79491db9f6 | ||
|
5241878e59 | ||
|
30b32af873 | ||
|
da8854cac6 | ||
|
39a035793c | ||
|
0a7bb1822e | ||
|
c4123375e0 | ||
|
52dc66349b | ||
|
9cb21c8557 | ||
|
50db9a29c4 | ||
|
94214ee725 | ||
|
37dfb67768 | ||
|
a7e65dca8d | ||
|
8bc7345aa7 | ||
|
ad3c3146f0 | ||
|
55be4543f2 | ||
|
8c4c112c25 | ||
|
d80fdfa9e8 | ||
|
1565091b1d | ||
|
d056549406 | ||
|
f5758e1789 | ||
|
6a44a19f17 | ||
|
b1b95c2c17 | ||
|
de95624d31 | ||
|
b5ab220652 | ||
|
1bce426fcf | ||
|
ba86c6363c | ||
|
67abfe159e | ||
|
025cc5266e | ||
|
5a6f4840ad | ||
|
9e0fdbe431 | ||
|
b7f88ec4b7 | ||
|
7b0589988f | ||
|
71889c139a | ||
|
a6984cddb0 | ||
|
3f40b688ee | ||
|
6c81505870 | ||
|
10afe83ce3 | ||
|
a786f996a1 | ||
|
6e5b5401c6 | ||
|
71c0fcb737 | ||
|
add49cda17 | ||
|
e1ad063834 | ||
|
ee41c780f4 | ||
|
980e7d3ca8 | ||
|
dedf932fe8 | ||
|
3bd7df955b | ||
|
dab77b2c8d | ||
|
f983d9ae93 | ||
|
bfe4342697 | ||
|
a47a42cb45 | ||
|
eed0174e90 | ||
|
0c46b8b2d5 | ||
|
208e38358e | ||
|
c1452e0d17 | ||
|
c97abdb939 | ||
|
f242922a2e | ||
|
a37803d2b8 | ||
|
09d04c4c9b | ||
|
e5028481e2 | ||
|
5a961371a7 | ||
|
e5208ccb50 | ||
|
4c1d9171b2 | ||
|
a9e7bf1950 | ||
|
f2729f73cb | ||
|
175d3cc9b7 | ||
|
5c50b0b189 | ||
|
74282b0a93 | ||
|
b6d4952e2e | ||
|
fdcce458c1 | ||
|
ca255fe326 | ||
|
a3b8be82fe | ||
|
ac63dc33c9 | ||
|
903421fb5d | ||
|
00ffd2db33 | ||
|
1e1045e73e | ||
|
be8e4b2b8a | ||
|
a71a13c2fc | ||
|
1aeab287e6 | ||
|
2a95bc6be3 | ||
|
4fd0615501 | ||
|
8b3c37aa36 | ||
|
dc2f388310 | ||
|
99a94421ab | ||
|
6a6d8e9fc4 | ||
|
592c17e4f2 | ||
|
9605396bef | ||
|
c89ec2fd49 | ||
|
410ec54617 | ||
|
90d3172dfe | ||
|
d18b2553cf | ||
|
dd747b2a98 | ||
|
9490b94886 | ||
|
372c0b667e | ||
|
01aee1a859 | ||
|
c9e27c2470 | ||
|
4ffa9cc397 | ||
|
b625cc1acc | ||
|
f03c437e2f | ||
|
824c802b63 | ||
|
b3932dfa10 | ||
|
a54965dd2c | ||
|
46b678fca8 | ||
|
be0d5d4163 | ||
|
5fec3f9b34 | ||
|
1164aa4efc | ||
|
ff85a5a2a2 | ||
|
08b437974e | ||
|
15fd2c314c | ||
|
e6e8ab0415 | ||
|
b1ca3929e3 | ||
|
c05380d518 | ||
|
2842ce97b8 | ||
|
78dea1d4f9 | ||
|
5a3eb89e38 | ||
|
3342902d9a |
53 changed files with 1618 additions and 624 deletions
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
58
.github/workflows/build.yml
vendored
58
.github/workflows/build.yml
vendored
|
@ -15,7 +15,7 @@ jobs:
|
|||
sudo apt-get install cabal-install
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
@ -37,24 +37,47 @@ jobs:
|
|||
mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
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 Source Code
|
||||
name: Build
|
||||
needs: package_source
|
||||
strategy:
|
||||
matrix:
|
||||
build: [linux.x86_64, linux.aarch64, linux.armv6hf, darwin.x86_64, windows.x86_64]
|
||||
build: [linux.x86_64, linux.aarch64, linux.armv6hf, linux.riscv64, darwin.x86_64, darwin.aarch64, windows.x86_64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- name: Build source
|
||||
run: |
|
||||
|
@ -63,9 +86,9 @@ jobs:
|
|||
( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} )
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bin
|
||||
name: ${{matrix.build}}.bin
|
||||
path: bin/
|
||||
|
||||
package_binary:
|
||||
|
@ -74,25 +97,25 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- 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@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: deploy
|
||||
path: deploy/
|
||||
|
@ -103,11 +126,16 @@ 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@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- name: Upload to GitHub
|
||||
env:
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -20,3 +20,4 @@ cabal.config
|
|||
/parts/
|
||||
/prime/
|
||||
*.snap
|
||||
/dist-newstyle/
|
||||
|
|
|
@ -3,28 +3,10 @@
|
|||
# binaries previously built and deployed to GitHub.
|
||||
|
||||
function multi_arch_docker::install_docker_buildx() {
|
||||
# Install up-to-date version of docker, with buildx support.
|
||||
local -r docker_apt_repo='https://download.docker.com/linux/ubuntu'
|
||||
curl -fsSL "${docker_apt_repo}/gpg" | sudo apt-key add -
|
||||
local -r os="$(lsb_release -cs)"
|
||||
sudo add-apt-repository "deb [arch=amd64] $docker_apt_repo $os stable"
|
||||
sudo apt-get update
|
||||
sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce
|
||||
|
||||
# Enable docker daemon experimental support (for 'pull --platform').
|
||||
local -r config='/etc/docker/daemon.json'
|
||||
if [[ -e "$config" ]]; then
|
||||
sudo sed -i -e 's/{/{ "experimental": true, /' "$config"
|
||||
else
|
||||
echo '{ "experimental": true }' | sudo tee "$config"
|
||||
fi
|
||||
sudo systemctl restart docker
|
||||
|
||||
# Install QEMU multi-architecture support for docker buildx.
|
||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
|
||||
# Instantiate docker buildx builder with multi-architecture support.
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
docker buildx create --name mybuilder
|
||||
docker buildx use mybuilder
|
||||
# Start up buildx and verify that all is OK.
|
||||
|
@ -98,6 +80,7 @@ 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
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
# 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
|
||||
|
44
CHANGELOG.md
44
CHANGELOG.md
|
@ -1,3 +1,47 @@
|
|||
## Git
|
||||
### Added
|
||||
- SC2327/SC2328: Warn about capturing the output of redirected commands.
|
||||
- SC2329: Warn when (non-escaping) functions are never invoked.
|
||||
- SC2330: Warn about unsupported glob matches with [[ .. ]] in BusyBox.
|
||||
- SC2331: Suggest using standard -e instead of unary -a in tests.
|
||||
- SC2332: Warn about `[ ! -o opt ]` being unconditionally true in Bash.
|
||||
- SC3062: Warn about bashism `[ -o opt ]`.
|
||||
- Precompiled binaries for Linux riscv64 (linux.riscv64)
|
||||
### Changed
|
||||
- SC2002 about Useless Use Of Cat is now disabled by default. It can be
|
||||
re-enabled with `--enable=useless-use-of-cat` or equivalent directive.
|
||||
- SC2015 about `A && B || C` no longer triggers when B is a test command.
|
||||
- SC3012: Do not warn about `\<` and `\>` in test/[] as specified in POSIX.1-2024
|
||||
### Fixed
|
||||
- SC2218 about function use-before-define is now more accurate.
|
||||
- SC2317 about unreachable commands is now less spammy for nested ones.
|
||||
- SC2292, optional suggestion for [[ ]], now triggers for Busybox.
|
||||
|
||||
### Removed
|
||||
- SC3013: removed since the operators `-ot/-nt/-ef` are specified in POSIX.1-2024
|
||||
|
||||
## v0.10.0 - 2024-03-07
|
||||
### Added
|
||||
- Precompiled binaries for macOS ARM64 (darwin.aarch64)
|
||||
- Added support for busybox sh
|
||||
- Added flag --rcfile to specify an rc file by name.
|
||||
- Added `extended-analysis=true` directive to enable/disable dataflow analysis
|
||||
(with a corresponding --extended-analysis flag).
|
||||
- SC2324: Warn when x+=1 appends instead of increments
|
||||
- SC2325: Warn about multiple `!`s in dash/sh.
|
||||
- SC2326: Warn about `foo | ! bar` in bash/dash/sh.
|
||||
- SC3012: Warn about lexicographic-compare bashism in test like in [ ]
|
||||
- SC3013: Warn bashism `test _ -op/-nt/-ef _` like in [ ]
|
||||
- SC3014: Warn bashism `test _ == _` like in [ ]
|
||||
- SC3015: Warn bashism `test _ =~ _` like in [ ]
|
||||
- SC3016: Warn bashism `test -v _` like in [ ]
|
||||
- SC3017: Warn bashism `test -a _` like in [ ]
|
||||
|
||||
### Fixed
|
||||
- source statements with here docs now work correctly
|
||||
- "(Array.!): undefined array element" error should no longer occur
|
||||
|
||||
|
||||
## v0.9.0 - 2022-12-12
|
||||
### Added
|
||||
- SC2316: Warn about 'local readonly foo' and similar (thanks, patrickxia!)
|
||||
|
|
25
README.md
25
README.md
|
@ -77,7 +77,7 @@ You can see ShellCheck suggestions directly in a variety of editors.
|
|||
|
||||
* Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck).
|
||||
|
||||
* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
|
||||
* Pulsar Edit (former Atom), through [linter-shellcheck-pulsar](https://github.com/pulsar-cooperative/linter-shellcheck-pulsar).
|
||||
|
||||
* VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck).
|
||||
|
||||
|
@ -110,8 +110,11 @@ 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 Check](https://trunk.io/products/check) (universal linter; [allows you to explicitly version your shellcheck install](https://github.com/trunk-io/plugins/blob/bcbb361dcdbe4619af51ea7db474d7fb87540d20/.trunk/trunk.yaml#L32)) via the [shellcheck plugin](https://github.com/trunk-io/plugins/blob/main/linters/shellcheck/plugin.yaml)
|
||||
* [CodeRabbit](https://coderabbit.ai/)
|
||||
|
||||
Most other services, including [GitLab](https://about.gitlab.com/), let you install
|
||||
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
|
||||
|
@ -193,6 +196,12 @@ 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
|
||||
|
@ -221,17 +230,26 @@ 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)
|
||||
|
||||
or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
|
||||
(including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds).
|
||||
|
||||
There are currently no official binaries for Apple Silicon, but third party builds are available via
|
||||
[ShellCheck for Visual Studio Code](https://github.com/vscode-shellcheck/shellcheck-binaries/releases).
|
||||
|
||||
Distro packages already come with a `man` page. If you are building from source, it can be installed with:
|
||||
|
||||
```console
|
||||
|
@ -299,10 +317,6 @@ 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`):
|
||||
|
@ -548,4 +562,3 @@ 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)!
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
Name: ShellCheck
|
||||
Version: 0.9.0
|
||||
Version: 0.10.0
|
||||
Synopsis: Shell script analysis tool
|
||||
License: GPL-3
|
||||
License-file: LICENSE
|
||||
|
@ -46,21 +46,21 @@ library
|
|||
semigroups
|
||||
build-depends:
|
||||
-- The lower bounds are based on GHC 7.10.3
|
||||
-- The upper bounds are based on GHC 9.4.3
|
||||
aeson >= 1.4.0 && < 2.2,
|
||||
-- The upper bounds are based on GHC 9.8.1
|
||||
aeson >= 1.4.0 && < 2.3,
|
||||
array >= 0.5.1 && < 0.6,
|
||||
base >= 4.8.0.0 && < 5,
|
||||
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.9,
|
||||
filepath >= 1.4.0 && < 1.5,
|
||||
mtl >= 2.2.2 && < 2.3,
|
||||
bytestring >= 0.10.6 && < 0.13,
|
||||
containers >= 0.5.6 && < 0.8,
|
||||
deepseq >= 1.4.1 && < 1.6,
|
||||
Diff >= 0.4.0 && < 1.1,
|
||||
fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9),
|
||||
filepath >= 1.4.0 && < 1.6,
|
||||
mtl >= 2.2.2 && < 2.4,
|
||||
parsec >= 3.1.14 && < 3.2,
|
||||
QuickCheck >= 2.14.2 && < 2.15,
|
||||
QuickCheck >= 2.14.2 && < 2.16,
|
||||
regex-tdfa >= 1.2.0 && < 1.4,
|
||||
transformers >= 0.4.2 && < 0.6,
|
||||
transformers >= 0.4.2 && < 0.7,
|
||||
|
||||
-- getXdgDirectory from 1.2.3.0
|
||||
directory >= 1.2.3 && < 1.4,
|
||||
|
|
|
@ -11,3 +11,7 @@ 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.
|
||||
|
|
40
build/darwin.aarch64/Dockerfile
Normal file
40
build/darwin.aarch64/Dockerfile
Normal file
|
@ -0,0 +1,40 @@
|
|||
FROM ghcr.io/shepherdjerred/macos-cross-compiler:latest
|
||||
|
||||
ENV TARGET aarch64-apple-darwin22
|
||||
ENV TARGETNAME darwin.aarch64
|
||||
|
||||
# Build dependencies
|
||||
USER root
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
ENV LC_ALL C.utf8
|
||||
|
||||
# Install basic deps
|
||||
RUN apt-get update && apt-get install -y automake autoconf build-essential curl xz-utils qemu-user-static
|
||||
|
||||
# Install a more suitable host compiler
|
||||
WORKDIR /host-ghc
|
||||
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin
|
||||
RUN curl -L 'https://downloads.haskell.org/~ghc/8.10.7/ghc-8.10.7-x86_64-deb10-linux.tar.xz' | tar xJ --strip-components=1
|
||||
RUN ./configure && make install
|
||||
|
||||
# Build GHC. We have to use an old version because cross-compilation across OS has since broken.
|
||||
WORKDIR /ghc
|
||||
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.7/ghc-8.10.7-src.tar.xz" | tar xJ --strip-components=1
|
||||
RUN apt-get install -y llvm-12
|
||||
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
|
||||
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
|
||||
RUN make install
|
||||
|
||||
# Due to an apparent cabal bug, we specify our options directly to cabal
|
||||
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
|
||||
ENV CABALOPTS "--ghc-options;-optc-Os -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;--constraint=hashable==1.3.5.0"
|
||||
|
||||
# Prebuild the dependencies
|
||||
RUN cabal update
|
||||
RUN IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
|
||||
|
||||
# Copy the build script
|
||||
COPY build /usr/bin
|
||||
|
||||
WORKDIR /scratch
|
||||
ENTRYPOINT ["/usr/bin/build"]
|
16
build/darwin.aarch64/build
Executable file
16
build/darwin.aarch64/build
Executable file
|
@ -0,0 +1,16 @@
|
|||
#!/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"
|
1
build/darwin.aarch64/tag
Normal file
1
build/darwin.aarch64/tag
Normal file
|
@ -0,0 +1 @@
|
|||
koalaman/scbuilder-darwin-aarch64
|
|
@ -6,15 +6,18 @@ ENV TARGETNAME darwin.x86_64
|
|||
# Build dependencies
|
||||
USER root
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
RUN apt-get update && apt-get install -y ghc automake autoconf llvm curl
|
||||
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/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1
|
||||
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
|
||||
RUN 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
|
||||
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/local/bin
|
||||
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
|
||||
|
|
|
@ -4,7 +4,6 @@ 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"
|
||||
|
|
|
@ -6,19 +6,29 @@ ENV TARGETNAME linux.aarch64
|
|||
# Build dependencies
|
||||
USER root
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
RUN apt-get update && apt-get install -y ghc automake autoconf build-essential llvm curl qemu-user-static gcc-$TARGET
|
||||
|
||||
# These deps are from 20.04, because GHC's compiler/llvm support moves slowly
|
||||
RUN apt-get update && apt-get install -y llvm gcc-$TARGET
|
||||
|
||||
# The rest are from 22.10
|
||||
RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list
|
||||
# Kinetic does not receive updates anymore, switch to last available
|
||||
RUN sed -e 's/archive.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list
|
||||
RUN sed -e 's/security.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list
|
||||
|
||||
RUN apt-get update && apt-get install -y ghc alex happy automake autoconf build-essential curl qemu-user-static
|
||||
|
||||
# Build GHC
|
||||
WORKDIR /ghc
|
||||
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1
|
||||
RUN curl -L "https://downloads.haskell.org/~ghc/9.2.8/ghc-9.2.8-src.tar.xz" | tar xJ --strip-components=1
|
||||
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
|
||||
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
|
||||
RUN make install
|
||||
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/local/bin
|
||||
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;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
|
||||
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;-c;hashable -arch-native"
|
||||
|
||||
# Prebuild the dependencies
|
||||
RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
|
||||
|
|
|
@ -4,7 +4,6 @@ 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"
|
||||
|
|
|
@ -1,25 +1,7 @@
|
|||
# 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.
|
||||
#
|
||||
# This Docker file uses a custom QEmu fork with patches to follow execve
|
||||
# to build all of ShellCheck emulated.
|
||||
|
||||
FROM ubuntu:20.04
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ENV TARGETNAME linux.armv6hf
|
||||
|
||||
|
@ -27,34 +9,34 @@ ENV TARGETNAME linux.armv6hf
|
|||
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
|
||||
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 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
|
||||
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 pirun apt-get update
|
||||
RUN pirun apt-get install -y ghc cabal-install
|
||||
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"
|
||||
RUN pirun cabal update
|
||||
RUN IFS=";" && pirun cabal install --dependencies-only $CABALOPTS ShellCheck
|
||||
RUN IFS=';' && pirun cabal install $CABALOPTS --lib fgl
|
||||
# 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
|
||||
WORKDIR /pi/scratch
|
||||
COPY build /pi/usr/bin
|
||||
ENTRYPOINT ["/bin/pirun", "/usr/bin/build"]
|
||||
COPY build /chroot/bin
|
||||
ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"]
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
#!/bin/sh
|
||||
set -xe
|
||||
cd /scratch
|
||||
mkdir /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
|
||||
|
|
93
build/linux.armv6hf/cabal.project.freeze
Normal file
93
build/linux.armv6hf/cabal.project.freeze
Normal file
|
@ -0,0 +1,93 @@
|
|||
active-repositories: hackage.haskell.org:merge
|
||||
constraints: any.Diff ==0.5,
|
||||
any.OneTuple ==0.4.2,
|
||||
any.QuickCheck ==2.14.3,
|
||||
QuickCheck -old-random +templatehaskell,
|
||||
any.StateVar ==1.2.2,
|
||||
any.aeson ==2.2.3.0,
|
||||
aeson +ordered-keymap,
|
||||
any.array ==0.5.4.0,
|
||||
any.assoc ==1.1.1,
|
||||
assoc -tagged,
|
||||
any.base ==4.15.1.0,
|
||||
any.base-orphans ==0.9.2,
|
||||
any.bifunctors ==5.6.2,
|
||||
bifunctors +tagged,
|
||||
any.binary ==0.8.8.0,
|
||||
any.bytestring ==0.10.12.1,
|
||||
any.character-ps ==0.1,
|
||||
any.comonad ==5.0.8,
|
||||
comonad +containers +distributive +indexed-traversable,
|
||||
any.containers ==0.6.4.1,
|
||||
any.contravariant ==1.5.5,
|
||||
contravariant +semigroups +statevar +tagged,
|
||||
any.data-array-byte ==0.1.0.1,
|
||||
any.data-fix ==0.3.3,
|
||||
any.deepseq ==1.4.5.0,
|
||||
any.directory ==1.3.6.2,
|
||||
any.distributive ==0.6.2.1,
|
||||
distributive +semigroups +tagged,
|
||||
any.dlist ==1.0,
|
||||
dlist -werror,
|
||||
any.exceptions ==0.10.4,
|
||||
any.fgl ==5.8.2.0,
|
||||
fgl +containers042,
|
||||
any.filepath ==1.4.2.1,
|
||||
any.foldable1-classes-compat ==0.1,
|
||||
foldable1-classes-compat +tagged,
|
||||
any.generically ==0.1.1,
|
||||
any.ghc-bignum ==1.1,
|
||||
any.ghc-boot-th ==9.0.2,
|
||||
any.ghc-prim ==0.7.0,
|
||||
any.hashable ==1.4.6.0,
|
||||
hashable -arch-native +integer-gmp -random-initial-seed,
|
||||
any.indexed-traversable ==0.1.4,
|
||||
any.indexed-traversable-instances ==0.1.2,
|
||||
any.integer-conversion ==0.1.1,
|
||||
any.integer-logarithms ==1.0.3.1,
|
||||
integer-logarithms -check-bounds +integer-gmp,
|
||||
any.mtl ==2.2.2,
|
||||
any.network-uri ==2.6.4.2,
|
||||
any.parsec ==3.1.14.0,
|
||||
any.pretty ==1.1.3.6,
|
||||
any.primitive ==0.9.0.0,
|
||||
any.process ==1.6.13.2,
|
||||
any.random ==1.2.1.2,
|
||||
any.regex-base ==0.94.0.2,
|
||||
any.regex-tdfa ==1.3.2.2,
|
||||
regex-tdfa +doctest -force-o2,
|
||||
any.rts ==1.0.2,
|
||||
any.scientific ==0.3.8.0,
|
||||
scientific -integer-simple,
|
||||
any.semialign ==1.3.1,
|
||||
semialign +semigroupoids,
|
||||
any.semigroupoids ==6.0.1,
|
||||
semigroupoids +comonad +containers +contravariant +distributive +tagged +unordered-containers,
|
||||
any.splitmix ==0.1.0.5,
|
||||
splitmix -optimised-mixer,
|
||||
any.stm ==2.5.0.0,
|
||||
any.strict ==0.5,
|
||||
any.tagged ==0.8.8,
|
||||
tagged +deepseq +transformers,
|
||||
any.template-haskell ==2.17.0.0,
|
||||
any.text ==1.2.5.0,
|
||||
any.text-iso8601 ==0.1.1,
|
||||
any.text-short ==0.1.6,
|
||||
text-short -asserts,
|
||||
any.th-abstraction ==0.7.0.0,
|
||||
any.th-compat ==0.1.5,
|
||||
any.these ==1.2.1,
|
||||
any.time ==1.9.3,
|
||||
any.time-compat ==1.9.7,
|
||||
any.transformers ==0.5.6.2,
|
||||
any.transformers-compat ==0.7.2,
|
||||
transformers-compat -five +five-three -four +generic-deriving +mtl -three -two,
|
||||
any.unix ==2.7.2.2,
|
||||
any.unordered-containers ==0.2.20,
|
||||
unordered-containers -debug,
|
||||
any.uuid-types ==1.0.6,
|
||||
any.vector ==0.13.1.0,
|
||||
vector +boundschecks -internalchecks -unsafechecks -wall,
|
||||
any.vector-stream ==0.1.0.1,
|
||||
any.witherable ==0.5
|
||||
index-state: hackage.haskell.org 2024-06-18T02:21:19Z
|
48
build/linux.armv6hf/scutil
Normal file
48
build/linux.armv6hf/scutil
Normal file
|
@ -0,0 +1,48 @@
|
|||
#!/bin/dash
|
||||
# Various ShellCheck build utility functions
|
||||
|
||||
# Generally set a ulimit to avoid QEmu using too much memory
|
||||
ulimit -v "$((10*1024*1024))"
|
||||
# If we happen to invoke or run under QEmu, make sure to follow execve.
|
||||
# This requires a patched QEmu.
|
||||
export QEMU_EXECVE=1
|
||||
|
||||
# Retry a command until it succeeds
|
||||
# Usage: scutil retry 3 mycmd
|
||||
retry() {
|
||||
n="$1"
|
||||
ret=1
|
||||
shift
|
||||
while [ "$n" -gt 0 ]
|
||||
do
|
||||
"$@"
|
||||
ret=$?
|
||||
[ "$ret" = 0 ] && break
|
||||
n=$((n-1))
|
||||
done
|
||||
return "$ret"
|
||||
}
|
||||
|
||||
# Install all dependencies from a freeze file
|
||||
# Usage: scutil install_from_freeze /path/cabal.project.freeze cabal install
|
||||
install_from_freeze() {
|
||||
linefeed=$(printf '\nx')
|
||||
linefeed=${linefeed%x}
|
||||
flags=$(
|
||||
sed 's/constraints:/&\n /' "$1" |
|
||||
grep -vw -e rts -e base |
|
||||
sed -n -e 's/^ *\([^,]*\).*/\1/p' |
|
||||
sed -e 's/any\.\([^ ]*\) ==\(.*\)/\1-\2/; te; s/.*/--constraint\n&/; :e')
|
||||
shift
|
||||
# shellcheck disable=SC2086
|
||||
( IFS=$linefeed; set -x; "$@" $flags )
|
||||
}
|
||||
|
||||
# Run a command under emulation.
|
||||
# This assumes the correct emulator is named 'qemu' and the chroot is /chroot
|
||||
# Usage: scutil emu echo "Hello World"
|
||||
emu() {
|
||||
chroot /chroot /bin/qemu /usr/bin/env "$@"
|
||||
}
|
||||
|
||||
"$@"
|
46
build/linux.riscv64/Dockerfile
Normal file
46
build/linux.riscv64/Dockerfile
Normal file
|
@ -0,0 +1,46 @@
|
|||
FROM ubuntu:24.04
|
||||
|
||||
ENV TARGETNAME linux.riscv64
|
||||
ENV TARGET riscv64-linux-gnu
|
||||
|
||||
USER root
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
# Init base
|
||||
RUN apt-get update -y
|
||||
|
||||
# Install qemu
|
||||
RUN apt-get install -y --no-install-recommends build-essential ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev curl ca-certificates python3-virtualenv git python3-setuptools debootstrap
|
||||
WORKDIR /qemu
|
||||
RUN git clone --depth 1 https://github.com/koalaman/qemu .
|
||||
RUN ./configure --target-list=riscv64-linux-user --static --disable-system --disable-pie --disable-werror
|
||||
RUN cd build && ninja qemu-riscv64
|
||||
ENV QEMU_EXECVE 1
|
||||
|
||||
# Convenience utility
|
||||
COPY scutil /bin/scutil
|
||||
# We have to copy to /usr/bin because debootstrap will try to symlink /bin and fail if it exists
|
||||
COPY scutil /chroot/usr/bin/scutil
|
||||
RUN chmod +x /bin/scutil /chroot/usr/bin/scutil
|
||||
|
||||
# Set up a riscv64 userspace
|
||||
WORKDIR /
|
||||
RUN debootstrap --arch=riscv64 --variant=minbase --components=main,universe --foreign noble /chroot http://ports.ubuntu.com/ubuntu-ports
|
||||
RUN cp /qemu/build/qemu-riscv64 /chroot/bin/qemu
|
||||
RUN scutil emu /debootstrap/debootstrap --second-stage
|
||||
|
||||
# Install deps in the chroot
|
||||
RUN scutil emu apt-get update
|
||||
RUN scutil emu apt-get install -y --no-install-recommends ghc cabal-install
|
||||
RUN scutil emu cabal update
|
||||
|
||||
# Generated with: cabal freeze -c 'hashable -arch-native'. We put it in /etc so cabal won't find it.
|
||||
COPY cabal.project.freeze /chroot/etc
|
||||
|
||||
# Build all dependencies from the freeze file. The emulator segfaults at random,
|
||||
# so retry a few times.
|
||||
RUN scutil install_from_freeze /chroot/etc/cabal.project.freeze retry 5 emu cabal install --keep-going
|
||||
|
||||
# Copy the build script
|
||||
COPY build /chroot/bin/build
|
||||
ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"]
|
21
build/linux.riscv64/build
Executable file
21
build/linux.riscv64/build
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/bin/sh
|
||||
set -xe
|
||||
IFS=';'
|
||||
{
|
||||
mkdir -p /tmp/scratch
|
||||
cd /tmp/scratch
|
||||
tar xzv --strip-components=1
|
||||
chmod +x striptests && ./striptests
|
||||
# Use a freeze file to ensure we use the same dependencies we cached during
|
||||
# the docker image build. We don't want to spend time compiling anything new.
|
||||
cp /etc/cabal.project.freeze .
|
||||
mkdir "$TARGETNAME"
|
||||
# Retry in case of random segfault
|
||||
scutil retry 3 cabal build --enable-executable-static
|
||||
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
|
||||
ls -l "$TARGETNAME"
|
||||
"$TARGET-strip" -s "$TARGETNAME/shellcheck"
|
||||
ls -l "$TARGETNAME"
|
||||
"$TARGETNAME/shellcheck" --version
|
||||
} >&2
|
||||
tar czv "$TARGETNAME"
|
93
build/linux.riscv64/cabal.project.freeze
Normal file
93
build/linux.riscv64/cabal.project.freeze
Normal file
|
@ -0,0 +1,93 @@
|
|||
active-repositories: hackage.haskell.org:merge
|
||||
constraints: any.Diff ==0.5,
|
||||
any.OneTuple ==0.4.2,
|
||||
any.QuickCheck ==2.14.3,
|
||||
QuickCheck -old-random +templatehaskell,
|
||||
any.StateVar ==1.2.2,
|
||||
any.aeson ==2.2.3.0,
|
||||
aeson +ordered-keymap,
|
||||
any.array ==0.5.4.0,
|
||||
any.assoc ==1.1.1,
|
||||
assoc -tagged,
|
||||
any.base ==4.17.2.0,
|
||||
any.base-orphans ==0.9.2,
|
||||
any.bifunctors ==5.6.2,
|
||||
bifunctors +tagged,
|
||||
any.binary ==0.8.9.1,
|
||||
any.bytestring ==0.11.5.2,
|
||||
any.character-ps ==0.1,
|
||||
any.comonad ==5.0.8,
|
||||
comonad +containers +distributive +indexed-traversable,
|
||||
any.containers ==0.6.7,
|
||||
any.contravariant ==1.5.5,
|
||||
contravariant +semigroups +statevar +tagged,
|
||||
any.data-fix ==0.3.3,
|
||||
any.deepseq ==1.4.8.0,
|
||||
any.directory ==1.3.7.1,
|
||||
any.distributive ==0.6.2.1,
|
||||
distributive +semigroups +tagged,
|
||||
any.dlist ==1.0,
|
||||
dlist -werror,
|
||||
any.exceptions ==0.10.5,
|
||||
any.fgl ==5.8.2.0,
|
||||
fgl +containers042,
|
||||
any.filepath ==1.4.2.2,
|
||||
any.foldable1-classes-compat ==0.1,
|
||||
foldable1-classes-compat +tagged,
|
||||
any.generically ==0.1.1,
|
||||
any.ghc-bignum ==1.3,
|
||||
any.ghc-boot-th ==9.4.7,
|
||||
any.ghc-prim ==0.9.1,
|
||||
any.hashable ==1.4.6.0,
|
||||
hashable -arch-native +integer-gmp -random-initial-seed,
|
||||
any.indexed-traversable ==0.1.4,
|
||||
any.indexed-traversable-instances ==0.1.2,
|
||||
any.integer-conversion ==0.1.1,
|
||||
any.integer-logarithms ==1.0.3.1,
|
||||
integer-logarithms -check-bounds +integer-gmp,
|
||||
any.mtl ==2.2.2,
|
||||
any.network-uri ==2.6.4.2,
|
||||
any.os-string ==2.0.3,
|
||||
any.parsec ==3.1.16.1,
|
||||
any.pretty ==1.1.3.6,
|
||||
any.primitive ==0.9.0.0,
|
||||
any.process ==1.6.17.0,
|
||||
any.random ==1.2.1.2,
|
||||
any.regex-base ==0.94.0.2,
|
||||
any.regex-tdfa ==1.3.2.2,
|
||||
regex-tdfa +doctest -force-o2,
|
||||
any.rts ==1.0.2,
|
||||
any.scientific ==0.3.8.0,
|
||||
scientific -integer-simple,
|
||||
any.semialign ==1.3.1,
|
||||
semialign +semigroupoids,
|
||||
any.semigroupoids ==6.0.1,
|
||||
semigroupoids +comonad +containers +contravariant +distributive +tagged +unordered-containers,
|
||||
any.splitmix ==0.1.0.5,
|
||||
splitmix -optimised-mixer,
|
||||
any.stm ==2.5.1.0,
|
||||
any.strict ==0.5,
|
||||
any.tagged ==0.8.8,
|
||||
tagged +deepseq +transformers,
|
||||
any.template-haskell ==2.19.0.0,
|
||||
any.text ==2.0.2,
|
||||
any.text-iso8601 ==0.1.1,
|
||||
any.text-short ==0.1.6,
|
||||
text-short -asserts,
|
||||
any.th-abstraction ==0.7.0.0,
|
||||
any.th-compat ==0.1.5,
|
||||
any.these ==1.2.1,
|
||||
any.time ==1.12.2,
|
||||
any.time-compat ==1.9.7,
|
||||
any.transformers ==0.5.6.2,
|
||||
any.transformers-compat ==0.7.2,
|
||||
transformers-compat -five +five-three -four +generic-deriving +mtl -three -two,
|
||||
any.unix ==2.7.3,
|
||||
any.unordered-containers ==0.2.20,
|
||||
unordered-containers -debug,
|
||||
any.uuid-types ==1.0.6,
|
||||
any.vector ==0.13.1.0,
|
||||
vector +boundschecks -internalchecks -unsafechecks -wall,
|
||||
any.vector-stream ==0.1.0.1,
|
||||
any.witherable ==0.5
|
||||
index-state: hackage.haskell.org 2024-06-17T00:48:51Z
|
1
build/linux.riscv64/tag
Normal file
1
build/linux.riscv64/tag
Normal file
|
@ -0,0 +1 @@
|
|||
koalaman/scbuilder-linux-riscv64
|
|
@ -1,4 +1,8 @@
|
|||
FROM alpine:latest
|
||||
FROM alpine:3.16
|
||||
# alpine:3.16 (GHC 9.0.1): 5.8 megabytes
|
||||
# alpine:3.17 (GHC 9.0.2): 15.0 megabytes
|
||||
# alpine:3.18 (GHC 9.4.4): 29.0 megabytes
|
||||
# alpine:3.19 (GHC 9.4.7): 29.0 megabytes
|
||||
|
||||
ENV TARGETNAME linux.x86_64
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ WORKDIR /haskell
|
|||
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-x86_64-unknown-mingw32.tar.xz" | tar xJ --strip-components=1
|
||||
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/* .
|
||||
RUN curl -L "https://curl.se/windows/dl-8.7.1_7/curl-8.7.1_7-win64-mingw.zip" | busybox unzip - && mv curl-*-win64-mingw/bin/* .
|
||||
ENV WINEPATH /haskell/bin
|
||||
|
||||
# It's unknown whether Cabal on Windows suffers from the same issue
|
||||
|
|
|
@ -8,7 +8,6 @@ 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"
|
||||
|
|
|
@ -56,6 +56,13 @@ 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
|
||||
|
@ -71,6 +78,11 @@ 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.
|
||||
|
@ -85,7 +97,8 @@ 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* and *ksh*.
|
||||
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash*, *ksh*,
|
||||
and *busybox*.
|
||||
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.
|
||||
|
@ -243,6 +256,12 @@ 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).
|
||||
|
@ -298,7 +317,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 directory
|
||||
will look in `~/.shellcheckrc` followed by the `$XDG_CONFIG_HOME`
|
||||
(usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on
|
||||
Windows. Only the first file found will be used.
|
||||
|
||||
|
@ -378,10 +397,10 @@ long list of wonderful contributors.
|
|||
|
||||
# COPYRIGHT
|
||||
|
||||
Copyright 2012-2022, Vidar Holen and contributors.
|
||||
Copyright 2012-2024, 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)
|
||||
sh(1) bash(1) dash(1) ksh(1)
|
||||
|
|
|
@ -76,7 +76,8 @@ data Options = Options {
|
|||
externalSources :: Bool,
|
||||
sourcePaths :: [FilePath],
|
||||
formatterOptions :: FormatterOptions,
|
||||
minSeverity :: Severity
|
||||
minSeverity :: Severity,
|
||||
rcfile :: Maybe FilePath
|
||||
}
|
||||
|
||||
defaultOptions = Options {
|
||||
|
@ -86,7 +87,8 @@ defaultOptions = Options {
|
|||
formatterOptions = newFormatterOptions {
|
||||
foColorOption = ColorAuto
|
||||
},
|
||||
minSeverity = StyleC
|
||||
minSeverity = StyleC,
|
||||
rcfile = Nothing
|
||||
}
|
||||
|
||||
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
|
||||
|
@ -100,6 +102,8 @@ 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 ++ ")",
|
||||
|
@ -107,6 +111,9 @@ 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')",
|
||||
|
@ -115,7 +122,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)",
|
||||
"Specify dialect (sh, bash, dash, ksh, busybox)",
|
||||
Option "S" ["severity"]
|
||||
(ReqArg (Flag "severity") "SEVERITY")
|
||||
"Minimum severity of errors to consider (error, warning, info, style)",
|
||||
|
@ -252,9 +259,9 @@ runFormatter sys format options files = do
|
|||
else SomeProblems
|
||||
|
||||
parseEnum name value list =
|
||||
case filter ((== value) . fst) list of
|
||||
[(name, value)] -> return value
|
||||
[] -> do
|
||||
case lookup value list of
|
||||
Just value -> return value
|
||||
Nothing -> do
|
||||
printErr $ "Unknown value for --" ++ name ++ ". " ++
|
||||
"Valid options are: " ++ (intercalate ", " $ map fst list)
|
||||
throwError SupportFailure
|
||||
|
@ -367,6 +374,11 @@ parseOption flag options =
|
|||
}
|
||||
}
|
||||
|
||||
Flag "rcfile" str -> do
|
||||
return options {
|
||||
rcfile = Just str
|
||||
}
|
||||
|
||||
Flag "enable" value ->
|
||||
let cs = checkSpec options in return options {
|
||||
checkSpec = cs {
|
||||
|
@ -374,6 +386,14 @@ 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
|
||||
|
||||
|
@ -391,12 +411,20 @@ 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)
|
||||
return SystemInterface {
|
||||
return (newSystemInterface :: SystemInterface IO) {
|
||||
siReadFile = get cache inputs,
|
||||
siFindSource = findSourceFile inputs (sourcePaths options),
|
||||
siGetConfig = getConfig configCache
|
||||
|
@ -441,18 +469,33 @@ 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 = 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 =
|
||||
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
|
||||
|
||||
findConfig paths =
|
||||
case paths of
|
||||
|
@ -490,7 +533,7 @@ ioInterface options files = do
|
|||
where
|
||||
handler :: FilePath -> IOException -> IO (String, Bool)
|
||||
handler file err = do
|
||||
putStrLn $ file ++ ": " ++ show err
|
||||
hPutStrLn stderr $ file ++ ": " ++ show err
|
||||
return ("", True)
|
||||
|
||||
andM a b arg = do
|
||||
|
|
|
@ -23,7 +23,7 @@ description: |
|
|||
# snap connect shellcheck:removable-media
|
||||
|
||||
version: git
|
||||
base: core18
|
||||
base: core20
|
||||
grade: stable
|
||||
confinement: strict
|
||||
|
||||
|
@ -40,16 +40,16 @@ parts:
|
|||
source: .
|
||||
build-packages:
|
||||
- cabal-install
|
||||
- squid
|
||||
stage-packages:
|
||||
- libatomic1
|
||||
override-build: |
|
||||
# See comments in .snapsquid.conf
|
||||
[ "$http_proxy" ] && {
|
||||
squid3 -f .snapsquid.conf
|
||||
export http_proxy="http://localhost:8888"
|
||||
sleep 3
|
||||
}
|
||||
# Give ourselves enough memory to build
|
||||
dd if=/dev/zero of=/tmp/swap bs=1M count=2000
|
||||
mkswap /tmp/swap
|
||||
swapon /tmp/swap
|
||||
|
||||
cabal sandbox init
|
||||
cabal update || cat /var/log/squid/*
|
||||
cabal update
|
||||
cabal install -j
|
||||
|
||||
install -d $SNAPCRAFT_PART_INSTALL/usr/bin
|
||||
|
|
|
@ -138,7 +138,7 @@ data InnerToken t =
|
|||
| Inner_T_WhileExpression [t] [t]
|
||||
| Inner_T_Annotation [Annotation] t
|
||||
| Inner_T_Pipe String
|
||||
| Inner_T_CoProc (Maybe String) t
|
||||
| Inner_T_CoProc (Maybe Token) t
|
||||
| Inner_T_CoProcBody t
|
||||
| Inner_T_Include t
|
||||
| Inner_T_SourceCommand t t
|
||||
|
@ -152,6 +152,7 @@ data Annotation =
|
|||
| ShellOverride String
|
||||
| SourcePath String
|
||||
| ExternalSources Bool
|
||||
| ExtendedAnalysis Bool
|
||||
deriving (Show, Eq)
|
||||
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
|
||||
|
||||
|
@ -205,7 +206,7 @@ pattern T_Annotation id anns t = OuterToken id (Inner_T_Annotation anns t)
|
|||
pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c)
|
||||
pattern T_Array id t = OuterToken id (Inner_T_Array t)
|
||||
pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)
|
||||
pattern TA_Parentesis id t = OuterToken id (Inner_TA_Parenthesis t)
|
||||
pattern TA_Parenthesis 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)
|
||||
|
@ -258,7 +259,7 @@ pattern T_Subshell id l = OuterToken id (Inner_T_Subshell l)
|
|||
pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l)
|
||||
pattern T_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_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 #-}
|
||||
{-# 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 #-}
|
||||
|
||||
instance Eq Token where
|
||||
OuterToken _ a == OuterToken _ b = a == b
|
||||
|
|
|
@ -31,6 +31,7 @@ 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)
|
||||
|
||||
|
@ -157,9 +158,10 @@ isFlag token =
|
|||
_ -> False
|
||||
|
||||
-- Is this token a flag where the - is unquoted?
|
||||
isUnquotedFlag token = fromMaybe False $ do
|
||||
str <- getLeadingUnquotedString token
|
||||
return $ "-" `isPrefixOf` str
|
||||
isUnquotedFlag token =
|
||||
case getLeadingUnquotedString token of
|
||||
Just ('-':_) -> True
|
||||
_ -> False
|
||||
|
||||
-- getGnuOpts "erd:u:" will parse a list of arguments tokens like `read`
|
||||
-- -re -d : -u 3 bar
|
||||
|
@ -444,6 +446,12 @@ 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
|
||||
|
@ -758,8 +766,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" == "ash"
|
||||
prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "ash"
|
||||
prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "busybox sh"
|
||||
prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "busybox ash"
|
||||
|
||||
-- Get the shell executable from a string like '/usr/bin/env bash'
|
||||
executableFromShebang :: String -> String
|
||||
|
@ -776,7 +784,8 @@ executableFromShebang = shellFor
|
|||
[x] -> basename x
|
||||
(first:second:args) | basename first == "busybox" ->
|
||||
case basename second of
|
||||
"sh" -> "ash" -- busybox sh is ash
|
||||
"sh" -> "busybox sh"
|
||||
"ash" -> "busybox ash"
|
||||
x -> x
|
||||
(first:args) | basename first == "env" ->
|
||||
fromEnvArgs args
|
||||
|
@ -856,8 +865,7 @@ 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
|
||||
match <- matchRegex re s
|
||||
index <- match !!! 0
|
||||
index:_ <- matchRegex re s
|
||||
return $ matchAllStrings variableNameRegex index
|
||||
where
|
||||
re = mkRegex "(\\[.*\\])"
|
||||
|
@ -868,8 +876,7 @@ 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 ]
|
||||
match <- matchRegex re mods
|
||||
offsets <- match !!! 1
|
||||
_:offsets:_ <- matchRegex re mods
|
||||
return $ matchAllStrings variableNameRegex offsets
|
||||
where
|
||||
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
|
||||
|
@ -886,11 +893,17 @@ isUnmodifiedParameterExpansion t =
|
|||
in getBracedReference str == str
|
||||
_ -> False
|
||||
|
||||
-- Return the referenced variable if (and only if) it's an unmodified parameter expansion.
|
||||
getUnmodifiedParameterExpansion t =
|
||||
case t of
|
||||
T_DollarBraced _ _ list -> do
|
||||
let str = concat $ oversimplify list
|
||||
guard $ getBracedReference str == str
|
||||
return str
|
||||
_ -> Nothing
|
||||
|
||||
--- A list of the element and all its parents up to the root node.
|
||||
getPath tree t = t :
|
||||
case Map.lookup (getId t) tree of
|
||||
Nothing -> []
|
||||
Just parent -> getPath tree parent
|
||||
getPath tree = NE.unfoldr $ \t -> (t, Map.lookup (getId t) tree)
|
||||
|
||||
isClosingFileOp op =
|
||||
case op of
|
||||
|
@ -903,5 +916,11 @@ 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]
|
||||
_ -> Nothing
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -41,6 +41,7 @@ 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)
|
||||
|
@ -88,6 +89,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,
|
||||
-- A linear (bad) analysis of data flow
|
||||
variableFlow :: [StackData],
|
||||
-- A map from Id to Token
|
||||
|
@ -103,7 +106,7 @@ 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 :: CF.CFGAnalysis
|
||||
cfgAnalysis :: Maybe CF.CFGAnalysis
|
||||
} deriving (Show)
|
||||
|
||||
-- TODO: Cache results of common AST ops here
|
||||
|
@ -196,8 +199,10 @@ makeCommentWithFix severity id code str fix =
|
|||
}
|
||||
in force withFix
|
||||
|
||||
-- makeParameters :: CheckSpec -> Parameters
|
||||
makeParameters spec = params
|
||||
where
|
||||
extendedAnalysis = fromMaybe True $ msum [asExtendedAnalysis spec, getExtendedAnalysisDirective root]
|
||||
params = Parameters {
|
||||
rootNode = root,
|
||||
shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec,
|
||||
|
@ -206,26 +211,35 @@ makeParameters spec = params
|
|||
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 = CF.analyzeControlFlow cfParams root
|
||||
cfgAnalysis = do
|
||||
guard extendedAnalysis
|
||||
return $ CF.analyzeControlFlow cfParams root
|
||||
}
|
||||
cfParams = CF.CFGParameters {
|
||||
CF.cfLastpipe = hasLastpipe params,
|
||||
|
@ -284,8 +298,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" == Dash -- busybox sh is a specific shell, not posix sh
|
||||
prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == Dash
|
||||
prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == BusyboxSh -- busybox sh is a specific shell, not posix sh
|
||||
prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == BusyboxSh
|
||||
|
||||
determineShellTest = determineShellTest' Nothing
|
||||
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
|
||||
|
@ -333,14 +347,14 @@ isQuoteFree = isQuoteFreeNode False
|
|||
|
||||
isQuoteFreeNode strict shell tree t =
|
||||
isQuoteFreeElement t ||
|
||||
(fromMaybe False $ msum $ map isQuoteFreeContext $ drop 1 $ getPath tree t)
|
||||
(fromMaybe False $ msum $ map isQuoteFreeContext $ NE.tail $ getPath tree t)
|
||||
where
|
||||
-- Is this node self-quoting in itself?
|
||||
isQuoteFreeElement t =
|
||||
case t of
|
||||
T_Assignment {} -> assignmentIsQuoting t
|
||||
T_FdRedirect {} -> True
|
||||
_ -> False
|
||||
T_Assignment id _ _ _ _ -> assignmentIsQuoting id
|
||||
T_FdRedirect {} -> True
|
||||
_ -> False
|
||||
|
||||
-- Are any subnodes inherently self-quoting?
|
||||
isQuoteFreeContext t =
|
||||
|
@ -350,7 +364,7 @@ isQuoteFreeNode strict shell tree t =
|
|||
TC_Binary _ DoubleBracket _ _ _ -> return True
|
||||
TA_Sequence {} -> return True
|
||||
T_Arithmetic {} -> return True
|
||||
T_Assignment {} -> return $ assignmentIsQuoting t
|
||||
T_Assignment id _ _ _ _ -> return $ assignmentIsQuoting id
|
||||
T_Redirecting {} -> return False
|
||||
T_DoubleQuoted _ _ -> return True
|
||||
T_DollarDoubleQuoted _ _ -> return True
|
||||
|
@ -365,11 +379,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 t = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand t)
|
||||
assignmentIsQuoting id = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand id)
|
||||
shellParsesParamsAsAssignments = shell /= Sh
|
||||
|
||||
-- Is this assignment a parameter to a command like export/typeset/etc?
|
||||
isAssignmentParamToCommand (T_Assignment id _ _ _ _) =
|
||||
isAssignmentParamToCommand id =
|
||||
case Map.lookup id tree of
|
||||
Just (T_SimpleCommand _ _ (_:args)) -> id `elem` (map getId args)
|
||||
_ -> False
|
||||
|
@ -395,7 +409,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 $ getPath tree t
|
||||
findFirst findCommand $ NE.toList $ getPath tree t
|
||||
where
|
||||
findCommand t =
|
||||
case t of
|
||||
|
@ -409,7 +423,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) (tail $ getPath tree token)
|
||||
usedAsCommandName tree token = go (getId token) (NE.tail $ getPath tree token)
|
||||
where
|
||||
go currentId (T_NormalWord id [word]:rest)
|
||||
| currentId == getId word = go id rest
|
||||
|
@ -426,7 +440,9 @@ getPathM t = do
|
|||
return $ getPath (parentMap params) t
|
||||
|
||||
isParentOf tree parent child =
|
||||
elem (getId parent) . map getId $ getPath tree child
|
||||
any (\t -> parentId == getId t) (getPath tree child)
|
||||
where
|
||||
parentId = getId parent
|
||||
|
||||
parents params = getPath (parentMap params)
|
||||
|
||||
|
@ -525,7 +541,9 @@ getModifiedVariables t =
|
|||
T_BatsTest {} -> [
|
||||
(t, t, "lines", DataArray SourceExternal),
|
||||
(t, t, "status", DataString SourceInteger),
|
||||
(t, t, "output", DataString SourceExternal)
|
||||
(t, t, "output", DataString SourceExternal),
|
||||
(t, t, "stderr", DataString SourceExternal),
|
||||
(t, t, "stderr_lines", DataArray SourceExternal)
|
||||
]
|
||||
|
||||
-- Count [[ -v foo ]] as an "assignment".
|
||||
|
@ -547,8 +565,12 @@ getModifiedVariables t =
|
|||
T_FdRedirect _ ('{':var) op -> -- {foo}>&2 modifies foo
|
||||
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
|
||||
|
||||
T_CoProc _ name _ ->
|
||||
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
|
||||
T_CoProc _ Nothing _ ->
|
||||
[(t, t, "COPROC", DataArray SourceInteger)]
|
||||
|
||||
T_CoProc _ (Just token) _ -> do
|
||||
name <- maybeToList $ getLiteralString token
|
||||
[(t, t, name, DataArray SourceInteger)]
|
||||
|
||||
--Points to 'for' rather than variable
|
||||
T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)]
|
||||
|
@ -810,7 +832,7 @@ getReferencedVariables parents t =
|
|||
return (context, token, getBracedReference str)
|
||||
|
||||
isArithmeticAssignment t = case getPath parents t of
|
||||
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
|
||||
this NE.:| TA_Assignment _ "=" lhs _ :_ -> lhs == t
|
||||
_ -> False
|
||||
|
||||
isDereferencingBinaryOp = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
|
||||
|
@ -892,15 +914,6 @@ 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
|
||||
|
@ -918,6 +931,14 @@ 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
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||
|
|
|
@ -51,6 +51,7 @@ 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
|
||||
|
@ -111,8 +112,8 @@ data CFEdge =
|
|||
|
||||
-- Actions we track
|
||||
data CFEffect =
|
||||
CFSetProps Scope String (S.Set CFVariableProp)
|
||||
| CFUnsetProps Scope String (S.Set CFVariableProp)
|
||||
CFSetProps (Maybe Scope) String (S.Set CFVariableProp)
|
||||
| CFUnsetProps (Maybe Scope) String (S.Set CFVariableProp)
|
||||
| CFReadVariable String
|
||||
| CFWriteVariable String CFValue
|
||||
| CFWriteGlobal String CFValue
|
||||
|
@ -192,7 +193,7 @@ buildGraph params root =
|
|||
base
|
||||
|
||||
idToRange = M.fromList mapping
|
||||
isRealEdge (from, to, edge) = case edge of CFEFlow -> True; _ -> False
|
||||
isRealEdge (from, to, edge) = case edge of CFEFlow -> True; CFEExit -> True; _ -> False
|
||||
onlyRealEdges = filter isRealEdge edges
|
||||
(_, mainExit) = fromJust $ M.lookup (getId root) idToRange
|
||||
|
||||
|
@ -294,19 +295,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 $ map fst $ filter isStructural nodes
|
||||
structuralNodes = S.fromList [node | (node, CFStructuralNode) <- nodes]
|
||||
candidateNodes = S.filter isLinear structuralNodes
|
||||
edgesToCollapse = S.fromList $ filter filterEdges regularEdges
|
||||
|
||||
remapping :: M.Map Node Node
|
||||
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
|
||||
remapping = M.fromList $ map orderEdge $ S.toList edgesToCollapse
|
||||
recursiveRemapping = M.mapWithKey (\c _ -> recursiveLookup remapping c) remapping
|
||||
|
||||
filterEdges (a,b,_) =
|
||||
a `S.member` candidateNodes && b `S.member` candidateNodes
|
||||
|
||||
orderEdge (a,b,_) = if a < b then (a,b) else (b,a)
|
||||
counter = foldl' (\map key -> M.insertWith (+) key 1 map) M.empty
|
||||
orderEdge (a,b,_) = if a < b then (b,a) else (a,b)
|
||||
counter = M.fromListWith (+) . map (\key -> (key, 1))
|
||||
isRegularEdge (_, _, CFEFlow) = True
|
||||
isRegularEdge _ = False
|
||||
|
||||
|
@ -316,11 +317,6 @@ 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
|
||||
|
@ -494,7 +490,7 @@ build t = do
|
|||
TA_Binary _ _ a b -> sequentially [a,b]
|
||||
TA_Expansion _ list -> sequentially list
|
||||
TA_Sequence _ list -> sequentially list
|
||||
TA_Parentesis _ t -> build t
|
||||
TA_Parenthesis _ t -> build t
|
||||
|
||||
TA_Trinary _ cond a b -> do
|
||||
condition <- build cond
|
||||
|
@ -578,7 +574,7 @@ build t = do
|
|||
|
||||
T_Array _ list -> sequentially list
|
||||
|
||||
T_Assignment {} -> buildAssignment DefaultScope t
|
||||
T_Assignment {} -> buildAssignment Nothing t
|
||||
|
||||
T_Backgrounded id body -> do
|
||||
start <- newStructuralNode
|
||||
|
@ -614,15 +610,15 @@ build t = do
|
|||
|
||||
T_CaseExpression id t [] -> build t
|
||||
|
||||
T_CaseExpression id t list -> do
|
||||
T_CaseExpression id t list@(hd:tl) -> do
|
||||
start <- newStructuralNode
|
||||
token <- build t
|
||||
branches <- mapM buildBranch list
|
||||
branches <- mapM buildBranch (hd NE.:| tl)
|
||||
end <- newStructuralNode
|
||||
|
||||
let neighbors = zip branches $ tail branches
|
||||
let (_, firstCond, _) = head branches
|
||||
let (_, lastCond, lastBody) = last branches
|
||||
let neighbors = zip (NE.toList branches) $ NE.tail branches
|
||||
let (_, firstCond, _) = NE.head branches
|
||||
let (_, lastCond, lastBody) = NE.last branches
|
||||
|
||||
linkRange start token
|
||||
linkRange token firstCond
|
||||
|
@ -672,10 +668,18 @@ build t = do
|
|||
status <- newNodeRange $ CFSetExitCode id
|
||||
linkRange cond status
|
||||
|
||||
T_CoProc id maybeName t -> do
|
||||
let name = fromMaybe "COPROC" maybeName
|
||||
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
|
||||
|
||||
start <- newStructuralNode
|
||||
parent <- newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable name CFValueArray
|
||||
parent <- newNodeRange parentNode
|
||||
child <- subshell id "coproc" $ build t
|
||||
end <- newNodeRange $ CFSetExitCode id
|
||||
|
||||
|
@ -857,8 +861,8 @@ build t = do
|
|||
status <- newNodeRange (CFSetExitCode id)
|
||||
linkRange assignments status
|
||||
|
||||
T_SimpleCommand id vars list@(cmd:_) ->
|
||||
handleCommand t vars list $ getUnquotedLiteral cmd
|
||||
T_SimpleCommand id vars (cmd:args) ->
|
||||
handleCommand t vars (cmd NE.:| args) $ getUnquotedLiteral cmd
|
||||
|
||||
T_SingleQuoted _ _ -> none
|
||||
|
||||
|
@ -887,7 +891,9 @@ build t = do
|
|||
T_Less _ -> none
|
||||
T_ParamSubSpecialChar _ _ -> none
|
||||
|
||||
x -> error ("Unimplemented: " ++ show x)
|
||||
x -> do
|
||||
error ("Unimplemented: " ++ show x) -- STRIP
|
||||
none
|
||||
|
||||
-- Still in `where` clause
|
||||
forInHelper id name words body = do
|
||||
|
@ -923,8 +929,8 @@ handleCommand cmd vars args literalCmd = do
|
|||
-- TODO: Handle assignments in declaring commands
|
||||
|
||||
case literalCmd of
|
||||
Just "exit" -> regularExpansion vars args $ handleExit
|
||||
Just "return" -> regularExpansion vars args $ handleReturn
|
||||
Just "exit" -> regularExpansion vars (NE.toList args) $ handleExit
|
||||
Just "return" -> regularExpansion vars (NE.toList args) $ handleReturn
|
||||
Just "unset" -> regularExpansionWithStatus vars args $ handleUnset args
|
||||
|
||||
Just "declare" -> handleDeclare args
|
||||
|
@ -947,14 +953,14 @@ handleCommand cmd vars args literalCmd = do
|
|||
-- This will mostly behave like 'command' but ok
|
||||
Just "builtin" ->
|
||||
case args of
|
||||
[_] -> regular
|
||||
(_:newargs@(newcmd:_)) ->
|
||||
handleCommand newcmd vars newargs $ getLiteralString newcmd
|
||||
_ NE.:| [] -> regular
|
||||
(_ NE.:| newcmd:newargs) ->
|
||||
handleCommand newcmd vars (newcmd NE.:| newargs) $ getLiteralString newcmd
|
||||
Just "command" ->
|
||||
case args of
|
||||
[_] -> regular
|
||||
(_:newargs@(newcmd:_)) ->
|
||||
handleOthers (getId newcmd) vars newargs $ getLiteralString newcmd
|
||||
_ NE.:| [] -> regular
|
||||
(_ NE.:| newcmd:newargs) ->
|
||||
handleOthers (getId newcmd) vars (newcmd NE.:| newargs) $ getLiteralString newcmd
|
||||
_ -> regular
|
||||
|
||||
where
|
||||
|
@ -982,7 +988,7 @@ handleCommand cmd vars args literalCmd = do
|
|||
unreachable <- newNode CFUnreachable
|
||||
return $ Range ret unreachable
|
||||
|
||||
handleUnset (cmd:args) = do
|
||||
handleUnset (cmd NE.:| args) = do
|
||||
case () of
|
||||
_ | "n" `elem` flagNames -> unsetWith CFUndefineNameref
|
||||
_ | "v" `elem` flagNames -> unsetWith CFUndefineVariable
|
||||
|
@ -994,14 +1000,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) -> getLiteralString t >>= (return . (,) t)) names
|
||||
literalNames = mapMaybe (\(_, t) -> (,) t <$> getLiteralString 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:args) = do
|
||||
handleDeclare (cmd NE.:| 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
|
||||
|
@ -1028,9 +1034,9 @@ handleCommand cmd vars args literalCmd = do
|
|||
|
||||
scope isFunc =
|
||||
case () of
|
||||
_ | global -> GlobalScope
|
||||
_ | isFunc -> LocalScope
|
||||
_ -> DefaultScope
|
||||
_ | global -> Just GlobalScope
|
||||
_ | isFunc -> Just LocalScope
|
||||
_ -> Nothing
|
||||
|
||||
addedProps = S.fromList $ concat $ [
|
||||
[ CFVPArray | array ],
|
||||
|
@ -1058,7 +1064,7 @@ handleCommand cmd vars args literalCmd = do
|
|||
let
|
||||
id = getId t
|
||||
pre = [t]
|
||||
literal = fromJust $ getLiteralStringExt (const $ Just "\0") t
|
||||
literal = getLiteralStringDef "\0" t
|
||||
isKnown = '\0' `notElem` literal
|
||||
match = fmap head $ variableAssignRegex `matchRegex` literal
|
||||
name = fromMaybe literal match
|
||||
|
@ -1090,7 +1096,7 @@ handleCommand cmd vars args literalCmd = do
|
|||
in
|
||||
concatMap (drop 1) plusses
|
||||
|
||||
handlePrintf (cmd:args) =
|
||||
handlePrintf (cmd NE.:| args) =
|
||||
newNodeRange $ CFApplyEffects $ maybeToList findVar
|
||||
where
|
||||
findVar = do
|
||||
|
@ -1099,7 +1105,7 @@ handleCommand cmd vars args literalCmd = do
|
|||
name <- getLiteralString arg
|
||||
return $ IdTagged (getId arg) $ CFWriteVariable name CFValueString
|
||||
|
||||
handleWait (cmd:args) =
|
||||
handleWait (cmd NE.:| args) =
|
||||
newNodeRange $ CFApplyEffects $ maybeToList findVar
|
||||
where
|
||||
findVar = do
|
||||
|
@ -1108,7 +1114,7 @@ handleCommand cmd vars args literalCmd = do
|
|||
name <- getLiteralString arg
|
||||
return $ IdTagged (getId arg) $ CFWriteVariable name CFValueInteger
|
||||
|
||||
handleMapfile (cmd:args) =
|
||||
handleMapfile (cmd NE.:| args) =
|
||||
newNodeRange $ CFApplyEffects [findVar]
|
||||
where
|
||||
findVar =
|
||||
|
@ -1128,7 +1134,7 @@ handleCommand cmd vars args literalCmd = do
|
|||
guard $ isVariableName name
|
||||
return (getId c, name)
|
||||
|
||||
handleRead (cmd:args) = newNodeRange $ CFApplyEffects main
|
||||
handleRead (cmd NE.:| args) = newNodeRange $ CFApplyEffects main
|
||||
where
|
||||
main = fromMaybe fallback $ do
|
||||
flags <- getGnuOpts flagsForRead args
|
||||
|
@ -1158,7 +1164,7 @@ handleCommand cmd vars args literalCmd = do
|
|||
in
|
||||
map (\(id, name) -> IdTagged id $ CFWriteVariable name value) namesOrDefault
|
||||
|
||||
handleDEFINE (cmd:args) =
|
||||
handleDEFINE (cmd NE.:| args) =
|
||||
newNodeRange $ CFApplyEffects $ maybeToList findVar
|
||||
where
|
||||
findVar = do
|
||||
|
@ -1168,14 +1174,14 @@ handleCommand cmd vars args literalCmd = do
|
|||
return $ IdTagged (getId name) $ CFWriteVariable str CFValueString
|
||||
|
||||
handleOthers id vars args cmd =
|
||||
regularExpansion vars args $ do
|
||||
regularExpansion vars (NE.toList args) $ do
|
||||
exe <- newNodeRange $ CFExecuteCommand cmd
|
||||
status <- newNodeRange $ CFSetExitCode id
|
||||
linkRange exe status
|
||||
|
||||
regularExpansion vars args p = do
|
||||
args <- sequentially args
|
||||
assignments <- mapM (buildAssignment PrefixScope) vars
|
||||
assignments <- mapM (buildAssignment (Just PrefixScope)) vars
|
||||
exe <- p
|
||||
dropAssignments <-
|
||||
if null vars
|
||||
|
@ -1187,15 +1193,15 @@ handleCommand cmd vars args literalCmd = do
|
|||
|
||||
linkRanges $ [args] ++ assignments ++ [exe] ++ dropAssignments
|
||||
|
||||
regularExpansionWithStatus vars args@(cmd:_) p = do
|
||||
initial <- regularExpansion vars args p
|
||||
regularExpansionWithStatus vars args@(cmd NE.:| _) p = do
|
||||
initial <- regularExpansion vars (NE.toList args) p
|
||||
status <- newNodeRange $ CFSetExitCode (getId cmd)
|
||||
linkRange initial status
|
||||
|
||||
|
||||
none = newStructuralNode
|
||||
|
||||
data Scope = DefaultScope | GlobalScope | LocalScope | PrefixScope
|
||||
data Scope = GlobalScope | LocalScope | PrefixScope
|
||||
deriving (Eq, Ord, Show, Generic, NFData)
|
||||
|
||||
buildAssignment scope t = do
|
||||
|
@ -1209,10 +1215,10 @@ buildAssignment scope t = do
|
|||
let valueType = if null indices then f id value else CFValueArray
|
||||
let scoper =
|
||||
case scope of
|
||||
PrefixScope -> CFWritePrefix
|
||||
LocalScope -> CFWriteLocal
|
||||
GlobalScope -> CFWriteGlobal
|
||||
DefaultScope -> CFWriteVariable
|
||||
Just PrefixScope -> CFWritePrefix
|
||||
Just LocalScope -> CFWriteLocal
|
||||
Just GlobalScope -> CFWriteGlobal
|
||||
Nothing -> CFWriteVariable
|
||||
write <- newNodeRange $ applySingle $ IdTagged id $ scoper var valueType
|
||||
linkRanges [expand, index, read, write]
|
||||
where
|
||||
|
@ -1301,7 +1307,10 @@ findPostDominators mainexit graph = asArray
|
|||
reversed = grev withExitEdges
|
||||
postDoms = dom reversed mainexit
|
||||
(_, maxNode) = nodeRange graph
|
||||
asArray = array (0, maxNode) postDoms
|
||||
-- Holes in the array cause "Exception: (Array.!): undefined array element" while
|
||||
-- inspecting/debugging, so fill the array first and then update.
|
||||
initializedArray = listArray (0, maxNode) $ repeat []
|
||||
asArray = initializedArray // postDoms
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||
|
|
|
@ -59,6 +59,8 @@ module ShellCheck.CFGAnalysis (
|
|||
,getIncomingState
|
||||
,getOutgoingState
|
||||
,doesPostDominate
|
||||
,variableMayBeDeclaredInteger
|
||||
,variableMayBeAssignedInteger
|
||||
,ShellCheck.CFGAnalysis.runTests -- STRIP
|
||||
) where
|
||||
|
||||
|
@ -131,7 +133,7 @@ internalToExternal s =
|
|||
literalValue = Nothing
|
||||
}
|
||||
}
|
||||
flatVars = M.unionsWith (\_ last -> last) $ map mapStorage [sGlobalValues s, sLocalValues s, sPrefixValues s]
|
||||
flatVars = M.unions $ map mapStorage [sPrefixValues s, sLocalValues s, sGlobalValues s]
|
||||
|
||||
-- Conveniently get the state before a token id
|
||||
getIncomingState :: CFGAnalysis -> Id -> Maybe ProgramState
|
||||
|
@ -153,6 +155,20 @@ doesPostDominate analysis target base = fromMaybe False $ do
|
|||
(targetStart, _) <- M.lookup target $ tokenToRange analysis
|
||||
return $ targetStart `elem` (postDominators analysis ! baseEnd)
|
||||
|
||||
-- See if any execution path results in the variable containing a state
|
||||
variableMayHaveState :: ProgramState -> String -> CFVariableProp -> Maybe Bool
|
||||
variableMayHaveState state var property = do
|
||||
value <- M.lookup var $ variablesInScope state
|
||||
return $ any (S.member property) $ variableProperties value
|
||||
|
||||
-- See if any execution path declares the variable an integer (declare -i).
|
||||
variableMayBeDeclaredInteger state var = variableMayHaveState state var CFVPInteger
|
||||
|
||||
-- See if any execution path suggests the variable may contain an integer value
|
||||
variableMayBeAssignedInteger state var = do
|
||||
value <- M.lookup var $ variablesInScope state
|
||||
return $ (numericalStatus $ variableValue value) >= NumericalStatusMaybe
|
||||
|
||||
getDataForNode analysis node = M.lookup node $ nodeToData analysis
|
||||
|
||||
-- The current state of data flow at a point in the program, potentially as a diff
|
||||
|
@ -283,7 +299,6 @@ 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
|
||||
|
@ -657,7 +672,7 @@ vmPatch base diff =
|
|||
_ | vmIsQuickEqual base diff -> diff
|
||||
_ -> VersionedMap {
|
||||
mapVersion = -1,
|
||||
mapStorage = M.unionWith (flip const) (mapStorage base) (mapStorage diff)
|
||||
mapStorage = M.union (mapStorage diff) (mapStorage base)
|
||||
}
|
||||
|
||||
-- Set a variable. This includes properties. Applies it to the appropriate scope.
|
||||
|
@ -814,7 +829,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 <- fromMaybe (f rest) (return <$> get (stackState s) key)
|
||||
res <- maybe (f rest) return (get (stackState s) key)
|
||||
modifySTRef (dependencies s) $ S.insert $ dep key res
|
||||
return res
|
||||
|
||||
|
@ -1104,34 +1119,34 @@ transferEffect ctx effect =
|
|||
|
||||
CFSetProps scope name props ->
|
||||
case scope of
|
||||
DefaultScope -> do
|
||||
Nothing -> do
|
||||
state <- readVariable ctx name
|
||||
writeVariable ctx name $ addProperties props state
|
||||
GlobalScope -> do
|
||||
Just GlobalScope -> do
|
||||
state <- readGlobal ctx name
|
||||
writeGlobal ctx name $ addProperties props state
|
||||
LocalScope -> do
|
||||
Just LocalScope -> do
|
||||
out <- readSTRef (cOutput ctx)
|
||||
state <- readLocal ctx name
|
||||
writeLocal ctx name $ addProperties props state
|
||||
PrefixScope -> do
|
||||
Just PrefixScope -> do
|
||||
-- Prefix values become local
|
||||
state <- readLocal ctx name
|
||||
writeLocal ctx name $ addProperties props state
|
||||
|
||||
CFUnsetProps scope name props ->
|
||||
case scope of
|
||||
DefaultScope -> do
|
||||
Nothing -> do
|
||||
state <- readVariable ctx name
|
||||
writeVariable ctx name $ removeProperties props state
|
||||
GlobalScope -> do
|
||||
Just GlobalScope -> do
|
||||
state <- readGlobal ctx name
|
||||
writeGlobal ctx name $ removeProperties props state
|
||||
LocalScope -> do
|
||||
Just LocalScope -> do
|
||||
out <- readSTRef (cOutput ctx)
|
||||
state <- readLocal ctx name
|
||||
writeLocal ctx name $ removeProperties props state
|
||||
PrefixScope -> do
|
||||
Just PrefixScope -> do
|
||||
-- Prefix values become local
|
||||
state <- readLocal ctx name
|
||||
writeLocal ctx name $ removeProperties props state
|
||||
|
@ -1271,7 +1286,7 @@ dataflow ctx entry = do
|
|||
else do
|
||||
let (next, rest) = S.deleteFindMin ps
|
||||
nexts <- process states next
|
||||
writeSTRef pending $ foldl (flip S.insert) rest nexts
|
||||
writeSTRef pending $ S.union (S.fromList nexts) rest
|
||||
f (n-1) pending states
|
||||
|
||||
process states node = do
|
||||
|
@ -1335,7 +1350,7 @@ analyzeControlFlow params t =
|
|||
|
||||
-- All nodes we've touched
|
||||
invocations <- readSTRef $ cInvocations ctx
|
||||
let invokedNodes = M.fromDistinctAscList $ map (\c -> (c, ())) $ S.toList $ M.keysSet $ groupByNode $ M.map snd invocations
|
||||
let invokedNodes = M.fromSet (const ()) $ S.unions $ map (M.keysSet . snd) $ M.elems invocations
|
||||
|
||||
-- Invoke all functions that were declared but not invoked
|
||||
-- This is so that we still get warnings for dead code
|
||||
|
@ -1358,7 +1373,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.unionWith (flip const) baseStates invokedStates
|
||||
let allStates = M.union invokedStates baseStates
|
||||
|
||||
-- Convert to external states
|
||||
let nodeToData = M.map (\(a,b) -> (internalToExternal a, internalToExternal b)) allStates
|
||||
|
|
|
@ -25,6 +25,7 @@ import ShellCheck.ASTLib
|
|||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
|
||||
import Debug.Trace -- DO NOT SUBMIT
|
||||
import Data.Either
|
||||
import Data.Functor
|
||||
import Data.List
|
||||
|
@ -86,6 +87,7 @@ checkScript sys spec = do
|
|||
asCheckSourced = csCheckSourced spec,
|
||||
asExecutionMode = Executed,
|
||||
asTokenPositions = tokenPositions,
|
||||
asExtendedAnalysis = csExtendedAnalysis spec,
|
||||
asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec
|
||||
} where as = newAnalysisSpec root
|
||||
let analysisMessages =
|
||||
|
@ -508,5 +510,55 @@ prop_rcCanSuppressEarlyProblems2 = null result
|
|||
csScript = "!/bin/bash\necho 'hello world'"
|
||||
}
|
||||
|
||||
prop_sourceWithHereDocWorks = null result
|
||||
where
|
||||
result = checkWithIncludes [("bar", "true\n")] "source bar << eof\nlol\neof"
|
||||
|
||||
prop_hereDocsAreParsedWithoutTrailingLinefeed = 1044 `elem` result
|
||||
where
|
||||
result = check "cat << eof"
|
||||
|
||||
prop_hereDocsWillHaveParsedIndices = null result
|
||||
where
|
||||
result = check "#!/bin/bash\nmy_array=(a b)\ncat <<EOF >> ./test\n $(( 1 + my_array[1] ))\nEOF"
|
||||
|
||||
prop_rcCanSuppressDfa = null result
|
||||
where
|
||||
result = checkWithRc "extended-analysis=false" emptyCheckSpec {
|
||||
csScript = "#!/bin/sh\nexit; foo;"
|
||||
}
|
||||
|
||||
prop_fileCanSuppressDfa = null $ traceShowId result
|
||||
where
|
||||
result = checkWithRc "" emptyCheckSpec {
|
||||
csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;"
|
||||
}
|
||||
|
||||
prop_fileWinsWhenSuppressingDfa1 = null result
|
||||
where
|
||||
result = checkWithRc "extended-analysis=true" emptyCheckSpec {
|
||||
csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;"
|
||||
}
|
||||
|
||||
prop_fileWinsWhenSuppressingDfa2 = result == [2317]
|
||||
where
|
||||
result = checkWithRc "extended-analysis=false" emptyCheckSpec {
|
||||
csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;"
|
||||
}
|
||||
|
||||
prop_flagWinsWhenSuppressingDfa1 = result == [2317]
|
||||
where
|
||||
result = checkWithRc "extended-analysis=false" emptyCheckSpec {
|
||||
csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;",
|
||||
csExtendedAnalysis = Just True
|
||||
}
|
||||
|
||||
prop_flagWinsWhenSuppressingDfa2 = null result
|
||||
where
|
||||
result = checkWithRc "extended-analysis=true" emptyCheckSpec {
|
||||
csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;",
|
||||
csExtendedAnalysis = Just False
|
||||
}
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
{-# 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
|
||||
|
@ -42,6 +43,7 @@ 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)
|
||||
|
@ -181,16 +183,15 @@ 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
|
||||
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
|
||||
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
|
||||
|
||||
where
|
||||
basename = reverse . takeWhile (/= '/') . reverse
|
||||
|
@ -299,7 +300,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] |
|
||||
(fromMaybe "" $ getLiteralString first) /= "length"
|
||||
onlyLiteralString first /= "length"
|
||||
&& not (willSplit first || willSplit second) -> do
|
||||
checkOp first
|
||||
warn (getId t) 2307
|
||||
|
@ -930,7 +931,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] $ do
|
||||
whenShell [Sh, Dash, BusyboxSh] $ 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."
|
||||
|
@ -954,7 +955,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] $ do -- Ksh allows it, Sh doesn't support local
|
||||
whenShell [Bash, Dash, BusyboxSh] $ 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."
|
||||
|
@ -1005,8 +1006,8 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
|
|||
sequence_ $ do
|
||||
options <- getLiteralString arg1
|
||||
getoptsVar <- getLiteralString name
|
||||
(T_WhileExpression _ _ body) <- findFirst whileLoop path
|
||||
caseCmd@(T_CaseExpression _ var _) <- mapMaybe findCase body !!! 0
|
||||
(T_WhileExpression _ _ body) <- findFirst whileLoop (NE.toList path)
|
||||
T_CaseExpression id var list <- mapMaybe findCase body !!! 0
|
||||
|
||||
-- Make sure getopts name and case variable matches
|
||||
[T_DollarBraced _ _ bracedWord] <- return $ getWordParts var
|
||||
|
@ -1016,11 +1017,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) caseCmd
|
||||
return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) id list
|
||||
f _ = return ()
|
||||
|
||||
check :: Id -> [String] -> Token -> Analysis
|
||||
check optId opts (T_CaseExpression id _ list) = do
|
||||
check :: Id -> [String] -> Id -> [(CaseType, [Token], [Token])] -> Analysis
|
||||
check optId opts id list = do
|
||||
unless (Nothing `M.member` handledMap) $ do
|
||||
mapM_ (warnUnhandled optId id) $ catMaybes $ M.keys notHandled
|
||||
|
||||
|
@ -1236,8 +1237,7 @@ checkSudoArgs = CommandCheck (Basename "sudo") f
|
|||
where
|
||||
f t = sequence_ $ do
|
||||
opts <- parseOpts $ arguments t
|
||||
let nonFlags = [x | ("",(x, _)) <- opts]
|
||||
commandArg <- nonFlags !!! 0
|
||||
(_,(commandArg, _)) <- find (null . fst) opts
|
||||
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?"
|
||||
|
@ -1430,26 +1430,27 @@ prop_checkBackreferencingDeclaration6 = verify (checkBackreferencingDeclaration
|
|||
prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration "declare") "declare x=var $k=$x"
|
||||
checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check
|
||||
where
|
||||
check t = foldM_ perArg M.empty $ arguments t
|
||||
check t = do
|
||||
maybeCfga <- asks cfgAnalysis
|
||||
mapM_ (\cfga -> foldM_ (perArg cfga) M.empty $ arguments t) maybeCfga
|
||||
|
||||
perArg leftArgs t =
|
||||
perArg cfga leftArgs t =
|
||||
case t of
|
||||
T_Assignment id _ name idx t -> do
|
||||
warnIfBackreferencing leftArgs $ t:idx
|
||||
warnIfBackreferencing cfga leftArgs $ t:idx
|
||||
return $ M.insert name id leftArgs
|
||||
t -> do
|
||||
warnIfBackreferencing leftArgs [t]
|
||||
warnIfBackreferencing cfga leftArgs [t]
|
||||
return leftArgs
|
||||
|
||||
warnIfBackreferencing backrefs l = do
|
||||
references <- findReferences l
|
||||
warnIfBackreferencing cfga backrefs l = do
|
||||
references <- findReferences cfga 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 list = do
|
||||
cfga <- asks cfgAnalysis
|
||||
findReferences cfga list = do
|
||||
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
|
||||
|
|
|
@ -78,7 +78,7 @@ controlFlowEffectChecks = [
|
|||
runNodeChecks :: [ControlFlowNodeCheck] -> ControlFlowCheck
|
||||
runNodeChecks perNode = do
|
||||
cfg <- asks cfgAnalysis
|
||||
runOnAll cfg
|
||||
mapM_ runOnAll cfg
|
||||
where
|
||||
getData datas n@(node, label) = do
|
||||
(pre, post) <- M.lookup node datas
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE ViewPatterns #-}
|
||||
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
|
||||
|
||||
import ShellCheck.AST
|
||||
|
@ -60,6 +61,9 @@ checks = [
|
|||
,checkBraceExpansionVars
|
||||
,checkMultiDimensionalArrays
|
||||
,checkPS1Assignments
|
||||
,checkMultipleBangs
|
||||
,checkBangAfterPipe
|
||||
,checkNegatedUnaryOps
|
||||
]
|
||||
|
||||
testChecker (ForShell _ t) =
|
||||
|
@ -73,22 +77,24 @@ 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, Bash] f
|
||||
checkForDecimals = ForShell [Sh, Dash, BusyboxSh, Bash] f
|
||||
where
|
||||
f t@(TA_Expansion id _) = sequence_ $ do
|
||||
str <- getLiteralString t
|
||||
first <- str !!! 0
|
||||
guard $ isDigit first && '.' `elem` str
|
||||
first:rest <- getLiteralString t
|
||||
guard $ isDigit first && '.' `elem` rest
|
||||
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 = verify checkBashisms "[ foo -nt bar ]"
|
||||
prop_checkBashisms2 = verifyNot 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*}"
|
||||
|
@ -104,6 +110,7 @@ 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"
|
||||
|
@ -184,49 +191,82 @@ prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
|
|||
prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}"
|
||||
prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}"
|
||||
prop_checkBashisms99 = verify checkBashisms "#!/bin/dash\necho [^f]oo"
|
||||
checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
prop_checkBashisms100 = verify checkBashisms "read -r"
|
||||
prop_checkBashisms101 = verify checkBashisms "read"
|
||||
prop_checkBashisms102 = verifyNot checkBashisms "read -r foo"
|
||||
prop_checkBashisms103 = verifyNot checkBashisms "read foo"
|
||||
prop_checkBashisms104 = verifyNot checkBashisms "read ''"
|
||||
prop_checkBashisms105 = verifyNot checkBashisms "#!/bin/busybox sh\nset -o pipefail"
|
||||
prop_checkBashisms106 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[[ \"$x\" = \"$x\" ]]"
|
||||
prop_checkBashisms107 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[ \"$x\" == \"$x\" ]"
|
||||
prop_checkBashisms108 = verifyNot checkBashisms "#!/bin/busybox sh\necho magic &> /dev/null"
|
||||
prop_checkBashisms109 = verifyNot checkBashisms "#!/bin/busybox sh\ntrap stop EXIT SIGTERM"
|
||||
prop_checkBashisms110 = verifyNot checkBashisms "#!/bin/busybox sh\nsource /dev/null"
|
||||
prop_checkBashisms111 = verify checkBashisms "#!/bin/dash\nx='test'\n${x:0:3}" -- SC3057
|
||||
prop_checkBashisms112 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x:0:3}" -- SC3057
|
||||
prop_checkBashisms113 = verify checkBashisms "#!/bin/dash\nx='test'\n${x/st/xt}" -- SC3060
|
||||
prop_checkBashisms114 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x/st/xt}" -- SC3060
|
||||
prop_checkBashisms115 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x}" -- SC3053
|
||||
prop_checkBashisms116 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x[1]}" -- SC3054
|
||||
prop_checkBashisms117 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x[@]}" -- SC3055
|
||||
prop_checkBashisms118 = verify checkBashisms "#!/bin/busybox sh\nxyz=1\n${!x*}" -- SC3056
|
||||
prop_checkBashisms119 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x^^[t]}" -- SC3059
|
||||
prop_checkBashisms120 = verify checkBashisms "#!/bin/sh\n[ x == y ]"
|
||||
prop_checkBashisms121 = verifyNot checkBashisms "#!/bin/sh\n# shellcheck shell=busybox\n[ x == y ]"
|
||||
prop_checkBashisms122 = verify checkBashisms "#!/bin/dash\n$'a'"
|
||||
prop_checkBashisms123 = verifyNot checkBashisms "#!/bin/busybox sh\n$'a'"
|
||||
prop_checkBashisms124 = verify checkBashisms "#!/bin/dash\ntype -p test"
|
||||
prop_checkBashisms125 = verifyNot checkBashisms "#!/bin/busybox sh\ntype -p test"
|
||||
prop_checkBashisms126 = verifyNot checkBashisms "#!/bin/busybox sh\nread -p foo -r bar"
|
||||
prop_checkBashisms127 = verifyNot checkBashisms "#!/bin/busybox sh\necho -ne foo"
|
||||
prop_checkBashisms128 = verify checkBashisms "#!/bin/dash\ntype -p test"
|
||||
prop_checkBashisms129 = verify checkBashisms "#!/bin/sh\n[ -k /tmp ]"
|
||||
prop_checkBashisms130 = verifyNot checkBashisms "#!/bin/dash\ntest -k /tmp"
|
||||
prop_checkBashisms131 = verify checkBashisms "#!/bin/sh\n[ -o errexit ]"
|
||||
checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
|
||||
params <- ask
|
||||
kludge params t
|
||||
where
|
||||
-- This code was copy-pasted from Analytics where params was a variable
|
||||
kludge params = bashism
|
||||
where
|
||||
isDash = shellType params == Dash
|
||||
isBusyboxSh = shellType params == BusyboxSh
|
||||
isDash = shellType params == Dash || isBusyboxSh
|
||||
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 _) = warnMsg id 3003 "$'..' is"
|
||||
bashism (T_DollarSingleQuoted id _) =
|
||||
unless isBusyboxSh $ warnMsg id 3003 "$'..' is"
|
||||
bashism (T_DollarDoubleQuoted id _) = warnMsg id 3004 "$\"..\" is"
|
||||
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id 3005 "arithmetic for loops are"
|
||||
bashism (T_Arithmetic id _) = warnMsg id 3006 "standalone ((..)) is"
|
||||
bashism (T_DollarBracket id _) = warnMsg id 3007 "$[..] in place of $((..)) is"
|
||||
bashism (T_SelectIn id _ _ _) = warnMsg id 3008 "select loops are"
|
||||
bashism (T_BraceExpansion id _) = warnMsg id 3009 "brace expansion is"
|
||||
bashism (T_Condition id DoubleBracket _) = warnMsg id 3010 "[[ ]] is"
|
||||
bashism (T_Condition id DoubleBracket _) =
|
||||
unless isBusyboxSh $ warnMsg id 3010 "[[ ]] is"
|
||||
bashism (T_HereString id _) = warnMsg id 3011 "here-strings are"
|
||||
bashism (TC_Binary id SingleBracket op _ _)
|
||||
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
|
||||
unless isDash $ warnMsg id 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 (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 (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 _) _)) = warnMsg id 3020 "&> is"
|
||||
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) =
|
||||
unless isBusyboxSh $ 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"
|
||||
|
@ -246,7 +286,8 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||
warnMsg id 3028 $ str ++ " is"
|
||||
|
||||
bashism t@(T_DollarBraced id _ token) = do
|
||||
mapM_ check expansion
|
||||
unless isBusyboxSh $ mapM_ check simpleExpansions
|
||||
mapM_ check advancedExpansions
|
||||
when (isBashVariable var) $
|
||||
warnMsg id 3028 $ var ++ " is"
|
||||
where
|
||||
|
@ -274,7 +315,11 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||
|
||||
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
||||
| t `isCommand` "echo" && argString `matches` flagRegex =
|
||||
if isDash
|
||||
if isBusyboxSh
|
||||
then
|
||||
unless (argString `matches` busyboxFlagRegex) $
|
||||
warnMsg (getId arg) 3036 "echo flags besides -n and -e"
|
||||
else if isDash
|
||||
then
|
||||
when (argString /= "-n") $
|
||||
warnMsg (getId arg) 3036 "echo flags besides -n"
|
||||
|
@ -283,6 +328,7 @@ checkBashisms = ForShell [Sh, Dash] $ \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) =
|
||||
|
@ -356,7 +402,8 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||
(\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
|
||||
return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is"
|
||||
|
||||
when (name == "source") $ warnMsg id 3046 "'source' in place of '.' is"
|
||||
when (name == "source" && not isBusyboxSh) $
|
||||
warnMsg id 3046 "'source' in place of '.' is"
|
||||
when (name == "trap") $
|
||||
let
|
||||
check token = sequence_ $ do
|
||||
|
@ -365,7 +412,7 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||
return $ do
|
||||
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
|
||||
warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is"
|
||||
when ("SIG" `isPrefixOf` upper) $
|
||||
when (not isBusyboxSh && "SIG" `isPrefixOf` upper) $
|
||||
warnMsg (getId token) 3048
|
||||
"prefixing signal names with 'SIG' is"
|
||||
when (not isDash && upper /= str) $
|
||||
|
@ -379,6 +426,9 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||
let literal = onlyLiteralString format
|
||||
guard $ "%q" `isInfixOf` literal
|
||||
return $ warnMsg (getId format) 3050 "printf %q is"
|
||||
|
||||
when (name == "read" && all isFlag rest) $
|
||||
warnMsg (getId cmd) 3061 "read without a variable is"
|
||||
where
|
||||
unsupportedCommands = [
|
||||
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
||||
|
@ -392,17 +442,19 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||
("hash", Just $ if isDash then ["r", "v"] else ["r"]),
|
||||
("jobs", Just ["l", "p"]),
|
||||
("printf", Just []),
|
||||
("read", Just $ if isDash then ["r", "p"] else ["r"]),
|
||||
("read", Just $ if isDash || isBusyboxSh then ["r", "p"] else ["r"]),
|
||||
("readonly", Just ["p"]),
|
||||
("trap", Just []),
|
||||
("type", Just []),
|
||||
("type", Just $ if isBusyboxSh then ["p"] else []),
|
||||
("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" = warnMsg id 3051 "'source' in place of '.' is"
|
||||
| getCommandName src == Just "source" =
|
||||
unless isBusyboxSh $
|
||||
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
|
||||
|
@ -410,14 +462,16 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||
bashism _ = return ()
|
||||
|
||||
varChars="_0-9a-zA-Z"
|
||||
expansion = let re = mkRegex in [
|
||||
advancedExpansions = 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 = [
|
||||
|
@ -443,6 +497,50 @@ checkBashisms = ForShell [Sh, Dash] $ \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,')"
|
||||
|
@ -558,5 +656,46 @@ checkPS1Assignments = ForShell [Bash] f
|
|||
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
|
||||
|
||||
|
||||
prop_checkMultipleBangs1 = verify checkMultipleBangs "! ! true"
|
||||
prop_checkMultipleBangs2 = verifyNot checkMultipleBangs "! true"
|
||||
checkMultipleBangs = ForShell [Dash, BusyboxSh, Sh] f
|
||||
where
|
||||
f token = case token of
|
||||
T_Banged id (T_Banged _ _) ->
|
||||
err id 2325 "Multiple ! in front of pipelines are a bash/ksh extension. Use only 0 or 1."
|
||||
_ -> return ()
|
||||
|
||||
|
||||
prop_checkBangAfterPipe1 = verify checkBangAfterPipe "true | ! true"
|
||||
prop_checkBangAfterPipe2 = verifyNot checkBangAfterPipe "true | ( ! true )"
|
||||
prop_checkBangAfterPipe3 = verifyNot checkBangAfterPipe "! ! true | true"
|
||||
checkBangAfterPipe = ForShell [Dash, BusyboxSh, Sh, Bash] f
|
||||
where
|
||||
f token = case token of
|
||||
T_Pipeline _ _ cmds -> mapM_ check cmds
|
||||
_ -> return ()
|
||||
|
||||
check token = case token of
|
||||
T_Banged id _ ->
|
||||
err id 2326 "! is not allowed in the middle of pipelines. Use command group as in cmd | { ! cmd; } if necessary."
|
||||
_ -> return ()
|
||||
|
||||
|
||||
prop_checkNegatedUnaryOps1 = verify checkNegatedUnaryOps "[ ! -o braceexpand ]"
|
||||
prop_checkNegatedUnaryOps2 = verifyNot checkNegatedUnaryOps "[ -o braceexpand ]"
|
||||
prop_checkNegatedUnaryOps3 = verifyNot checkNegatedUnaryOps "[[ ! -o braceexpand ]]"
|
||||
prop_checkNegatedUnaryOps4 = verifyNot checkNegatedUnaryOps "! [ -o braceexpand ]"
|
||||
prop_checkNegatedUnaryOps5 = verify checkNegatedUnaryOps "[ ! -a file ]"
|
||||
checkNegatedUnaryOps = ForShell [Bash] f
|
||||
where
|
||||
f token = case token of
|
||||
TC_Unary id SingleBracket "!" (TC_Unary _ _ op _) | op `elem` ["-a", "-o"] ->
|
||||
err id 2332 $ msg op
|
||||
_ -> return ()
|
||||
|
||||
msg "-o" = "[ ! -o opt ] is always true because -o becomes logical OR. Use [[ ]] or ! [ -o opt ]."
|
||||
msg "-a" = "[ ! -a file ] is always true because -a becomes logical AND. Use -e instead."
|
||||
msg _ = pleaseReport "unhandled negated unary message"
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||
|
|
|
@ -49,6 +49,7 @@ 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
|
||||
|
@ -62,6 +63,9 @@ internalVariables = [
|
|||
, "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP",
|
||||
"FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION",
|
||||
"flags_error", "flags_return"
|
||||
|
||||
-- Bats
|
||||
,"stderr", "stderr_lines"
|
||||
]
|
||||
|
||||
specialIntegerVariables = [
|
||||
|
@ -75,7 +79,7 @@ variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
|
|||
"EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM",
|
||||
"READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS",
|
||||
"SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE",
|
||||
"HISTSIZE", "LINES"
|
||||
"HISTSIZE", "LINES", "BASH_MONOSECONDS", "BASH_TRAPSIG"
|
||||
|
||||
-- shflags
|
||||
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
|
||||
|
@ -156,11 +160,15 @@ shellForExecutable name =
|
|||
"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:"
|
||||
|
|
|
@ -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 = groupWith sourceFile comments
|
||||
fileGroups = NE.groupWith sourceFile comments
|
||||
outputGroup group = do
|
||||
let filename = sourceFile (head group)
|
||||
let filename = sourceFile (NE.head group)
|
||||
result <- siReadFile sys (Just True) filename
|
||||
let contents = either (const "") id result
|
||||
outputFile filename contents group
|
||||
outputFile filename contents (NE.toList group)
|
||||
|
||||
outputFile filename contents warnings = do
|
||||
let comments = makeNonVirtual warnings contents
|
||||
|
|
|
@ -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 = groupWith sourceFile comments
|
||||
f :: [PositionedComment] -> IO ()
|
||||
groups = NE.groupWith sourceFile comments
|
||||
f :: NE.NonEmpty PositionedComment -> IO ()
|
||||
f group = do
|
||||
let filename = sourceFile (head group)
|
||||
let filename = sourceFile (NE.head group)
|
||||
result <- siReadFile sys (Just True) filename
|
||||
let contents = either (const "") id result
|
||||
outputResult filename contents group
|
||||
outputResult filename contents (NE.toList group)
|
||||
|
||||
outputResult filename contents warnings = do
|
||||
let comments = makeNonVirtual warnings contents
|
||||
|
|
|
@ -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 = groupWith sourceFile comments
|
||||
f :: [PositionedComment] -> IO ()
|
||||
groups = NE.groupWith sourceFile comments
|
||||
f :: NE.NonEmpty PositionedComment -> IO ()
|
||||
f group = do
|
||||
let filename = sourceFile (head group)
|
||||
let filename = sourceFile (NE.head group)
|
||||
result <- siReadFile sys (Just True) filename
|
||||
let contents = either (const "") id result
|
||||
let comments' = makeNonVirtual comments contents
|
||||
|
|
|
@ -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 = groupWith sourceFile comments
|
||||
let fileGroups = NE.groupWith sourceFile comments
|
||||
mapM_ (outputForFile color sys) fileGroups
|
||||
|
||||
outputForFile color sys comments = do
|
||||
let fileName = sourceFile (head comments)
|
||||
let fileName = sourceFile (NE.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 = groupWith lineNo comments
|
||||
let groups = NE.groupWith lineNo comments
|
||||
forM_ groups $ \commentsForLine -> do
|
||||
let lineNum = fromIntegral $ lineNo (head commentsForLine)
|
||||
let lineNum = fromIntegral $ lineNo (NE.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 commentsForLine (fromIntegral lineNum) fileLines
|
||||
showFixedString color (toList 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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{-
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
Copyright 2012-2024 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, csOptionalChecks)
|
||||
, CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csExtendedAnalysis, csOptionalChecks)
|
||||
, CheckResult(crFilename, crComments)
|
||||
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
|
||||
, ParseResult(prComments, prTokenPositions, prRoot)
|
||||
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asOptionalChecks)
|
||||
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asExtendedAnalysis, asOptionalChecks)
|
||||
, AnalysisResult(arComments)
|
||||
, FormatterOptions(foColorOption, foWikiLinkCount)
|
||||
, Shell(Ksh, Sh, Bash, Dash)
|
||||
, Shell(Ksh, Sh, Bash, Dash, BusyboxSh)
|
||||
, ExecutionMode(Executed, Sourced)
|
||||
, ErrorMessage
|
||||
, Code
|
||||
|
@ -39,11 +39,12 @@ module ShellCheck.Interface
|
|||
, ColorOption(ColorAuto, ColorAlways, ColorNever)
|
||||
, TokenComment(tcId, tcComment, tcFix)
|
||||
, emptyCheckResult
|
||||
, newParseResult
|
||||
, newAnalysisSpec
|
||||
, newAnalysisResult
|
||||
, newAnalysisSpec
|
||||
, newFormatterOptions
|
||||
, newParseResult
|
||||
, newPosition
|
||||
, newSystemInterface
|
||||
, newTokenComment
|
||||
, mockedSystemInterface
|
||||
, mockRcFile
|
||||
|
@ -99,6 +100,7 @@ data CheckSpec = CheckSpec {
|
|||
csIncludedWarnings :: Maybe [Integer],
|
||||
csShellTypeOverride :: Maybe Shell,
|
||||
csMinSeverity :: Severity,
|
||||
csExtendedAnalysis :: Maybe Bool,
|
||||
csOptionalChecks :: [String]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
|
@ -123,6 +125,7 @@ emptyCheckSpec = CheckSpec {
|
|||
csIncludedWarnings = Nothing,
|
||||
csShellTypeOverride = Nothing,
|
||||
csMinSeverity = StyleC,
|
||||
csExtendedAnalysis = Nothing,
|
||||
csOptionalChecks = []
|
||||
}
|
||||
|
||||
|
@ -135,6 +138,14 @@ newParseSpec = ParseSpec {
|
|||
psShellTypeOverride = Nothing
|
||||
}
|
||||
|
||||
newSystemInterface :: Monad m => SystemInterface m
|
||||
newSystemInterface =
|
||||
SystemInterface {
|
||||
siReadFile = \_ _ -> return $ Left "Not implemented",
|
||||
siFindSource = \_ _ _ name -> return name,
|
||||
siGetConfig = \_ -> return Nothing
|
||||
}
|
||||
|
||||
-- Parser input and output
|
||||
data ParseSpec = ParseSpec {
|
||||
psFilename :: String,
|
||||
|
@ -165,6 +176,7 @@ data AnalysisSpec = AnalysisSpec {
|
|||
asExecutionMode :: ExecutionMode,
|
||||
asCheckSourced :: Bool,
|
||||
asOptionalChecks :: [String],
|
||||
asExtendedAnalysis :: Maybe Bool,
|
||||
asTokenPositions :: Map.Map Id (Position, Position)
|
||||
}
|
||||
|
||||
|
@ -175,6 +187,7 @@ newAnalysisSpec token = AnalysisSpec {
|
|||
asExecutionMode = Executed,
|
||||
asCheckSourced = False,
|
||||
asOptionalChecks = [],
|
||||
asExtendedAnalysis = Nothing,
|
||||
asTokenPositions = Map.empty
|
||||
}
|
||||
|
||||
|
@ -212,7 +225,7 @@ newCheckDescription = CheckDescription {
|
|||
}
|
||||
|
||||
-- Supporting data types
|
||||
data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
|
||||
data Shell = Ksh | Sh | Bash | Dash | BusyboxSh deriving (Show, Eq)
|
||||
data ExecutionMode = Executed | Sourced deriving (Show, Eq)
|
||||
|
||||
type ErrorMessage = String
|
||||
|
@ -311,7 +324,7 @@ data ColorOption =
|
|||
|
||||
-- For testing
|
||||
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
|
||||
mockedSystemInterface files = SystemInterface {
|
||||
mockedSystemInterface files = (newSystemInterface :: SystemInterface Identity) {
|
||||
siReadFile = rf,
|
||||
siFindSource = fs,
|
||||
siGetConfig = const $ return Nothing
|
||||
|
@ -326,4 +339,3 @@ mockedSystemInterface files = SystemInterface {
|
|||
mockRcFile rcfile mock = mock {
|
||||
siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
|
||||
}
|
||||
|
||||
|
|
|
@ -46,6 +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 Test.QuickCheck.All (quickCheckAll)
|
||||
|
@ -140,15 +141,9 @@ carriageReturn = do
|
|||
parseProblemAt pos ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ."
|
||||
return '\r'
|
||||
|
||||
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
|
||||
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"
|
||||
return ' '
|
||||
|
||||
--------- Message/position annotation on top of user state
|
||||
|
@ -160,7 +155,7 @@ data Context =
|
|||
deriving (Show)
|
||||
|
||||
data HereDocContext =
|
||||
HereDocPending Token [Context] -- on linefeed, read this T_HereDoc
|
||||
HereDocPending Id Dashed Quoted String [Context] -- on linefeed, read this T_HereDoc
|
||||
deriving (Show)
|
||||
|
||||
data UserState = UserState {
|
||||
|
@ -238,12 +233,12 @@ addToHereDocMap id list = do
|
|||
hereDocMap = Map.insert id list map
|
||||
}
|
||||
|
||||
addPendingHereDoc t = do
|
||||
addPendingHereDoc id d q str = do
|
||||
state <- getState
|
||||
context <- getCurrentContexts
|
||||
let docs = pendingHereDocs state
|
||||
putState $ state {
|
||||
pendingHereDocs = HereDocPending t context : docs
|
||||
pendingHereDocs = HereDocPending id d q str context : docs
|
||||
}
|
||||
|
||||
popPendingHereDocs = do
|
||||
|
@ -826,7 +821,7 @@ readArithmeticContents =
|
|||
char ')'
|
||||
id <- endSpan start
|
||||
spacing
|
||||
return $ TA_Parentesis id s
|
||||
return $ TA_Parenthesis id s
|
||||
|
||||
readArithTerm = readGroup <|> readVariable <|> readExpansion
|
||||
|
||||
|
@ -1057,6 +1052,16 @@ 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
|
||||
|
@ -1194,7 +1199,7 @@ readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|>
|
|||
|
||||
readDollarBracedLiteral = do
|
||||
start <- startSpan
|
||||
vars <- (readBraceEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` bracedQuotable
|
||||
vars <- (readBraceEscaped <|> ((\x -> [x]) <$> anyChar)) `reluctantlyTill1` bracedQuotable
|
||||
id <- endSpan start
|
||||
return $ T_Literal id $ concat vars
|
||||
|
||||
|
@ -1556,7 +1561,7 @@ readGenericLiteral endChars = do
|
|||
return $ concat strings
|
||||
|
||||
readGenericLiteral1 endExp = do
|
||||
strings <- (readGenericEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` endExp
|
||||
strings <- (readGenericEscaped <|> ((\x -> [x]) <$> anyChar)) `reluctantlyTill1` endExp
|
||||
return $ concat strings
|
||||
|
||||
readGenericEscaped = do
|
||||
|
@ -1835,7 +1840,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 doc
|
||||
addPendingHereDoc hid dashed quoted endToken
|
||||
return doc
|
||||
where
|
||||
unquote :: String -> (Quoted, String)
|
||||
|
@ -1856,7 +1861,7 @@ readPendingHereDocs = do
|
|||
docs <- popPendingHereDocs
|
||||
mapM_ readDoc docs
|
||||
where
|
||||
readDoc (HereDocPending (T_HereDoc id dashed quoted endToken _) ctx) =
|
||||
readDoc (HereDocPending id dashed quoted endToken ctx) =
|
||||
swapContext ctx $
|
||||
do
|
||||
docStartPos <- getPosition
|
||||
|
@ -2283,22 +2288,31 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
|
|||
|
||||
subRead name script =
|
||||
withContext (ContextSource name) $
|
||||
inSeparateContext $
|
||||
subParse (initialPos name) (readScriptFile True) script
|
||||
inSeparateContext $ do
|
||||
oldState <- getState
|
||||
setState $ oldState { pendingHereDocs = [] }
|
||||
result <- subParse (initialPos name) (readScriptFile True) script
|
||||
newState <- getState
|
||||
setState $ newState { pendingHereDocs = pendingHereDocs oldState }
|
||||
return result
|
||||
readSource t = return t
|
||||
|
||||
|
||||
prop_readPipeline = isOk readPipeline "! cat /etc/issue | grep -i ubuntu"
|
||||
prop_readPipeline2 = isWarning readPipeline "!cat /etc/issue | grep -i ubuntu"
|
||||
prop_readPipeline3 = isOk readPipeline "for f; do :; done|cat"
|
||||
prop_readPipeline4 = isOk readPipeline "! ! true"
|
||||
prop_readPipeline5 = isOk readPipeline "true | ! true"
|
||||
readPipeline = do
|
||||
unexpecting "keyword/token" readKeyword
|
||||
do
|
||||
(T_Bang id) <- g_Bang
|
||||
pipe <- readPipeSequence
|
||||
return $ T_Banged id pipe
|
||||
<|>
|
||||
readPipeSequence
|
||||
readBanged readPipeSequence
|
||||
|
||||
readBanged parser = do
|
||||
pos <- getPosition
|
||||
(T_Bang id) <- g_Bang
|
||||
next <- readBanged parser
|
||||
return $ T_Banged id next
|
||||
<|> parser
|
||||
|
||||
prop_readAndOr = isOk readAndOr "grep -i lol foo || exit 1"
|
||||
prop_readAndOr1 = isOk readAndOr "# shellcheck disable=1\nfoo"
|
||||
|
@ -2354,14 +2368,14 @@ readTerm = do
|
|||
|
||||
readPipeSequence = do
|
||||
start <- startSpan
|
||||
(cmds, pipes) <- sepBy1WithSeparators readCommand
|
||||
(cmds, pipes) <- sepBy1WithSeparators (readBanged readCommand)
|
||||
(readPipe `thenSkip` (spacing >> readLineBreak))
|
||||
id <- endSpan start
|
||||
spacing
|
||||
return $ T_Pipeline id pipes cmds
|
||||
where
|
||||
sepBy1WithSeparators p s = do
|
||||
let elems = p >>= \x -> return ([x], [])
|
||||
let elems = (\x -> ([x], [])) <$> p
|
||||
let seps = do
|
||||
separator <- s
|
||||
return $ \(a,b) (c,d) -> (a++c, b ++ d ++ [separator])
|
||||
|
@ -2384,6 +2398,10 @@ readCommand = choice [
|
|||
]
|
||||
|
||||
readCmdName = do
|
||||
-- If the command name is `!` then
|
||||
optional . lookAhead . try $ do
|
||||
char '!'
|
||||
whitespace
|
||||
-- Ignore alias suppression
|
||||
optional . try $ do
|
||||
char '\\'
|
||||
|
@ -2777,17 +2795,29 @@ 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"
|
||||
whitespace
|
||||
spacing1
|
||||
choice [ try $ readCompoundCoProc start, readSimpleCoProc start ]
|
||||
where
|
||||
readCompoundCoProc start = do
|
||||
var <- optionMaybe $
|
||||
readVariableName `thenSkip` whitespace
|
||||
body <- readBody readCompoundCommand
|
||||
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)
|
||||
]
|
||||
id <- endSpan start
|
||||
return $ T_CoProc id var body
|
||||
readSimpleCoProc start = do
|
||||
|
@ -2891,8 +2921,8 @@ readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord)
|
|||
kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos)
|
||||
kludgeAwayQuotes s p =
|
||||
case s of
|
||||
first:rest@(_:_) ->
|
||||
let (last:backwards) = reverse rest
|
||||
first:second:rest ->
|
||||
let (last NE.:| backwards) = NE.reverse (second NE.:| rest)
|
||||
middle = reverse backwards
|
||||
in
|
||||
if first `elem` "'\"" && first == last
|
||||
|
@ -3322,10 +3352,12 @@ readScriptFile sourced = do
|
|||
then do
|
||||
commands <- readCompoundListOrEmpty
|
||||
id <- endSpan start
|
||||
readPendingHereDocs
|
||||
verifyEof
|
||||
let script = T_Annotation annotationId annotations $
|
||||
T_Script id shebang commands
|
||||
reparseIndices script
|
||||
userstate <- getState
|
||||
reparseIndices $ reattachHereDocs script (hereDocMap userstate)
|
||||
else do
|
||||
many anyChar
|
||||
id <- endSpan start
|
||||
|
@ -3335,8 +3367,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 scripts. Sorry!"
|
||||
Nothing -> parseProblemAt pos ErrorC 1008 "This shebang was unrecognized. ShellCheck only supports sh/bash/dash/ksh. Add a 'shell' directive to specify."
|
||||
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."
|
||||
|
||||
isValidShell s =
|
||||
let good = null s || any (`isPrefixOf` s) goodShells
|
||||
|
@ -3352,16 +3384,20 @@ readScriptFile sourced = do
|
|||
"sh",
|
||||
"ash",
|
||||
"dash",
|
||||
"busybox sh",
|
||||
"bash",
|
||||
"bats",
|
||||
"ksh"
|
||||
"ksh",
|
||||
"oksh"
|
||||
]
|
||||
badShells = [
|
||||
"awk",
|
||||
"csh",
|
||||
"expect",
|
||||
"fish",
|
||||
"perl",
|
||||
"python",
|
||||
"python3",
|
||||
"ruby",
|
||||
"tcsh",
|
||||
"zsh"
|
||||
|
@ -3414,13 +3450,22 @@ 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
|
||||
|
||||
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
|
||||
-- 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
|
||||
|
||||
parseWithNotes parser = do
|
||||
item <- parser
|
||||
|
@ -3438,9 +3483,8 @@ makeErrorFor parsecError =
|
|||
pos = errorPos parsecError
|
||||
|
||||
getStringFromParsec errors =
|
||||
case map f errors of
|
||||
r -> unwords (take 1 $ catMaybes $ reverse r) ++
|
||||
" Fix any mentioned problems and try again."
|
||||
headOrDefault "" (mapMaybe f $ reverse errors) ++
|
||||
" Fix any mentioned problems and try again."
|
||||
where
|
||||
f err =
|
||||
case err of
|
||||
|
@ -3471,8 +3515,7 @@ parseShell env name contents = do
|
|||
return newParseResult {
|
||||
prComments = map toPositionedComment $ nub $ parseNotes userstate ++ parseProblems state,
|
||||
prTokenPositions = Map.map startEndPosToPos (positionMap userstate),
|
||||
prRoot = Just $
|
||||
reattachHereDocs script (hereDocMap userstate)
|
||||
prRoot = Just script
|
||||
}
|
||||
Left err -> do
|
||||
let context = contextStack state
|
||||
|
@ -3490,13 +3533,11 @@ 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] $ filter isName list
|
||||
notesForContext list = zipWith ($) [first, second] [(pos, str) | ContextName pos str <- list]
|
||||
where
|
||||
isName (ContextName _ _) = True
|
||||
isName _ = False
|
||||
first (ContextName pos str) = ParseNote pos pos ErrorC 1073 $
|
||||
first (pos, str) = ParseNote pos pos ErrorC 1073 $
|
||||
"Couldn't parse this " ++ str ++ ". Fix to allow more checks."
|
||||
second (ContextName pos str) = ParseNote pos pos InfoC 1009 $
|
||||
second (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
|
||||
|
|
|
@ -12,6 +12,17 @@ then
|
|||
fail "There are uncommitted changes"
|
||||
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
|
||||
|
@ -34,33 +45,30 @@ 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 ...'"
|
||||
fi
|
||||
|
||||
if [[ $(git log -1 --pretty=%B) != *"CHANGELOG"* ]]
|
||||
then
|
||||
fail "Expected git log message to contain CHANGELOG"
|
||||
fi
|
||||
|
||||
i=1 j=1
|
||||
cat << EOF
|
||||
|
||||
Manual Checklist
|
||||
|
||||
$((i++)). Make sure none of the automated checks above failed
|
||||
$((i++)). Run \`build/build_builder build/*/\` to update all builder images.
|
||||
$((j++)). \`build/run_builder dist-newstyle/sdist/ShellCheck-*.tar.gz build/*/\` to verify that they work.
|
||||
$((j++)). \`for f in \$(cat build/*/tag); do docker push "\$f"; done\` to upload them.
|
||||
$((i++)). Run test/distrotest to ensure that most distros can build OOTB.
|
||||
$((i++)). Make sure GitHub Build currently passes: https://github.com/koalaman/shellcheck/actions
|
||||
$((i++)). Make sure 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.
|
||||
$((i++)). Make sure the Hackage package builds locally.
|
||||
|
||||
Release Steps
|
||||
|
||||
|
|
|
@ -17,13 +17,13 @@ and is still highly experimental.
|
|||
Make sure you're plugged in and have screen/tmux in place,
|
||||
then re-run with $0 --run to continue.
|
||||
|
||||
Also note that dist* will be deleted.
|
||||
Also note that dist*/ and .stack-work/ will be deleted.
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
echo "Deleting 'dist' and 'dist-newstyle'..."
|
||||
rm -rf dist dist-newstyle
|
||||
echo "Deleting 'dist', 'dist-newstyle', and '.stack-work'..."
|
||||
rm -rf dist dist-newstyle .stack-work
|
||||
|
||||
execs=$(find . -name shellcheck)
|
||||
|
||||
|
@ -74,13 +74,12 @@ 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: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
|
||||
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
|
||||
EOF
|
||||
|
||||
exit "$final"
|
||||
|
|
|
@ -18,21 +18,24 @@ import qualified ShellCheck.Parser
|
|||
|
||||
main = do
|
||||
putStrLn "Running ShellCheck tests..."
|
||||
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
|
||||
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)
|
||||
]
|
||||
if and results
|
||||
then exitSuccess
|
||||
else exitFailure
|
||||
|
|
|
@ -15,7 +15,7 @@ die() { echo "$*" >&2; exit 1; }
|
|||
command -v stack ||
|
||||
die "stack is missing"
|
||||
|
||||
stack setup || die "Failed to setup with default resolver"
|
||||
stack setup --allow-different-user || die "Failed to setup with default resolver"
|
||||
stack build --test || die "Failed to build/test with default resolver"
|
||||
|
||||
# Nice to haves, but not necessary
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue