78 Commits

Author SHA1 Message Date
Vidar Holen
37dfb67768 Stable version v0.10.0
This release is dedicated to LLMs, for finally fulfilling the promise of
1960s scifi: systems you can hack using logic games and creative lies.
2024-03-07 17:54:39 -08:00
Vidar Holen
a7e65dca8d Update some copyright years 2024-03-04 09:19:51 -08:00
Vidar Holen
8bc7345aa7 Remove outdated distros from testing 2024-03-03 16:11:44 -08:00
Vidar Holen
ad3c3146f0 Fix snap build 2024-03-03 12:34:29 -08:00
Vidar Holen
55be4543f2 Avoid stripping darwin.aarch64 binaries to keep code signature 2024-02-19 11:40:30 -08:00
Vidar Holen
8c4c112c25 Initial version of an ARM64 macOS build 2024-02-19 09:29:27 -08:00
Vidar Holen
d80fdfa9e8 Add extended-analysis directive to toggle DFA 2024-02-03 16:11:39 -08:00
Vidar Holen
1565091b1d Merge pull request #2892 from ottok/doc/pulsar-not-atom
Replace Atom reference with Pulsar Edit equivalent
2024-02-03 13:46:23 -08:00
Vidar Holen
d056549406 Merge pull request #2885 from juhp/patch-1
.cabal: allow Diff-0.5
2024-02-03 13:43:52 -08:00
Vidar Holen
f5758e1789 Merge branch 'tacerus-config' 2024-02-03 13:38:56 -08:00
Vidar Holen
6a44a19f17 Only read --rcfile once, and skip search if unavailable 2024-02-03 13:34:49 -08:00
Vidar Holen
b1b95c2c17 Merge pull request #2917 from grische/fix/tests-readme
Remove deprecated "install --enable-tests" command
2024-02-03 13:04:06 -08:00
Grische
de95624d31 Remove deprecated "install --enable-tests" command 2024-02-02 12:35:52 +01:00
Vidar Holen
b5ab220652 Merge pull request #2879 from slycordinator/winget
Add installation directions for winget
2024-01-21 11:44:58 -08:00
Georg Pfuetzenreuter
1bce426fcf Implement rcfile option
This introduces the "--rcfile" argument which allows a specific
shellcheckrc file to be passed.
If specified and the given file exists, the default locations
will not be searched and the specified file will be used.

Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
2024-01-21 02:59:47 +01:00
Joseph C. Sible
ba86c6363c Use maybe instead of fromMaybe and fmap 2024-01-02 14:46:07 -05:00
Joseph C. Sible
67abfe159e Remove most of the partial head and tail functions from src/ShellCheck/CFG.hs 2024-01-01 19:04:26 -05:00
Joseph C. Sible
025cc5266e Simplify isUnquotedFlag 2024-01-01 16:00:19 -05:00
Joseph C. Sible
5a6f4840ad Replace a few more occurrences of !!! with pattern matching 2024-01-01 14:18:52 -05:00
Joseph C. Sible
9e0fdbe431 Simplify isTransparentCommand 2023-12-31 18:13:32 -05:00
Joseph C. Sible
b7f88ec4b7 Stop building tuples that we never look at both sides of 2023-12-31 18:09:02 -05:00
Joseph C. Sible
7b0589988f Implement isCondition in terms of foldr 2023-12-31 17:21:50 -05:00
Joseph C. Sible
71889c139a Use a case expression instead of any and take 1 2023-12-31 16:44:21 -05:00
Joseph C. Sible
a6984cddb0 Switch then and else to remove a not 2023-12-31 16:40:18 -05:00
Joseph C. Sible
3f40b688ee Simplify getStringFromParsec 2023-12-31 16:33:34 -05:00
Joseph C. Sible
6c81505870 Use a pattern guard instead of fromJust in checkLoopKeywordScope 2023-12-31 16:26:03 -05:00
Joseph C. Sible
10afe83ce3 Use getLiteralStringDef instead of rebuilding it with fromJust 2023-12-31 16:23:45 -05:00
Joseph C. Sible
a786f996a1 Replace !!! with pattern-matching where it's easy 2023-12-31 15:55:06 -05:00
Joseph C. Sible
6e5b5401c6 Manually fuse elem and map in checkArrayValueUsedAsIndex 2023-12-31 02:31:07 -05:00
Joseph C. Sible
71c0fcb737 Manually fuse elem and map in isParentOf 2023-12-31 02:27:52 -05:00
Joseph C. Sible
add49cda17 Make getPath return a NonEmpty 2023-12-31 02:12:58 -05:00
Joseph C. Sible
e1ad063834 Implement getPath in terms of unfoldr 2023-12-31 01:59:53 -05:00
Otto Kekäläinen
ee41c780f4 Replace Atom reference with Pulsar Edit equivalent
Since Microsoft acquired GitHub and discontinued Atom in 2022,
the community started a fork at https://pulsar-edit.dev/. Linking
to an archived repository under the Atom organization does not make
sense anymore, so link to active Pulsar fork instead.
2023-12-31 10:47:40 +08:00
Joseph C. Sible
980e7d3ca8 Use <$> instead of >>= and return 2023-12-30 14:49:26 -05:00
Joseph C. Sible
dedf932fe8 Use traverse instead of sequence and map 2023-12-30 13:59:15 -05:00
Joseph C. Sible
3bd7df955b Use a pattern match instead of null and head in checkCommand 2023-12-29 14:18:42 -05:00
Joseph C. Sible
dab77b2c8d Implement parseEnum in terms of lookup 2023-12-21 13:48:47 -05:00
Joseph C. Sible
f983d9ae93 Simplify functionMap and remove unnecessary partiality 2023-12-21 13:35:22 -05:00
Joseph C. Sible
bfe4342697 Remove unnecessary partiality from check 2023-12-19 02:30:48 -05:00
Joseph C. Sible
a47a42cb45 Remove unnecessary partiality from isAssignmentParamToCommand 2023-12-19 02:17:59 -05:00
Joseph C. Sible
eed0174e90 Make "Unresolved scope in dependency" impossible 2023-12-19 02:06:45 -05:00
Joseph C. Sible
0c46b8b2d5 Use NonEmpty to remove partiality from handleCommand 2023-12-19 01:49:04 -05:00
Joseph C. Sible
208e38358e Use a list comprehension to remove partiality from notesForContext 2023-12-19 01:00:20 -05:00
Joseph C. Sible
c1452e0d17 Remove unnecessary partiality from kludgeAwayQuotes 2023-12-19 00:53:08 -05:00
Joseph C. Sible
c97abdb939 Make HereDocPending only hold the relevant pieces of a T_HereDoc instead of an arbitrary Token 2023-12-19 00:41:12 -05:00
Joseph C. Sible
f242922a2e Use onlyLiteralString in more places 2023-12-19 00:00:32 -05:00
Joseph C. Sible
a37803d2b8 Remove partial head function from src/ShellCheck/Formatter/CheckStyle.hs 2023-12-18 23:57:47 -05:00
Jens Petersen
09d04c4c9b .cabal: allow Diff-0.5 2023-12-15 22:40:48 +08:00
slycordinator
e5028481e2 Add installation directions for winge
ShellCheck is now available on winget, so we can add it to the installation methods.
2023-12-14 15:24:49 +09:00
Joseph C. Sible
5a961371a7 Remove partial head function from src/ShellCheck/Formatter/GCC.hs 2023-12-11 15:55:29 -05:00
Joseph C. Sible
e5208ccb50 Remove partial head function from src/ShellCheck/Formatter/JSON1.hs 2023-12-11 15:43:35 -05:00
Joseph C. Sible
4c1d9171b2 Remove partial head function from src/ShellCheck/Formatter/TTY.hs 2023-12-11 15:08:39 -05:00
Vidar Holen
a9e7bf1950 Reparse indices after attaching here docs (fixes #2846) 2023-12-10 19:13:34 -08:00
Vidar Holen
f2729f73cb Abuse STRIP to avoid crashes on unsupported AST nodes 2023-12-10 17:58:47 -08:00
Vidar Holen
175d3cc9b7 Merge pull request #2876 from andreasabel/master
Testsuite: report which module failed the tests
2023-12-10 17:34:51 -08:00
Vidar Holen
5c50b0b189 Merge branch 'grische-feature/busyboxsh-support' 2023-12-10 17:15:57 -08:00
Vidar Holen
74282b0a93 Recognize 'busybox' in --shell and directives. Add to doc texts. 2023-12-10 17:05:29 -08:00
Andreas Abel
b6d4952e2e Testsuite: report which module failed the tests
This also fixes the problem that the testsuite threw `exitFailure`
even when it succeeded (which I found inexplicable).

Once this is published, the testsuite could be enabled in Stackage again.
2023-12-06 18:41:53 +01:00
Grische
fdcce458c1 silence some shell expansions for busybox sh 2023-11-27 13:03:29 +01:00
Grische
ca255fe326 silence SC3046 and SC3051 for busybox sh 2023-11-27 13:03:17 +01:00
Grische
a3b8be82fe silence SC3048 for busybox sh 2023-11-27 13:03:07 +01:00
Grische
ac63dc33c9 silence SC3020 for busybox sh 2023-11-27 13:02:56 +01:00
Grische
903421fb5d silence SC3014 for busybox sh 2023-11-27 13:02:45 +01:00
Grische
00ffd2db33 silence SC3010 for busybox sh 2023-11-27 13:02:28 +01:00
Grische
1e1045e73e make busybox sh Dash-like 2023-11-27 13:01:22 +01:00
Grische
be8e4b2b8a add basic busybox sh support 2023-11-27 13:00:10 +01:00
Vidar Holen
a71a13c2fc Merge pull request #2837 from ulidtko/fix/missed-test(1)-bashisms
Fix: extend []-related bashism checks on `test` calls too
2023-11-08 13:06:26 -08:00
Joseph C. Sible
1aeab287e6 Add nil case that went missing in 4fd0615 2023-11-03 01:33:49 -04:00
Joseph C. Sible
2a95bc6be3 Switch to getLiteralStringDef to avoid an unnecessary fromJust 2023-10-16 20:00:31 -04:00
Joseph C. Sible
4fd0615501 Stop using head in isLeadingNumberVar 2023-10-16 00:55:04 -04:00
Joseph C. Sible
8b3c37aa36 Use find instead of listToMaybe and filter 2023-10-16 00:06:53 -04:00
Joseph C. Sible
dc2f388310 Adjust bounds to compile on 9.8
You'll need --allow-newer=fgl:deepseq for it to work too,
until haskell/fgl#111 gets merged.
2023-10-14 18:12:51 -04:00
Vidar Holen
99a94421ab Manually install 'hub' dependency 2023-10-08 19:42:31 -07:00
Vidar Holen
6a6d8e9fc4 Revert "Bump actions/checkout from 3 to 4"
This reverts commit 410ec54617.
2023-10-08 18:52:05 -07:00
Vidar Holen
592c17e4f2 Merge pull request #2824 from koalaman/dependabot/github_actions/actions/checkout-4
Bump actions/checkout from 3 to 4
2023-10-08 14:14:25 -07:00
Max Ulidtko
9605396bef Docs: describe fixes of PR #2837 in changelog 2023-10-01 21:23:25 +02:00
Max Ulidtko
c89ec2fd49 Fix: do []-related bashism checks on test(1) calls too 2023-10-01 19:57:19 +02:00
dependabot[bot]
410ec54617 Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-05 08:21:55 +00:00
31 changed files with 582 additions and 341 deletions

View File

@@ -47,7 +47,7 @@ jobs:
needs: package_source needs: package_source
strategy: strategy:
matrix: matrix:
build: [linux.x86_64, linux.aarch64, linux.armv6hf, darwin.x86_64, windows.x86_64] build: [linux.x86_64, linux.aarch64, linux.armv6hf, darwin.x86_64, darwin.aarch64, windows.x86_64]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -103,6 +103,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: Deploy environment: Deploy
steps: steps:
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install hub
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3

1
.gitignore vendored
View File

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

View File

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

View File

@@ -1,13 +1,23 @@
## Git ## v0.10.0 - 2024-03-07
### Added ### Added
- Precompiled binaries for macOS ARM64 (darwin.aarch64)
- Added support for busybox sh
- Added flag --rcfile to specify an rc file by name.
- Added `extended-analysis=true` directive to enable/disable dataflow analysis
(with a corresponding --extended-analysis flag).
- SC2324: Warn when x+=1 appends instead of increments - SC2324: Warn when x+=1 appends instead of increments
- SC2325: Warn about multiple `!`s in dash/sh. - SC2325: Warn about multiple `!`s in dash/sh.
- SC2326: Warn about `foo | ! bar` in bash/dash/sh. - SC2326: Warn about `foo | ! bar` in bash/dash/sh.
- SC3012: Warn about lexicographic-compare bashism in test like in [ ]
- SC3013: Warn bashism `test _ -op/-nt/-ef _` like in [ ]
- SC3014: Warn bashism `test _ == _` like in [ ]
- SC3015: Warn bashism `test _ =~ _` like in [ ]
- SC3016: Warn bashism `test -v _` like in [ ]
- SC3017: Warn bashism `test -a _` like in [ ]
### Fixed ### Fixed
- source statements with here docs now work correctly - source statements with here docs now work correctly
- "(Array.!): undefined array element" error should no longer occur
### Changed
## v0.9.0 - 2022-12-12 ## v0.9.0 - 2022-12-12

View File

@@ -77,7 +77,7 @@ You can see ShellCheck suggestions directly in a variety of editors.
* Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck). * Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck).
* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck). * Pulsar Edit (former Atom), through [linter-shellcheck-pulsar](https://github.com/pulsar-cooperative/linter-shellcheck-pulsar).
* VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck). * VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck).
@@ -194,6 +194,12 @@ On Windows (via [chocolatey](https://chocolatey.org/packages/shellcheck)):
C:\> choco install shellcheck C:\> choco install shellcheck
``` ```
Or Windows (via [winget](https://github.com/microsoft/winget-pkgs)):
```cmd
C:\> winget install --id koalaman.shellcheck
```
Or Windows (via [scoop](http://scoop.sh)): Or Windows (via [scoop](http://scoop.sh)):
```cmd ```cmd
@@ -303,10 +309,6 @@ Verify that `cabal` is installed and update its dependency list with
$ cabal install $ cabal install
Or if you intend to run the tests:
$ cabal install --enable-tests
This will compile ShellCheck and install it to your `~/.cabal/bin` directory. This will compile ShellCheck and install it to your `~/.cabal/bin` directory.
Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`): Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
@@ -552,4 +554,3 @@ Happy ShellChecking!
* The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221). * The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221).
* ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)! * ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)!

View File

@@ -1,5 +1,5 @@
Name: ShellCheck Name: ShellCheck
Version: 0.9.0 Version: 0.10.0
Synopsis: Shell script analysis tool Synopsis: Shell script analysis tool
License: GPL-3 License: GPL-3
License-file: LICENSE License-file: LICENSE
@@ -46,14 +46,14 @@ library
semigroups semigroups
build-depends: build-depends:
-- The lower bounds are based on GHC 7.10.3 -- The lower bounds are based on GHC 7.10.3
-- The upper bounds are based on GHC 9.6.1 -- The upper bounds are based on GHC 9.8.1
aeson >= 1.4.0 && < 2.2, aeson >= 1.4.0 && < 2.3,
array >= 0.5.1 && < 0.6, array >= 0.5.1 && < 0.6,
base >= 4.8.0.0 && < 5, base >= 4.8.0.0 && < 5,
bytestring >= 0.10.6 && < 0.12, bytestring >= 0.10.6 && < 0.13,
containers >= 0.5.6 && < 0.7, containers >= 0.5.6 && < 0.8,
deepseq >= 1.4.1 && < 1.5, deepseq >= 1.4.1 && < 1.6,
Diff >= 0.4.0 && < 0.5, Diff >= 0.4.0 && < 0.6,
fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9), fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9),
filepath >= 1.4.0 && < 1.5, filepath >= 1.4.0 && < 1.5,
mtl >= 2.2.2 && < 2.4, mtl >= 2.2.2 && < 2.4,

View File

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

17
build/darwin.aarch64/build Executable file
View File

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

1
build/darwin.aarch64/tag Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -152,6 +152,7 @@ data Annotation =
| ShellOverride String | ShellOverride String
| SourcePath String | SourcePath String
| ExternalSources Bool | ExternalSources Bool
| ExtendedAnalysis Bool
deriving (Show, Eq) deriving (Show, Eq)
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq) data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)

View File

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

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2022 Vidar Holen Copyright 2012-2024 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -19,6 +19,7 @@
-} -}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE PatternGuards #-}
module ShellCheck.Analytics (checker, optionalChecks, ShellCheck.Analytics.runTests) where module ShellCheck.Analytics (checker, optionalChecks, ShellCheck.Analytics.runTests) where
import ShellCheck.AST import ShellCheck.AST
@@ -46,6 +47,7 @@ import Data.Maybe
import Data.Ord import Data.Ord
import Data.Semigroup import Data.Semigroup
import Debug.Trace -- STRIP import Debug.Trace -- STRIP
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
import qualified Data.Set as S import qualified Data.Set as S
import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.All (forAllProperties)
@@ -344,13 +346,11 @@ dist a b
hasFloatingPoint params = shellType params == Ksh hasFloatingPoint params = shellType params == Ksh
-- Checks whether the current parent path is part of a condition -- Checks whether the current parent path is part of a condition
isCondition [] = False isCondition (x NE.:| xs) = foldr go (const False) xs x
isCondition [_] = False
isCondition (child:parent:rest) =
case child of
T_BatsTest {} -> True -- count anything in a @test as conditional
_ -> getId child `elem` map getId (getConditionChildren parent) || isCondition (parent:rest)
where where
go _ _ T_BatsTest{} = True -- count anything in a @test as conditional
go parent go_rest child =
getId child `elem` map getId (getConditionChildren parent) || go_rest parent
getConditionChildren t = getConditionChildren t =
case t of case t of
T_AndIf _ left right -> [left] T_AndIf _ left right -> [left]
@@ -467,9 +467,8 @@ checkAssignAteCommand _ (T_SimpleCommand id [T_Assignment _ _ _ _ assignmentTerm
where where
isCommonCommand (Just s) = s `elem` commonCommands isCommonCommand (Just s) = s `elem` commonCommands
isCommonCommand _ = False isCommonCommand _ = False
firstWordIsArg list = fromMaybe False $ do firstWordIsArg (head:_) = isGlob head || isUnquotedFlag head
head <- list !!! 0 firstWordIsArg [] = False
return $ isGlob head || isUnquotedFlag head
checkAssignAteCommand _ _ = return () checkAssignAteCommand _ _ = return ()
@@ -490,9 +489,7 @@ prop_checkWrongArit2 = verify checkWrongArithmeticAssignment "n=2; i=n*2"
checkWrongArithmeticAssignment params (T_SimpleCommand id [T_Assignment _ _ _ _ val] []) = checkWrongArithmeticAssignment params (T_SimpleCommand id [T_Assignment _ _ _ _ val] []) =
sequence_ $ do sequence_ $ do
str <- getNormalString val str <- getNormalString val
match <- matchRegex regex str var:op:_ <- matchRegex regex str
var <- match !!! 0
op <- match !!! 1
Map.lookup var references Map.lookup var references
return . warn (getId val) 2100 $ return . warn (getId val) 2100 $
"Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))" "Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))"
@@ -646,10 +643,10 @@ prop_checkShebang9 = verifyNotTree checkShebang "# shellcheck shell=sh\ntrue"
prop_checkShebang10 = verifyNotTree checkShebang "#!foo\n# shellcheck shell=sh ignore=SC2239\ntrue" prop_checkShebang10 = verifyNotTree checkShebang "#!foo\n# shellcheck shell=sh ignore=SC2239\ntrue"
prop_checkShebang11 = verifyTree checkShebang "#!/bin/sh/\ntrue" prop_checkShebang11 = verifyTree checkShebang "#!/bin/sh/\ntrue"
prop_checkShebang12 = verifyTree checkShebang "#!/bin/sh/ -xe\ntrue" prop_checkShebang12 = verifyTree checkShebang "#!/bin/sh/ -xe\ntrue"
prop_checkShebang13 = verifyTree checkShebang "#!/bin/busybox sh" prop_checkShebang13 = verifyNotTree checkShebang "#!/bin/busybox sh"
prop_checkShebang14 = verifyNotTree checkShebang "#!/bin/busybox sh\n# shellcheck shell=sh\n" prop_checkShebang14 = verifyNotTree checkShebang "#!/bin/busybox sh\n# shellcheck shell=sh\n"
prop_checkShebang15 = verifyNotTree checkShebang "#!/bin/busybox sh\n# shellcheck shell=dash\n" prop_checkShebang15 = verifyNotTree checkShebang "#!/bin/busybox sh\n# shellcheck shell=dash\n"
prop_checkShebang16 = verifyTree checkShebang "#!/bin/busybox ash" prop_checkShebang16 = verifyNotTree checkShebang "#!/bin/busybox ash"
prop_checkShebang17 = verifyNotTree checkShebang "#!/bin/busybox ash\n# shellcheck shell=dash\n" prop_checkShebang17 = verifyNotTree checkShebang "#!/bin/busybox ash\n# shellcheck shell=dash\n"
prop_checkShebang18 = verifyNotTree checkShebang "#!/bin/busybox ash\n# shellcheck shell=sh\n" prop_checkShebang18 = verifyNotTree checkShebang "#!/bin/busybox ash\n# shellcheck shell=sh\n"
checkShebang params (T_Annotation _ list t) = checkShebang params (T_Annotation _ list t) =
@@ -846,14 +843,14 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) =
getRedirs _ = [] getRedirs _ = []
special x = "/dev/" `isPrefixOf` concat (oversimplify x) special x = "/dev/" `isPrefixOf` concat (oversimplify x)
isInput t = isInput t =
case drop 1 $ getPath (parentMap params) t of case NE.tail $ getPath (parentMap params) t of
T_IoFile _ op _:_ -> T_IoFile _ op _:_ ->
case op of case op of
T_Less _ -> True T_Less _ -> True
_ -> False _ -> False
_ -> False _ -> False
isOutput t = isOutput t =
case drop 1 $ getPath (parentMap params) t of case NE.tail $ getPath (parentMap params) t of
T_IoFile _ op _:_ -> T_IoFile _ op _:_ ->
case op of case op of
T_Greater _ -> True T_Greater _ -> True
@@ -1087,7 +1084,7 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
return $ if name == "find" then getFindCommand cmd else if name == "git" then getGitCommand cmd else if name == "mumps" then getMumpsCommand cmd else name return $ if name == "find" then getFindCommand cmd else if name == "git" then getGitCommand cmd else if name == "mumps" then getMumpsCommand cmd else name
isProbablyOk = isProbablyOk =
any isOkAssignment (take 3 $ getPath parents t) any isOkAssignment (NE.take 3 $ getPath parents t)
|| commandName `elem` [ || commandName `elem` [
"trap" "trap"
,"sh" ,"sh"
@@ -1204,6 +1201,7 @@ checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do
case shellType params of case shellType params of
Sh -> return () -- These are unsupported and will be caught by bashism checks. Sh -> return () -- These are unsupported and will be caught by bashism checks.
Dash -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting." Dash -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting."
BusyboxSh -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting."
_ -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting (or switch to [[ .. ]])." _ -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting (or switch to [[ .. ]])."
when (op `elem` arithmeticBinaryTestOps) $ do when (op `elem` arithmeticBinaryTestOps) $ do
@@ -1264,7 +1262,8 @@ checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do
str = concat $ oversimplify c str = concat $ oversimplify c
var = getBracedReference str var = getBracedReference str
in fromMaybe False $ do in fromMaybe False $ do
state <- CF.getIncomingState (cfgAnalysis params) id cfga <- cfgAnalysis params
state <- CF.getIncomingState cfga id
value <- Map.lookup var $ CF.variablesInScope state value <- Map.lookup var $ CF.variablesInScope state
return $ CF.numericalStatus (CF.variableValue value) >= CF.NumericalStatusMaybe return $ CF.numericalStatus (CF.variableValue value) >= CF.NumericalStatusMaybe
_ -> _ ->
@@ -1442,14 +1441,14 @@ prop_checkConstantNullary5 = verify checkConstantNullary "[[ true ]]"
prop_checkConstantNullary6 = verify checkConstantNullary "[ 1 ]" prop_checkConstantNullary6 = verify checkConstantNullary "[ 1 ]"
prop_checkConstantNullary7 = verify checkConstantNullary "[ false ]" prop_checkConstantNullary7 = verify checkConstantNullary "[ false ]"
checkConstantNullary _ (TC_Nullary _ _ t) | isConstant t = checkConstantNullary _ (TC_Nullary _ _ t) | isConstant t =
case fromMaybe "" $ getLiteralString t of case onlyLiteralString t of
"false" -> err (getId t) 2158 "[ false ] is true. Remove the brackets." "false" -> err (getId t) 2158 "[ false ] is true. Remove the brackets."
"0" -> err (getId t) 2159 "[ 0 ] is true. Use 'false' instead." "0" -> err (getId t) 2159 "[ 0 ] is true. Use 'false' instead."
"true" -> style (getId t) 2160 "Instead of '[ true ]', just use 'true'." "true" -> style (getId t) 2160 "Instead of '[ true ]', just use 'true'."
"1" -> style (getId t) 2161 "Instead of '[ 1 ]', use 'true'." "1" -> style (getId t) 2161 "Instead of '[ 1 ]', use 'true'."
_ -> err (getId t) 2078 "This expression is constant. Did you forget a $ somewhere?" _ -> err (getId t) 2078 "This expression is constant. Did you forget a $ somewhere?"
where where
string = fromMaybe "" $ getLiteralString t string = onlyLiteralString t
checkConstantNullary _ _ = return () checkConstantNullary _ _ = return ()
@@ -1458,9 +1457,8 @@ prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar" prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
checkForDecimals params t@(TA_Expansion id _) = sequence_ $ do checkForDecimals params t@(TA_Expansion id _) = sequence_ $ do
guard $ not (hasFloatingPoint params) guard $ not (hasFloatingPoint params)
str <- getLiteralString t first:rest <- getLiteralString t
first <- str !!! 0 guard $ isDigit first && '.' `elem` rest
guard $ isDigit first && '.' `elem` str
return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk." return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
checkForDecimals _ _ = return () checkForDecimals _ _ = return ()
@@ -1494,7 +1492,7 @@ checkArithmeticDeref params t@(TA_Expansion _ [T_DollarBraced id _ l]) =
where where
isException [] = True isException [] = True
isException s@(h:_) = any (`elem` "/.:#%?*@$-!+=^,") s || isDigit h isException s@(h:_) = any (`elem` "/.:#%?*@$-!+=^,") s || isDigit h
getWarning = fromMaybe noWarning . msum . map warningFor $ parents params t getWarning = fromMaybe noWarning . msum . NE.map warningFor $ parents params t
warningFor t = warningFor t =
case t of case t of
T_Arithmetic {} -> return normalWarning T_Arithmetic {} -> return normalWarning
@@ -1822,7 +1820,7 @@ checkInexplicablyUnquoted params (T_NormalWord id tokens) = mapM_ check (tails t
T_Literal id s T_Literal id s
| not (quotesSingleThing a && quotesSingleThing b | not (quotesSingleThing a && quotesSingleThing b
|| s `elem` ["=", ":", "/"] || s `elem` ["=", ":", "/"]
|| isSpecial (getPath (parentMap params) trapped) || isSpecial (NE.toList $ getPath (parentMap params) trapped)
) -> ) ->
warnAboutLiteral id warnAboutLiteral id
_ -> return () _ -> return ()
@@ -2040,7 +2038,7 @@ doVariableFlowAnalysis readFunc writeFunc empty flow = evalState (
-- from $foo=bar to foo=bar. This is not pretty but ok. -- from $foo=bar to foo=bar. This is not pretty but ok.
quotesMayConflictWithSC2281 params t = quotesMayConflictWithSC2281 params t =
case getPath (parentMap params) t of case getPath (parentMap params) t of
_ : T_NormalWord parentId (me:T_Literal _ ('=':_):_) : T_SimpleCommand _ _ (cmd:_) : _ -> _ NE.:| T_NormalWord parentId (me:T_Literal _ ('=':_):_) : T_SimpleCommand _ _ (cmd:_) : _ ->
(getId t) == (getId me) && (parentId == getId cmd) (getId t) == (getId me) && (parentId == getId cmd)
_ -> False _ -> False
@@ -2146,7 +2144,8 @@ checkSpacefulnessCfg' dirtyPass params token@(T_DollarBraced id _ list) =
&& not (usedAsCommandName parents token) && not (usedAsCommandName parents token)
isClean = fromMaybe False $ do isClean = fromMaybe False $ do
state <- CF.getIncomingState (cfgAnalysis params) id cfga <- cfgAnalysis params
state <- CF.getIncomingState cfga id
value <- Map.lookup name $ CF.variablesInScope state value <- Map.lookup name $ CF.variablesInScope state
return $ isCleanState value return $ isCleanState value
@@ -2275,7 +2274,7 @@ checkFunctionsUsedExternally params t =
(Just str, t) -> do (Just str, t) -> do
let name = basename str let name = basename str
let args = skipOver t argv let args = skipOver t argv
let argStrings = map (\x -> (fromMaybe "" $ getLiteralString x, x)) args let argStrings = map (\x -> (onlyLiteralString x, x)) args
let candidates = getPotentialCommands name argStrings let candidates = getPotentialCommands name argStrings
mapM_ (checkArg name (getId t)) candidates mapM_ (checkArg name (getId t)) candidates
_ -> return () _ -> return ()
@@ -2651,7 +2650,7 @@ checkPrefixAssignmentReference params t@(T_DollarBraced id _ value) =
check path check path
where where
name = getBracedReference $ concat $ oversimplify value name = getBracedReference $ concat $ oversimplify value
path = getPath (parentMap params) t path = NE.toList $ getPath (parentMap params) t
idPath = map getId path idPath = map getId path
check [] = return () check [] = return ()
@@ -2700,7 +2699,7 @@ checkCharRangeGlob p t@(T_Glob id str) |
return $ isCommandMatch cmd (`elem` ["tr", "read"]) return $ isCommandMatch cmd (`elem` ["tr", "read"])
-- Check if this is a dereferencing context like [[ -v array[operandhere] ]] -- Check if this is a dereferencing context like [[ -v array[operandhere] ]]
isDereferenced = fromMaybe False . msum . map isDereferencingOp . getPath (parentMap p) isDereferenced = fromMaybe False . msum . NE.map isDereferencingOp . getPath (parentMap p)
isDereferencingOp t = isDereferencingOp t =
case t of case t of
TC_Binary _ DoubleBracket str _ _ -> return $ isDereferencingBinaryOp str TC_Binary _ DoubleBracket str _ _ -> return $ isDereferencingBinaryOp str
@@ -2751,19 +2750,18 @@ prop_checkLoopKeywordScope5 = verify checkLoopKeywordScope "if true; then break;
prop_checkLoopKeywordScope6 = verify checkLoopKeywordScope "while true; do true | { break; }; done" prop_checkLoopKeywordScope6 = verify checkLoopKeywordScope "while true; do true | { break; }; done"
prop_checkLoopKeywordScope7 = verifyNot checkLoopKeywordScope "#!/bin/ksh\nwhile true; do true | { break; }; done" prop_checkLoopKeywordScope7 = verifyNot checkLoopKeywordScope "#!/bin/ksh\nwhile true; do true | { break; }; done"
checkLoopKeywordScope params t | checkLoopKeywordScope params t |
name `elem` map Just ["continue", "break"] = Just name <- getCommandName t, name `elem` ["continue", "break"] =
if not $ any isLoop path if any isLoop path
then if any isFunction $ take 1 path then case map subshellType $ filter (not . isFunction) path of
-- breaking at a source/function invocation is an abomination. Let's ignore it.
then err (getId t) 2104 $ "In functions, use return instead of " ++ fromJust name ++ "."
else err (getId t) 2105 $ fromJust name ++ " is only valid in loops."
else case map subshellType $ filter (not . isFunction) path of
Just str:_ -> warn (getId t) 2106 $ Just str:_ -> warn (getId t) 2106 $
"This only exits the subshell caused by the " ++ str ++ "." "This only exits the subshell caused by the " ++ str ++ "."
_ -> return () _ -> return ()
else case path of
-- breaking at a source/function invocation is an abomination. Let's ignore it.
h:_ | isFunction h -> err (getId t) 2104 $ "In functions, use return instead of " ++ name ++ "."
_ -> err (getId t) 2105 $ name ++ " is only valid in loops."
where where
name = getCommandName t path = let p = getPath (parentMap params) t in NE.filter relevant p
path = let p = getPath (parentMap params) t in filter relevant p
subshellType t = case leadType params t of subshellType t = case leadType params t of
NoneScope -> Nothing NoneScope -> Nothing
SubshellScope str -> return str SubshellScope str -> return str
@@ -2782,6 +2780,7 @@ checkFunctionDeclarations params
when (hasKeyword && hasParens) $ when (hasKeyword && hasParens) $
err id 2111 "ksh does not allow 'function' keyword and '()' at the same time." err id 2111 "ksh does not allow 'function' keyword and '()' at the same time."
Dash -> forSh Dash -> forSh
BusyboxSh -> forSh
Sh -> forSh Sh -> forSh
where where
@@ -2823,13 +2822,11 @@ checkUnpassedInFunctions params root =
execWriter $ mapM_ warnForGroup referenceGroups execWriter $ mapM_ warnForGroup referenceGroups
where where
functionMap :: Map.Map String Token functionMap :: Map.Map String Token
functionMap = Map.fromList $ functionMap = Map.fromList $ execWriter $ doAnalysis (tell . maybeToList . findFunction) root
map (\t@(T_Function _ _ _ name _) -> (name,t)) functions
functions = execWriter $ doAnalysis (tell . maybeToList . findFunction) root
findFunction t@(T_Function id _ _ name body) findFunction t@(T_Function id _ _ name body)
| any (isPositionalReference t) flow && not (any isPositionalAssignment flow) | any (isPositionalReference t) flow && not (any isPositionalAssignment flow)
= return t = return (name,t)
where flow = getVariableFlow params body where flow = getVariableFlow params body
findFunction _ = Nothing findFunction _ = Nothing
@@ -3217,7 +3214,7 @@ checkLoopVariableReassignment params token =
return $ do return $ do
warn (getId token) 2165 "This nested loop overrides the index variable of its parent." warn (getId token) 2165 "This nested loop overrides the index variable of its parent."
warn (getId next) 2167 "This parent loop has its index variable overridden." warn (getId next) 2167 "This parent loop has its index variable overridden."
path = drop 1 $ getPath (parentMap params) token path = NE.tail $ getPath (parentMap params) token
loopVariable :: Token -> Maybe String loopVariable :: Token -> Maybe String
loopVariable t = loopVariable t =
case t of case t of
@@ -3290,17 +3287,17 @@ checkReturnAgainstZero params token =
-- We don't want to warn about composite expressions like -- We don't want to warn about composite expressions like
-- [[ $? -eq 0 || $? -eq 4 ]] since these can be annoying to rewrite. -- [[ $? -eq 0 || $? -eq 4 ]] since these can be annoying to rewrite.
isOnlyTestInCommand t = isOnlyTestInCommand t =
case getPath (parentMap params) t of case NE.tail $ getPath (parentMap params) t of
_:(T_Condition {}):_ -> True (T_Condition {}):_ -> True
_:(T_Arithmetic {}):_ -> True (T_Arithmetic {}):_ -> True
_:(TA_Sequence _ [_]):(T_Arithmetic {}):_ -> True (TA_Sequence _ [_]):(T_Arithmetic {}):_ -> True
-- Some negations and groupings are also fine -- Some negations and groupings are also fine
_:next@(TC_Unary _ _ "!" _):_ -> isOnlyTestInCommand next next@(TC_Unary _ _ "!" _):_ -> isOnlyTestInCommand next
_: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_Parentesis _ _):_ -> isOnlyTestInCommand next
_ -> False _ -> False
-- TODO: Do better $? tracking and filter on whether -- TODO: Do better $? tracking and filter on whether
@@ -3320,7 +3317,7 @@ checkReturnAgainstZero params token =
isFirstCommandInFunction = fromMaybe False $ do isFirstCommandInFunction = fromMaybe False $ do
let path = getPath (parentMap params) token let path = getPath (parentMap params) token
func <- listToMaybe $ filter isFunction path func <- find isFunction path
cmd <- getClosestCommand (parentMap params) token cmd <- getClosestCommand (parentMap params) token
return $ getId cmd == getId (getFirstCommandInFunction func) return $ getId cmd == getId (getFirstCommandInFunction func)
@@ -3365,7 +3362,7 @@ checkRedirectedNowhere params token =
_ -> return () _ -> return ()
where where
isInExpansion t = isInExpansion t =
case drop 1 $ getPath (parentMap params) t of case NE.tail $ getPath (parentMap params) t of
T_DollarExpansion _ [_] : _ -> True T_DollarExpansion _ [_] : _ -> True
T_Backticked _ [_] : _ -> True T_Backticked _ [_] : _ -> True
t@T_Annotation {} : _ -> isInExpansion t t@T_Annotation {} : _ -> isInExpansion t
@@ -3839,7 +3836,7 @@ checkSubshelledTests params t =
isFunctionBody path = isFunctionBody path =
case path of case path of
(_:f:_) -> isFunction f (_ NE.:| f:_) -> isFunction f
_ -> False _ -> False
isTestStructure t = isTestStructure t =
@@ -3866,7 +3863,7 @@ checkSubshelledTests params t =
-- This technically also triggers for `if true; then ( test ); fi` -- This technically also triggers for `if true; then ( test ); fi`
-- but it's still a valid suggestion. -- but it's still a valid suggestion.
isCompoundCondition chain = isCompoundCondition chain =
case dropWhile skippable (drop 1 chain) of case dropWhile skippable (NE.tail chain) of
T_IfExpression {} : _ -> True T_IfExpression {} : _ -> True
T_WhileExpression {} : _ -> True T_WhileExpression {} : _ -> True
T_UntilExpression {} : _ -> True T_UntilExpression {} : _ -> True
@@ -4029,7 +4026,7 @@ checkUselessBang params t = when (hasSetE params) $ mapM_ check (getNonReturning
isFunctionBody t = isFunctionBody t =
case getPath (parentMap params) t of case getPath (parentMap params) t of
_:T_Function {}:_-> True _ NE.:| T_Function {}:_-> True
_ -> False _ -> False
dropLast t = dropLast t =
@@ -4044,7 +4041,8 @@ prop_checkModifiedArithmeticInRedirection3 = verifyNot checkModifiedArithmeticIn
prop_checkModifiedArithmeticInRedirection4 = verify checkModifiedArithmeticInRedirection "cat <<< $((i++))" prop_checkModifiedArithmeticInRedirection4 = verify checkModifiedArithmeticInRedirection "cat <<< $((i++))"
prop_checkModifiedArithmeticInRedirection5 = verify checkModifiedArithmeticInRedirection "cat << foo\n$((i++))\nfoo\n" prop_checkModifiedArithmeticInRedirection5 = verify checkModifiedArithmeticInRedirection "cat << foo\n$((i++))\nfoo\n"
prop_checkModifiedArithmeticInRedirection6 = verifyNot checkModifiedArithmeticInRedirection "#!/bin/dash\nls > $((i=i+1))" prop_checkModifiedArithmeticInRedirection6 = verifyNot checkModifiedArithmeticInRedirection "#!/bin/dash\nls > $((i=i+1))"
checkModifiedArithmeticInRedirection params t = unless (shellType params == Dash) $ prop_checkModifiedArithmeticInRedirection7 = verifyNot checkModifiedArithmeticInRedirection "#!/bin/busybox sh\ncat << foo\n$((i++))\nfoo\n"
checkModifiedArithmeticInRedirection params t = unless (shellType params == Dash || shellType params == BusyboxSh) $
case t of case t of
T_Redirecting _ redirs (T_SimpleCommand _ _ (_:_)) -> mapM_ checkRedirs redirs T_Redirecting _ redirs (T_SimpleCommand _ _ (_:_)) -> mapM_ checkRedirs redirs
_ -> return () _ -> return ()
@@ -4356,6 +4354,7 @@ checkEqualsInCommand params originalToken =
Bash -> errWithFix id 2277 "Use BASH_ARGV0 to assign to $0 in bash (or use [ ] to compare)." bashfix Bash -> errWithFix id 2277 "Use BASH_ARGV0 to assign to $0 in bash (or use [ ] to compare)." bashfix
Ksh -> err id 2278 "$0 can't be assigned in Ksh (but it does reflect the current function)." Ksh -> err id 2278 "$0 can't be assigned in Ksh (but it does reflect the current function)."
Dash -> err id 2279 "$0 can't be assigned in Dash. This becomes a command name." Dash -> err id 2279 "$0 can't be assigned in Dash. This becomes a command name."
BusyboxSh -> err id 2279 "$0 can't be assigned in Busybox Ash. This becomes a command name."
_ -> err id 2280 "$0 can't be assigned this way, and there is no portable alternative." _ -> err id 2280 "$0 can't be assigned this way, and there is no portable alternative."
leadingNumberMsg id = leadingNumberMsg id =
err id 2282 "Variable names can't start with numbers, so this is interpreted as a command." err id 2282 "Variable names can't start with numbers, so this is interpreted as a command."
@@ -4378,9 +4377,9 @@ checkEqualsInCommand params originalToken =
return $ isVariableName str return $ isVariableName str
isLeadingNumberVar s = isLeadingNumberVar s =
let lead = takeWhile (/= '=') s case takeWhile (/= '=') s of
in not (null lead) && isDigit (head lead) lead@(x:_) -> isDigit x && all isVariableChar lead && not (all isDigit lead)
&& all isVariableChar lead && not (all isDigit lead) [] -> False
msg cmd leading (T_Literal litId s) = do msg cmd leading (T_Literal litId s) = do
-- There are many different cases, and the order of the branches matter. -- There are many different cases, and the order of the branches matter.
@@ -4510,7 +4509,7 @@ prop_checkCommandWithTrailingSymbol9 = verifyNot checkCommandWithTrailingSymbol
checkCommandWithTrailingSymbol _ t = checkCommandWithTrailingSymbol _ t =
case t of case t of
T_SimpleCommand _ _ (cmd:_) -> T_SimpleCommand _ _ (cmd:_) ->
let str = fromJust $ getLiteralStringExt (\_ -> Just "x") cmd let str = getLiteralStringDef "x" cmd
last = lastOrDefault 'x' str last = lastOrDefault 'x' str
in in
case str of case str of
@@ -4625,7 +4624,8 @@ checkArrayValueUsedAsIndex params _ =
-- Is this one of the 'for' arrays? -- Is this one of the 'for' arrays?
(loopWord, _) <- find ((==arrayName) . snd) arrays (loopWord, _) <- find ((==arrayName) . snd) arrays
-- Are we still in this loop? -- Are we still in this loop?
guard $ getId loop `elem` map getId (getPath parents t) let loopId = getId loop
guard $ any (\t -> loopId == getId t) (getPath parents t)
return [ return [
makeComment WarningC (getId loopWord) 2302 "This loops over values. To loop over keys, use \"${!array[@]}\".", makeComment WarningC (getId loopWord) 2302 "This loops over values. To loop over keys, use \"${!array[@]}\".",
makeComment WarningC (getId arrayRef) 2303 $ (e4m name) ++ " is an array value, not a key. Use directly or loop over keys instead." makeComment WarningC (getId arrayRef) 2303 $ (e4m name) ++ " is an array value, not a key. Use directly or loop over keys instead."
@@ -4707,7 +4707,7 @@ checkSetESuppressed params t =
literalArg <- getUnquotedLiteral cmd literalArg <- getUnquotedLiteral cmd
Map.lookup literalArg functions_ Map.lookup literalArg functions_
checkCmd cmd = go $ getPath (parentMap params) cmd checkCmd cmd = go $ NE.toList $ getPath (parentMap params) cmd
where where
go (child:parent:rest) = do go (child:parent:rest) = do
case parent of case parent of
@@ -4821,15 +4821,15 @@ checkExtraMaskedReturns params t =
++ "separately to avoid masking its return value (or use '|| true' " ++ "separately to avoid masking its return value (or use '|| true' "
++ "to ignore).") ++ "to ignore).")
isMaskDeliberate t = hasParent isOrIf t isMaskDeliberate t = any isOrIf $ NE.init $ parents params t
where where
isOrIf _ (T_OrIf _ _ (T_Pipeline _ _ [T_Redirecting _ _ cmd])) isOrIf (T_OrIf _ _ (T_Pipeline _ _ [T_Redirecting _ _ cmd]))
= getCommandBasename cmd `elem` [Just "true", Just ":"] = getCommandBasename cmd `elem` [Just "true", Just ":"]
isOrIf _ _ = False isOrIf _ = False
isCheckedElsewhere t = hasParent isDeclaringCommand t isCheckedElsewhere t = any isDeclaringCommand $ NE.tail $ parents params t
where where
isDeclaringCommand t _ = fromMaybe False $ do isDeclaringCommand t = fromMaybe False $ do
cmd <- getCommand t cmd <- getCommand t
basename <- getCommandBasename cmd basename <- getCommandBasename cmd
return $ return $
@@ -4849,16 +4849,7 @@ checkExtraMaskedReturns params t =
,"shopt" ,"shopt"
] ]
isTransparentCommand t = fromMaybe False $ do isTransparentCommand t = getCommandBasename t == Just "time"
basename <- getCommandBasename t
return $ basename == "time"
parentChildPairs t = go $ parents params t
where
go (child:parent:rest) = (parent, child):go (parent:rest)
go _ = []
hasParent pred t = any (uncurry pred) (parentChildPairs t)
-- hard error on negated command that is not last -- hard error on negated command that is not last
@@ -4907,7 +4898,8 @@ prop_checkCommandIsUnreachable3 = verifyNot checkCommandIsUnreachable "foo; bar
checkCommandIsUnreachable params t = checkCommandIsUnreachable params t =
case t of case t of
T_Pipeline {} -> sequence_ $ do T_Pipeline {} -> sequence_ $ do
state <- CF.getIncomingState (cfgAnalysis params) id cfga <- cfgAnalysis params
state <- CF.getIncomingState cfga id
guard . not $ CF.stateIsReachable state guard . not $ CF.stateIsReachable state
guard . not $ isSourced params t guard . not $ isSourced params t
return $ info id 2317 "Command appears to be unreachable. Check usage (or ignore if invoked indirectly)." return $ info id 2317 "Command appears to be unreachable. Check usage (or ignore if invoked indirectly)."
@@ -4929,14 +4921,15 @@ checkOverwrittenExitCode params t =
_ -> return () _ -> return ()
where where
check id = sequence_ $ do check id = sequence_ $ do
state <- CF.getIncomingState (cfgAnalysis params) id cfga <- cfgAnalysis params
state <- CF.getIncomingState cfga id
let exitCodeIds = CF.exitCodes state let exitCodeIds = CF.exitCodes state
guard . not $ S.null exitCodeIds guard . not $ S.null exitCodeIds
let idToToken = idMap params let idToToken = idMap params
exitCodeTokens <- sequence $ map (\k -> Map.lookup k idToToken) $ S.toList exitCodeIds exitCodeTokens <- traverse (\k -> Map.lookup k idToToken) $ S.toList exitCodeIds
return $ do return $ do
when (all isCondition exitCodeTokens && not (usedUnconditionally t exitCodeIds)) $ when (all isCondition exitCodeTokens && not (usedUnconditionally cfga t exitCodeIds)) $
warn id 2319 "This $? refers to a condition, not a command. Assign to a variable to avoid it being overwritten." warn id 2319 "This $? refers to a condition, not a command. Assign to a variable to avoid it being overwritten."
when (all isPrinting exitCodeTokens) $ when (all isPrinting exitCodeTokens) $
warn id 2320 "This $? refers to echo/printf, not a previous command. Assign to variable to avoid it being overwritten." warn id 2320 "This $? refers to echo/printf, not a previous command. Assign to variable to avoid it being overwritten."
@@ -4949,8 +4942,8 @@ checkOverwrittenExitCode params t =
-- If we don't do anything based on the condition, assume we wanted the condition itself -- If we don't do anything based on the condition, assume we wanted the condition itself
-- This helps differentiate `x; [ $? -gt 0 ] && exit $?` vs `[ cond ]; exit $?` -- This helps differentiate `x; [ $? -gt 0 ] && exit $?` vs `[ cond ]; exit $?`
usedUnconditionally t testIds = usedUnconditionally cfga t testIds =
all (\c -> CF.doesPostDominate (cfgAnalysis params) (getId t) c) testIds all (\c -> CF.doesPostDominate cfga (getId t) c) testIds
isPrinting t = isPrinting t =
case getCommandBasename t of case getCommandBasename t of
@@ -5020,7 +5013,8 @@ prop_checkPlusEqualsNumber9 = verifyNot checkPlusEqualsNumber "declare -ia var;
checkPlusEqualsNumber params t = checkPlusEqualsNumber params t =
case t of case t of
T_Assignment id Append var _ word -> sequence_ $ do T_Assignment id Append var _ word -> sequence_ $ do
state <- CF.getIncomingState (cfgAnalysis params) id cfga <- cfgAnalysis params
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." return $ warn id 2324 "var+=1 will append, not increment. Use (( var += 1 )), declare -i var, or quote number to silence."
@@ -5031,7 +5025,7 @@ checkPlusEqualsNumber params t =
let let
unquotedLiteral = getUnquotedLiteral word unquotedLiteral = getUnquotedLiteral word
isEmpty = unquotedLiteral == Just "" isEmpty = unquotedLiteral == Just ""
isUnquotedNumber = not isEmpty && fromMaybe False (all isDigit <$> unquotedLiteral) isUnquotedNumber = not isEmpty && maybe False (all isDigit) unquotedLiteral
isNumericalVariableName = fromMaybe False $ do isNumericalVariableName = fromMaybe False $ do
str <- unquotedLiteral str <- unquotedLiteral
CF.variableMayBeAssignedInteger state str CF.variableMayBeAssignedInteger state str

View File

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

View File

@@ -51,6 +51,7 @@ import Control.Monad.Identity
import Data.Array.Unboxed import Data.Array.Unboxed
import Data.Array.ST import Data.Array.ST
import Data.List hiding (map) import Data.List hiding (map)
import qualified Data.List.NonEmpty as NE
import Data.Maybe import Data.Maybe
import qualified Data.Map as M import qualified Data.Map as M
import qualified Data.Set as S import qualified Data.Set as S
@@ -111,8 +112,8 @@ data CFEdge =
-- Actions we track -- Actions we track
data CFEffect = data CFEffect =
CFSetProps Scope String (S.Set CFVariableProp) CFSetProps (Maybe Scope) String (S.Set CFVariableProp)
| CFUnsetProps Scope String (S.Set CFVariableProp) | CFUnsetProps (Maybe Scope) String (S.Set CFVariableProp)
| CFReadVariable String | CFReadVariable String
| CFWriteVariable String CFValue | CFWriteVariable String CFValue
| CFWriteGlobal String CFValue | CFWriteGlobal String CFValue
@@ -578,7 +579,7 @@ build t = do
T_Array _ list -> sequentially list T_Array _ list -> sequentially list
T_Assignment {} -> buildAssignment DefaultScope t T_Assignment {} -> buildAssignment Nothing t
T_Backgrounded id body -> do T_Backgrounded id body -> do
start <- newStructuralNode start <- newStructuralNode
@@ -614,15 +615,15 @@ build t = do
T_CaseExpression id t [] -> build t T_CaseExpression id t [] -> build t
T_CaseExpression id t list -> do T_CaseExpression id t list@(hd:tl) -> do
start <- newStructuralNode start <- newStructuralNode
token <- build t token <- build t
branches <- mapM buildBranch list branches <- mapM buildBranch (hd NE.:| tl)
end <- newStructuralNode end <- newStructuralNode
let neighbors = zip branches $ tail branches let neighbors = zip (NE.toList branches) $ NE.tail branches
let (_, firstCond, _) = head branches let (_, firstCond, _) = NE.head branches
let (_, lastCond, lastBody) = last branches let (_, lastCond, lastBody) = NE.last branches
linkRange start token linkRange start token
linkRange token firstCond linkRange token firstCond
@@ -857,8 +858,8 @@ build t = do
status <- newNodeRange (CFSetExitCode id) status <- newNodeRange (CFSetExitCode id)
linkRange assignments status linkRange assignments status
T_SimpleCommand id vars list@(cmd:_) -> T_SimpleCommand id vars (cmd:args) ->
handleCommand t vars list $ getUnquotedLiteral cmd handleCommand t vars (cmd NE.:| args) $ getUnquotedLiteral cmd
T_SingleQuoted _ _ -> none T_SingleQuoted _ _ -> none
@@ -887,7 +888,9 @@ build t = do
T_Less _ -> none T_Less _ -> none
T_ParamSubSpecialChar _ _ -> none T_ParamSubSpecialChar _ _ -> none
x -> error ("Unimplemented: " ++ show x) x -> do
error ("Unimplemented: " ++ show x) -- STRIP
none
-- Still in `where` clause -- Still in `where` clause
forInHelper id name words body = do forInHelper id name words body = do
@@ -923,8 +926,8 @@ handleCommand cmd vars args literalCmd = do
-- TODO: Handle assignments in declaring commands -- TODO: Handle assignments in declaring commands
case literalCmd of case literalCmd of
Just "exit" -> regularExpansion vars args $ handleExit Just "exit" -> regularExpansion vars (NE.toList args) $ handleExit
Just "return" -> regularExpansion vars args $ handleReturn Just "return" -> regularExpansion vars (NE.toList args) $ handleReturn
Just "unset" -> regularExpansionWithStatus vars args $ handleUnset args Just "unset" -> regularExpansionWithStatus vars args $ handleUnset args
Just "declare" -> handleDeclare args Just "declare" -> handleDeclare args
@@ -947,14 +950,14 @@ handleCommand cmd vars args literalCmd = do
-- This will mostly behave like 'command' but ok -- This will mostly behave like 'command' but ok
Just "builtin" -> Just "builtin" ->
case args of case args of
[_] -> regular _ NE.:| [] -> regular
(_:newargs@(newcmd:_)) -> (_ NE.:| newcmd:newargs) ->
handleCommand newcmd vars newargs $ getLiteralString newcmd handleCommand newcmd vars (newcmd NE.:| newargs) $ getLiteralString newcmd
Just "command" -> Just "command" ->
case args of case args of
[_] -> regular _ NE.:| [] -> regular
(_:newargs@(newcmd:_)) -> (_ NE.:| newcmd:newargs) ->
handleOthers (getId newcmd) vars newargs $ getLiteralString newcmd handleOthers (getId newcmd) vars (newcmd NE.:| newargs) $ getLiteralString newcmd
_ -> regular _ -> regular
where where
@@ -982,7 +985,7 @@ handleCommand cmd vars args literalCmd = do
unreachable <- newNode CFUnreachable unreachable <- newNode CFUnreachable
return $ Range ret unreachable return $ Range ret unreachable
handleUnset (cmd:args) = do handleUnset (cmd NE.:| args) = do
case () of case () of
_ | "n" `elem` flagNames -> unsetWith CFUndefineNameref _ | "n" `elem` flagNames -> unsetWith CFUndefineNameref
_ | "v" `elem` flagNames -> unsetWith CFUndefineVariable _ | "v" `elem` flagNames -> unsetWith CFUndefineVariable
@@ -994,14 +997,14 @@ handleCommand cmd vars args literalCmd = do
(names, flags) = partition (null . fst) pairs (names, flags) = partition (null . fst) pairs
flagNames = map fst flags flagNames = map fst flags
literalNames :: [(Token, String)] -- Literal names to unset, e.g. [(myfuncToken, "myfunc")] literalNames :: [(Token, String)] -- Literal names to unset, e.g. [(myfuncToken, "myfunc")]
literalNames = mapMaybe (\(_, t) -> getLiteralString t >>= (return . (,) t)) names literalNames = mapMaybe (\(_, t) -> (,) t <$> getLiteralString t) names
-- Apply a constructor like CFUndefineVariable to each literalName, and tag with its id -- Apply a constructor like CFUndefineVariable to each literalName, and tag with its id
unsetWith c = newNodeRange $ CFApplyEffects $ map (\(token, name) -> IdTagged (getId token) $ c name) literalNames unsetWith c = newNodeRange $ CFApplyEffects $ map (\(token, name) -> IdTagged (getId token) $ c name) literalNames
variableAssignRegex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)=" variableAssignRegex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)="
handleDeclare (cmd:args) = do handleDeclare (cmd NE.:| args) = do
isFunc <- asks cfIsFunction isFunc <- asks cfIsFunction
-- This is a bit of a kludge: we don't have great support for things like -- This is a bit of a kludge: we don't have great support for things like
-- 'declare -i x=$x' so do one round with declare x=$x, followed by declare -i x -- 'declare -i x=$x' so do one round with declare x=$x, followed by declare -i x
@@ -1028,9 +1031,9 @@ handleCommand cmd vars args literalCmd = do
scope isFunc = scope isFunc =
case () of case () of
_ | global -> GlobalScope _ | global -> Just GlobalScope
_ | isFunc -> LocalScope _ | isFunc -> Just LocalScope
_ -> DefaultScope _ -> Nothing
addedProps = S.fromList $ concat $ [ addedProps = S.fromList $ concat $ [
[ CFVPArray | array ], [ CFVPArray | array ],
@@ -1058,7 +1061,7 @@ handleCommand cmd vars args literalCmd = do
let let
id = getId t id = getId t
pre = [t] pre = [t]
literal = fromJust $ getLiteralStringExt (const $ Just "\0") t literal = getLiteralStringDef "\0" t
isKnown = '\0' `notElem` literal isKnown = '\0' `notElem` literal
match = fmap head $ variableAssignRegex `matchRegex` literal match = fmap head $ variableAssignRegex `matchRegex` literal
name = fromMaybe literal match name = fromMaybe literal match
@@ -1090,7 +1093,7 @@ handleCommand cmd vars args literalCmd = do
in in
concatMap (drop 1) plusses concatMap (drop 1) plusses
handlePrintf (cmd:args) = handlePrintf (cmd NE.:| args) =
newNodeRange $ CFApplyEffects $ maybeToList findVar newNodeRange $ CFApplyEffects $ maybeToList findVar
where where
findVar = do findVar = do
@@ -1099,7 +1102,7 @@ handleCommand cmd vars args literalCmd = do
name <- getLiteralString arg name <- getLiteralString arg
return $ IdTagged (getId arg) $ CFWriteVariable name CFValueString return $ IdTagged (getId arg) $ CFWriteVariable name CFValueString
handleWait (cmd:args) = handleWait (cmd NE.:| args) =
newNodeRange $ CFApplyEffects $ maybeToList findVar newNodeRange $ CFApplyEffects $ maybeToList findVar
where where
findVar = do findVar = do
@@ -1108,7 +1111,7 @@ handleCommand cmd vars args literalCmd = do
name <- getLiteralString arg name <- getLiteralString arg
return $ IdTagged (getId arg) $ CFWriteVariable name CFValueInteger return $ IdTagged (getId arg) $ CFWriteVariable name CFValueInteger
handleMapfile (cmd:args) = handleMapfile (cmd NE.:| args) =
newNodeRange $ CFApplyEffects [findVar] newNodeRange $ CFApplyEffects [findVar]
where where
findVar = findVar =
@@ -1128,7 +1131,7 @@ handleCommand cmd vars args literalCmd = do
guard $ isVariableName name guard $ isVariableName name
return (getId c, name) return (getId c, name)
handleRead (cmd:args) = newNodeRange $ CFApplyEffects main handleRead (cmd NE.:| args) = newNodeRange $ CFApplyEffects main
where where
main = fromMaybe fallback $ do main = fromMaybe fallback $ do
flags <- getGnuOpts flagsForRead args flags <- getGnuOpts flagsForRead args
@@ -1158,7 +1161,7 @@ handleCommand cmd vars args literalCmd = do
in in
map (\(id, name) -> IdTagged id $ CFWriteVariable name value) namesOrDefault map (\(id, name) -> IdTagged id $ CFWriteVariable name value) namesOrDefault
handleDEFINE (cmd:args) = handleDEFINE (cmd NE.:| args) =
newNodeRange $ CFApplyEffects $ maybeToList findVar newNodeRange $ CFApplyEffects $ maybeToList findVar
where where
findVar = do findVar = do
@@ -1168,14 +1171,14 @@ handleCommand cmd vars args literalCmd = do
return $ IdTagged (getId name) $ CFWriteVariable str CFValueString return $ IdTagged (getId name) $ CFWriteVariable str CFValueString
handleOthers id vars args cmd = handleOthers id vars args cmd =
regularExpansion vars args $ do regularExpansion vars (NE.toList args) $ do
exe <- newNodeRange $ CFExecuteCommand cmd exe <- newNodeRange $ CFExecuteCommand cmd
status <- newNodeRange $ CFSetExitCode id status <- newNodeRange $ CFSetExitCode id
linkRange exe status linkRange exe status
regularExpansion vars args p = do regularExpansion vars args p = do
args <- sequentially args args <- sequentially args
assignments <- mapM (buildAssignment PrefixScope) vars assignments <- mapM (buildAssignment (Just PrefixScope)) vars
exe <- p exe <- p
dropAssignments <- dropAssignments <-
if null vars if null vars
@@ -1187,15 +1190,15 @@ handleCommand cmd vars args literalCmd = do
linkRanges $ [args] ++ assignments ++ [exe] ++ dropAssignments linkRanges $ [args] ++ assignments ++ [exe] ++ dropAssignments
regularExpansionWithStatus vars args@(cmd:_) p = do regularExpansionWithStatus vars args@(cmd NE.:| _) p = do
initial <- regularExpansion vars args p initial <- regularExpansion vars (NE.toList args) p
status <- newNodeRange $ CFSetExitCode (getId cmd) status <- newNodeRange $ CFSetExitCode (getId cmd)
linkRange initial status linkRange initial status
none = newStructuralNode none = newStructuralNode
data Scope = DefaultScope | GlobalScope | LocalScope | PrefixScope data Scope = GlobalScope | LocalScope | PrefixScope
deriving (Eq, Ord, Show, Generic, NFData) deriving (Eq, Ord, Show, Generic, NFData)
buildAssignment scope t = do buildAssignment scope t = do
@@ -1209,10 +1212,10 @@ buildAssignment scope t = do
let valueType = if null indices then f id value else CFValueArray let valueType = if null indices then f id value else CFValueArray
let scoper = let scoper =
case scope of case scope of
PrefixScope -> CFWritePrefix Just PrefixScope -> CFWritePrefix
LocalScope -> CFWriteLocal Just LocalScope -> CFWriteLocal
GlobalScope -> CFWriteGlobal Just GlobalScope -> CFWriteGlobal
DefaultScope -> CFWriteVariable Nothing -> CFWriteVariable
write <- newNodeRange $ applySingle $ IdTagged id $ scoper var valueType write <- newNodeRange $ applySingle $ IdTagged id $ scoper var valueType
linkRanges [expand, index, read, write] linkRanges [expand, index, read, write]
where where

View File

@@ -299,7 +299,6 @@ depsToState set = foldl insert newInternalState $ S.toList set
PrefixScope -> (sPrefixValues, insertPrefix) PrefixScope -> (sPrefixValues, insertPrefix)
LocalScope -> (sLocalValues, insertLocal) LocalScope -> (sLocalValues, insertLocal)
GlobalScope -> (sGlobalValues, insertGlobal) GlobalScope -> (sGlobalValues, insertGlobal)
DefaultScope -> error $ pleaseReport "Unresolved scope in dependency"
alreadyExists = isJust $ vmLookup name $ mapToCheck state alreadyExists = isJust $ vmLookup name $ mapToCheck state
in in
@@ -830,7 +829,7 @@ lookupStack' functionOnly get dep def ctx key = do
f (s:rest) = do f (s:rest) = do
-- Go up the stack until we find the value, and add -- Go up the stack until we find the value, and add
-- a dependency on each state (including where it was found) -- a dependency on each state (including where it was found)
res <- fromMaybe (f rest) (return <$> get (stackState s) key) res <- maybe (f rest) return (get (stackState s) key)
modifySTRef (dependencies s) $ S.insert $ dep key res modifySTRef (dependencies s) $ S.insert $ dep key res
return res return res
@@ -1120,34 +1119,34 @@ transferEffect ctx effect =
CFSetProps scope name props -> CFSetProps scope name props ->
case scope of case scope of
DefaultScope -> do Nothing -> do
state <- readVariable ctx name state <- readVariable ctx name
writeVariable ctx name $ addProperties props state writeVariable ctx name $ addProperties props state
GlobalScope -> do Just GlobalScope -> do
state <- readGlobal ctx name state <- readGlobal ctx name
writeGlobal ctx name $ addProperties props state writeGlobal ctx name $ addProperties props state
LocalScope -> do Just LocalScope -> do
out <- readSTRef (cOutput ctx) out <- readSTRef (cOutput ctx)
state <- readLocal ctx name state <- readLocal ctx name
writeLocal ctx name $ addProperties props state writeLocal ctx name $ addProperties props state
PrefixScope -> do Just PrefixScope -> do
-- Prefix values become local -- Prefix values become local
state <- readLocal ctx name state <- readLocal ctx name
writeLocal ctx name $ addProperties props state writeLocal ctx name $ addProperties props state
CFUnsetProps scope name props -> CFUnsetProps scope name props ->
case scope of case scope of
DefaultScope -> do Nothing -> do
state <- readVariable ctx name state <- readVariable ctx name
writeVariable ctx name $ removeProperties props state writeVariable ctx name $ removeProperties props state
GlobalScope -> do Just GlobalScope -> do
state <- readGlobal ctx name state <- readGlobal ctx name
writeGlobal ctx name $ removeProperties props state writeGlobal ctx name $ removeProperties props state
LocalScope -> do Just LocalScope -> do
out <- readSTRef (cOutput ctx) out <- readSTRef (cOutput ctx)
state <- readLocal ctx name state <- readLocal ctx name
writeLocal ctx name $ removeProperties props state writeLocal ctx name $ removeProperties props state
PrefixScope -> do Just PrefixScope -> do
-- Prefix values become local -- Prefix values become local
state <- readLocal ctx name state <- readLocal ctx name
writeLocal ctx name $ removeProperties props state writeLocal ctx name $ removeProperties props state

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@
-} -}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ViewPatterns #-}
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
import ShellCheck.AST import ShellCheck.AST
@@ -75,12 +76,11 @@ verifyNot c s = producesComments (testChecker c) s == Just False
prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))" prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar" prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar" prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
checkForDecimals = ForShell [Sh, Dash, Bash] f checkForDecimals = ForShell [Sh, Dash, BusyboxSh, Bash] f
where where
f t@(TA_Expansion id _) = sequence_ $ do f t@(TA_Expansion id _) = sequence_ $ do
str <- getLiteralString t first:rest <- getLiteralString t
first <- str !!! 0 guard $ isDigit first && '.' `elem` rest
guard $ isDigit first && '.' `elem` str
return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk." return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
f _ = return () f _ = return ()
@@ -91,6 +91,9 @@ prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)" prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
prop_checkBashisms5 = verify checkBashisms "source file" prop_checkBashisms5 = verify checkBashisms "source file"
prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]" prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
prop_checkBashisms6b = verify checkBashisms "test \"$a\" == 42"
prop_checkBashisms6c = verify checkBashisms "[ foo =~ bar ]"
prop_checkBashisms6d = verify checkBashisms "test foo =~ bar"
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}" prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}" prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}" prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
@@ -106,6 +109,7 @@ prop_checkBashisms18 = verify checkBashisms "foo &> /dev/null"
prop_checkBashisms19 = verify checkBashisms "foo > file*.txt" prop_checkBashisms19 = verify checkBashisms "foo > file*.txt"
prop_checkBashisms20 = verify checkBashisms "read -ra foo" prop_checkBashisms20 = verify checkBashisms "read -ra foo"
prop_checkBashisms21 = verify checkBashisms "[ -a foo ]" prop_checkBashisms21 = verify checkBashisms "[ -a foo ]"
prop_checkBashisms21b = verify checkBashisms "test -a foo"
prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]" prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]"
prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT" prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT"
prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM" prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM"
@@ -191,18 +195,37 @@ prop_checkBashisms101 = verify checkBashisms "read"
prop_checkBashisms102 = verifyNot checkBashisms "read -r foo" prop_checkBashisms102 = verifyNot checkBashisms "read -r foo"
prop_checkBashisms103 = verifyNot checkBashisms "read foo" prop_checkBashisms103 = verifyNot checkBashisms "read foo"
prop_checkBashisms104 = verifyNot checkBashisms "read ''" prop_checkBashisms104 = verifyNot checkBashisms "read ''"
checkBashisms = ForShell [Sh, Dash] $ \t -> do prop_checkBashisms105 = verifyNot checkBashisms "#!/bin/busybox sh\nset -o pipefail"
prop_checkBashisms106 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[[ \"$x\" = \"$x\" ]]"
prop_checkBashisms107 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[ \"$x\" == \"$x\" ]"
prop_checkBashisms108 = verifyNot checkBashisms "#!/bin/busybox sh\necho magic &> /dev/null"
prop_checkBashisms109 = verifyNot checkBashisms "#!/bin/busybox sh\ntrap stop EXIT SIGTERM"
prop_checkBashisms110 = verifyNot checkBashisms "#!/bin/busybox sh\nsource /dev/null"
prop_checkBashisms111 = verify checkBashisms "#!/bin/dash\nx='test'\n${x:0:3}" -- SC3057
prop_checkBashisms112 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x:0:3}" -- SC3057
prop_checkBashisms113 = verify checkBashisms "#!/bin/dash\nx='test'\n${x/st/xt}" -- SC3060
prop_checkBashisms114 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x/st/xt}" -- SC3060
prop_checkBashisms115 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x}" -- SC3053
prop_checkBashisms116 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x[1]}" -- SC3054
prop_checkBashisms117 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x[@]}" -- SC3055
prop_checkBashisms118 = verify checkBashisms "#!/bin/busybox sh\nxyz=1\n${!x*}" -- SC3056
prop_checkBashisms119 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x^^[t]}" -- SC3059
prop_checkBashisms120 = verify checkBashisms "#!/bin/sh\n[ x == y ]"
prop_checkBashisms121 = verifyNot checkBashisms "#!/bin/sh\n# shellcheck shell=busybox\n[ x == y ]"
checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
params <- ask params <- ask
kludge params t kludge params t
where where
-- This code was copy-pasted from Analytics where params was a variable -- This code was copy-pasted from Analytics where params was a variable
kludge params = bashism kludge params = bashism
where where
isDash = shellType params == Dash isBusyboxSh = shellType params == BusyboxSh
isDash = shellType params == Dash || isBusyboxSh
warnMsg id code s = warnMsg id code s =
if isDash if isDash
then err id code $ "In dash, " ++ s ++ " not supported." then err id code $ "In dash, " ++ s ++ " not supported."
else warn id code $ "In POSIX sh, " ++ s ++ " undefined." else warn id code $ "In POSIX sh, " ++ s ++ " undefined."
asStr = getLiteralString
bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is" bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is"
bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is" bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is"
@@ -213,27 +236,43 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
bashism (T_DollarBracket id _) = warnMsg id 3007 "$[..] in place of $((..)) is" bashism (T_DollarBracket id _) = warnMsg id 3007 "$[..] in place of $((..)) is"
bashism (T_SelectIn id _ _ _) = warnMsg id 3008 "select loops are" bashism (T_SelectIn id _ _ _) = warnMsg id 3008 "select loops are"
bashism (T_BraceExpansion id _) = warnMsg id 3009 "brace expansion is" bashism (T_BraceExpansion id _) = warnMsg id 3009 "brace expansion is"
bashism (T_Condition id DoubleBracket _) = warnMsg id 3010 "[[ ]] is" bashism (T_Condition id DoubleBracket _) =
unless isBusyboxSh $ warnMsg id 3010 "[[ ]] is"
bashism (T_HereString id _) = warnMsg id 3011 "here-strings are" bashism (T_HereString id _) = warnMsg id 3011 "here-strings are"
bashism (TC_Binary id SingleBracket op _ _) bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] = | op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is" unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is"
bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs])
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is"
bashism (TC_Binary id SingleBracket op _ _) bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "-ot", "-nt", "-ef" ] = | op `elem` [ "-ot", "-nt", "-ef" ] =
unless isDash $ warnMsg id 3013 $ op ++ " is" unless isDash $ warnMsg id 3013 $ op ++ " is"
bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs])
| op `elem` [ "-ot", "-nt", "-ef" ] =
unless isDash $ warnMsg id 3013 $ op ++ " is"
bashism (TC_Binary id SingleBracket "==" _ _) = bashism (TC_Binary id SingleBracket "==" _ _) =
warnMsg id 3014 "== in place of = is" 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 "=~" _ _) = bashism (TC_Binary id SingleBracket "=~" _ _) =
warnMsg id 3015 "=~ regex matching is" 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" _) = bashism (TC_Unary id SingleBracket "-v" _) =
warnMsg id 3016 "unary -v (in place of [ -n \"${var+x}\" ]) is" 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" _) = bashism (TC_Unary id _ "-a" _) =
warnMsg id 3017 "unary -a in place of -e is" 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"
bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are" bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id 3020 "&> is" bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) =
unless isBusyboxSh $ warnMsg id 3020 "&> is"
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) file)) = bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) file)) =
unless (all isDigit $ onlyLiteralString file) $ warnMsg id 3021 ">& filename (as opposed to >& fd) is" unless (all isDigit $ onlyLiteralString file) $ warnMsg id 3021 ">& filename (as opposed to >& fd) is"
bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are" bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are"
@@ -253,7 +292,8 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
warnMsg id 3028 $ str ++ " is" warnMsg id 3028 $ str ++ " is"
bashism t@(T_DollarBraced id _ token) = do bashism t@(T_DollarBraced id _ token) = do
mapM_ check expansion unless isBusyboxSh $ mapM_ check simpleExpansions
mapM_ check advancedExpansions
when (isBashVariable var) $ when (isBashVariable var) $
warnMsg id 3028 $ var ++ " is" warnMsg id 3028 $ var ++ " is"
where where
@@ -363,7 +403,8 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
(\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is" return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is"
when (name == "source") $ warnMsg id 3046 "'source' in place of '.' is" when (name == "source" && not isBusyboxSh) $
warnMsg id 3046 "'source' in place of '.' is"
when (name == "trap") $ when (name == "trap") $
let let
check token = sequence_ $ do check token = sequence_ $ do
@@ -372,7 +413,7 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
return $ do return $ do
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $ when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is" warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is"
when ("SIG" `isPrefixOf` upper) $ when (not isBusyboxSh && "SIG" `isPrefixOf` upper) $
warnMsg (getId token) 3048 warnMsg (getId token) 3048
"prefixing signal names with 'SIG' is" "prefixing signal names with 'SIG' is"
when (not isDash && upper /= str) $ when (not isDash && upper /= str) $
@@ -412,7 +453,9 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
("wait", Just []) ("wait", Just [])
] ]
bashism t@(T_SourceCommand id src _) bashism t@(T_SourceCommand id src _)
| getCommandName src == Just "source" = warnMsg id 3051 "'source' in place of '.' is" | getCommandName src == Just "source" =
unless isBusyboxSh $
warnMsg id 3051 "'source' in place of '.' is"
bashism (TA_Expansion _ (T_Literal id str : _)) bashism (TA_Expansion _ (T_Literal id str : _))
| str `matches` radix = warnMsg id 3052 "arithmetic base conversion is" | str `matches` radix = warnMsg id 3052 "arithmetic base conversion is"
where where
@@ -420,14 +463,16 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
bashism _ = return () bashism _ = return ()
varChars="_0-9a-zA-Z" varChars="_0-9a-zA-Z"
expansion = let re = mkRegex in [ advancedExpansions = let re = mkRegex in [
(re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"), (re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"),
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"), (re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"),
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"), (re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"),
(re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"), (re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is")
]
simpleExpansions = let re = mkRegex in [
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", 3057, "string indexing is"), (re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", 3057, "string indexing is"),
(re $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"), (re $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is") (re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is")
] ]
bashVars = [ bashVars = [
@@ -570,7 +615,7 @@ checkPS1Assignments = ForShell [Bash] f
prop_checkMultipleBangs1 = verify checkMultipleBangs "! ! true" prop_checkMultipleBangs1 = verify checkMultipleBangs "! ! true"
prop_checkMultipleBangs2 = verifyNot checkMultipleBangs "! true" prop_checkMultipleBangs2 = verifyNot checkMultipleBangs "! true"
checkMultipleBangs = ForShell [Dash, Sh] f checkMultipleBangs = ForShell [Dash, BusyboxSh, Sh] f
where where
f token = case token of f token = case token of
T_Banged id (T_Banged _ _) -> T_Banged id (T_Banged _ _) ->
@@ -581,7 +626,7 @@ checkMultipleBangs = ForShell [Dash, Sh] f
prop_checkBangAfterPipe1 = verify checkBangAfterPipe "true | ! true" prop_checkBangAfterPipe1 = verify checkBangAfterPipe "true | ! true"
prop_checkBangAfterPipe2 = verifyNot checkBangAfterPipe "true | ( ! true )" prop_checkBangAfterPipe2 = verifyNot checkBangAfterPipe "true | ( ! true )"
prop_checkBangAfterPipe3 = verifyNot checkBangAfterPipe "! ! true | true" prop_checkBangAfterPipe3 = verifyNot checkBangAfterPipe "! ! true | true"
checkBangAfterPipe = ForShell [Dash, Sh, Bash] f checkBangAfterPipe = ForShell [Dash, BusyboxSh, Sh, Bash] f
where where
f token = case token of f token = case token of
T_Pipeline _ _ cmds -> mapM_ check cmds T_Pipeline _ _ cmds -> mapM_ check cmds

View File

@@ -156,6 +156,9 @@ shellForExecutable name =
"sh" -> return Sh "sh" -> return Sh
"bash" -> return Bash "bash" -> return Bash
"bats" -> return Bash "bats" -> return Bash
"busybox" -> return BusyboxSh -- Used for directives and --shell=busybox
"busybox sh" -> return BusyboxSh
"busybox ash" -> return BusyboxSh
"dash" -> return Dash "dash" -> return Dash
"ash" -> return Dash -- There's also a warning for this. "ash" -> return Dash -- There's also a warning for this.
"ksh" -> return Ksh "ksh" -> return Ksh

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2024 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -21,14 +21,14 @@
module ShellCheck.Interface module ShellCheck.Interface
( (
SystemInterface(..) SystemInterface(..)
, CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csOptionalChecks) , CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csExtendedAnalysis, csOptionalChecks)
, CheckResult(crFilename, crComments) , CheckResult(crFilename, crComments)
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride) , ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
, ParseResult(prComments, prTokenPositions, prRoot) , ParseResult(prComments, prTokenPositions, prRoot)
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asOptionalChecks) , AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asExtendedAnalysis, asOptionalChecks)
, AnalysisResult(arComments) , AnalysisResult(arComments)
, FormatterOptions(foColorOption, foWikiLinkCount) , FormatterOptions(foColorOption, foWikiLinkCount)
, Shell(Ksh, Sh, Bash, Dash) , Shell(Ksh, Sh, Bash, Dash, BusyboxSh)
, ExecutionMode(Executed, Sourced) , ExecutionMode(Executed, Sourced)
, ErrorMessage , ErrorMessage
, Code , Code
@@ -100,6 +100,7 @@ data CheckSpec = CheckSpec {
csIncludedWarnings :: Maybe [Integer], csIncludedWarnings :: Maybe [Integer],
csShellTypeOverride :: Maybe Shell, csShellTypeOverride :: Maybe Shell,
csMinSeverity :: Severity, csMinSeverity :: Severity,
csExtendedAnalysis :: Maybe Bool,
csOptionalChecks :: [String] csOptionalChecks :: [String]
} deriving (Show, Eq) } deriving (Show, Eq)
@@ -124,6 +125,7 @@ emptyCheckSpec = CheckSpec {
csIncludedWarnings = Nothing, csIncludedWarnings = Nothing,
csShellTypeOverride = Nothing, csShellTypeOverride = Nothing,
csMinSeverity = StyleC, csMinSeverity = StyleC,
csExtendedAnalysis = Nothing,
csOptionalChecks = [] csOptionalChecks = []
} }
@@ -174,6 +176,7 @@ data AnalysisSpec = AnalysisSpec {
asExecutionMode :: ExecutionMode, asExecutionMode :: ExecutionMode,
asCheckSourced :: Bool, asCheckSourced :: Bool,
asOptionalChecks :: [String], asOptionalChecks :: [String],
asExtendedAnalysis :: Maybe Bool,
asTokenPositions :: Map.Map Id (Position, Position) asTokenPositions :: Map.Map Id (Position, Position)
} }
@@ -184,6 +187,7 @@ newAnalysisSpec token = AnalysisSpec {
asExecutionMode = Executed, asExecutionMode = Executed,
asCheckSourced = False, asCheckSourced = False,
asOptionalChecks = [], asOptionalChecks = [],
asExtendedAnalysis = Nothing,
asTokenPositions = Map.empty asTokenPositions = Map.empty
} }
@@ -221,7 +225,7 @@ newCheckDescription = CheckDescription {
} }
-- Supporting data types -- Supporting data types
data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq) data Shell = Ksh | Sh | Bash | Dash | BusyboxSh deriving (Show, Eq)
data ExecutionMode = Executed | Sourced deriving (Show, Eq) data ExecutionMode = Executed | Sourced deriving (Show, Eq)
type ErrorMessage = String type ErrorMessage = String
@@ -335,4 +339,3 @@ mockedSystemInterface files = (newSystemInterface :: SystemInterface Identity) {
mockRcFile rcfile mock = mock { mockRcFile rcfile mock = mock {
siGetConfig = const . return $ Just (".shellcheckrc", rcfile) siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
} }

View File

@@ -46,6 +46,7 @@ import Text.Parsec.Error
import Text.Parsec.Pos import Text.Parsec.Pos
import qualified Control.Monad.Reader as Mr import qualified Control.Monad.Reader as Mr
import qualified Control.Monad.State as Ms import qualified Control.Monad.State as Ms
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
import Test.QuickCheck.All (quickCheckAll) import Test.QuickCheck.All (quickCheckAll)
@@ -160,7 +161,7 @@ data Context =
deriving (Show) deriving (Show)
data HereDocContext = data HereDocContext =
HereDocPending Token [Context] -- on linefeed, read this T_HereDoc HereDocPending Id Dashed Quoted String [Context] -- on linefeed, read this T_HereDoc
deriving (Show) deriving (Show)
data UserState = UserState { data UserState = UserState {
@@ -238,12 +239,12 @@ addToHereDocMap id list = do
hereDocMap = Map.insert id list map hereDocMap = Map.insert id list map
} }
addPendingHereDoc t = do addPendingHereDoc id d q str = do
state <- getState state <- getState
context <- getCurrentContexts context <- getCurrentContexts
let docs = pendingHereDocs state let docs = pendingHereDocs state
putState $ state { putState $ state {
pendingHereDocs = HereDocPending t context : docs pendingHereDocs = HereDocPending id d q str context : docs
} }
popPendingHereDocs = do popPendingHereDocs = do
@@ -1057,6 +1058,16 @@ readAnnotationWithoutPrefix sandboxed = do
"This shell type is unknown. Use e.g. sh or bash." "This shell type is unknown. Use e.g. sh or bash."
return [ShellOverride shell] return [ShellOverride shell]
"extended-analysis" -> do
pos <- getPosition
value <- plainOrQuoted $ many1 letter
case value of
"true" -> return [ExtendedAnalysis True]
"false" -> return [ExtendedAnalysis False]
_ -> do
parseNoteAt pos ErrorC 1146 "Unknown extended-analysis value. Expected true/false."
return []
"external-sources" -> do "external-sources" -> do
pos <- getPosition pos <- getPosition
value <- plainOrQuoted $ many1 letter value <- plainOrQuoted $ many1 letter
@@ -1194,7 +1205,7 @@ readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|>
readDollarBracedLiteral = do readDollarBracedLiteral = do
start <- startSpan start <- startSpan
vars <- (readBraceEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` bracedQuotable vars <- (readBraceEscaped <|> ((\x -> [x]) <$> anyChar)) `reluctantlyTill1` bracedQuotable
id <- endSpan start id <- endSpan start
return $ T_Literal id $ concat vars return $ T_Literal id $ concat vars
@@ -1556,7 +1567,7 @@ readGenericLiteral endChars = do
return $ concat strings return $ concat strings
readGenericLiteral1 endExp = do readGenericLiteral1 endExp = do
strings <- (readGenericEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` endExp strings <- (readGenericEscaped <|> ((\x -> [x]) <$> anyChar)) `reluctantlyTill1` endExp
return $ concat strings return $ concat strings
readGenericEscaped = do readGenericEscaped = do
@@ -1835,7 +1846,7 @@ readHereDoc = called "here document" $ do
-- add empty tokens for now, read the rest in readPendingHereDocs -- add empty tokens for now, read the rest in readPendingHereDocs
let doc = T_HereDoc hid dashed quoted endToken [] let doc = T_HereDoc hid dashed quoted endToken []
addPendingHereDoc doc addPendingHereDoc hid dashed quoted endToken
return doc return doc
where where
unquote :: String -> (Quoted, String) unquote :: String -> (Quoted, String)
@@ -1856,7 +1867,7 @@ readPendingHereDocs = do
docs <- popPendingHereDocs docs <- popPendingHereDocs
mapM_ readDoc docs mapM_ readDoc docs
where where
readDoc (HereDocPending (T_HereDoc id dashed quoted endToken _) ctx) = readDoc (HereDocPending id dashed quoted endToken ctx) =
swapContext ctx $ swapContext ctx $
do do
docStartPos <- getPosition docStartPos <- getPosition
@@ -2370,7 +2381,7 @@ readPipeSequence = do
return $ T_Pipeline id pipes cmds return $ T_Pipeline id pipes cmds
where where
sepBy1WithSeparators p s = do sepBy1WithSeparators p s = do
let elems = p >>= \x -> return ([x], []) let elems = (\x -> ([x], [])) <$> p
let seps = do let seps = do
separator <- s separator <- s
return $ \(a,b) (c,d) -> (a++c, b ++ d ++ [separator]) return $ \(a,b) (c,d) -> (a++c, b ++ d ++ [separator])
@@ -2904,8 +2915,8 @@ readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord)
kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos) kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos)
kludgeAwayQuotes s p = kludgeAwayQuotes s p =
case s of case s of
first:rest@(_:_) -> first:second:rest ->
let (last:backwards) = reverse rest let (last NE.:| backwards) = NE.reverse (second NE.:| rest)
middle = reverse backwards middle = reverse backwards
in in
if first `elem` "'\"" && first == last if first `elem` "'\"" && first == last
@@ -3339,7 +3350,8 @@ readScriptFile sourced = do
verifyEof verifyEof
let script = T_Annotation annotationId annotations $ let script = T_Annotation annotationId annotations $
T_Script id shebang commands T_Script id shebang commands
reparseIndices script userstate <- getState
reparseIndices $ reattachHereDocs script (hereDocMap userstate)
else do else do
many anyChar many anyChar
id <- endSpan start id <- endSpan start
@@ -3349,8 +3361,8 @@ readScriptFile sourced = do
verifyShebang pos s = do verifyShebang pos s = do
case isValidShell s of case isValidShell s of
Just True -> return () Just True -> return ()
Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/dash/ksh scripts. Sorry!" Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/dash/ksh/'busybox sh' scripts. Sorry!"
Nothing -> parseProblemAt pos ErrorC 1008 "This shebang was unrecognized. ShellCheck only supports sh/bash/dash/ksh. Add a 'shell' directive to specify." Nothing -> parseProblemAt pos ErrorC 1008 "This shebang was unrecognized. ShellCheck only supports sh/bash/dash/ksh/'busybox sh'. Add a 'shell' directive to specify."
isValidShell s = isValidShell s =
let good = null s || any (`isPrefixOf` s) goodShells let good = null s || any (`isPrefixOf` s) goodShells
@@ -3366,6 +3378,7 @@ readScriptFile sourced = do
"sh", "sh",
"ash", "ash",
"dash", "dash",
"busybox sh",
"bash", "bash",
"bats", "bats",
"ksh" "ksh"
@@ -3453,9 +3466,8 @@ makeErrorFor parsecError =
pos = errorPos parsecError pos = errorPos parsecError
getStringFromParsec errors = getStringFromParsec errors =
case map f errors of headOrDefault "" (mapMaybe f $ reverse errors) ++
r -> unwords (take 1 $ catMaybes $ reverse r) ++ " Fix any mentioned problems and try again."
" Fix any mentioned problems and try again."
where where
f err = f err =
case err of case err of
@@ -3486,8 +3498,7 @@ parseShell env name contents = do
return newParseResult { return newParseResult {
prComments = map toPositionedComment $ nub $ parseNotes userstate ++ parseProblems state, prComments = map toPositionedComment $ nub $ parseNotes userstate ++ parseProblems state,
prTokenPositions = Map.map startEndPosToPos (positionMap userstate), prTokenPositions = Map.map startEndPosToPos (positionMap userstate),
prRoot = Just $ prRoot = Just script
reattachHereDocs script (hereDocMap userstate)
} }
Left err -> do Left err -> do
let context = contextStack state let context = contextStack state
@@ -3505,13 +3516,11 @@ parseShell env name contents = do
-- A final pass for ignoring parse errors after failed parsing -- A final pass for ignoring parse errors after failed parsing
isIgnored stack note = any (contextItemDisablesCode False (codeForParseNote note)) stack isIgnored stack note = any (contextItemDisablesCode False (codeForParseNote note)) stack
notesForContext list = zipWith ($) [first, second] $ filter isName list notesForContext list = zipWith ($) [first, second] [(pos, str) | ContextName pos str <- list]
where where
isName (ContextName _ _) = True first (pos, str) = ParseNote pos pos ErrorC 1073 $
isName _ = False
first (ContextName pos str) = ParseNote pos pos ErrorC 1073 $
"Couldn't parse this " ++ str ++ ". Fix to allow more checks." "Couldn't parse this " ++ str ++ ". Fix to allow more checks."
second (ContextName pos str) = ParseNote pos pos InfoC 1009 $ second (pos, str) = ParseNote pos pos InfoC 1009 $
"The mentioned syntax error was in this " ++ str ++ "." "The mentioned syntax error was in this " ++ str ++ "."
-- Go over all T_UnparsedIndex and reparse them as either arithmetic or text -- Go over all T_UnparsedIndex and reparse them as either arithmetic or text

View File

@@ -76,8 +76,6 @@ archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-dev
# Ubuntu LTS # Ubuntu LTS
ubuntu:22.04 apt-get update && apt-get install -y cabal-install ubuntu:22.04 apt-get update && apt-get install -y cabal-install
ubuntu:20.04 apt-get update && apt-get install -y cabal-install ubuntu:20.04 apt-get update && apt-get install -y cabal-install
ubuntu:18.04 apt-get update && apt-get install -y cabal-install
ubuntu:16.04 apt-get update && apt-get install -y cabal-install
# Stack on Ubuntu LTS # Stack on Ubuntu LTS
ubuntu: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: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

View File

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