mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 16:59:20 +08:00
Compare commits
78 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
37dfb67768 | ||
|
a7e65dca8d | ||
|
8bc7345aa7 | ||
|
ad3c3146f0 | ||
|
55be4543f2 | ||
|
8c4c112c25 | ||
|
d80fdfa9e8 | ||
|
1565091b1d | ||
|
d056549406 | ||
|
f5758e1789 | ||
|
6a44a19f17 | ||
|
b1b95c2c17 | ||
|
de95624d31 | ||
|
b5ab220652 | ||
|
1bce426fcf | ||
|
ba86c6363c | ||
|
67abfe159e | ||
|
025cc5266e | ||
|
5a6f4840ad | ||
|
9e0fdbe431 | ||
|
b7f88ec4b7 | ||
|
7b0589988f | ||
|
71889c139a | ||
|
a6984cddb0 | ||
|
3f40b688ee | ||
|
6c81505870 | ||
|
10afe83ce3 | ||
|
a786f996a1 | ||
|
6e5b5401c6 | ||
|
71c0fcb737 | ||
|
add49cda17 | ||
|
e1ad063834 | ||
|
ee41c780f4 | ||
|
980e7d3ca8 | ||
|
dedf932fe8 | ||
|
3bd7df955b | ||
|
dab77b2c8d | ||
|
f983d9ae93 | ||
|
bfe4342697 | ||
|
a47a42cb45 | ||
|
eed0174e90 | ||
|
0c46b8b2d5 | ||
|
208e38358e | ||
|
c1452e0d17 | ||
|
c97abdb939 | ||
|
f242922a2e | ||
|
a37803d2b8 | ||
|
09d04c4c9b | ||
|
e5028481e2 | ||
|
5a961371a7 | ||
|
e5208ccb50 | ||
|
4c1d9171b2 | ||
|
a9e7bf1950 | ||
|
f2729f73cb | ||
|
175d3cc9b7 | ||
|
5c50b0b189 | ||
|
74282b0a93 | ||
|
b6d4952e2e | ||
|
fdcce458c1 | ||
|
ca255fe326 | ||
|
a3b8be82fe | ||
|
ac63dc33c9 | ||
|
903421fb5d | ||
|
00ffd2db33 | ||
|
1e1045e73e | ||
|
be8e4b2b8a | ||
|
a71a13c2fc | ||
|
1aeab287e6 | ||
|
2a95bc6be3 | ||
|
4fd0615501 | ||
|
8b3c37aa36 | ||
|
dc2f388310 | ||
|
99a94421ab | ||
|
6a6d8e9fc4 | ||
|
592c17e4f2 | ||
|
9605396bef | ||
|
c89ec2fd49 | ||
|
410ec54617 |
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -20,3 +20,4 @@ cabal.config
|
|||||||
/parts/
|
/parts/
|
||||||
/prime/
|
/prime/
|
||||||
*.snap
|
*.snap
|
||||||
|
/dist-newstyle/
|
||||||
|
@@ -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
|
|
||||||
|
|
16
CHANGELOG.md
16
CHANGELOG.md
@@ -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
|
||||||
|
13
README.md
13
README.md
@@ -77,7 +77,7 @@ You can see ShellCheck suggestions directly in a variety of editors.
|
|||||||
|
|
||||||
* Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck).
|
* 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)!
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
|
40
build/darwin.aarch64/Dockerfile
Normal file
40
build/darwin.aarch64/Dockerfile
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
FROM ghcr.io/shepherdjerred/macos-cross-compiler:latest
|
||||||
|
|
||||||
|
ENV TARGET aarch64-apple-darwin22
|
||||||
|
ENV TARGETNAME darwin.aarch64
|
||||||
|
|
||||||
|
# Build dependencies
|
||||||
|
USER root
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
ENV LC_ALL C.utf8
|
||||||
|
|
||||||
|
# Install basic deps
|
||||||
|
RUN apt-get update && apt-get install -y automake autoconf build-essential curl xz-utils qemu-user-static
|
||||||
|
|
||||||
|
# Install a more suitable host compiler
|
||||||
|
WORKDIR /host-ghc
|
||||||
|
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin
|
||||||
|
RUN curl -L 'https://downloads.haskell.org/~ghc/8.10.7/ghc-8.10.7-x86_64-deb10-linux.tar.xz' | tar xJ --strip-components=1
|
||||||
|
RUN ./configure && make install
|
||||||
|
|
||||||
|
# Build GHC. We have to use an old version because cross-compilation across OS has since broken.
|
||||||
|
WORKDIR /ghc
|
||||||
|
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.7/ghc-8.10.7-src.tar.xz" | tar xJ --strip-components=1
|
||||||
|
RUN apt-get install -y llvm-12
|
||||||
|
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
|
||||||
|
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
|
||||||
|
RUN make install
|
||||||
|
|
||||||
|
# Due to an apparent cabal bug, we specify our options directly to cabal
|
||||||
|
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
|
||||||
|
ENV CABALOPTS "--ghc-options;-optc-Os -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;--constraint=hashable==1.3.5.0"
|
||||||
|
|
||||||
|
# Prebuild the dependencies
|
||||||
|
RUN cabal update
|
||||||
|
RUN IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
|
||||||
|
|
||||||
|
# Copy the build script
|
||||||
|
COPY build /usr/bin
|
||||||
|
|
||||||
|
WORKDIR /scratch
|
||||||
|
ENTRYPOINT ["/usr/bin/build"]
|
17
build/darwin.aarch64/build
Executable file
17
build/darwin.aarch64/build
Executable 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
1
build/darwin.aarch64/tag
Normal file
@@ -0,0 +1 @@
|
|||||||
|
koalaman/scbuilder-darwin-aarch64
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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,8 +469,23 @@ 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 =
|
||||||
|
case rcfile options of
|
||||||
|
Just file -> do
|
||||||
|
-- We have a specified rcfile. Ignore normal rcfile resolution.
|
||||||
|
(path, result) <- readIORef cache
|
||||||
|
if path == "/"
|
||||||
|
then return result
|
||||||
|
else do
|
||||||
|
result <- readConfig file
|
||||||
|
when (isNothing result) $
|
||||||
|
hPutStrLn stderr $ "Warning: unable to read --rcfile " ++ file
|
||||||
|
writeIORef cache ("/", result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
Nothing -> do
|
||||||
path <- normalize filename
|
path <- normalize filename
|
||||||
let dir = takeDirectory path
|
let dir = takeDirectory path
|
||||||
(previousPath, result) <- readIORef cache
|
(previousPath, result) <- readIORef cache
|
||||||
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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,12 +341,12 @@ 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
|
||||||
|
|
||||||
@@ -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 =
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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,14 +183,13 @@ 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
|
||||||
else if name == "builtin" && not (null rest) then
|
| name == "builtin", (h:_) <- rest ->
|
||||||
let t' = T_SimpleCommand id cmdPrefix rest
|
let t' = T_SimpleCommand id cmdPrefix rest
|
||||||
selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest
|
selectedBuiltin = onlyLiteralString h
|
||||||
in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
|
in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
|
||||||
else do
|
| otherwise -> 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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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,8 +3466,7 @@ 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 =
|
||||||
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
|
||||||
|
Reference in New Issue
Block a user