Compare commits

...

99 commits

Author SHA1 Message Date
Vidar Holen
20d11c1c33 Merge branch 'e-kwsm-tautologically-false' 2025-05-17 00:56:52 +00:00
Vidar Holen
47d358c1d4 Tighten SC2333/SC2334 to only trigger against literals. 2025-05-17 00:55:50 +00:00
Vidar Holen
ad58768563 Merge branch 'tautologically-false' of github.com:e-kwsm/shellcheck into e-kwsm-tautologically-false 2025-05-12 17:04:34 +00:00
Vidar Holen
62a8ecf9bf Merge branch 'e-kwsm-SC3013' 2025-04-27 16:16:49 -07:00
Vidar Holen
0b5410d759
Merge pull request #3193 from iehrenwald/master
Add python3 to the list of badShells
2025-04-27 16:09:19 -07:00
iehrenwald
975cfeee50
Merge pull request #1 from iehrenwald/add_python3_badshell
Add python3 to the list of badShells
2025-04-25 14:20:01 -04:00
Ian Ehrenwald
b381658dbc
Add python3 to the list of badShells 2025-04-25 14:11:07 -04:00
Vidar Holen
950578ae0e Merge branch 'Flu-ignore-sc2015-true' 2025-04-11 19:15:34 -07:00
Vidar Holen
f78714e0f6 Add ":" alongside "true" for SC2015 2025-04-11 19:14:53 -07:00
Vidar Holen
de07ec1c56 Merge branch 'ignore-sc2015-true' of github.com:Flu/shellcheck into Flu-ignore-sc2015-true 2025-04-11 19:14:15 -07:00
Vidar Holen
85066dd805 Merge remote-tracking branch 'refs/remotes/origin/master' 2025-04-11 14:17:29 -07:00
Vidar Holen
140274b810 Merge branch 'e-kwsm-SC3013-unary' 2025-04-11 14:14:36 -07:00
Vidar Holen
dc41f0cc5b Refactor checks for POSIX test flags 2025-04-11 14:14:09 -07:00
Vidar Holen
fbb8386797
Merge pull request #3170 from e-kwsm/SC3012
fix(SC3012)!: do not warn about `\<` and `\>` in test/[] as specified in POSIX.1-2024
2025-04-09 10:51:44 -07:00
Eisuke Kawashima
efb5a5a274
fix(SC3013): check POSIX-compliant unary operators for test and [
fix #2125
2025-04-09 19:21:53 +09:00
Vidar Holen
553a80f77a Also ignore SC2119 for :? and :+. 2025-04-08 21:21:50 -07:00
Vidar Holen
7fc992d0dc Suppress SC2119/SC2120 for ${1:-default} (fixes #2023) 2025-04-08 20:52:52 -07:00
Vidar Holen
c553288085
Merge pull request #3106 from larryv/updatevars-bash-5.3
Recognize internal variables new in bash 5.3
2025-04-08 20:09:29 -07:00
Vidar Holen
1be41dd652
Merge pull request #3082 from silby/oksh
Recognize "oksh" executable name as ksh
2025-04-08 20:08:53 -07:00
Vidar Holen
2eddec86d3
Merge pull request #3185 from e-kwsm/man
doc: update man
2025-04-08 20:08:05 -07:00
Vidar Holen
c41f3a4b8a Warn about [ ! -o opt ] (and -a) being unconditionally true (fixes #3174) 2025-04-08 10:53:52 -07:00
Vidar Holen
574c6d18fb Suggest using test -e instead of -a (fixes #3174). 2025-04-08 10:23:10 -07:00
Eisuke Kawashima
e4853af5b0
doc: update man 2025-04-08 19:31:58 +09:00
Vidar Holen
72af76f443 Supress SC2093 when execfail is set (fixes #3178) 2025-04-06 19:58:13 -07:00
Vidar Holen
8ff0c5be7a Suppress SC2216 when piping to cp/mv/rm -i (fixes #3141). 2025-04-06 19:27:29 -07:00
Eisuke Kawashima
4f628cbe2a
feat: check tautologically-false conditionals
- fix #3179 — negation of SC2055, `[ x = y -a x = z]`
- fix #3181 — negation of SC2056, `(( x == y && x == z ))`
- fix #3180 — negation of SC2252, `[ x = y ] && [ x = z ]`
2025-04-04 18:21:35 +09:00
Eisuke Kawashima
bc60607f9e
fix(SC3012)!: do not warn about \< and \> in test/[] as specified in POSIX.1-2024
https://pubs.opengroup.org/onlinepubs/9799919799/utilities/test.html
fix #3168
2025-03-24 06:32:32 +09:00
Eisuke Kawashima
3a9ddae06b
fix(SC3013)!: remove SC3013 since the operators are specified by POSIX.1-2024
https://pubs.opengroup.org/onlinepubs/9799919799/utilities/test.html
fix #3167
2025-03-24 06:24:12 +09:00
Adrian Fluturel
cbf0b33463 Skip SC2015 when the last command is true 2025-01-07 03:24:29 +01:00
Lawrence Velázquez
fe315a25c4
Recognize internal variables new in bash 5.3
From the bug-bash@gnu.org announcement "Bash-5.3-beta available":

    q. GLOBSORT: new variable to specify how to sort the results of
       pathname expansion (name, size, blocks, mtime, atime, ctime,
       none) in ascending or descending order.

    w. BASH_MONOSECONDS: new dynamic variable that returns the value of
       the system's monotonic clock, if one is available.

    x. BASH_TRAPSIG: new variable, set to the numeric signal number of
       the trap being executed while it's running.

https://lists.gnu.org/archive/html/bug-bash/2024-12/msg00120.html
2024-12-28 03:20:19 -05:00
Joseph C. Sible
d3001f337a Simplify getParseOutput 2024-12-13 23:57:50 -05:00
Joseph C. Sible
7deb7e853b Use mapM_ instead of sequence_ and <$> 2024-12-13 23:47:55 -05:00
Joseph C. Sible
26b949b9b0 Use mapM_ instead of isJust and fromJust 2024-12-13 23:45:32 -05:00
Joseph C. Sible
5adfea21ee Use the result of the comparison directly instead of an if/else 2024-12-13 23:20:48 -05:00
Joseph C. Sible
0ecaf2b5f1 Use foldr instead of explicit recursion 2024-12-13 23:19:36 -05:00
Joseph C. Sible
195b70db8c Use unless instead of when and not 2024-12-13 23:06:49 -05:00
Vidar Holen
3c75d82db5 Fix stacktest complaining about permissions on /mnt 2024-11-29 13:00:36 -08:00
Vidar Holen
7f3f014d49 Allow latest QuickCheck 2024-11-28 11:51:22 -08:00
Evan Silberman
944d87915a Recognize "oksh" executable name as ksh
A portable version of OpenBSD's ksh is distributed with the executable
name oksh [1]. It's a descendant of pdksh and can be shellchecked as
ksh.

[1]: https://github.com/ibara/oksh
2024-11-11 11:24:21 -08:00
Vidar Holen
47bff1d5fd Add 24.04 to distrotest LTS 2024-11-03 16:54:45 -08:00
Vidar Holen
0ee46a0f33 Update filepath dependency 2024-11-03 14:19:08 -08:00
Vidar Holen
792466bc22 Update Diff dependency (fixes #3075) 2024-11-03 13:56:51 -08:00
Vidar Holen
097018754b Mention that SC2002 (UUOC) is now no longer enabled by default. 2024-10-27 18:10:00 -07:00
Vidar Holen
f2932ebcdc Remember to add changelog to release messages (fixes #3051) 2024-10-27 16:02:56 -07:00
Vidar Holen
5e3e98bcb0 Use CFG to determine use-before-define for SC2218 (fixes #3070) 2024-10-27 15:43:30 -07:00
Vidar Holen
68bc17b8ea
Merge pull request #3056 from random1223/patch-1
Update README.md and add Codety into the tool list
2024-10-27 12:47:16 -07:00
Tony
5c2be767ab
Update README.md
Add Codety Scanner into the static analysis solution list. 
Here are the examples of the result:
* Codety's pull request code review example: https://github.com/codetyio/codety-scanner/pull/66#issuecomment-2339438925
* Codety's GitHub code scan result example : https://github.com/codetyio/codety-scanner/runs/29907371258

Codety Scanner is open source: https://github.com/codetyio/codety-scanner
2024-09-09 18:56:18 -07:00
Vidar Holen
79e43c4550 Allow parsing arbitrary coproc names (fixes #3048) 2024-09-07 17:14:52 -07:00
Vidar Holen
ca65071d77 Run unit tests in GitHub actions 2024-09-01 14:08:15 -07:00
Vidar Holen
8a1b24c7af Fix paths for CI binary packaging after upgrade 2024-09-01 13:56:44 -07:00
Vidar Holen
88e441453b Make SC2002 optional (useless-use-of-cat) 2024-08-31 18:31:47 -07:00
Vidar Holen
1487e57a46 Suppress unused warnings about stderr and stderr_lines from bats tests, fixing tests. 2024-08-31 18:27:18 -07:00
Vidar Holen
68e6f02267 Expand list of recognized unicode spaces (and rewrite for performance) 2024-08-31 18:00:49 -07:00
Vidar Holen
c7611dfcc6 Use dynamic artifact name to work around issue with v4 uploader 2024-08-19 18:37:29 -07:00
Vidar Holen
15f132e167
Merge pull request #2972 from s1204IT/master
Upgrade build workflow dependencies
2024-08-19 17:48:54 -07:00
Vidar Holen
4e69767b03
Merge pull request #2988 from bryanhonof/bryanhonof.add-flox
Add Flox to list of installation methods
2024-08-19 17:46:55 -07:00
Vidar Holen
8bf8cf5cc7
Merge pull request #3018 from hasit/patch-1
Update README.md to add CodeRabbit to the list of services that use ShellCheck
2024-08-19 17:46:34 -07:00
Vidar Holen
17ebc3dda0
Merge pull request #2973 from jandubois/bats-stderr
Add new bats variables stderr and stderr_lines
2024-08-04 16:52:59 -07:00
Vidar Holen
4cd76283da
Merge pull request #3011 from sertonix/busybox-3003
Fix SC3003, SC3036 and SC3045 for busybox shell
2024-08-04 16:52:15 -07:00
Vidar Holen
cd6fdee99b
Merge pull request #3034 from dereckson/SC2016-oc
Whitelist oc to avoid SC2016 false positive
2024-08-04 16:50:30 -07:00
Vidar Holen
c831616f3a
Merge pull request #3037 from ember91/master
Fix typos and trailing whitespace
2024-08-04 16:49:38 -07:00
Emil Berg
38c5ba7c79 Fix typos and trailing whitespace 2024-08-03 08:49:40 +02:00
Sébastien Santoro
2696c6472d Whitelist oc to avoid SC2016 false positive
Fixes #3033.
2024-07-31 13:33:25 +00:00
Hasit Mistry
d590a35ff8
Update README.md 2024-07-09 14:22:19 -07:00
Sertonix
6d2f3d8628 Allow 'echo -e' in busybox shell 2024-07-09 16:58:50 +02:00
Sertonix
4c85274921 Fix SC3045 for busybox shell 2024-07-09 16:57:44 +02:00
Sertonix
6593096ba0 Allow SC3003 on busybox shell 2024-07-09 16:56:59 +02:00
Joseph C. Sible
98b8dc0720 Use fromList instead of reimplementing it in terms of foldl 2024-07-07 01:28:06 -04:00
Joseph C. Sible
95c0cc2e4b Simplify removeUnnecessaryStructuralNodes 2024-07-07 01:28:06 -04:00
Joseph C. Sible
e5fdec970a Swap the order of the tuple returned by orderEdge 2024-07-07 01:28:06 -04:00
Joseph C. Sible
8746c6e7f2 Switch the order of the maps to avoid unnecessary unionWith instead of union 2024-07-07 01:28:06 -04:00
Joseph C. Sible
61b7e66f80 Use sets instead of maps that never use their values 2024-07-07 01:28:06 -04:00
Joseph C. Sible
b408f54620 Simplify invokedNodes 2024-07-07 01:28:00 -04:00
Vidar Holen
3946cbd4a0 Upgrade docker build images 2024-06-24 05:12:21 +00:00
Vidar Holen
c4b7b79b8b Merge branch 'mengzhuo-main' 2024-06-18 01:53:21 +00:00
Vidar Holen
23e76de4f2 Allow riscv64 image to run without binfmt_misc 2024-06-18 01:52:56 +00:00
Meng Zhuo
15de97e33f Add linux.riscv64 precompiled support 2024-05-30 19:20:21 +08:00
Bryan Honof
78d1ee0222
Add Flox to list of installation methods 2024-05-24 17:15:09 +02:00
Vidar Holen
ac8fb00504 Account for BusyBox support of [[ ]] (fixes #2967) 2024-05-04 16:45:52 -07:00
Vidar Holen
a13cb85f49 Fixed broken test due to bad build cache 2024-05-04 16:34:21 -07:00
Vidar Holen
a7a906e2cb Allow SC2154 to trigger in arrays (fixes #2970) 2024-05-04 16:29:51 -07:00
Vidar Holen
d705716dc4 Account for annotations in SC2215. Fixes #2975. 2024-05-04 15:22:09 -07:00
Vidar Holen
76ff702e93 Supress SC2015 about A && B || C when B is a test. 2024-05-04 15:12:13 -07:00
Vidar Holen
4f81dbe839 Add warning about uninvoked functions, reduce repeated triggering of SC2317 (fixes #2966) 2024-05-04 14:35:26 -07:00
Jan Dubois
796c6bd848 Add new bats variables stderr and stderr_lines
These are being set by `run --separate-stderr` and have been introduced
in https://github.com/bats-core/bats-core/releases/tag/v1.5.0
2024-04-24 19:07:57 -07:00
Syuugo
69fe4e1306
Upgrade build workflow dependencies 2024-04-25 10:35:43 +09:00
Vidar Holen
2c5155e43d Warn about capturing the output of redirected commands. 2024-04-14 18:47:19 -07:00
Vidar Holen
04a86245a1 Remove trailing space in output (fixes #2961) 2024-04-08 20:24:28 -07:00
Vidar Holen
79491db9f6
Merge pull request #2938 from larryv/reword-SC2324
Recommend `typeset` instead of `declare` in SC2324
2024-04-07 13:27:14 -07:00
Vidar Holen
5241878e59 Update Windows build image with new cURL URL 2024-04-05 17:15:04 -07:00
Vidar Holen
30b32af873 Add updating build images to release checks 2024-04-05 17:14:59 -07:00
Vidar Holen
da8854cac6
Merge pull request #2942 from jansorg/fix-builders
Fix builders for Linux
2024-04-04 19:40:13 -07:00
Vidar Holen
39a035793c
Merge pull request #2960 from hugos99/patch-1
Update README.md to add macOS Arm64 pre-compiled binaries link
2024-04-04 19:23:28 -07:00
Hugo Sousa
0a7bb1822e
Update README.md to add macOS Arm64 pre-compiled binaries link 2024-04-04 12:26:20 +01:00
Joachim Ansorg
c4123375e0 build smaller ShellCheck binary for Linux x86_64 2024-03-12 18:00:36 +01:00
Joachim Ansorg
52dc66349b fix build of linux.aarch64 2024-03-12 17:36:20 +01:00
Lawrence Velázquez
9cb21c8557
Recommend typeset instead of declare in SC2324
Bash has both `typeset` and `declare`, but ksh has `typeset` only.
Recommend the more portable alternative to users.
2024-03-08 18:24:08 -05:00
Vidar Holen
50db9a29c4 Check source details before git details 2024-03-07 19:11:32 -08:00
Vidar Holen
94214ee725 Post-release CHANGELOG 2024-03-07 19:11:12 -08:00
39 changed files with 877 additions and 268 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,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`, An image can be built and tagged using `build_builder`,
and run on a source tarball using `run_builder`. and run on a source tarball using `run_builder`.
Tip: Are you developing an image that relies on QEmu usermode emulation?
It's easy to accidentally depend on binfmt\_misc on the host OS.
Do a `echo 0 | sudo tee /proc/sys/fs/binfmt_misc/status` before testing.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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 "$@"
}
"$@"

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

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

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

View file

@ -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 ENV TARGETNAME linux.x86_64

View file

@ -12,7 +12,7 @@ WORKDIR /haskell
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-x86_64-unknown-mingw32.tar.xz" | tar xJ --strip-components=1 RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-x86_64-unknown-mingw32.tar.xz" | tar xJ --strip-components=1
WORKDIR /haskell/bin WORKDIR /haskell/bin
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-mingw32.zip" | busybox unzip - RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-mingw32.zip" | busybox unzip -
RUN curl -L "https://curl.se/windows/dl-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 ENV WINEPATH /haskell/bin
# It's unknown whether Cabal on Windows suffers from the same issue # It's unknown whether Cabal on Windows suffers from the same issue

View file

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

View file

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

View file

@ -138,7 +138,7 @@ data InnerToken t =
| Inner_T_WhileExpression [t] [t] | Inner_T_WhileExpression [t] [t]
| Inner_T_Annotation [Annotation] t | Inner_T_Annotation [Annotation] t
| Inner_T_Pipe String | Inner_T_Pipe String
| Inner_T_CoProc (Maybe String) t | Inner_T_CoProc (Maybe Token) t
| Inner_T_CoProcBody t | Inner_T_CoProcBody t
| Inner_T_Include t | Inner_T_Include t
| Inner_T_SourceCommand t t | Inner_T_SourceCommand t t
@ -206,7 +206,7 @@ pattern T_Annotation id anns t = OuterToken id (Inner_T_Annotation anns t)
pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c) pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c)
pattern T_Array id t = OuterToken id (Inner_T_Array t) pattern T_Array id t = OuterToken id (Inner_T_Array t)
pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l) pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)
pattern TA_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 T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value)
pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3) pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3)
pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1) pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1)
@ -259,7 +259,7 @@ pattern T_Subshell id l = OuterToken id (Inner_T_Subshell l)
pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l) pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l)
pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l) pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l)
{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, TA_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 instance Eq Token where
OuterToken _ a == OuterToken _ b = a == b OuterToken _ a == OuterToken _ b = a == b

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,13 +17,13 @@ and is still highly experimental.
Make sure you're plugged in and have screen/tmux in place, Make sure you're plugged in and have screen/tmux in place,
then re-run with $0 --run to continue. then re-run with $0 --run to continue.
Also note that dist* will be deleted. Also note that dist*/ and .stack-work/ will be deleted.
EOF EOF
exit 0 exit 0
} }
echo "Deleting 'dist' and 'dist-newstyle'..." echo "Deleting 'dist', 'dist-newstyle', and '.stack-work'..."
rm -rf dist dist-newstyle rm -rf dist dist-newstyle .stack-work
execs=$(find . -name shellcheck) execs=$(find . -name shellcheck)
@ -74,11 +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 archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
# Ubuntu LTS # Ubuntu LTS
ubuntu:24.04 apt-get update && apt-get install -y cabal-install
ubuntu:22.04 apt-get update && apt-get install -y cabal-install ubuntu:22.04 apt-get update && apt-get install -y cabal-install
ubuntu:20.04 apt-get update && apt-get install -y cabal-install ubuntu:20.04 apt-get update && apt-get install -y cabal-install
# Stack on Ubuntu LTS # Stack on Ubuntu LTS
ubuntu: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 EOF
exit "$final" exit "$final"

View file

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