95 Commits

Author SHA1 Message Date
Vidar Holen
a526ee0829 Stable version 0.9.0
This release is dedicated to Mindustry: the most fun you can
have with open source (outside of shell scripting of course).
2022-12-12 21:49:19 -08:00
Vidar Holen
8c5fdc3522 Update copyright years 2022-12-12 21:49:19 -08:00
Vidar Holen
ae199edb68 Let distrotest fail fast when there remaining executables 2022-12-11 20:51:39 -08:00
Vidar Holen
7cfcf6db8a Fix stack build 2022-12-11 19:31:58 -08:00
Vidar Holen
a7c5be93dc Tighten bounds on packages 2022-12-11 19:22:21 -08:00
Vidar Holen
8754c21244 Avoid $ trigger TH 2022-12-11 19:22:06 -08:00
Vidar Holen
985ca2530d Add Docker testing for older and newer Ubuntu versions 2022-12-11 19:22:06 -08:00
Vidar Holen
3cae6cd6ab Allow building on deepseq < 1.4.2.0 2022-12-11 15:06:24 -08:00
Vidar Holen
74b1745a19 Fix compiler error on some GHC versions
Fixes the following error:

    src/ShellCheck/CFGAnalysis.hs:1394:40: error:
	* Couldn't match expected type `[S.Set a]'
		      with actual type `M.Map String FunctionValue'
	* In the second argument of `($)', namely
	    `mapStorage $ sFunctionTargets state'
	  In the expression: S.unions $ mapStorage $ sFunctionTargets state
	  In an equation for `declaredFuncs':
	      declaredFuncs = S.unions $ mapStorage $ sFunctionTargets state
	* Relevant bindings include
	    declaredFuncs :: S.Set a
	      (bound at src/ShellCheck/CFGAnalysis.hs:1394:13)
2022-12-11 15:06:24 -08:00
Vidar Holen
495e34d101 Add missing Semigroup import for older GHC 2022-12-11 14:19:24 -08:00
Vidar Holen
2a16a4e8c1 Add missing imports for later GHC versions 2022-12-10 15:17:08 -08:00
Vidar Holen
0786b2bf3c Merge pull request #2601 from mavit/colon-after-exec
Permit colon after exec
2022-11-02 21:18:56 -07:00
Vidar Holen
84d8530f14 Add SVG logo 2022-10-29 12:50:37 -07:00
Vidar Holen
86e2b76730 Improve SC1059 error message 2022-10-29 12:50:26 -07:00
Vidar Holen
b770984dfc Try to parse the inside of traps (fixes #2584) 2022-10-13 21:04:38 -07:00
Vidar Holen
d9c9e60fb0 Allow arbitrary bats @test names (fixes #2587) 2022-10-13 20:21:59 -07:00
Vidar Holen
14056a7f3a Don't suggest pgrep for ps -p .. | grep (fixes #2597) 2022-10-12 20:22:02 -07:00
Vidar Holen
a524929b69 Remove outdated test 2022-10-12 20:22:02 -07:00
Vidar Holen
fa7943ac0e Revert "Add employer mandated disclaimer"
This reverts commit 5202072a34.
2022-10-11 20:10:34 -07:00
Vidar Holen
81c2ecaccb Remove true/false from SC2216/SC2217 (fixes #2603) 2022-10-11 19:40:29 -07:00
Vidar Holen
fcba462a99 Merge pull request #2602 from DoxasticFox/issue-2550
Fix false positive for SC2312 when using `time`
2022-10-09 11:55:39 -07:00
Christian Nassif-Haynes
43aca62ca7 Fix false positive for SC2312 when using time 2022-10-10 03:33:38 +11:00
Peter Oliver
128351f5ef Permit colon after exec
ShellCheck throws warning SC2093 when a script contains commands that could never be executed because they are after an `exec`.  Command `:` does nothing, so add it to the list of commands that don’t trigger this warning.
2022-10-07 17:02:31 +01:00
Vidar Holen
d71d6ff294 Merge pull request #2581 from larryv/updatebashvars
Support more Bash internal variables
2022-09-25 11:24:45 -07:00
Vidar Holen
bd65b67578 Merge pull request #2586 from DoxasticFox/issue-2537
Suppress SC2311 with `set -o posix`
2022-09-25 11:04:30 -07:00
Vidar Holen
149b4dbd6f Merge pull request #2588 from DoxasticFox/issue-2563
Add `mapfile` to harmless commands for SC2094
2022-09-25 11:03:45 -07:00
Christian Nassif-Haynes
ef5f9a7af5 Add mapfile to harmless commands for SC2094 2022-09-25 03:04:20 +10:00
Christian Nassif-Haynes
581981ba76 Suppress SC2311 with set -o posix 2022-09-24 07:20:48 +10:00
Vidar Holen
fcc473e27f Include inherited env for DFA of leftover functions (fixes #2560) 2022-09-21 18:11:18 -07:00
Lawrence Velázquez
0845b81183 Add READLINE_POINT to list of variables without spaces 2022-09-20 20:39:17 -04:00
Lawrence Velázquez
966fb3e3dd Recognize more Bash internal variables
- BASH_ARGV0, introduced in Bash 5.0
  - BASH_COMPAT, 4.3
  - BASH_LOADABLES_PATH, 4.4
  - CHILD_MAX, 4.3
  - EPOCHREALTIME, 5.0
  - EPOCHSECONDS, 5.0
  - EXECIGNORE, 4.4
  - INSIDE_EMACS, 4.4
  - PS0, 4.4
  - READLINE_ARGUMENT, 5.2
  - READLINE_MARK, 5.1
  - SRANDOM, 5.1

Fixes #1780 and #2554.
2022-09-20 20:39:10 -04:00
Lawrence Velázquez
f28462b01c Remove duplicate "COPROC" from internal vars list 2022-09-20 20:25:21 -04:00
Lawrence Velázquez
ccab132b38 Reflow lists of internal shell variables
No functional changes; this just makes the next few commits cleaner.
2022-09-20 20:24:37 -04:00
Vidar Holen
4806719035 Handle variable assignments from read in CFG 2022-08-02 15:47:59 -07:00
Vidar Holen
0df9345142 Trace numerical status, use for SC2071 (ref #2541) 2022-08-02 11:29:56 -07:00
Vidar Holen
77069f7445 Store postdominators as Array Node [Node] for a significant win 2022-07-31 15:43:24 -07:00
Vidar Holen
04db46381f Use Data.Map.Strict instead for a ~15% parsing speedup 2022-07-29 09:02:45 -07:00
Vidar Holen
c76b8d9a32 Let annotations take effect earlier (fixes #2534) 2022-07-28 11:05:16 -07:00
Vidar Holen
d0dd81e1fa Allow quoting values in directives (fixes #2517) 2022-07-28 08:56:44 -07:00
Vidar Holen
f440912279 Refactor to not generate Parameters twice 2022-07-28 08:26:56 -07:00
Vidar Holen
3ce310e939 Plug space leaks when processing multiple files 2022-07-27 14:42:57 -07:00
Vidar Holen
a30ac402eb Don't use & for updates as result is unspecified
This fixes `Prelude.foldl1: empty list []` when script has `( exit )`
2022-07-27 11:30:26 -07:00
Vidar Holen
4a27c9a8d5 Fix overlap check 2022-07-26 15:33:25 -07:00
Vidar Holen
b5f5e6347d Discard next rather than existing fixes when they overlap 2022-07-26 13:41:22 -07:00
Vidar Holen
c57e447c89 Correctly discard overlapping fixes in diff output (fixes #2370) 2022-07-26 10:16:12 -07:00
Vidar Holen
e9784fa9a7 Refine #2544 to not warn when $? postdominates [ ] (fixes #2544) 2022-07-25 12:00:59 -07:00
Vidar Holen
f1148b8b41 Include postdominators in CFGResult 2022-07-25 12:00:53 -07:00
Vidar Holen
982681fc05 Add unit test to ensure SC2321 does not trigger on associative arrays 2022-07-24 14:30:31 -07:00
Vidar Holen
52dac51cd4 SC2323: Warn about redundant parens in a[(x+1)] and $(( ((x)) )) (ref: #1666) 2022-07-24 14:18:38 -07:00
Vidar Holen
30bb0e0093 SC2321: Warn about redundant $(()) in arr[$((i))]=x (ref: #1666) 2022-07-24 14:18:38 -07:00
Vidar Holen
d1d574c091 Merge branch 'ygeyzel-grammer_fix_sc2183' 2022-07-23 15:39:26 -07:00
Vidar Holen
ea4e0091c7 Additionally pluralize 'arguments' in SC2183 2022-07-23 15:38:42 -07:00
Vidar Holen
81d9f7e640 Merge branch 'grammer_fix_sc2183' of https://github.com/ygeyzel/shellcheck into ygeyzel-grammer_fix_sc2183 2022-07-23 15:34:22 -07:00
Vidar Holen
69469c3603 Merge pull request #2543 from ygeyzel/SC2028-escape-chars
Add escape characters to SC2028: \a, \b, \e, \f, \v, \\, \', \OOO, \xHH
2022-07-23 15:30:00 -07:00
Vidar Holen
5cf6e01ce9 Warn when $? refers to echo or condition (ref #2541) 2022-07-23 09:39:26 -07:00
ygeyzel
f7857028f7 Add escape characters to SC2028: \a, \b, \e, \f, \v, \\, \', \OOO, \xHH 2022-07-23 19:28:37 +03:00
Vidar Holen
b261ec24f9 Include exit codes in DFA (ref #2541) 2022-07-23 08:50:19 -07:00
Vidar Holen
819470fa1d Omit SC3021 about >& file unless definitely non-numeric (fixes #2520) 2022-07-22 17:06:24 -07:00
Vidar Holen
2f28847b08 Normalize spaces around = in unit tests 2022-07-22 16:35:14 -07:00
Vidar Holen
e47480e93a Also emit SC2004 for array indices (fixes #1666) 2022-07-22 16:29:52 -07:00
Vidar Holen
9caeec104b SC2318: Warn about backreferencing in declare x=1 y=$x (fixes #1653) 2022-07-22 12:40:59 -07:00
Vidar Holen
95b3cbf071 Qualify Data.Map as M instead of tedious Map 2022-07-22 11:11:09 -07:00
Vidar Holen
e7f05d662a In addition to start/end, track sets of nodes belonging to tokens 2022-07-22 10:29:19 -07:00
Vidar Holen
3ee4419ef4 Suppress SC2086 for variables declared -i (ref #2541) 2022-07-22 08:53:27 -07:00
Vidar Holen
8dc0fdb4cc Precompile new fgl dependency on armv6hf 2022-07-20 12:52:57 -07:00
Vidar Holen
da4885a71d Use DFA for SC2086 2022-07-20 08:08:44 -07:00
Vidar Holen
642ad86125 Add SC2317 warning about unreachable commands 2022-07-20 08:08:41 -07:00
Vidar Holen
f77a545282 Control Flow Graph / Data Flow Analysis support 2022-07-20 08:08:24 -07:00
Vidar Holen
7946bf5657 Upgrade cURL for Windows build image 2022-07-20 08:08:24 -07:00
Vidar Holen
cc04b40119 Freeze macOS dependency by sha256 2022-07-20 08:08:24 -07:00
Vidar Holen
c3bce51de3 Allow text to build on Fedora by installing dependencies 2022-07-20 08:08:24 -07:00
Vidar Holen
a4042f7523 Parse &&/|| as left-associative 2022-07-20 08:08:24 -07:00
Vidar Holen
363c0633e0 When reparsing array indices, do it recursively 2022-07-20 08:08:24 -07:00
ygeyzel
7ceb1f1519 SC2183 grammer fix: 'variable' instead of 'variables' if only one variable 2022-07-17 21:46:42 +03:00
Vidar Holen
f1bdda54cb Merge pull request #2500 from Fdawgs/patch-1
ci: update github actions
2022-05-20 20:36:36 -07:00
Frazer Smith
9aa4c22aa6 ci: update github actions 2022-05-16 06:56:46 +00:00
Vidar Holen
399c04cc17 Mention SC2316 in changelog 2022-05-06 10:11:52 -07:00
Vidar Holen
fd595d1058 Only trigger SC2316 on unquoted words. 2022-05-06 10:06:12 -07:00
Vidar Holen
7c44e1060f Merge branch 'patrickxia-master' 2022-05-06 09:50:08 -07:00
Rune Juhl Jacobsen
2821552688 Fix bug in 2126 when using after/before flags with grep
Using `--after-context`/`-A` or `--before-context`/`-B` would give a warning
recommending the user to use `grep -c`, even though that would give a different
result than using `grep | wc -l`:

```fundamental
$ echo -e "1\n2\n3" | grep -cA 3 1
1
$ echo -e "1\n2\n3" | grep -A 3 1 | wc -l
3
```
2022-05-06 09:17:23 -07:00
Vidar Holen
2034e3886e Merge pull request #2414 from runejuhl/fix-grep-after-before
Fix bug in 2126 when using after/before flags with grep
2022-05-06 09:15:04 -07:00
Patrick Xia
fa15c0a454 add SC2316: error on multiple declarations like 'readonly local' 2022-05-05 16:19:07 -07:00
Vidar Holen
88cdb4e2c9 Warn about spaces around = in alias (fixes #2442) 2022-02-03 19:23:46 -08:00
Vidar Holen
2292e852e5 Switch linux-x86_64 build from Ubuntu to Alpine for musl 2022-01-23 14:23:56 -08:00
Vidar Holen
ade2bf7b87 Allow parsing [[ x = ["$y"] ]] (fixes #2165) 2022-01-09 16:50:50 -08:00
Vidar Holen
e6e558946c Improve decoding of single quoted literals (fixes #2418) 2021-12-21 14:30:39 -08:00
Rune Juhl Jacobsen
3a118246ef Fix bug in 2126 when using after/before flags with grep
Using `--after-context`/`-A` or `--before-context`/`-B` would give a warning
recommending the user to use `grep -c`, even though that would give a different
result than using `grep | wc -l`:

```fundamental
$ echo -e "1\n2\n3" | grep -cA 3 1
1
$ echo -e "1\n2\n3" | grep -A 3 1 | wc -l
3
```
2021-12-14 16:03:29 +01:00
Vidar Holen
dd626686c4 Merge pull request #2375 from scop/spelling
Comment spelling fixes
2021-12-05 14:27:08 -08:00
Vidar Holen
866cbd0aa4 Merge branch 'tmp' 2021-12-04 17:37:25 -08:00
Vidar Holen
d7971dafd1 Minor formatting fixes 2021-12-04 17:37:12 -08:00
Martin Schulze
9092080a84 bats: Add check for useless negation (SC2314/15) 2021-11-15 12:01:12 +01:00
Vidar Holen
499c99372e Rewrite SC2032 warning and mention line number (fixes #2353) 2021-11-14 21:34:21 -08:00
Vidar Holen
d9a9d5db86 Mark prefix/postfix inc/dec as integers (fixes #2376) 2021-11-14 16:39:32 -08:00
Ville Skyttä
c5de58ae84 Comment spelling fixes 2021-11-13 12:50:53 +02:00
Vidar Holen
4c186c20b9 Post-release CHANGELOG update 2021-11-06 23:18:19 -07:00
36 changed files with 4746 additions and 842 deletions

View File

@@ -15,7 +15,7 @@ jobs:
sudo apt-get install cabal-install
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
@@ -37,7 +37,7 @@ jobs:
mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz
- name: Upload artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: source
path: source/
@@ -51,10 +51,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Download artifacts
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
- name: Build source
run: |
@@ -63,7 +63,7 @@ jobs:
( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} )
- name: Upload artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: bin
path: bin/
@@ -74,10 +74,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Download artifacts
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
- name: Work around GitHub permissions bug
run: chmod +x bin/*/shellcheck*
@@ -92,7 +92,7 @@ jobs:
rm -rf */ README* LICENSE*
- name: Upload artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: deploy
path: deploy/
@@ -104,10 +104,10 @@ jobs:
environment: Deploy
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Download artifacts
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
- name: Upload to GitHub
env:

View File

@@ -1,3 +1,25 @@
## v0.9.0 - 2022-12-12
### Added
- SC2316: Warn about 'local readonly foo' and similar (thanks, patrickxia!)
- SC2317: Warn about unreachable commands
- SC2318: Warn about backreferences in 'declare x=1 y=$x'
- SC2319/SC2320: Warn when $? refers to echo/printf/[ ]/[[ ]]/test
- SC2321: Suggest removing $((..)) in array[$((idx))]=val
- SC2322: Suggest collapsing double parentheses in arithmetic contexts
- SC2323: Suggest removing wrapping parentheses in a[(x+1)]=val
### Fixed
- SC2086: Now uses DFA to make more accurate predictions about values
- SC2086: No longer warns about values declared as integer with declare -i
### Changed
- ShellCheck now has a Data Flow Analysis engine to make smarter decisions
based on control flow rather than just syntax. Existing checks will
gradually start using it, which may cause them to trigger differently
(but more accurately).
- Values in directives/shellcheckrc can now be quoted with '' or ""
## v0.8.0 - 2021-11-06
### Added
- `disable=all` now conveniently disables all warnings

10
LICENSE
View File

@@ -1,13 +1,3 @@
Employer mandated disclaimer:
I am providing code in the repository to you under an open source license.
Because this is my personal repository, the license you receive to my code is
from me and other individual contributors, and not my employer (Facebook).
- Vidar "koala_man" Holen
----
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007

View File

@@ -1,5 +1,5 @@
Name: ShellCheck
Version: 0.8.0
Version: 0.9.0
Synopsis: Shell script analysis tool
License: GPL-3
License-file: LICENSE
@@ -45,19 +45,26 @@ library
build-depends:
semigroups
build-depends:
aeson,
array,
base >= 4.8.0.0 && < 5,
bytestring,
containers >= 0.5,
deepseq >= 1.4.0.0,
Diff >= 0.2.0,
directory >= 1.2.3.0,
mtl >= 2.2.1,
filepath,
parsec,
regex-tdfa,
QuickCheck >= 2.7.4,
-- The lower bounds are based on GHC 7.10.3
-- The upper bounds are based on GHC 9.4.3
aeson >= 1.4.0 && < 2.2,
array >= 0.5.1 && < 0.6,
base >= 4.8.0.0 && < 5,
bytestring >= 0.10.6 && < 0.12,
containers >= 0.5.6 && < 0.7,
deepseq >= 1.4.1 && < 1.5,
Diff >= 0.4.0 && < 0.5,
fgl >= 5.7.0 && < 5.9,
filepath >= 1.4.0 && < 1.5,
mtl >= 2.2.2 && < 2.3,
parsec >= 3.1.14 && < 3.2,
QuickCheck >= 2.14.2 && < 2.15,
regex-tdfa >= 1.2.0 && < 1.4,
transformers >= 0.4.2 && < 0.6,
-- getXdgDirectory from 1.2.3.0
directory >= 1.2.3 && < 1.4,
-- When cabal supports it, move this to setup-depends:
process
exposed-modules:
@@ -66,11 +73,15 @@ library
ShellCheck.Analytics
ShellCheck.Analyzer
ShellCheck.AnalyzerLib
ShellCheck.CFG
ShellCheck.CFGAnalysis
ShellCheck.Checker
ShellCheck.Checks.Commands
ShellCheck.Checks.ControlFlow
ShellCheck.Checks.Custom
ShellCheck.Checks.ShellSupport
ShellCheck.Data
ShellCheck.Debug
ShellCheck.Fixer
ShellCheck.Formatter.Format
ShellCheck.Formatter.CheckStyle
@@ -82,6 +93,7 @@ library
ShellCheck.Formatter.Quiet
ShellCheck.Interface
ShellCheck.Parser
ShellCheck.Prelude
ShellCheck.Regex
other-modules:
Paths_ShellCheck
@@ -94,17 +106,19 @@ executable shellcheck
build-depends:
aeson,
array,
base >= 4 && < 5,
base,
bytestring,
containers,
deepseq >= 1.4.0.0,
Diff >= 0.2.0,
directory >= 1.2.3.0,
mtl >= 2.2.1,
deepseq,
Diff,
directory,
fgl,
mtl,
filepath,
parsec >= 3.0,
QuickCheck >= 2.7.4,
parsec,
QuickCheck,
regex-tdfa,
transformers,
ShellCheck
default-language: Haskell98
main-is: shellcheck.hs
@@ -114,17 +128,19 @@ test-suite test-shellcheck
build-depends:
aeson,
array,
base >= 4 && < 5,
base,
bytestring,
containers,
deepseq >= 1.4.0.0,
Diff >= 0.2.0,
directory >= 1.2.3.0,
mtl >= 2.2.1,
deepseq,
Diff,
directory,
fgl,
filepath,
mtl,
parsec,
QuickCheck >= 2.7.4,
QuickCheck,
regex-tdfa,
transformers,
ShellCheck
default-language: Haskell98
main-is: test/shellcheck.hs

View File

@@ -1,5 +1,4 @@
# DIGEST:sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76
FROM liushuyu/osxcross:latest
FROM liushuyu/osxcross@sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76
ENV TARGET x86_64-apple-darwin18
ENV TARGETNAME darwin.x86_64

View File

@@ -52,6 +52,7 @@ RUN pirun apt-get install -y ghc cabal-install
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--gcc-options;-Os -Wl,--gc-sections -ffunction-sections -fdata-sections"
RUN pirun cabal update
RUN IFS=";" && pirun cabal install --dependencies-only $CABALOPTS ShellCheck
RUN IFS=';' && pirun cabal install $CABALOPTS --lib fgl
# Copy the build script
WORKDIR /pi/scratch

View File

@@ -1,16 +1,10 @@
FROM ubuntu:20.04
FROM alpine:latest
ENV TARGETNAME linux.x86_64
# Install GHC and cabal
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y ghc curl xz-utils
# So we'd like a later version of Cabal that supports --enable-executable-static,
# but we can't use Ubuntu 20.10 because coreutils has switched to new syscalls that
# the TravisCI kernel doesn't support. Download it manually.
RUN curl "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/bin
RUN apk add ghc cabal g++ libffi-dev curl bash
# Use ld.bfd instead of ld.gold due to
# x86_64-linux-gnu/libpthread.a(pthread_cond_init.o)(.note.stapsdt+0x14): error:

View File

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

294
doc/shellcheck_logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 244 KiB

View File

@@ -282,6 +282,9 @@ Here is an example `.shellcheckrc`:
source-path=SCRIPTDIR
source-path=/mnt/chroot
# Since 0.9.0, values can be quoted with '' or "" to allow spaces
source-path="My Documents/scripts"
# Allow opening any 'source'd file, even if not specified as input
external-sources=true
@@ -375,7 +378,7 @@ long list of wonderful contributors.
# COPYRIGHT
Copyright 2012-2021, Vidar Holen and contributors.
Copyright 2012-2022, Vidar Holen and contributors.
Licensed under the GNU General Public License version 3 or later,
see https://gnu.org/licenses/gpl.html

View File

@@ -34,6 +34,8 @@ import qualified ShellCheck.Formatter.Quiet
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Control.Monad.Except
import Data.Bits
import Data.Char
@@ -225,7 +227,7 @@ runFormatter sys format options files = do
f :: Status -> FilePath -> IO Status
f status file = do
newStatus <- process file `catch` handler file
return $ status `mappend` newStatus
return $! status `mappend` newStatus
handler :: FilePath -> IOException -> IO Status
handler file e = reportFailure file (show e)
reportFailure file str = do

View File

@@ -45,6 +45,7 @@ data InnerToken t =
| Inner_TA_Variable String [t]
| Inner_TA_Expansion [t]
| Inner_TA_Sequence [t]
| Inner_TA_Parenthesis t
| Inner_TA_Trinary t t t
| Inner_TA_Unary String t
| Inner_TC_And ConditionType String t t
@@ -141,7 +142,7 @@ data InnerToken t =
| Inner_T_CoProcBody t
| Inner_T_Include t
| Inner_T_SourceCommand t t
| Inner_T_BatsTest t t
| Inner_T_BatsTest String t
deriving (Show, Eq, Functor, Foldable, Traversable)
data Annotation =
@@ -204,6 +205,7 @@ pattern T_Annotation id anns t = OuterToken id (Inner_T_Annotation anns t)
pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c)
pattern T_Array id t = OuterToken id (Inner_T_Array t)
pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)
pattern TA_Parentesis id t = OuterToken id (Inner_TA_Parenthesis t)
pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value)
pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3)
pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1)
@@ -256,7 +258,7 @@ pattern T_Subshell id l = OuterToken id (Inner_T_Subshell l)
pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l)
pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l)
{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-}
{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, TA_Parentesis, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-}
instance Eq Token where
OuterToken _ a == OuterToken _ b = a == b

View File

@@ -21,6 +21,7 @@
module ShellCheck.ASTLib where
import ShellCheck.AST
import ShellCheck.Prelude
import ShellCheck.Regex
import Control.Monad.Writer
@@ -138,7 +139,7 @@ getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
flag (x, '-':args) = map (\v -> (x, [v])) args
flag (x, _) = [ (x, "") ]
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
getFlagsUntil _ _ = error $ pleaseReport "getFlags on non-command"
-- Get all flags in a GNU way, up until --
getAllFlags :: Token -> [(Token, String)]
@@ -369,6 +370,21 @@ getGlobOrLiteralString = getLiteralStringExt f
f (T_Glob _ str) = return str
f _ = Nothing
prop_getLiteralString1 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x01") == Just "\1"
prop_getLiteralString2 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\xyz") == Just "\\xyz"
prop_getLiteralString3 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x1") == Just "\x1"
prop_getLiteralString4 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x1y") == Just "\x1y"
prop_getLiteralString5 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\xy") == Just "\\xy"
prop_getLiteralString6 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x") == Just "\\x"
prop_getLiteralString7 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1x") == Just "\1x"
prop_getLiteralString8 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\12x") == Just "\o12x"
prop_getLiteralString9 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\123x") == Just "\o123x"
prop_getLiteralString10 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1234") == Just "\o123\&4"
prop_getLiteralString11 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1") == Just "\1"
prop_getLiteralString12 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\12") == Just "\o12"
prop_getLiteralString13 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\123") == Just "\o123"
-- Maybe get the literal value of a token, using a custom function
-- to map unrecognized Tokens into strings.
getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String
@@ -401,14 +417,15 @@ getLiteralStringExt more = g
'\\' -> '\\' : rest
'x' ->
case cs of
(x:y:more) ->
if isHexDigit x && isHexDigit y
then chr (16*(digitToInt x) + (digitToInt y)) : rest
else '\\':c:rest
(x:y:more) | isHexDigit x && isHexDigit y ->
chr (16*(digitToInt x) + (digitToInt y)) : decodeEscapes more
(x:more) | isHexDigit x ->
chr (digitToInt x) : decodeEscapes more
more -> '\\' : 'x' : decodeEscapes more
_ | isOctDigit c ->
let digits = take 3 $ takeWhile isOctDigit (c:cs)
num = parseOct digits
in (if num < 256 then chr num else '?') : rest
let (digits, more) = spanMax isOctDigit 3 (c:cs)
num = (parseOct digits) `mod` 256
in (chr num) : decodeEscapes more
_ -> '\\' : c : rest
where
rest = decodeEscapes cs
@@ -416,6 +433,11 @@ getLiteralStringExt more = g
where
f n "" = n
f n (c:rest) = f (n * 8 + digitToInt c) rest
spanMax f n list =
let (first, second) = span f list
(prefix, suffix) = splitAt n first
in
(prefix, suffix ++ second)
decodeEscapes (c:cs) = c : decodeEscapes cs
decodeEscapes [] = []
@@ -764,5 +786,122 @@ executableFromShebang = shellFor
basename s = reverse . takeWhile (/= '/') . reverse $ s
skipFlags = dropWhile ("-" `isPrefixOf`)
-- Determining if a name is a variable
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
isVariableChar x = isVariableStartChar x || isDigit x
isSpecialVariableChar = (`elem` "*@#?-$!")
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
prop_isVariableName1 = isVariableName "_fo123"
prop_isVariableName2 = not $ isVariableName "4"
prop_isVariableName3 = not $ isVariableName "test: "
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
isVariableName _ = False
-- Get the variable name from an expansion like ${var:-foo}
prop_getBracedReference1 = getBracedReference "foo" == "foo"
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
prop_getBracedReference3 = getBracedReference "#" == "#"
prop_getBracedReference4 = getBracedReference "##" == "#"
prop_getBracedReference5 = getBracedReference "#!" == "!"
prop_getBracedReference6 = getBracedReference "!#" == "#"
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
prop_getBracedReference10 = getBracedReference "foo: -1" == "foo"
prop_getBracedReference11 = getBracedReference "!os*" == ""
prop_getBracedReference11b = getBracedReference "!os@" == ""
prop_getBracedReference12 = getBracedReference "!os?bar**" == ""
prop_getBracedReference13 = getBracedReference "foo[bar]" == "foo"
getBracedReference s = fromMaybe s $
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
where
noPrefix = dropPrefix s
dropPrefix (c:rest) | c `elem` "!#" = rest
dropPrefix cs = cs
takeName s = do
let name = takeWhile isVariableChar s
guard . not $ null name
return name
getSpecial (c:_) | isSpecialVariableChar c = return [c]
getSpecial _ = fail "empty or not special"
nameExpansion ('!':next:rest) = do -- e.g. ${!foo*bar*}
guard $ isVariableChar next -- e.g. ${!@}
first <- find (not . isVariableChar) rest
guard $ first `elem` "*?@"
return ""
nameExpansion _ = Nothing
-- Get the variable modifier like /a/b in ${var/a/b}
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
prop_getBracedModifier4 = getBracedModifier "foo[@]@Q" == "[@]@Q"
prop_getBracedModifier5 = getBracedModifier "@@Q" == "@Q"
getBracedModifier s = headOrDefault "" $ do
let var = getBracedReference s
a <- dropModifier s
dropPrefix var a
where
dropPrefix [] t = return t
dropPrefix (a:b) (c:d) | a == c = dropPrefix b d
dropPrefix _ _ = []
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
dropModifier x = [x]
-- Get the variables from indices like ["x", "y"] in ${var[x+y+1]}
prop_getIndexReferences1 = getIndexReferences "var[x+y+1]" == ["x", "y"]
getIndexReferences s = fromMaybe [] $ do
match <- matchRegex re s
index <- match !!! 0
return $ matchAllStrings variableNameRegex index
where
re = mkRegex "(\\[.*\\])"
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
getOffsetReferences mods = fromMaybe [] $ do
-- if mods start with [, then drop until ]
match <- matchRegex re mods
offsets <- match !!! 1
return $ matchAllStrings variableNameRegex offsets
where
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
-- Returns whether a token is a parameter expansion without any modifiers.
-- True for $var ${var} $1 $#
-- False for ${#var} ${var[x]} ${var:-0}
isUnmodifiedParameterExpansion t =
case t of
T_DollarBraced _ False _ -> True
T_DollarBraced _ _ list ->
let str = concat $ oversimplify list
in getBracedReference str == str
_ -> False
--- A list of the element and all its parents up to the root node.
getPath tree t = t :
case Map.lookup (getId t) tree of
Nothing -> []
Just parent -> getPath tree parent
isClosingFileOp op =
case op of
T_IoDuplicate _ (T_GREATAND _) "-" -> True
T_IoDuplicate _ (T_LESSAND _) "-" -> True
_ -> False
getEnableDirectives root =
case root of
T_Annotation _ list _ -> [s | EnableComment s <- list]
_ -> []
return []
runTests = $quickCheckAll

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2019 Vidar Holen
Copyright 2012-2022 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -25,6 +25,7 @@ import ShellCheck.Interface
import Data.List
import Data.Monoid
import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.ControlFlow
import qualified ShellCheck.Checks.Custom
import qualified ShellCheck.Checks.ShellSupport
@@ -34,19 +35,21 @@ analyzeScript :: AnalysisSpec -> AnalysisResult
analyzeScript spec = newAnalysisResult {
arComments =
filterByAnnotation spec params . nub $
runAnalytics spec
++ runChecker params (checkers spec params)
runChecker params (checkers spec params)
}
where
params = makeParameters spec
checkers spec params = mconcat $ map ($ params) [
ShellCheck.Analytics.checker spec,
ShellCheck.Checks.Commands.checker spec,
ShellCheck.Checks.ControlFlow.checker spec,
ShellCheck.Checks.Custom.checker,
ShellCheck.Checks.ShellSupport.checker
]
optionalChecks = mconcat $ [
ShellCheck.Analytics.optionalChecks,
ShellCheck.Checks.Commands.optionalChecks
ShellCheck.Checks.Commands.optionalChecks,
ShellCheck.Checks.ControlFlow.optionalChecks
]

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2021 Vidar Holen
Copyright 2012-2022 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -23,13 +23,16 @@ module ShellCheck.AnalyzerLib where
import ShellCheck.AST
import ShellCheck.ASTLib
import qualified ShellCheck.CFGAnalysis as CF
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Prelude
import ShellCheck.Regex
import Control.Arrow (first)
import Control.DeepSeq
import Control.Monad
import Control.Monad.Identity
import Control.Monad.RWS
import Control.Monad.State
@@ -87,6 +90,8 @@ data Parameters = Parameters {
hasPipefail :: Bool,
-- A linear (bad) analysis of data flow
variableFlow :: [StackData],
-- A map from Id to Token
idMap :: Map.Map Id Token,
-- A map from Id to parent Token
parentMap :: Map.Map Id Token,
-- The shell type, such as Bash or Ksh
@@ -96,7 +101,9 @@ data Parameters = Parameters {
-- The root node of the AST
rootNode :: Token,
-- 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)
cfgAnalysis :: CF.CFGAnalysis
} deriving (Show)
-- TODO: Cache results of common AST ops here
@@ -189,35 +196,42 @@ makeCommentWithFix severity id code str fix =
}
in force withFix
makeParameters spec =
let params = Parameters {
makeParameters spec = params
where
params = Parameters {
rootNode = root,
shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec,
hasSetE = containsSetE root,
hasLastpipe =
case shellType params of
Bash -> containsLastpipe root
Bash -> isOptionSet "lastpipe" root
Dash -> False
Sh -> False
Ksh -> True,
hasInheritErrexit =
case shellType params of
Bash -> containsInheritErrexit root
Bash -> isOptionSet "inherit_errexit" root
Dash -> True
Sh -> True
Ksh -> False,
hasPipefail =
case shellType params of
Bash -> containsPipefail root
Bash -> isOptionSet "pipefail" root
Dash -> True
Sh -> True
Ksh -> containsPipefail root,
Ksh -> isOptionSet "pipefail" root,
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
idMap = getTokenMap root,
parentMap = getParentTree root,
variableFlow = getVariableFlow params root,
tokenPositions = asTokenPositions spec
} in params
where root = asScript spec
tokenPositions = asTokenPositions spec,
cfgAnalysis = CF.analyzeControlFlow cfParams root
}
cfParams = CF.CFGParameters {
CF.cfLastpipe = hasLastpipe params,
CF.cfPipefail = hasPipefail params
}
root = asScript spec
-- Does this script mention 'set -e' anywhere?
@@ -234,13 +248,14 @@ containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
_ -> False
re = mkRegex "[[:space:]]-[^-]*e"
containsPipefail root = isNothing $ doAnalysis (guard . not . isPipefail) root
containsSetOption opt root = isNothing $ doAnalysis (guard . not . isPipefail) root
where
isPipefail t =
case t of
T_SimpleCommand {} ->
t `isUnqualifiedCommand` "set" &&
("pipefail" `elem` oversimplify t ||
(opt `elem` oversimplify t ||
"o" `elem` map snd (getAllFlags t))
_ -> False
@@ -254,12 +269,8 @@ containsShopt shopt root =
(shopt `elem` oversimplify t)
_ -> False
-- Does this script mention 'shopt -s inherit_errexit' anywhere?
containsInheritErrexit = containsShopt "inherit_errexit"
-- Does this script mention 'shopt -s lastpipe' anywhere?
-- Also used as a hack.
containsLastpipe = containsShopt "lastpipe"
-- Does this script mention 'shopt -s $opt' or 'set -o $opt' anywhere?
isOptionSet opt root = containsShopt opt root || containsSetOption opt root
prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh
@@ -351,7 +362,7 @@ isQuoteFreeNode strict shell tree t =
T_SelectIn {} -> return (not strict)
_ -> Nothing
-- Check whether this assigment 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
-- by POSIX: https://austingroupbugs.net/view.php?id=351
assignmentIsQuoting t = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand t)
@@ -408,12 +419,6 @@ usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
getId word == currentId || getId (getCommandTokenOrThis t) == currentId
go _ _ = False
-- A list of the element and all its parents up to the root node.
getPath tree t = t :
case Map.lookup (getId t) tree of
Nothing -> []
Just parent -> getPath tree parent
-- Version of the above taking the map from the current context
-- Todo: give this the name "getPath"
getPathM t = do
@@ -511,13 +516,11 @@ getModifiedVariables t =
T_SimpleCommand {} ->
getModifiedVariableCommand t
TA_Unary _ "++|" v@(TA_Variable _ name _) ->
[(t, v, name, DataString $ SourceFrom [v])]
TA_Unary _ "|++" v@(TA_Variable _ name _) ->
[(t, v, name, DataString $ SourceFrom [v])]
TA_Unary _ op v@(TA_Variable _ name _) | "--" `isInfixOf` op || "++" `isInfixOf` op ->
[(t, v, name, DataString SourceInteger)]
TA_Assignment _ op (TA_Variable _ name _) rhs -> do
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
return (t, t, name, DataString $ SourceFrom [rhs])
return (t, t, name, DataString SourceInteger)
T_BatsTest {} -> [
(t, t, "lines", DataArray SourceExternal),
@@ -561,12 +564,6 @@ getModifiedVariables t =
return (place, t, str, DataString SourceChecked)
_ -> Nothing
isClosingFileOp op =
case op of
T_IoDuplicate _ (T_GREATAND _) "-" -> True
T_IoDuplicate _ (T_LESSAND _) "-" -> True
_ -> False
-- Consider 'export/declare -x' a reference, since it makes the var available
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
@@ -748,13 +745,6 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
getModifiedVariableCommand _ = []
getIndexReferences s = fromMaybe [] $ do
match <- matchRegex re s
index <- match !!! 0
return $ matchAllStrings variableNameRegex index
where
re = mkRegex "(\\[.*\\])"
-- Given a NormalWord like foo or foo[$bar], get foo.
-- Primarily used to get references for [[ -v foo[bar] ]]
getVariableForTestDashV :: Token -> Maybe String
@@ -769,18 +759,6 @@ getVariableForTestDashV t = do
-- in a non-constant expression (while filtering out foo$x[$y])
toStr _ = return "\0"
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
getOffsetReferences mods = fromMaybe [] $ do
-- if mods start with [, then drop until ]
match <- matchRegex re mods
offsets <- match !!! 1
return $ matchAllStrings variableNameRegex offsets
where
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
getReferencedVariables parents t =
case t of
T_DollarBraced id _ l -> let str = concat $ oversimplify l in
@@ -859,17 +837,6 @@ isConfusedGlobRegex ('*':_) = True
isConfusedGlobRegex [x,'*'] | x `notElem` "\\." = True
isConfusedGlobRegex _ = False
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
isVariableChar x = isVariableStartChar x || isDigit x
isSpecialVariableChar = (`elem` "*@#?-$!")
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
prop_isVariableName1 = isVariableName "_fo123"
prop_isVariableName2 = not $ isVariableName "4"
prop_isVariableName3 = not $ isVariableName "test: "
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
isVariableName _ = False
getVariablesFromLiteralToken token =
getVariablesFromLiteral (getLiteralStringDef " " token)
@@ -882,73 +849,6 @@ getVariablesFromLiteral string =
where
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
-- Get the variable name from an expansion like ${var:-foo}
prop_getBracedReference1 = getBracedReference "foo" == "foo"
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
prop_getBracedReference3 = getBracedReference "#" == "#"
prop_getBracedReference4 = getBracedReference "##" == "#"
prop_getBracedReference5 = getBracedReference "#!" == "!"
prop_getBracedReference6 = getBracedReference "!#" == "#"
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
prop_getBracedReference11= getBracedReference "!os*" == ""
prop_getBracedReference11b= getBracedReference "!os@" == ""
prop_getBracedReference12= getBracedReference "!os?bar**" == ""
prop_getBracedReference13= getBracedReference "foo[bar]" == "foo"
getBracedReference s = fromMaybe s $
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
where
noPrefix = dropPrefix s
dropPrefix (c:rest) | c `elem` "!#" = rest
dropPrefix cs = cs
takeName s = do
let name = takeWhile isVariableChar s
guard . not $ null name
return name
getSpecial (c:_) | isSpecialVariableChar c = return [c]
getSpecial _ = fail "empty or not special"
nameExpansion ('!':next:rest) = do -- e.g. ${!foo*bar*}
guard $ isVariableChar next -- e.g. ${!@}
first <- find (not . isVariableChar) rest
guard $ first `elem` "*?@"
return ""
nameExpansion _ = Nothing
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
prop_getBracedModifier4 = getBracedModifier "foo[@]@Q" == "[@]@Q"
prop_getBracedModifier5 = getBracedModifier "@@Q" == "@Q"
getBracedModifier s = headOrDefault "" $ do
let var = getBracedReference s
a <- dropModifier s
dropPrefix var a
where
dropPrefix [] t = return t
dropPrefix (a:b) (c:d) | a == c = dropPrefix b d
dropPrefix _ _ = []
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
dropModifier x = [x]
-- Useful generic functions.
-- Get element 0 or a default. Like `head` but safe.
headOrDefault _ (a:_) = a
headOrDefault def _ = def
-- Get the last element or a default. Like `last` but safe.
lastOrDefault def [] = def
lastOrDefault _ list = last list
--- Get element n of a list, or Nothing. Like `!!` but safe.
(!!!) list i =
case drop i list of
[] -> Nothing
(r:_) -> Just r
-- Run a command if the shell is in the given list
whenShell l c = do
@@ -1001,17 +901,6 @@ isBashLike params =
Dash -> False
Sh -> False
-- Returns whether a token is a parameter expansion without any modifiers.
-- True for $var ${var} $1 $#
-- False for ${#var} ${var[x]} ${var:-0}
isUnmodifiedParameterExpansion t =
case t of
T_DollarBraced _ False _ -> True
T_DollarBraced _ _ list ->
let str = concat $ oversimplify list
in getBracedReference str == str
_ -> False
isTrueAssignmentSource c =
case c of
DataString SourceChecked -> False

1307
src/ShellCheck/CFG.hs Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2020 Vidar Holen
Copyright 2012-2022 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -20,9 +20,10 @@
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
import ShellCheck.Analyzer
import ShellCheck.ASTLib
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Analyzer
import Data.Either
import Data.Functor
@@ -85,7 +86,7 @@ checkScript sys spec = do
asCheckSourced = csCheckSourced spec,
asExecutionMode = Executed,
asTokenPositions = tokenPositions,
asOptionalChecks = csOptionalChecks spec
asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec
} where as = newAnalysisSpec root
let analysisMessages =
maybe []
@@ -243,6 +244,9 @@ prop_canStripPrefixAndSource2 =
prop_canSourceDynamicWhenRedirected =
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
prop_canRedirectWithSpaces =
null $ checkWithIncludes [("my file", "")] "#shellcheck source=\"my file\"\n. \"$1\""
prop_recursiveAnalysis =
[2086] == checkRecursive [("lib", "echo $1")] "source lib"
@@ -412,6 +416,15 @@ prop_sourcePathAddsAnnotation = result == [2086]
csCheckSourced = True
}
prop_sourcePathWorksWithSpaces = result == [2086]
where
f "dir/myscript" _ ["my path"] "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source-path='my path'\nsource lib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_sourcePathRedirectsDirective = result == [2086]
where
f "dir/myscript" _ _ "lib" = return "foo/lib"
@@ -483,6 +496,17 @@ prop_fileCannotEnableExternalSources2 = result == [1144]
csCheckSourced = True
}
prop_rcCanSuppressEarlyProblems1 = null result
where
result = checkWithRc "disable=1071" emptyCheckSpec {
csScript = "#!/bin/zsh\necho $1"
}
prop_rcCanSuppressEarlyProblems2 = null result
where
result = checkWithRc "disable=1104" emptyCheckSpec {
csScript = "!/bin/bash\necho 'hello world'"
}
return []
runTests = $quickCheckAll

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2021 Vidar Holen
Copyright 2012-2022 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -27,21 +27,28 @@ module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Co
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.AnalyzerLib
import ShellCheck.CFG
import qualified ShellCheck.CFGAnalysis as CF
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Prelude
import ShellCheck.Regex
import Control.Monad
import Control.Monad.RWS
import Data.Char
import Data.Functor.Identity
import qualified Data.Graph.Inductive.Graph as G
import Data.List
import Data.Maybe
import qualified Data.Map.Strict as Map
import qualified Data.Map.Strict as M
import qualified Data.Set as S
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
import Debug.Trace -- STRIP
data CommandName = Exactly String | Basename String
deriving (Eq, Ord)
@@ -98,8 +105,10 @@ commandChecks = [
,checkUnquotedEchoSpaces
,checkEvalArray
]
++ map checkArgComparison declaringCommands
++ map checkArgComparison ("alias" : declaringCommands)
++ map checkMaskedReturns declaringCommands
++ map checkMultipleDeclaring declaringCommands
++ map checkBackreferencingDeclaration declaringCommands
optionalChecks = map fst optionalCommandChecks
@@ -112,7 +121,7 @@ optionalCommandChecks = [
cdNegative = "command -v javac"
}, checkWhich)
]
optionalCheckMap = Map.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks
optionalCheckMap = M.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks
prop_verifyOptionalExamples = all check optionalCommandChecks
where
@@ -161,27 +170,27 @@ prop_checkGenericOptsT1 = checkGetOpts "-x -- -y" ["x"] ["-y"] $ return . getGen
prop_checkGenericOptsT2 = checkGetOpts "-xy --" ["x", "y"] [] $ return . getGenericOpts
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
buildCommandMap = foldl' addCheck Map.empty
buildCommandMap :: [CommandCheck] -> M.Map CommandName (Token -> Analysis)
buildCommandMap = foldl' addCheck M.empty
where
addCheck map (CommandCheck name function) =
Map.insertWith composeAnalyzers name function map
M.insertWith composeAnalyzers name function map
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
checkCommand :: M.Map CommandName (Token -> Analysis) -> Token -> Analysis
checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do
name <- getLiteralString cmd
return $
if '/' `elem` name
then
Map.findWithDefault nullCheck (Basename $ basename name) map t
M.findWithDefault nullCheck (Basename $ basename name) map t
else if name == "builtin" && not (null rest) then
let t' = T_SimpleCommand id cmdPrefix rest
selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest
in Map.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
else do
Map.findWithDefault nullCheck (Exactly name) map t
Map.findWithDefault nullCheck (Basename name) map t
M.findWithDefault nullCheck (Exactly name) map t
M.findWithDefault nullCheck (Basename name) map t
where
basename = reverse . takeWhile (/= '/') . reverse
@@ -203,22 +212,22 @@ checker spec params = getChecker $ commandChecks ++ optionals
optionals =
if "all" `elem` keys
then map snd optionalCommandChecks
else mapMaybe (\x -> Map.lookup x optionalCheckMap) keys
else mapMaybe (\x -> M.lookup x optionalCheckMap) keys
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'"
prop_checkTr2a = verify checkTr "tr '[a-z]' '[A-Z]'"
prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'"
prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'"
prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'"
prop_checkTr3a = verifyNot checkTr "tr -d '[:upper:]'"
prop_checkTr3b = verifyNot checkTr "tr -d '|/_[:upper:]'"
prop_checkTr4 = verifyNot checkTr "ls [a-z]"
prop_checkTr5 = verify checkTr "tr foo bar"
prop_checkTr6 = verify checkTr "tr 'hello' 'world'"
prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
prop_checkTr12= verifyNot checkTr "tr '[=e=]' 'e'"
prop_checkTr10 = verifyNot checkTr "tr --squeeze-repeats rl lr"
prop_checkTr11 = verifyNot checkTr "tr abc '[d*]'"
prop_checkTr12 = verifyNot checkTr "tr '[=e=]' 'e'"
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
where
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
@@ -330,20 +339,20 @@ prop_checkGrepRe6 = verifyNot checkGrepRe "grep foo \\*.mp3"
prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file"
prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg"
prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"
prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file"
prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo"
prop_checkGrepRe12= verifyNot checkGrepRe "grep -F 'Foo*' file"
prop_checkGrepRe13= verifyNot checkGrepRe "grep -- -foo bar*"
prop_checkGrepRe14= verifyNot checkGrepRe "grep -e -foo bar*"
prop_checkGrepRe15= verifyNot checkGrepRe "grep --regex -foo bar*"
prop_checkGrepRe16= verifyNot checkGrepRe "grep --include 'Foo*' file"
prop_checkGrepRe17= verifyNot checkGrepRe "grep --exclude 'Foo*' file"
prop_checkGrepRe18= verifyNot checkGrepRe "grep --exclude-dir 'Foo*' file"
prop_checkGrepRe19= verify checkGrepRe "grep -- 'Foo*' file"
prop_checkGrepRe20= verifyNot checkGrepRe "grep --fixed-strings 'Foo*' file"
prop_checkGrepRe21= verifyNot checkGrepRe "grep -o 'x*' file"
prop_checkGrepRe22= verifyNot checkGrepRe "grep --only-matching 'x*' file"
prop_checkGrepRe23= verifyNot checkGrepRe "grep '.*' file"
prop_checkGrepRe10 = verifyNot checkGrepRe "grep '^aa*' file"
prop_checkGrepRe11 = verifyNot checkGrepRe "grep --include=*.png foo"
prop_checkGrepRe12 = verifyNot checkGrepRe "grep -F 'Foo*' file"
prop_checkGrepRe13 = verifyNot checkGrepRe "grep -- -foo bar*"
prop_checkGrepRe14 = verifyNot checkGrepRe "grep -e -foo bar*"
prop_checkGrepRe15 = verifyNot checkGrepRe "grep --regex -foo bar*"
prop_checkGrepRe16 = verifyNot checkGrepRe "grep --include 'Foo*' file"
prop_checkGrepRe17 = verifyNot checkGrepRe "grep --exclude 'Foo*' file"
prop_checkGrepRe18 = verifyNot checkGrepRe "grep --exclude-dir 'Foo*' file"
prop_checkGrepRe19 = verify checkGrepRe "grep -- 'Foo*' file"
prop_checkGrepRe20 = verifyNot checkGrepRe "grep --fixed-strings 'Foo*' file"
prop_checkGrepRe21 = verifyNot checkGrepRe "grep -o 'x*' file"
prop_checkGrepRe22 = verifyNot checkGrepRe "grep --only-matching 'x*' file"
prop_checkGrepRe23 = verifyNot checkGrepRe "grep '.*' file"
checkGrepRe = CommandCheck (Basename "grep") check where
check cmd = f cmd (arguments cmd)
@@ -391,7 +400,7 @@ checkGrepRe = CommandCheck (Basename "grep") check where
prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT"
prop_checkTrapQuotes1a = verify checkTrapQuotes "trap \"echo `ls`\" INT"
prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT"
prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
@@ -472,9 +481,16 @@ prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\n
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
prop_checkUnusedEchoEscapes6 = verify checkUnusedEchoEscapes "echo '\\506'"
prop_checkUnusedEchoEscapes7 = verify checkUnusedEchoEscapes "echo '\\5a'"
prop_checkUnusedEchoEscapes8 = verifyNot checkUnusedEchoEscapes "echo '\\8a'"
prop_checkUnusedEchoEscapes9 = verifyNot checkUnusedEchoEscapes "echo '\\d5a'"
prop_checkUnusedEchoEscapes10 = verify checkUnusedEchoEscapes "echo '\\x4a'"
prop_checkUnusedEchoEscapes11 = verify checkUnusedEchoEscapes "echo '\\xat'"
prop_checkUnusedEchoEscapes12 = verifyNot checkUnusedEchoEscapes "echo '\\xth'"
checkUnusedEchoEscapes = CommandCheck (Basename "echo") f
where
hasEscapes = mkRegex "\\\\[rnt]"
hasEscapes = mkRegex "\\\\([rntabefv\\']|[0-7]{1,3}|x([0-9]|[A-F]|[a-f]){1,2})"
f cmd =
whenShell [Sh, Bash, Ksh] $
unless (cmd `hasFlag` "e") $
@@ -648,19 +664,19 @@ prop_checkPrintfVar6 = verify checkPrintfVar "printf foo bar baz"
prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz"
prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\""
prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
prop_checkPrintfVar11= verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
prop_checkPrintfVar12= verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1"
prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
prop_checkPrintfVar16= verifyNot checkPrintfVar "printf $'string'"
prop_checkPrintfVar17= verify checkPrintfVar "printf '%-*s\\n' 1"
prop_checkPrintfVar18= verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
prop_checkPrintfVar19= verifyNot checkPrintfVar "printf '%(%s)T'"
prop_checkPrintfVar20= verifyNot checkPrintfVar "printf '%d %(%s)T' 42"
prop_checkPrintfVar21= verify checkPrintfVar "printf '%d %(%s)T'"
prop_checkPrintfVar22= verify checkPrintfVar "printf '%s\n%s' foo"
prop_checkPrintfVar10 = verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
prop_checkPrintfVar11 = verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
prop_checkPrintfVar12 = verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
prop_checkPrintfVar13 = verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
prop_checkPrintfVar14 = verify checkPrintfVar "printf '%*s\\n' 1"
prop_checkPrintfVar15 = verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
prop_checkPrintfVar16 = verifyNot checkPrintfVar "printf $'string'"
prop_checkPrintfVar17 = verify checkPrintfVar "printf '%-*s\\n' 1"
prop_checkPrintfVar18 = verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
prop_checkPrintfVar19 = verifyNot checkPrintfVar "printf '%(%s)T'"
prop_checkPrintfVar20 = verifyNot checkPrintfVar "printf '%d %(%s)T' 42"
prop_checkPrintfVar21 = verify checkPrintfVar "printf '%d %(%s)T'"
prop_checkPrintfVar22 = verify checkPrintfVar "printf '%s\n%s' foo"
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
@@ -674,6 +690,7 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
let formats = getPrintfFormats string
let formatCount = length formats
let argCount = length more
let pluraliseIfMany word n = if n > 1 then word ++ "s" else word
return $ if
| argCount == 0 && formatCount == 0 ->
@@ -689,7 +706,8 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
return () -- Great: a suitable number of arguments
| otherwise ->
warn (getId format) 2183 $
"This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments."
"This format string has " ++ show formatCount ++ " " ++ pluraliseIfMany "variable" formatCount ++
", but is passed " ++ show argCount ++ pluraliseIfMany " argument" argCount ++ "."
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
info (getId format) 2059
@@ -735,7 +753,7 @@ getPrintfFormats = getFormats
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \______ /
-- V V V V V
-- flags field width precision format character rest
-- field width and precision can be specified with a '*' instead of a digit,
-- field width and precision can be specified with an '*' instead of a digit,
-- in which case printf will accept one more argument for each '*' used
@@ -941,6 +959,22 @@ checkLocalScope = CommandCheck (Exactly "local") $ \t ->
unless (any isFunctionLike path) $
err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
prop_checkMultipleDeclaring1 = verify (checkMultipleDeclaring "local") "q() { local readonly var=1; }"
prop_checkMultipleDeclaring2 = verifyNot (checkMultipleDeclaring "local") "q() { local var=1; }"
prop_checkMultipleDeclaring3 = verify (checkMultipleDeclaring "readonly") "readonly local foo=5"
prop_checkMultipleDeclaring4 = verify (checkMultipleDeclaring "export") "export readonly foo=5"
prop_checkMultipleDeclaring5 = verifyNot (checkMultipleDeclaring "local") "f() { local -r foo=5; }"
prop_checkMultipleDeclaring6 = verifyNot (checkMultipleDeclaring "declare") "declare -rx foo=5"
prop_checkMultipleDeclaring7 = verifyNot (checkMultipleDeclaring "readonly") "readonly 'local' foo=5"
checkMultipleDeclaring cmd = CommandCheck (Exactly cmd) (mapM_ check . arguments)
where
check t = sequence_ $ do
lit <- getUnquotedLiteral t
guard $ lit `elem` declaringCommands
return $ err (getId $ getCommandTokenOrThis t) 2316 $
"This applies " ++ cmd ++ " to the variable named " ++ lit ++
", which is probably not what you want. Use a separate command or the appropriate `declare` options instead."
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
@@ -987,20 +1021,20 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
check :: Id -> [String] -> Token -> Analysis
check optId opts (T_CaseExpression id _ list) = do
unless (Nothing `Map.member` handledMap) $ do
mapM_ (warnUnhandled optId id) $ catMaybes $ Map.keys notHandled
unless (Nothing `M.member` handledMap) $ do
mapM_ (warnUnhandled optId id) $ catMaybes $ M.keys notHandled
unless (any (`Map.member` handledMap) [Just "*",Just "?"]) $
unless (any (`M.member` handledMap) [Just "*",Just "?"]) $
warn id 2220 "Invalid flags are not handled. Add a *) case."
mapM_ warnRedundant $ Map.toList notRequested
mapM_ warnRedundant $ M.toList notRequested
where
handledMap = Map.fromList (concatMap getHandledStrings list)
requestedMap = Map.fromList $ map (\x -> (Just x, ())) opts
handledMap = M.fromList (concatMap getHandledStrings list)
requestedMap = M.fromList $ map (\x -> (Just x, ())) opts
notHandled = Map.difference requestedMap handledMap
notRequested = Map.difference handledMap requestedMap
notHandled = M.difference requestedMap handledMap
notRequested = M.difference handledMap requestedMap
warnUnhandled optId caseId str =
warn caseId 2213 $ "getopts specified -" ++ (e4m str) ++ ", but it's not handled by this 'case'."
@@ -1044,10 +1078,10 @@ prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*
prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"
prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*"
prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home"
prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"
prop_checkCatastrophicRm13= verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"
prop_checkCatastrophicRm10 = verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
prop_checkCatastrophicRm11 = verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
prop_checkCatastrophicRm12 = verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"
prop_checkCatastrophicRm13 = verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"
prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"
prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"
checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
@@ -1253,6 +1287,7 @@ prop_checkArgComparison3 = verifyNot (checkArgComparison "declare") "declare a=b
prop_checkArgComparison4 = verify (checkArgComparison "export") "export a +=b"
prop_checkArgComparison7 = verifyNot (checkArgComparison "declare") "declare -a +i foo"
prop_checkArgComparison8 = verify (checkArgComparison "let") "let x = 0"
prop_checkArgComparison9 = verify (checkArgComparison "alias") "alias x =0"
-- This mirrors checkSecondArgIsComparison but for arguments to local/readonly/declare/export
checkArgComparison cmd = CommandCheck (Exactly cmd) wordsWithEqual
where
@@ -1353,10 +1388,10 @@ checkUnquotedEchoSpaces = CommandCheck (Basename "echo") check
m <- asks tokenPositions
redir <- getClosestCommandM t
sequence_ $ do
let positions = mapMaybe (\c -> Map.lookup (getId c) m) args
let positions = mapMaybe (\c -> M.lookup (getId c) m) args
let pairs = zip positions (drop 1 positions)
(T_Redirecting _ redirTokens _) <- redir
let redirPositions = mapMaybe (\c -> fst <$> Map.lookup (getId c) m) redirTokens
let redirPositions = mapMaybe (\c -> fst <$> M.lookup (getId c) m) redirTokens
guard $ any (hasSpacesBetween redirPositions) pairs
return $ info (getId t) 2291 "Quote repeated spaces to avoid them collapsing into one."
@@ -1386,5 +1421,51 @@ checkEvalArray = CommandCheck (Exactly "eval") (mapM_ check . concatMap getWordP
_ -> False
prop_checkBackreferencingDeclaration1 = verify (checkBackreferencingDeclaration "declare") "declare x=1 y=foo$x"
prop_checkBackreferencingDeclaration2 = verify (checkBackreferencingDeclaration "readonly") "readonly x=1 y=$((1+x))"
prop_checkBackreferencingDeclaration3 = verify (checkBackreferencingDeclaration "local") "local x=1 y=$(echo $x)"
prop_checkBackreferencingDeclaration4 = verify (checkBackreferencingDeclaration "local") "local x=1 y[$x]=z"
prop_checkBackreferencingDeclaration5 = verify (checkBackreferencingDeclaration "declare") "declare x=var $x=1"
prop_checkBackreferencingDeclaration6 = verify (checkBackreferencingDeclaration "declare") "declare x=var $x=1"
prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration "declare") "declare x=var $k=$x"
checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check
where
check t = foldM_ perArg M.empty $ arguments t
perArg leftArgs t =
case t of
T_Assignment id _ name idx t -> do
warnIfBackreferencing leftArgs $ t:idx
return $ M.insert name id leftArgs
t -> do
warnIfBackreferencing leftArgs [t]
return leftArgs
warnIfBackreferencing backrefs l = do
references <- findReferences l
let reused = M.intersection backrefs references
mapM msg $ M.toList reused
msg (name, id) = warn id 2318 $ "This assignment is used again in this '" ++ cmd ++ "', but won't have taken effect. Use two '" ++ cmd ++ "'s."
findReferences list = do
cfga <- asks cfgAnalysis
let graph = CF.graph cfga
let nodesMap = CF.tokenToNodes cfga
let nodes = S.unions $ map (\id -> M.findWithDefault S.empty id nodesMap) $ map getId $ list
let labels = mapMaybe (G.lab graph) $ S.toList nodes
let references = M.fromList $ concatMap refFromLabel labels
return references
refFromLabel lab =
case lab of
CFApplyEffects effects -> mapMaybe refFromEffect effects
_ -> []
refFromEffect e =
case e of
IdTagged id (CFReadVariable name) -> return (name, id)
_ -> Nothing
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -0,0 +1,101 @@
{-
Copyright 2022 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
-- Checks that run on the Control Flow Graph (as opposed to the AST)
-- This is scaffolding for a work in progress.
module ShellCheck.Checks.ControlFlow (checker, optionalChecks, ShellCheck.Checks.ControlFlow.runTests) where
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.CFG hiding (cfgAnalysis)
import ShellCheck.CFGAnalysis
import ShellCheck.AnalyzerLib
import ShellCheck.Data
import ShellCheck.Interface
import Control.Monad
import Control.Monad.Reader
import Data.Graph.Inductive.Graph
import qualified Data.Map as M
import qualified Data.Set as S
import Data.List
import Data.Maybe
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
optionalChecks :: [CheckDescription]
optionalChecks = []
-- A check that runs on the entire graph
type ControlFlowCheck = Analysis
-- A check invoked once per node, with its (pre,post) data
type ControlFlowNodeCheck = LNode CFNode -> (ProgramState, ProgramState) -> Analysis
-- A check invoked once per effect, with its node's (pre,post) data
type ControlFlowEffectCheck = IdTagged CFEffect -> Node -> (ProgramState, ProgramState) -> Analysis
checker :: AnalysisSpec -> Parameters -> Checker
checker spec params = Checker {
perScript = const $ sequence_ controlFlowChecks,
perToken = const $ return ()
}
controlFlowChecks :: [ControlFlowCheck]
controlFlowChecks = [
runNodeChecks controlFlowNodeChecks
]
controlFlowNodeChecks :: [ControlFlowNodeCheck]
controlFlowNodeChecks = [
runEffectChecks controlFlowEffectChecks
]
controlFlowEffectChecks :: [ControlFlowEffectCheck]
controlFlowEffectChecks = [
]
runNodeChecks :: [ControlFlowNodeCheck] -> ControlFlowCheck
runNodeChecks perNode = do
cfg <- asks cfgAnalysis
runOnAll cfg
where
getData datas n@(node, label) = do
(pre, post) <- M.lookup node datas
return (n, (pre, post))
runOn :: (LNode CFNode, (ProgramState, ProgramState)) -> Analysis
runOn (node, prepost) = mapM_ (\c -> c node prepost) perNode
runOnAll cfg = mapM_ runOn $ mapMaybe (getData $ nodeToData cfg) $ labNodes (graph cfg)
runEffectChecks :: [ControlFlowEffectCheck] -> ControlFlowNodeCheck
runEffectChecks list = checkNode
where
checkNode (node, label) prepost =
case label of
CFApplyEffects effects -> mapM_ (\effect -> mapM_ (\c -> c effect node prepost) list) effects
_ -> return ()
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -25,6 +25,7 @@ import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.AnalyzerLib
import ShellCheck.Interface
import ShellCheck.Prelude
import ShellCheck.Regex
import Control.Monad
@@ -91,55 +92,57 @@ prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}"
prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}"
prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}"
prop_checkBashisms13= verify checkBashisms "exec -c env"
prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \""
prop_checkBashisms15= verify checkBashisms "let n++"
prop_checkBashisms16= verify checkBashisms "echo $RANDOM"
prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))"
prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"
prop_checkBashisms19= verify checkBashisms "foo > file*.txt"
prop_checkBashisms20= verify checkBashisms "read -ra foo"
prop_checkBashisms21= verify checkBashisms "[ -a foo ]"
prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]"
prop_checkBashisms23= verify checkBashisms "trap mything ERR INT"
prop_checkBashisms24= verifyNot checkBashisms "trap mything INT TERM"
prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123"
prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM"
prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*"
prop_checkBashisms28= verify checkBashisms "exec {n}>&2"
prop_checkBashisms29= verify checkBashisms "echo ${!var}"
prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\""
prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\""
prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo"
prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo"
prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo"
prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM"
prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }"
prop_checkBashisms40= verify checkBashisms "echo $(<file)"
prop_checkBashisms41= verify checkBashisms "echo `<file`"
prop_checkBashisms42= verify checkBashisms "trap foo int"
prop_checkBashisms43= verify checkBashisms "trap foo sigint"
prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"
prop_checkBashisms55= verify checkBashisms "#!/bin/sh\necho ${@%foo}"
prop_checkBashisms56= verifyNot checkBashisms "#!/bin/sh\necho ${##}"
prop_checkBashisms57= verifyNot checkBashisms "#!/bin/dash\nulimit -c 0"
prop_checkBashisms58= verify checkBashisms "#!/bin/sh\nulimit -c 0"
prop_checkBashisms10 = verify checkBashisms "echo ${var:4:12}"
prop_checkBashisms11 = verifyNot checkBashisms "echo ${var:-4}"
prop_checkBashisms12 = verify checkBashisms "echo ${var//foo/bar}"
prop_checkBashisms13 = verify checkBashisms "exec -c env"
prop_checkBashisms14 = verify checkBashisms "echo -n \"Foo: \""
prop_checkBashisms15 = verify checkBashisms "let n++"
prop_checkBashisms16 = verify checkBashisms "echo $RANDOM"
prop_checkBashisms17 = verify checkBashisms "echo $((RANDOM%6+1))"
prop_checkBashisms18 = verify checkBashisms "foo &> /dev/null"
prop_checkBashisms19 = verify checkBashisms "foo > file*.txt"
prop_checkBashisms20 = verify checkBashisms "read -ra foo"
prop_checkBashisms21 = verify checkBashisms "[ -a foo ]"
prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]"
prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT"
prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM"
prop_checkBashisms25 = verify checkBashisms "cat < /dev/tcp/host/123"
prop_checkBashisms26 = verify checkBashisms "trap mything ERR SIGTERM"
prop_checkBashisms27 = verify checkBashisms "echo *[^0-9]*"
prop_checkBashisms28 = verify checkBashisms "exec {n}>&2"
prop_checkBashisms29 = verify checkBashisms "echo ${!var}"
prop_checkBashisms30 = verify checkBashisms "printf -v '%s' \"$1\""
prop_checkBashisms31 = verify checkBashisms "printf '%q' \"$1\""
prop_checkBashisms32 = verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
prop_checkBashisms33 = verify checkBashisms "#!/bin/sh\necho -n foo"
prop_checkBashisms34 = verifyNot checkBashisms "#!/bin/dash\necho -n foo"
prop_checkBashisms35 = verifyNot checkBashisms "#!/bin/dash\nlocal foo"
prop_checkBashisms36 = verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
prop_checkBashisms37 = verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
prop_checkBashisms38 = verify checkBashisms "RANDOM=9; echo $RANDOM"
prop_checkBashisms39 = verify checkBashisms "foo-bar() { true; }"
prop_checkBashisms40 = verify checkBashisms "echo $(<file)"
prop_checkBashisms41 = verify checkBashisms "echo `<file`"
prop_checkBashisms42 = verify checkBashisms "trap foo int"
prop_checkBashisms43 = verify checkBashisms "trap foo sigint"
prop_checkBashisms44 = verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
prop_checkBashisms45 = verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
prop_checkBashisms46 = verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
prop_checkBashisms47 = verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
prop_checkBashisms48 = verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
prop_checkBashisms49 = verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
prop_checkBashisms50 = verify checkBashisms "#!/bin/sh\ncmd >& file"
prop_checkBashisms51 = verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
prop_checkBashisms52 = verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
prop_checkBashisms52b = verifyNot checkBashisms "#!/bin/sh\ncmd >& $var"
prop_checkBashisms52c = verify checkBashisms "#!/bin/sh\ncmd >& $dir/$var"
prop_checkBashisms53 = verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
prop_checkBashisms54 = verify checkBashisms "#!/bin/sh\nfoo+=bar"
prop_checkBashisms55 = verify checkBashisms "#!/bin/sh\necho ${@%foo}"
prop_checkBashisms56 = verifyNot checkBashisms "#!/bin/sh\necho ${##}"
prop_checkBashisms57 = verifyNot checkBashisms "#!/bin/dash\nulimit -c 0"
prop_checkBashisms58 = verify checkBashisms "#!/bin/sh\nulimit -c 0"
prop_checkBashisms59 = verify checkBashisms "#!/bin/sh\njobs -s"
prop_checkBashisms60 = verifyNot checkBashisms "#!/bin/sh\njobs -p"
prop_checkBashisms61 = verifyNot checkBashisms "#!/bin/sh\njobs -lp"
@@ -224,7 +227,8 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
warnMsg id 3018 $ filter (/= '|') op ++ " is"
bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id 3020 "&> is"
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id 3021 ">& is"
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) file)) =
unless (all isDigit $ onlyLiteralString file) $ warnMsg id 3021 ">& filename (as opposed to >& fd) is"
bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are"
bashism (T_FdRedirect id num _)
| all isDigit num && length num > 1 = warnMsg id 3023 "FDs outside 0-9 are"
@@ -440,9 +444,9 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
_ -> False
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
prop_checkEchoSed1b= verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")"
prop_checkEchoSed1b = verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")"
prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
prop_checkEchoSed2b= verify checkEchoSed "rm $(sed -e 's,foo,bar,' <<< $cow)"
prop_checkEchoSed2b = verify checkEchoSed "rm $(sed -e 's,foo,bar,' <<< $cow)"
checkEchoSed = ForShell [Bash, Ksh] f
where
f (T_Redirecting id lefts r) =
@@ -528,11 +532,11 @@ checkMultiDimensionalArrays = ForShell [Bash] f
isMultiDim l = getBracedModifier (concat $ oversimplify l) `matches` re
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
prop_checkPS11a = verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '"
prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '"
prop_checkPS14a= verify checkPS1Assignments "export PS1=$'\\e[3m; '"
prop_checkPS14a = verify checkPS1Assignments "export PS1=$'\\e[3m; '"
prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"
prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'"

View File

@@ -2,9 +2,27 @@ module ShellCheck.Data where
import ShellCheck.Interface
import Data.Version (showVersion)
import Paths_ShellCheck (version)
shellcheckVersion = showVersion version -- VERSIONSTRING
{-
If you are here because you saw an error about Paths_ShellCheck in this file,
simply comment out the import below and define the version as a constant string.
Instead of:
import Paths_ShellCheck (version)
shellcheckVersion = showVersion version
Use:
-- import Paths_ShellCheck (version)
shellcheckVersion = "kludge"
-}
import Paths_ShellCheck (version)
shellcheckVersion = showVersion version -- VERSIONSTRING
internalVariables = [
-- Generic
@@ -12,23 +30,26 @@ internalVariables = [
-- Bash
"BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
"BASH_ARGV", "BASH_CMDS", "BASH_COMMAND", "BASH_EXECUTION_STRING",
"BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL",
"BASH_VERSINFO", "BASH_VERSION", "COMP_CWORD", "COMP_KEY",
"COMP_LINE", "COMP_POINT", "COMP_TYPE", "COMP_WORDBREAKS",
"COMP_WORDS", "COPROC", "DIRSTACK", "EUID", "FUNCNAME", "GROUPS",
"HISTCMD", "HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE",
"OLDPWD", "OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD",
"RANDOM", "READLINE_LINE", "READLINE_POINT", "REPLY", "SECONDS",
"SHELLOPTS", "SHLVL", "UID", "BASH_ENV", "BASH_XTRACEFD", "CDPATH",
"COLUMNS", "COMPREPLY", "EMACS", "ENV", "FCEDIT", "FIGNORE",
"BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND",
"BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_LOADABLES_PATH",
"BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL", "BASH_VERSINFO",
"BASH_VERSION", "COMP_CWORD", "COMP_KEY", "COMP_LINE", "COMP_POINT",
"COMP_TYPE", "COMP_WORDBREAKS", "COMP_WORDS", "COPROC", "DIRSTACK",
"EPOCHREALTIME", "EPOCHSECONDS", "EUID", "FUNCNAME", "GROUPS", "HISTCMD",
"HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE", "OLDPWD",
"OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD", "RANDOM",
"READLINE_ARGUMENT", "READLINE_LINE", "READLINE_MARK", "READLINE_POINT",
"REPLY", "SECONDS", "SHELLOPTS", "SHLVL", "SRANDOM", "UID", "BASH_COMPAT",
"BASH_ENV", "BASH_XTRACEFD", "CDPATH", "CHILD_MAX", "COLUMNS",
"COMPREPLY", "EMACS", "ENV", "EXECIGNORE", "FCEDIT", "FIGNORE",
"FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",
"HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",
"IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
"LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LINES", "MAIL",
"MAILCHECK", "MAILPATH", "OPTERR", "PATH", "POSIXLY_CORRECT",
"PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL",
"TIMEFORMAT", "TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC",
"IGNOREEOF", "INPUTRC", "INSIDE_EMACS", "LANG", "LC_ALL", "LC_COLLATE",
"LC_CTYPE", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME",
"LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH",
"POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1",
"PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR",
"auto_resume", "histchars",
-- Other
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
@@ -43,13 +64,18 @@ internalVariables = [
"flags_error", "flags_return"
]
specialVariablesWithoutSpaces = [
"$", "-", "?", "!", "#"
specialIntegerVariables = [
"$", "?", "!", "#"
]
specialVariablesWithoutSpaces = "-" : specialIntegerVariables
variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID",
"EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM",
"READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS",
"SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE",
"HISTSIZE", "LINES"
-- shflags
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
@@ -95,10 +121,10 @@ commonCommands = [
nonReadingCommands = [
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
"cp", "du", "echo", "export", "false", "fg", "fuser", "getconf",
"cp", "du", "echo", "export", "fg", "fuser", "getconf",
"getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
"set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname"
"set", "sleep", "touch", "trap", "ulimit", "unalias", "uname"
]
sampleWords = [
@@ -138,5 +164,6 @@ shellForExecutable name =
_ -> Nothing
flagsForRead = "sreu:n:N:i:p:a:t:"
flagsForMapfile = "d:n:O:s:u:C:c:t"
declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"]

313
src/ShellCheck/Debug.hs Normal file
View File

@@ -0,0 +1,313 @@
{-
This file contains useful functions for debugging and developing ShellCheck.
To invoke them interactively, run:
cabal repl
At the ghci prompt, enter:
:load ShellCheck.Debug
You can now invoke the functions. Here are some examples:
shellcheckString "echo $1"
stringToAst "(( x+1 ))"
stringToCfg "if foo; then bar; else baz; fi"
writeFile "/tmp/test.dot" $ stringToCfgViz "while foo; do bar; done"
The latter file can be rendered to png with GraphViz:
dot -Tpng /tmp/test.dot > /tmp/test.png
To run all unit tests in a module:
ShellCheck.Parser.runTests
ShellCheck.Analytics.runTests
To run a specific test:
:load ShellCheck.Analytics
prop_checkUuoc3
If you make code changes, reload in seconds at any time with:
:r
===========================================================================
Crash course in printf debugging in Haskell:
import Debug.Trace
greet 0 = return ()
-- Print when a function is invoked
greet n | trace ("calling greet " ++ show n) False = undefined
greet n = do
putStrLn "Enter name"
name <- getLine
-- Print at some point in any monadic function
traceM $ "user entered " ++ name
putStrLn $ "Hello " ++ name
-- Print a value before passing it on
greet $ traceShowId (n - 1)
===========================================================================
If you want to invoke `ghci` directly, such as on `shellcheck.hs`, to
debug all of ShellCheck including I/O, you may see an error like this:
src/ShellCheck/Data.hs:5:1: error:
Could not load module Paths_ShellCheck
it is a hidden module in the package ShellCheck-0.8.0
This can easily be circumvented by running `./setgitversion` or manually
editing src/ShellCheck/Data.hs to replace the auto-deduced version number
with a constant string as indicated.
Afterwards, you can run the ShellCheck tool, as if from the shell, with:
$ ghci shellcheck.hs
ghci> runMain ["-x", "file.sh"]
-}
module ShellCheck.Debug () where
import ShellCheck.Analyzer
import ShellCheck.AST
import ShellCheck.CFG
import ShellCheck.Checker
import ShellCheck.CFGAnalysis as CF
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Prelude
import Control.Monad
import Control.Monad.Identity
import Control.Monad.RWS
import Control.Monad.Writer
import Data.Graph.Inductive.Graph as G
import Data.List
import Data.Maybe
import qualified Data.Map as M
import qualified Data.Set as S
-- Run all of ShellCheck (minus output formatters)
shellcheckString :: String -> CheckResult
shellcheckString scriptString =
runIdentity $ checkScript dummySystemInterface checkSpec
where
checkSpec :: CheckSpec
checkSpec = emptyCheckSpec {
csScript = scriptString
}
dummySystemInterface :: SystemInterface Identity
dummySystemInterface = mockedSystemInterface [
-- A tiny, fake filesystem for sourced files
("lib/mylib1.sh", "foo=$(cat $1 | wc -l)"),
("lib/mylib2.sh", "bar=42")
]
-- Parameters used when generating Control Flow Graphs
cfgParams :: CFGParameters
cfgParams = CFGParameters {
cfLastpipe = False,
cfPipefail = False
}
-- An example script to play with
exampleScript :: String
exampleScript = unlines [
"#!/bin/sh",
"count=0",
"for file in *",
"do",
" (( count++ ))",
"done",
"echo $count"
]
-- Parse the script string into ShellCheck's ParseResult
parseScriptString :: String -> ParseResult
parseScriptString scriptString =
runIdentity $ parseScript dummySystemInterface parseSpec
where
parseSpec :: ParseSpec
parseSpec = newParseSpec {
psFilename = "myscript",
psScript = scriptString
}
-- Parse the script string into an Abstract Syntax Tree
stringToAst :: String -> Token
stringToAst scriptString =
case maybeRoot of
Just root -> root
Nothing -> error $ "Script failed to parse: " ++ show parserWarnings
where
parseResult :: ParseResult
parseResult = parseScriptString scriptString
maybeRoot :: Maybe Token
maybeRoot = prRoot parseResult
parserWarnings :: [PositionedComment]
parserWarnings = prComments parseResult
astToCfgResult :: Token -> CFGResult
astToCfgResult = buildGraph cfgParams
astToDfa :: Token -> CFGAnalysis
astToDfa = analyzeControlFlow cfgParams
astToCfg :: Token -> CFGraph
astToCfg = cfGraph . astToCfgResult
stringToCfg :: String -> CFGraph
stringToCfg = astToCfg . stringToAst
stringToDfa :: String -> CFGAnalysis
stringToDfa = astToDfa . stringToAst
cfgToGraphViz :: CFGraph -> String
cfgToGraphViz = cfgToGraphVizWith show
stringToCfgViz :: String -> String
stringToCfgViz = cfgToGraphViz . stringToCfg
stringToDfaViz :: String -> String
stringToDfaViz = dfaToGraphViz . stringToDfa
-- Dump a Control Flow Graph as GraphViz with extended information
stringToDetailedCfgViz :: String -> String
stringToDetailedCfgViz scriptString = cfgToGraphVizWith nodeLabel graph
where
ast :: Token
ast = stringToAst scriptString
cfgResult :: CFGResult
cfgResult = astToCfgResult ast
graph :: CFGraph
graph = cfGraph cfgResult
idToToken :: M.Map Id Token
idToToken = M.fromList $ execWriter $ doAnalysis (\c -> tell [(getId c, c)]) ast
idToNode :: M.Map Id (Node, Node)
idToNode = cfIdToRange cfgResult
nodeToStartIds :: M.Map Node (S.Set Id)
nodeToStartIds =
M.fromListWith S.union $
map (\(id, (start, _)) -> (start, S.singleton id)) $
M.toList idToNode
nodeToEndIds :: M.Map Node (S.Set Id)
nodeToEndIds =
M.fromListWith S.union $
map (\(id, (_, end)) -> (end, S.singleton id)) $
M.toList idToNode
formatId :: Id -> String
formatId id = fromMaybe ("Unknown " ++ show id) $ do
(OuterToken _ token) <- M.lookup id idToToken
firstWord <- words (show token) !!! 0
-- Strip off "Inner_"
(_ : tokenName) <- return $ dropWhile (/= '_') firstWord
return $ tokenName ++ " " ++ show id
formatGroup :: S.Set Id -> String
formatGroup set = intercalate ", " $ map formatId $ S.toList set
nodeLabel (node, label) = unlines [
show node ++ ". " ++ show label,
"Begin: " ++ formatGroup (M.findWithDefault S.empty node nodeToStartIds),
"End: " ++ formatGroup (M.findWithDefault S.empty node nodeToEndIds)
]
-- Dump a Control Flow Graph with Data Flow Analysis as GraphViz
dfaToGraphViz :: CF.CFGAnalysis -> String
dfaToGraphViz analysis = cfgToGraphVizWith label $ CF.graph analysis
where
label (node, label) =
let
desc = show node ++ ". " ++ show label
in
fromMaybe ("No DFA available\n\n" ++ desc) $ do
(pre, post) <- M.lookup node $ CF.nodeToData analysis
return $ unlines [
"Precondition: " ++ show pre,
"",
desc,
"",
"Postcondition: " ++ show post
]
-- Dump an Control Flow Graph to GraphViz with a given node formatter
cfgToGraphVizWith :: (LNode CFNode -> String) -> CFGraph -> String
cfgToGraphVizWith nodeLabel graph = concat [
"digraph {\n",
concatMap dumpNode (labNodes graph),
concatMap dumpLink (labEdges graph),
tagVizEntries graph,
"}\n"
]
where
dumpNode l@(node, label) = show node ++ " [label=" ++ quoteViz (nodeLabel l) ++ "]\n"
dumpLink (from, to, typ) = show from ++ " -> " ++ show to ++ " [style=" ++ quoteViz (edgeStyle typ) ++ "]\n"
edgeStyle CFEFlow = "solid"
edgeStyle CFEExit = "bold"
edgeStyle CFEFalseFlow = "dotted"
quoteViz str = "\"" ++ escapeViz str ++ "\""
escapeViz [] = []
escapeViz (c:rest) =
case c of
'\"' -> '\\' : '\"' : escapeViz rest
'\n' -> '\\' : 'l' : escapeViz rest
'\\' -> '\\' : '\\' : escapeViz rest
_ -> c : escapeViz rest
-- Dump an Abstract Syntax Tree (or branch thereof) to GraphViz format
astToGraphViz :: Token -> String
astToGraphViz token = concat [
"digraph {\n",
formatTree token,
"}\n"
]
where
formatTree :: Token -> String
formatTree t = snd $ execRWS (doStackAnalysis push pop t) () []
push :: Token -> RWS () String [Int] ()
push (OuterToken (Id n) inner) = do
stack <- get
put (n : stack)
case stack of
[] -> return ()
(top:_) -> tell $ show top ++ " -> " ++ show n ++ "\n"
tell $ show n ++ " [label=" ++ quoteViz (show n ++ ": " ++ take 32 (show inner)) ++ "]\n"
pop :: Token -> RWS () String [Int] ()
pop _ = modify tail
-- For each entry point, set the rank so that they'll align in the graph
tagVizEntries :: CFGraph -> String
tagVizEntries graph = "{ rank=same " ++ rank ++ " }"
where
entries = mapMaybe find $ labNodes graph
find (node, CFEntryPoint name) = return (node, name)
find _ = Nothing
rank = unwords $ map (\(c, _) -> show c) entries

View File

@@ -22,6 +22,8 @@
module ShellCheck.Fixer (applyFix, removeTabStops, mapPositions, Ranged(..), runTests) where
import ShellCheck.Interface
import ShellCheck.Prelude
import Control.Monad
import Control.Monad.State
import Data.Array
import Data.List
@@ -35,7 +37,7 @@ class Ranged a where
end :: a -> Position
overlap :: a -> a -> Bool
overlap x y =
(yStart >= xStart && yStart < xEnd) || (yStart < xStart && yEnd > xStart)
xEnd > yStart && yEnd > xStart
where
yStart = start y
yEnd = end y
@@ -86,6 +88,7 @@ instance Ranged Replacement where
instance Monoid Fix where
mempty = newFix
mappend = (<>)
mconcat = foldl mappend mempty -- fold left to right since <> discards right on overlap
instance Semigroup Fix where
f1 <> f2 =
@@ -228,7 +231,7 @@ applyReplacement2 rep string = do
let (l1, l2) = tmap posLine originalPos in
when (l1 /= 1 || l2 /= 1) $
error "ShellCheck internal error, please report: bad cross-line fix"
error $ pleaseReport "bad cross-line fix"
let replacer = repString rep
let shift = (length replacer) - (oldEnd - oldStart)

View File

@@ -88,7 +88,7 @@ outputError file error = putStrLn $ concat [
attr s v = concat [ s, "='", escape v, "' " ]
escape = concatMap escape'
escape' c = if isOk c then [c] else "&#" ++ show (ord c) ++ ";"
isOk x = any ($x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")]
isOk x = any ($ x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")]
severity "error" = "error"
severity "warning" = "warning"

View File

@@ -203,10 +203,9 @@ formatDoc color (DiffDoc name lf regions) =
buildFixMap :: [Fix] -> M.Map String Fix
buildFixMap fixes = perFile
where
splitFixes = concatMap splitFixByFile fixes
splitFixes = splitFixByFile $ mconcat fixes
perFile = groupByMap (posFile . repStartPos . head . fixReplacements) splitFixes
-- There are currently no multi-file fixes, but let's handle it anyways
splitFixByFile :: Fix -> [Fix]
splitFixByFile fix = map makeFix $ groupBy sameFile (fixReplacements fix)
where

View File

@@ -23,6 +23,7 @@ module ShellCheck.Formatter.JSON (format) where
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Control.DeepSeq
import Data.Aeson
import Data.IORef
import Data.Monoid
@@ -103,7 +104,7 @@ collectResult ref cr sys = mapM_ f groups
comments = crComments cr
groups = groupWith sourceFile comments
f :: [PositionedComment] -> IO ()
f group = modifyIORef ref (\x -> comments ++ x)
f group = deepseq comments $ modifyIORef ref (\x -> comments ++ x)
finish ref = do
list <- readIORef ref

View File

@@ -23,6 +23,7 @@ module ShellCheck.Formatter.JSON1 (format) where
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Control.DeepSeq
import Data.Aeson
import Data.IORef
import Data.Monoid
@@ -120,7 +121,7 @@ collectResult ref cr sys = mapM_ f groups
result <- siReadFile sys (Just True) filename
let contents = either (const "") id result
let comments' = makeNonVirtual comments contents
modifyIORef ref (\x -> comments' ++ x)
deepseq comments' $ modifyIORef ref (\x -> comments' ++ x)
finish ref = do
list <- readIORef ref

View File

@@ -23,6 +23,7 @@ import ShellCheck.Fixer
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Control.DeepSeq
import Control.Monad
import Data.Array
import Data.Foldable
@@ -88,7 +89,7 @@ rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err)
appendComments errRef comments max = do
previous <- readIORef errRef
let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments
writeIORef errRef . take max . nubBy equal . sort $ previous ++ current
writeIORef errRef $! force . take max . nubBy equal . sort $ previous ++ current
where
fst3 (x,_,_) = x
equal x y = fst3 x == fst3 y

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2021 Vidar Holen
Copyright 2012-2022 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -27,6 +27,7 @@ import ShellCheck.AST
import ShellCheck.ASTLib hiding (runTests)
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Prelude
import Control.Applicative ((<*), (*>))
import Control.Monad
@@ -37,7 +38,6 @@ import Data.Functor
import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub, find)
import Data.Maybe
import Data.Monoid
import Debug.Trace -- STRIP
import GHC.Exts (sortWith)
import Prelude hiding (readList)
import System.IO
@@ -46,7 +46,7 @@ import Text.Parsec.Error
import Text.Parsec.Pos
import qualified Control.Monad.Reader as Mr
import qualified Control.Monad.State as Ms
import qualified Data.Map as Map
import qualified Data.Map.Strict as Map
import Test.QuickCheck.All (quickCheckAll)
@@ -210,7 +210,7 @@ getNextIdSpanningTokenList list =
-- Get the span covered by an id
getSpanForId :: Monad m => Id -> SCParser m (SourcePos, SourcePos)
getSpanForId id =
Map.findWithDefault (error "Internal error: no position for id. Please report!") id <$>
Map.findWithDefault (error $ pleaseReport "no parser span for id") id <$>
getMap
-- Create a new id with the same span as an existing one
@@ -457,8 +457,8 @@ called s p = do
pos <- getPosition
withContext (ContextName pos s) p
withAnnotations anns =
withContext (ContextAnnotation anns)
withAnnotations anns p =
if null anns then p else withContext (ContextAnnotation anns) p
readConditionContents single =
readCondContents `attempting` lookAhead (do
@@ -556,7 +556,7 @@ readConditionContents single =
notFollowedBy2 (try (spacing >> string "]"))
x <- readNormalWord
pos <- getPosition
when (endedWith "]" x && notArrayIndex x) $ do
when (notArrayIndex x && endedWith "]" x && not (x `containsLiteral` "[")) $ do
parseProblemAt pos ErrorC 1020 $
"You need a space before the " ++ (if single then "]" else "]]") ++ "."
fail "Missing space before ]"
@@ -572,6 +572,7 @@ readConditionContents single =
endedWith _ _ = False
notArrayIndex (T_NormalWord id s@(_:T_Literal _ t:_)) = t /= "["
notArrayIndex _ = True
containsLiteral x s = s `isInfixOf` onlyLiteralString x
readCondAndOp = readAndOrOp TC_And "&&" False <|> readAndOrOp TC_And "-a" True
@@ -717,20 +718,20 @@ prop_a6 = isOk readArithmeticContents " 1 | 2 ||3|4"
prop_a7 = isOk readArithmeticContents "3*2**10"
prop_a8 = isOk readArithmeticContents "3"
prop_a9 = isOk readArithmeticContents "a^!-b"
prop_a10= isOk readArithmeticContents "! $?"
prop_a11= isOk readArithmeticContents "10#08 * 16#f"
prop_a12= isOk readArithmeticContents "\"$((3+2))\" + '37'"
prop_a13= isOk readArithmeticContents "foo[9*y+x]++"
prop_a14= isOk readArithmeticContents "1+`echo 2`"
prop_a15= isOk readArithmeticContents "foo[`echo foo | sed s/foo/4/g` * 3] + 4"
prop_a16= isOk readArithmeticContents "$foo$bar"
prop_a17= isOk readArithmeticContents "i<(0+(1+1))"
prop_a18= isOk readArithmeticContents "a?b:c"
prop_a19= isOk readArithmeticContents "\\\n3 +\\\n 2"
prop_a20= isOk readArithmeticContents "a ? b ? c : d : e"
prop_a21= isOk readArithmeticContents "a ? b : c ? d : e"
prop_a22= isOk readArithmeticContents "!!a"
prop_a23= isOk readArithmeticContents "~0"
prop_a10 = isOk readArithmeticContents "! $?"
prop_a11 = isOk readArithmeticContents "10#08 * 16#f"
prop_a12 = isOk readArithmeticContents "\"$((3+2))\" + '37'"
prop_a13 = isOk readArithmeticContents "foo[9*y+x]++"
prop_a14 = isOk readArithmeticContents "1+`echo 2`"
prop_a15 = isOk readArithmeticContents "foo[`echo foo | sed s/foo/4/g` * 3] + 4"
prop_a16 = isOk readArithmeticContents "$foo$bar"
prop_a17 = isOk readArithmeticContents "i<(0+(1+1))"
prop_a18 = isOk readArithmeticContents "a?b:c"
prop_a19 = isOk readArithmeticContents "\\\n3 +\\\n 2"
prop_a20 = isOk readArithmeticContents "a ? b ? c : d : e"
prop_a21 = isOk readArithmeticContents "a ? b : c ? d : e"
prop_a22 = isOk readArithmeticContents "!!a"
prop_a23 = isOk readArithmeticContents "~0"
readArithmeticContents :: Monad m => SCParser m Token
readArithmeticContents =
readSequence
@@ -819,11 +820,13 @@ readArithmeticContents =
return $ TA_Expansion id pieces
readGroup = do
start <- startSpan
char '('
s <- readSequence
char ')'
id <- endSpan start
spacing
return s
return $ TA_Parentesis id s
readArithTerm = readGroup <|> readVariable <|> readExpansion
@@ -923,8 +926,8 @@ prop_readCondition7 = isOk readCondition "[[ ${line} =~ ^[[:space:]]*# ]]"
prop_readCondition8 = isOk readCondition "[[ $l =~ ogg|flac ]]"
prop_readCondition9 = isOk readCondition "[ foo -a -f bar ]"
prop_readCondition10 = isOk readCondition "[[\na == b\n||\nc == d ]]"
prop_readCondition10a= isOk readCondition "[[\na == b ||\nc == d ]]"
prop_readCondition10b= isOk readCondition "[[ a == b\n||\nc == d ]]"
prop_readCondition10a = isOk readCondition "[[\na == b ||\nc == d ]]"
prop_readCondition10b = isOk readCondition "[[ a == b\n||\nc == d ]]"
prop_readCondition11 = isOk readCondition "[[ a == b ||\n c == d ]]"
prop_readCondition12 = isWarning readCondition "[ a == b \n -o c == d ]"
prop_readCondition13 = isOk readCondition "[[ foo =~ ^fo{1,3}$ ]]"
@@ -941,6 +944,9 @@ prop_readCondition23 = isOk readCondition "[[ -v arr[$var] ]]"
prop_readCondition25 = isOk readCondition "[[ lex.yy.c -ot program.l ]]"
prop_readCondition26 = isOk readScript "[[ foo ]]\\\n && bar"
prop_readCondition27 = not $ isOk readConditionCommand "[[ x ]] foo"
prop_readCondition28 = isOk readCondition "[[ x = [\"$1\"] ]]"
prop_readCondition29 = isOk readCondition "[[ x = [*] ]]"
readCondition = called "test expression" $ do
opos <- getPosition
start <- startSpan
@@ -985,6 +991,10 @@ prop_readAnnotation5 = isOk readAnnotation "# shellcheck disable=SC2002 # All ca
prop_readAnnotation6 = isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\n"
prop_readAnnotation7 = isOk readAnnotation "# shellcheck disable=SC1000,SC2000-SC3000,SC1001\n"
prop_readAnnotation8 = isOk readAnnotation "# shellcheck disable=all\n"
prop_readAnnotation9 = isOk readAnnotation "# shellcheck source='foo bar' source-path=\"baz etc\"\n"
prop_readAnnotation10 = isOk readAnnotation "# shellcheck disable='SC1234,SC2345' enable=\"foo\" shell='bash'\n"
prop_readAnnotation11 = isOk (readAnnotationWithoutPrefix False) "external-sources='true'"
readAnnotation = called "shellcheck directive" $ do
try readAnnotationPrefix
many1 linewhitespace
@@ -1000,12 +1010,19 @@ readAnnotationWithoutPrefix sandboxed = do
many linewhitespace
return $ concat values
where
plainOrQuoted p = quoted p <|> p
quoted p = do
c <- oneOf "'\""
start <- getPosition
str <- many1 $ noneOf (c:"\n")
char c <|> fail "Missing terminating quote for directive."
subParse start p str
readKey = do
keyPos <- getPosition
key <- many1 (letter <|> char '-')
char '=' <|> fail "Expected '=' after directive key"
annotations <- case key of
"disable" -> readElement `sepBy` char ','
"disable" -> plainOrQuoted $ readElement `sepBy` char ','
where
readElement = readRange <|> readAll
readAll = do
@@ -1020,21 +1037,21 @@ readAnnotationWithoutPrefix sandboxed = do
int <- many1 digit
return $ read int
"enable" -> readName `sepBy` char ','
"enable" -> plainOrQuoted $ readName `sepBy` char ','
where
readName = EnableComment <$> many1 (letter <|> char '-')
"source" -> do
filename <- many1 $ noneOf " \n"
filename <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n")
return [SourceOverride filename]
"source-path" -> do
dirname <- many1 $ noneOf " \n"
dirname <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n")
return [SourcePath dirname]
"shell" -> do
pos <- getPosition
shell <- many1 $ noneOf " \n"
shell <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n")
when (isNothing $ shellForExecutable shell) $
parseNoteAt pos ErrorC 1103
"This shell type is unknown. Use e.g. sh or bash."
@@ -1042,7 +1059,7 @@ readAnnotationWithoutPrefix sandboxed = do
"external-sources" -> do
pos <- getPosition
value <- many1 letter
value <- plainOrQuoted $ many1 letter
case value of
"true" ->
if sandboxed
@@ -1696,9 +1713,9 @@ readDollarBraced = called "parameter expansion" $ do
id <- endSpan start
return $ T_DollarBraced id True word
prop_readDollarExpansion1= isOk readDollarExpansion "$(echo foo; ls\n)"
prop_readDollarExpansion2= isOk readDollarExpansion "$( )"
prop_readDollarExpansion3= isOk readDollarExpansion "$( command \n#comment \n)"
prop_readDollarExpansion1 = isOk readDollarExpansion "$(echo foo; ls\n)"
prop_readDollarExpansion2 = isOk readDollarExpansion "$( )"
prop_readDollarExpansion3 = isOk readDollarExpansion "$( command \n#comment \n)"
readDollarExpansion = called "command expansion" $ do
start <- startSpan
try (string "$(")
@@ -1790,17 +1807,17 @@ prop_readHereDoc6 = isOk readScript "cat << foo\\ bar\ncow\nfoo bar"
prop_readHereDoc7 = isOk readScript "cat << foo\n\\$(f ())\nfoo"
prop_readHereDoc8 = isOk readScript "cat <<foo>>bar\netc\nfoo"
prop_readHereDoc9 = isOk readScript "if true; then cat << foo; fi\nbar\nfoo\n"
prop_readHereDoc10= isOk readScript "if true; then cat << foo << bar; fi\nfoo\nbar\n"
prop_readHereDoc11= isOk readScript "cat << foo $(\nfoo\n)lol\nfoo\n"
prop_readHereDoc12= isOk readScript "cat << foo|cat\nbar\nfoo"
prop_readHereDoc13= isOk readScript "cat <<'#!'\nHello World\n#!\necho Done"
prop_readHereDoc14= isWarning readScript "cat << foo\nbar\nfoo \n"
prop_readHereDoc15= isWarning readScript "cat <<foo\nbar\nfoo bar\nfoo"
prop_readHereDoc16= isOk readScript "cat <<- ' foo'\nbar\n foo\n"
prop_readHereDoc17= isWarning readScript "cat <<- ' foo'\nbar\n foo\n foo\n"
prop_readHereDoc18= isOk readScript "cat <<'\"foo'\nbar\n\"foo\n"
prop_readHereDoc20= isWarning readScript "cat << foo\n foo\n()\nfoo\n"
prop_readHereDoc21= isOk readScript "# shellcheck disable=SC1039\ncat << foo\n foo\n()\nfoo\n"
prop_readHereDoc10 = isOk readScript "if true; then cat << foo << bar; fi\nfoo\nbar\n"
prop_readHereDoc11 = isOk readScript "cat << foo $(\nfoo\n)lol\nfoo\n"
prop_readHereDoc12 = isOk readScript "cat << foo|cat\nbar\nfoo"
prop_readHereDoc13 = isOk readScript "cat <<'#!'\nHello World\n#!\necho Done"
prop_readHereDoc14 = isWarning readScript "cat << foo\nbar\nfoo \n"
prop_readHereDoc15 = isWarning readScript "cat <<foo\nbar\nfoo bar\nfoo"
prop_readHereDoc16 = isOk readScript "cat <<- ' foo'\nbar\n foo\n"
prop_readHereDoc17 = isWarning readScript "cat <<- ' foo'\nbar\n foo\n foo\n"
prop_readHereDoc18 = isOk readScript "cat <<'\"foo'\nbar\n\"foo\n"
prop_readHereDoc20 = isWarning readScript "cat << foo\n foo\n()\nfoo\n"
prop_readHereDoc21 = isOk readScript "# shellcheck disable=SC1039\ncat << foo\n foo\n()\nfoo\n"
prop_readHereDoc22 = isWarning readScript "cat << foo\r\ncow\r\nfoo\r\n"
prop_readHereDoc23 = isNotOk readScript "cat << foo \r\ncow\r\nfoo\r\n"
readHereDoc = called "here document" $ do
@@ -1914,7 +1931,7 @@ readPendingHereDocs = do
-- The end token is just a prefix
skipLine
| hasTrailer ->
error "ShellCheck bug, please report (here doc trailer)."
error $ pleaseReport "unexpected heredoc trailer"
-- The following cases assume no trailing text:
| dashed == Undashed && (not $ null leadingSpace) -> do
@@ -2095,6 +2112,7 @@ prop_readSimpleCommand11 = isOk readSimpleCommand "/\\* foo"
prop_readSimpleCommand12 = isWarning readSimpleCommand "elsif foo"
prop_readSimpleCommand13 = isWarning readSimpleCommand "ElseIf foo"
prop_readSimpleCommand14 = isWarning readSimpleCommand "elseif[$i==2]"
prop_readSimpleCommand15 = isWarning readSimpleCommand "trap 'foo\"bar' INT"
readSimpleCommand = called "simple command" $ do
prefix <- option [] readCmdPrefix
skipAnnotationAndWarn
@@ -2124,9 +2142,12 @@ readSimpleCommand = called "simple command" $ do
id2 <- getNewIdFor id1
let result = makeSimpleCommand id1 id2 prefix [cmd] suffix
if isCommand ["source", "."] cmd
then readSource result
else return result
case () of
_ | isCommand ["source", "."] cmd -> readSource result
_ | isCommand ["trap"] cmd -> do
syntaxCheckTrap result
return result
_ -> return result
where
isCommand strings (T_NormalWord _ [T_Literal _ s]) = s `elem` strings
isCommand _ _ = False
@@ -2146,6 +2167,17 @@ readSimpleCommand = called "simple command" $ do
parseProblemAtId (getId cmd) ErrorC 1131 "Use 'elif' to start another branch."
_ -> return ()
syntaxCheckTrap cmd =
case cmd of
(T_Redirecting _ _ (T_SimpleCommand _ _ (cmd:arg:_))) -> checkArg arg (getLiteralString arg)
_ -> return ()
where
checkArg _ Nothing = return ()
checkArg arg (Just ('-':_)) = return ()
checkArg arg (Just str) = do
(start,end) <- getSpanForId (getId arg)
subParse start (tryWithErrors (readCompoundListOrEmpty >> verifyEof) <|> return ()) str
commentWarning id =
parseProblemAtId id ErrorC 1127 "Was this intended as a comment? Use # in sh."
@@ -2282,7 +2314,7 @@ readAndOr = do
parseProblemAt apos ErrorC 1123 "ShellCheck directives are only valid in front of complete compound commands, like 'if', not e.g. individual 'elif' branches."
andOr <- withAnnotations annotations $
chainr1 readPipeline $ do
chainl1 readPipeline $ do
op <- g_AND_IF <|> g_OR_IF
readLineBreak
return $ case op of T_AND_IF id -> T_AndIf id
@@ -2483,16 +2515,29 @@ readBraceGroup = called "brace group" $ do
spacing
return $ T_BraceGroup id list
prop_readBatsTest = isOk readBatsTest "@test 'can parse' {\n true\n}"
prop_readBatsTest1 = isOk readBatsTest "@test 'can parse' {\n true\n}"
prop_readBatsTest2 = isOk readBatsTest "@test random text !(@*$Y&! {\n true\n}"
prop_readBatsTest3 = isOk readBatsTest "@test foo { bar { baz {\n true\n}"
prop_readBatsTest4 = isNotOk readBatsTest "@test foo \n{\n true\n}"
readBatsTest = called "bats @test" $ do
start <- startSpan
try $ string "@test"
try $ string "@test "
spacing
name <- readNormalWord
name <- readBatsName
spacing
test <- readBraceGroup
id <- endSpan start
return $ T_BatsTest id name test
where
readBatsName = do
line <- try . lookAhead $ many1 $ noneOf "\n"
let name = reverse $ f $ reverse line
string name
-- We want everything before the last " {" in a string, so we find everything after "{ " in its reverse
f ('{':' ':rest) = dropWhile isSpace rest
f (a:rest) = f rest
f [] = ""
prop_readWhileClause = isOk readWhileClause "while [[ -e foo ]]; do sleep 1; done"
readWhileClause = called "while loop" $ do
@@ -2521,7 +2566,7 @@ readDoGroup kwId = do
parseProblem ErrorC 1058 "Expected 'do'."
return "Expected 'do'"
acceptButWarn g_Semi ErrorC 1059 "No semicolons directly after 'do'."
acceptButWarn g_Semi ErrorC 1059 "Semicolon is not allowed directly after 'do'. You can just delete it."
allspacing
optional (do
@@ -2550,9 +2595,9 @@ prop_readForClause6 = isOk readForClause "for ((;;))\ndo echo $i\ndone"
prop_readForClause7 = isOk readForClause "for ((;;)) do echo $i\ndone"
prop_readForClause8 = isOk readForClause "for ((;;)) ; do echo $i\ndone"
prop_readForClause9 = isOk readForClause "for i do true; done"
prop_readForClause10= isOk readForClause "for ((;;)) { true; }"
prop_readForClause12= isWarning readForClause "for $a in *; do echo \"$a\"; done"
prop_readForClause13= isOk readForClause "for foo\nin\\\n bar\\\n baz\ndo true; done"
prop_readForClause10 = isOk readForClause "for ((;;)) { true; }"
prop_readForClause12 = isWarning readForClause "for $a in *; do echo \"$a\"; done"
prop_readForClause13 = isOk readForClause "for foo\nin\\\n bar\\\n baz\ndo true; done"
readForClause = called "for loop" $ do
pos <- getPosition
(T_For id) <- g_For
@@ -2684,10 +2729,10 @@ prop_readFunctionDefinition6 = isOk readFunctionDefinition "?(){ foo; }"
prop_readFunctionDefinition7 = isOk readFunctionDefinition "..(){ cd ..; }"
prop_readFunctionDefinition8 = isOk readFunctionDefinition "foo() (ls)"
prop_readFunctionDefinition9 = isOk readFunctionDefinition "function foo { true; }"
prop_readFunctionDefinition10= isOk readFunctionDefinition "function foo () { true; }"
prop_readFunctionDefinition11= isWarning readFunctionDefinition "function foo{\ntrue\n}"
prop_readFunctionDefinition12= isOk readFunctionDefinition "function []!() { true; }"
prop_readFunctionDefinition13= isOk readFunctionDefinition "@require(){ true; }"
prop_readFunctionDefinition10 = isOk readFunctionDefinition "function foo () { true; }"
prop_readFunctionDefinition11 = isWarning readFunctionDefinition "function foo{\ntrue\n}"
prop_readFunctionDefinition12 = isOk readFunctionDefinition "function []!() { true; }"
prop_readFunctionDefinition13 = isOk readFunctionDefinition "@require(){ true; }"
readFunctionDefinition = called "function" $ do
start <- startSpan
functionSignature <- try readFunctionSignature
@@ -2885,14 +2930,14 @@ prop_readAssignmentWord5 = isOk readAssignmentWord "b+=lol"
prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42"
prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42"
prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "
prop_readAssignmentWord9a= isOk readAssignmentWord "foo="
prop_readAssignmentWord9b= isOk readAssignmentWord "foo= "
prop_readAssignmentWord9c= isOk readAssignmentWord "foo= #bar"
prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'"
prop_readAssignmentWord13= isOk readAssignmentWord "var=( (1 2) (3 4) )"
prop_readAssignmentWord14= isOk readAssignmentWord "var=( 1 [2]=(3 4) )"
prop_readAssignmentWord15= isOk readAssignmentWord "var=(1 [2]=(3 4))"
prop_readAssignmentWord9a = isOk readAssignmentWord "foo="
prop_readAssignmentWord9b = isOk readAssignmentWord "foo= "
prop_readAssignmentWord9c = isOk readAssignmentWord "foo= #bar"
prop_readAssignmentWord11 = isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
prop_readAssignmentWord12 = isOk readAssignmentWord "a[b <<= 3 + c]='thing'"
prop_readAssignmentWord13 = isOk readAssignmentWord "var=( (1 2) (3 4) )"
prop_readAssignmentWord14 = isOk readAssignmentWord "var=( 1 [2]=(3 4) )"
prop_readAssignmentWord15 = isOk readAssignmentWord "var=(1 [2]=(3 4))"
readAssignmentWord = readAssignmentWordExt True
readWellFormedAssignment = readAssignmentWordExt False
readAssignmentWordExt lenient = called "variable assignment" $ do
@@ -3240,44 +3285,51 @@ prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world"
prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=("
prop_readScript5 = isOk readScript "#!/bin/bash\n#This is an empty script\n\n"
prop_readScript6 = isOk readScript "#!/usr/bin/env -S X=FOO bash\n#This is an empty script\n\n"
prop_readScript7 = isOk readScript "#!/bin/zsh\n# shellcheck disable=SC1071\nfor f (a b); echo $f\n"
readScriptFile sourced = do
start <- startSpan
pos <- getPosition
optional $ do
readUtf8Bom
parseProblem ErrorC 1082
"This file has a UTF-8 BOM. Remove it with: LC_CTYPE=C sed '1s/^...//' < yourscript ."
shebang <- readShebang <|> readEmptyLiteral
let (T_Literal _ shebangString) = shebang
allspacing
annotationStart <- startSpan
fileAnnotations <- readAnnotations
rcAnnotations <- if sourced
then return []
else do
filename <- Mr.asks currentFilename
readConfigFile filename
let annotations = fileAnnotations ++ rcAnnotations
annotationId <- endSpan annotationStart
let shellAnnotationSpecified =
any (\x -> case x of ShellOverride {} -> True; _ -> False) annotations
shellFlagSpecified <- isJust <$> Mr.asks shellTypeOverride
let ignoreShebang = shellAnnotationSpecified || shellFlagSpecified
unless ignoreShebang $
verifyShebang pos (executableFromShebang shebangString)
if ignoreShebang || isValidShell (executableFromShebang shebangString) /= Just False
then do
commands <- withAnnotations annotations readCompoundListOrEmpty
id <- endSpan start
verifyEof
let script = T_Annotation annotationId annotations $
T_Script id shebang commands
reparseIndices script
else do
many anyChar
id <- endSpan start
return $ T_Script id shebang []
-- Put the rc annotations on the stack so that one can ignore e.g. SC1084 in .shellcheckrc
withAnnotations rcAnnotations $ do
hasBom <- wasIncluded readUtf8Bom
shebang <- readShebang <|> readEmptyLiteral
let (T_Literal _ shebangString) = shebang
allspacing
annotationStart <- startSpan
fileAnnotations <- readAnnotations
-- Similarly put the filewide annotations on the stack to allow earlier suppression
withAnnotations fileAnnotations $ do
when (hasBom) $
parseProblemAt pos ErrorC 1082
"This file has a UTF-8 BOM. Remove it with: LC_CTYPE=C sed '1s/^...//' < yourscript ."
let annotations = fileAnnotations ++ rcAnnotations
annotationId <- endSpan annotationStart
let shellAnnotationSpecified =
any (\x -> case x of ShellOverride {} -> True; _ -> False) annotations
shellFlagSpecified <- isJust <$> Mr.asks shellTypeOverride
let ignoreShebang = shellAnnotationSpecified || shellFlagSpecified
unless ignoreShebang $
verifyShebang pos (executableFromShebang shebangString)
if ignoreShebang || isValidShell (executableFromShebang shebangString) /= Just False
then do
commands <- readCompoundListOrEmpty
id <- endSpan start
verifyEof
let script = T_Annotation annotationId annotations $
T_Script id shebang commands
reparseIndices script
else do
many anyChar
id <- endSpan start
return $ T_Script id shebang []
where
verifyShebang pos s = do
@@ -3370,16 +3422,6 @@ parsesCleanly parser string = runIdentity $ do
return $ Just . null $ parseNotes userState ++ parseProblems systemState
(Left _, _) -> return Nothing
-- For printf debugging: print the value of an expression
-- Example: return $ dump $ T_Literal id [c]
dump :: Show a => a -> a -- STRIP
dump x = trace (show x) x -- STRIP
-- Like above, but print a specific expression:
-- Example: return $ dumps ("Returning: " ++ [c]) $ T_Literal id [c]
dumps :: Show x => x -> a -> a -- STRIP
dumps t = trace (show t) -- STRIP
parseWithNotes parser = do
item <- parser
state <- getState
@@ -3459,9 +3501,9 @@ notesForContext list = zipWith ($) [first, second] $ filter isName list
-- Go over all T_UnparsedIndex and reparse them as either arithmetic or text
-- depending on declare -A statements.
reparseIndices root =
analyze blank blank f root
reparseIndices root = process root
where
process = analyze blank blank f
associative = getAssociativeArrays root
isAssociative s = s `elem` associative
f (T_Assignment id mode name indices value) = do
@@ -3486,8 +3528,9 @@ reparseIndices root =
fixAssignmentIndex name word =
case word of
T_UnparsedIndex id pos src ->
parsed name pos src
T_UnparsedIndex id pos src -> do
idx <- parsed name pos src
process idx -- Recursively parse for cases like x[y[z=1]]=1
_ -> return word
parsed name pos src =

51
src/ShellCheck/Prelude.hs Normal file
View File

@@ -0,0 +1,51 @@
{-
Copyright 2022 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
-- Generic basic utility functions
module ShellCheck.Prelude where
import Data.Semigroup
-- Get element 0 or a default. Like `head` but safe.
headOrDefault _ (a:_) = a
headOrDefault def _ = def
-- Get the last element or a default. Like `last` but safe.
lastOrDefault def [] = def
lastOrDefault _ list = last list
--- Get element n of a list, or Nothing. Like `!!` but safe.
(!!!) list i =
case drop i list of
[] -> Nothing
(r:_) -> Just r
-- Like mconcat but for Semigroups
sconcat1 :: (Semigroup t) => [t] -> t
sconcat1 [x] = x
sconcat1 (x:xs) = x <> sconcat1 xs
sconcatOrDefault def [] = def
sconcatOrDefault _ list = sconcat1 list
-- For more actionable "impossible" errors
pleaseReport str = "ShellCheck internal error, please report: " ++ str

View File

@@ -22,7 +22,8 @@ fi
cabal install --dependencies-only --enable-tests "${flags[@]}" ||
cabal install --dependencies-only "${flags[@]}" ||
die "can't install dependencies"
cabal install --dependencies-only --max-backjumps -1 "${flags[@]}" ||
die "can't install dependencies"
cabal configure --enable-tests "${flags[@]}" ||
die "configure failed"
cabal build ||

View File

@@ -25,6 +25,13 @@ exit 0
echo "Deleting 'dist' and 'dist-newstyle'..."
rm -rf dist dist-newstyle
execs=$(find . -name shellcheck)
if [ -n "$execs" ]
then
die "Found unexpected executables. Remove and try again: $execs"
fi
log=$(mktemp) || die "Can't create temp file"
date >> "$log" || die "Can't write to log"
@@ -63,14 +70,17 @@ debian:testing apt-get update && apt-get install -y cabal-install
ubuntu:latest apt-get update && apt-get install -y cabal-install
haskell:latest true
opensuse/leap:latest zypper install -y cabal-install ghc
fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils
fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils libstdc++-static gcc-c++
archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
# Ubuntu LTS
ubuntu:22.04 apt-get update && apt-get install -y cabal-install
ubuntu:20.04 apt-get update && apt-get install -y cabal-install
ubuntu:18.04 apt-get update && apt-get install -y cabal-install
ubuntu:16.04 apt-get update && apt-get install -y cabal-install
# Stack on Ubuntu LTS
ubuntu:20.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
ubuntu:22.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
EOF
exit "$final"

View File

@@ -5,8 +5,11 @@ import System.Exit
import qualified ShellCheck.Analytics
import qualified ShellCheck.AnalyzerLib
import qualified ShellCheck.ASTLib
import qualified ShellCheck.CFG
import qualified ShellCheck.CFGAnalysis
import qualified ShellCheck.Checker
import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.ControlFlow
import qualified ShellCheck.Checks.Custom
import qualified ShellCheck.Checks.ShellSupport
import qualified ShellCheck.Fixer
@@ -19,8 +22,11 @@ main = do
ShellCheck.Analytics.runTests
,ShellCheck.AnalyzerLib.runTests
,ShellCheck.ASTLib.runTests
,ShellCheck.CFG.runTests
,ShellCheck.CFGAnalysis.runTests
,ShellCheck.Checker.runTests
,ShellCheck.Checks.Commands.runTests
,ShellCheck.Checks.ControlFlow.runTests
,ShellCheck.Checks.Custom.runTests
,ShellCheck.Checks.ShellSupport.runTests
,ShellCheck.Fixer.runTests

View File

@@ -3,7 +3,7 @@
# various resolvers. It's run via distrotest.
resolvers=(
nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
# nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
)
die() { echo "$*" >&2; exit 1; }