157 Commits

Author SHA1 Message Date
Vidar Holen
4f5dc7094b ppc64le build producing 50MB executables 2021-02-23 18:28:56 -08:00
Vidar Holen
670c1de01b Merge pull request #2145 from avdv/fix-2143
Allow `env` to have flags and variables in shebang
2021-02-22 21:00:04 -08:00
Vidar Holen
b9b6975bfa Brand New Build!
Features Linux x86_64 docker builds for all archs
2021-02-22 19:12:57 -08:00
Claudio Bley
8bb5e01401 Allow env to have flags and variables in shebang
The `env` command has a `-S,--split-string` option which enables
having arguments for the command in a shebang.

Also, one could use variable assignments for the command since
`env` treats only the first word without a `=` character as the
command to run.

Fixes #2143.
2021-02-12 10:53:54 +01:00
Vidar Holen
15ff87cf80 Merge pull request #2119 from josephcsible/refactors
Various refactorings
2021-02-02 18:14:27 -08:00
Vidar Holen
dff8f9492a Improve SC2283 message and position 2021-01-05 10:07:39 -08:00
Vidar Holen
2e5c56b270 Parse heredocs correctly with carriage returns (fixes #2103) 2020-12-31 13:19:14 -08:00
Vidar Holen
9584266a8b Escape control characters when adding user data to messages 2020-12-31 12:28:48 -08:00
Vidar Holen
5fbaae2bb3 Don't treat ${!x@} as reference of x (fixes #2116) 2020-12-30 20:55:18 -08:00
Vidar Holen
fbb14d6b38 Improve checks for = in command names (fixes #2102) 2020-12-30 20:30:43 -08:00
Joseph C. Sible
2cfd1f2714 Fuse maps 2020-12-28 18:13:34 -05:00
Joseph C. Sible
953d9bc56d Remove unused helper stub 2020-12-28 18:13:34 -05:00
Joseph C. Sible
e272fa04ee Remove redundant bind and return 2020-12-28 18:13:34 -05:00
Joseph C. Sible
81e84c2939 Use execState instead of snd . runState 2020-12-28 18:13:34 -05:00
Joseph C. Sible
34939ca0b7 Fuse map into any 2020-12-28 18:13:34 -05:00
Joseph C. Sible
e7820479f0 Use find 2020-12-28 18:13:34 -05:00
Joseph C. Sible
8480563672 Use syntactic sugar instead of building lists by hand 2020-12-28 18:13:34 -05:00
Joseph C. Sible
dfbcc9595e Use mapM instead of reimplementing it 2020-12-28 17:48:58 -05:00
Joseph C. Sible
2c0766825e Implement groupByLink in terms of foldr 2020-12-28 17:45:11 -05:00
Joseph C. Sible
cb4f4e7edc Use mapM_ instead of reimplementing it 2020-12-28 17:34:52 -05:00
Joseph C. Sible
0607039d41 Simplify actualArgs 2020-12-28 17:21:47 -05:00
Joseph C. Sible
46f177b5be Simplify parseArgs 2020-12-28 17:19:08 -05:00
Joseph C. Sible
eaccd3d02c Simplify parser 2020-12-28 17:19:08 -05:00
Joseph C. Sible
35033a9f2f Remove unnecessary use of Maybe from shellFor 2020-12-28 17:09:50 -05:00
Vidar Holen
bd3299edd3 Treat 'exec $1' like '$1' for the purpose of quoting (fixes #2068) 2020-12-17 20:31:45 -08:00
Vidar Holen
cc3884cf9f Support env -S/--split-string in shebangs (fixes #2105) 2020-12-12 20:24:32 -08:00
Vidar Holen
6ba1af0898 Warn when a variable is assigned to itself 2020-12-11 20:28:36 -08:00
Vidar Holen
8e332ce879 Improve handling of trailing tokens for []/compounds (fixes #2091) 2020-12-06 21:26:24 -08:00
Vidar Holen
7e40d97e7a Merge pull request #1857 from lukelbd/conda-install-instructions
Add conda install instructions
2020-12-05 20:35:20 -08:00
Vidar Holen
775c0c11d7 Merge pull request #1899 from ArturKlauser/simplify-prepare-deploy
Simplify .prepare-deploy
2020-12-05 20:19:16 -08:00
Vidar Holen
5196ab1f95 Merge pull request #2097 from ylluminarious/patch-1
Add MacPorts as installation option in README.md
2020-12-05 20:14:46 -08:00
Vidar Holen
b625562d60 Add POSIX checks for more Bash-specific variables (fixes #2093) 2020-12-05 20:11:12 -08:00
George Plymale II
18e80284ec add macports as installation option in README.md 2020-12-01 16:15:22 -05:00
Vidar Holen
65044c2568 SC2095: Also warn if the command is backgrounded 2020-11-29 13:01:23 -08:00
Vidar Holen
61b7dd610d Merge pull request #2077 from keith/ks/readonly-masking
Add readonly to SC2155
2020-11-13 17:38:43 -08:00
Artur Klauser
4b0e5ca119 Simplify .prepare-deploy
Reduce amount of duplicated code.
2020-11-12 20:02:31 -08:00
Keith Smiley
619662adb6 Add readonly to SC2155
This adds a warning for readonly masking the return value of function.
This is mentioned in the wiki
https://github.com/koalaman/shellcheck/wiki/SC2155#problematic-code-in-the-case-of-readonly
but didn't actually produce a warning.

Fixes https://github.com/koalaman/shellcheck/issues/1336
2020-10-23 17:29:04 -07:00
Vidar Holen
28d3279ba6 Optional style warning about [ x$var = xval ] 2020-10-19 20:04:58 -07:00
Vidar Holen
256457c47a Use getopts parser to find 'read' arrays (fixes #2073) 2020-10-18 22:57:16 -07:00
Vidar Holen
3104cec770 SC2267: Warn about xargs -i (fixes #2058) 2020-10-18 22:10:14 -07:00
Vidar Holen
f100c2939e Rewrite getopts style option parser 2020-10-18 21:34:58 -07:00
Vidar Holen
8d99926554 Recognize local -x similarly to export (fixes #2069) 2020-10-18 15:15:31 -07:00
Vidar Holen
218deb6d01 Update SC2091/SC2092 message and ignore in quotes. 2020-09-08 19:30:13 -07:00
Vidar Holen
c4cc2debb7 Improve compatibility checks 2020-09-07 21:05:49 -07:00
Vidar Holen
cfd68ee0c2 Give each sh/dash compatibility warning its own SC3xxx error code 2020-09-01 16:48:14 -07:00
Vidar Holen
58783ab3cc Allow specifying ranges in disable directives 2020-09-01 16:22:15 -07:00
Vidar Holen
43191fa71d Suppress SC2035 for echo * and printf * (fixes #2036) 2020-09-01 14:19:28 -07:00
Vidar Holen
c9be7ab2eb Parse assignments according to spec (fixes #2022) 2020-08-23 18:46:13 -07:00
Vidar Holen
fb89cdf4ad Merge pull request #2042 from sshine/patch-1
Fix whitespace in README.md
2020-08-23 15:57:57 -07:00
Vidar Holen
9e59bcca91 Upgrade SC2169 (unsupported in dash) from warning to error (fixes #2013) 2020-08-23 15:49:20 -07:00
Vidar Holen
a62d9f10c2 Warn when using &/| between test statements 2020-08-23 15:43:33 -07:00
Simon Shine
e72fbb2640 Fix whitespace in README.md 2020-08-20 13:07:32 +02:00
Vidar Holen
17e591233f Merge branch 'donnerpeter-supportMinusNZ' 2020-08-08 15:02:07 -07:00
Vidar Holen
50067ddf94 Consider variables in -z/-n tests to be checked 2020-08-08 12:32:20 -07:00
Vidar Holen
3fa5b7d3bd Merge branch 'supportMinusNZ' of https://github.com/donnerpeter/shellcheck into donnerpeter-supportMinusNZ 2020-08-08 11:22:00 -07:00
Vidar Holen
5e6d50f493 Merge branch 'Gandalf--issue_1759_mapfile_proc_substition' 2020-08-07 16:41:52 -07:00
Vidar Holen
e779aedac3 Modernize getting mapfile array name 2020-08-07 16:41:18 -07:00
Vidar Holen
3ef1175566 Merge branch 'issue_1759_mapfile_proc_substition' of https://github.com/Gandalf-/shellcheck into Gandalf--issue_1759_mapfile_proc_substition 2020-08-07 15:57:59 -07:00
Vidar Holen
506ffa849b Merge branch 'glenjamin-patch-1' 2020-08-07 15:24:45 -07:00
Vidar Holen
b864242caa Merge branch 'patch-1' of https://github.com/glenjamin/shellcheck into glenjamin-patch-1 2020-08-07 15:24:36 -07:00
Vidar Holen
3e50a2fce8 Suppress SC2216 for du --files0-from or --exclude-from (fixes #1286) 2020-08-07 14:59:34 -07:00
Vidar Holen
10c2d827fa Merge pull request #2028 from Lin-Buo-Ren/github-issue-1643
Fix snap distribution unable to process scripts in Unicode(Chinese) (fixes #1643)
2020-08-07 11:35:50 -07:00
Vidar Holen
e0e2edd525 Merge pull request #2031 from umanwizard/patch-1
Update README.md
2020-08-07 11:34:36 -07:00
Brennan Vincent
c5b6d6f027 Update README.md
Minor typographical fixes ("macOS" everywhere, capitalize Homebrew)
2020-08-05 10:50:14 -04:00
林博仁(Buo-ren, Lin)
beee9b22ca Fix snap distribution unable to process scripts in Unicode(Chinese) (fixes #1643)
The snap runtime only supports C.UTF-8 locale, as other locales don't seem to be used now just hardcode it.

Fixes #1643.

Signed-off-by: 林博仁(Buo-ren, Lin) <Buo.Ren.Lin@gmail.com>
2020-08-03 11:46:20 +08:00
Vidar Holen
1ac2c31728 Warn when shell functions blatantly recurse (fixes #1994) 2020-07-27 21:50:33 -07:00
Vidar Holen
cc81bdee31 Improve SC1033/SC1034 message 2020-07-27 18:44:07 -07:00
Vidar Holen
34885142e7 Handle tilde expansion in pattern matching (fixes #1769) 2020-07-27 18:34:42 -07:00
Vidar Holen
14e6806092 Handle literal linefeeds in printf format strings (fixes #2007) 2020-07-25 17:36:22 -07:00
Vidar Holen
5d753212fb Improve handling of command prefixes like exec/command (fixes #2008) 2020-07-25 13:45:05 -07:00
Vidar Holen
5b86777f9d Warn about non-POSIX case modification expansions (fixes #1977) 2020-07-22 17:32:00 -07:00
Vidar Holen
7a9dbc042b Re-enable Windows job 2020-07-05 20:46:22 -07:00
Vidar Holen
9793d94206 Remove trailing whitespace 2020-07-05 20:30:18 -07:00
Vidar Holen
baab5b53e0 Use TravisCI workspaces 2020-07-01 21:21:38 -07:00
Vidar Holen
210cdcd01a Treat $x/ or $(x)/ as ./ when finding sourced files (fixes #1998) 2020-06-28 17:24:07 -07:00
Vidar Holen
1b884a17ea Merge pull request #2002 from stdedos/patch-1
Autolink https://www.shellcheck.net/
2020-06-28 16:05:24 -07:00
Vidar Holen
b52f58473d Merge pull request #1999 from aureliojargas/patch-1
SC1102: Fix typo in error message: substition
2020-06-28 16:04:56 -07:00
Vidar Holen
376e78b631 Merge branch 'onnozweers-patch-1' 2020-06-28 16:03:56 -07:00
Vidar Holen
40aacc3345 Merge branch 'patch-1' of https://github.com/onnozweers/shellcheck into onnozweers-patch-1 2020-06-28 16:03:44 -07:00
Vidar Holen
739eaadbf5 Warn about extra spaces between ((s in for((;;)) 2020-06-28 16:01:15 -07:00
Stavros Ntentos
6b88a341f3 Autolink https://www.shellcheck.net/ 2020-06-28 01:47:02 +03:00
Aurelio Jargas
a61d8a232c SC1102: Fix typo in error message: substition 2020-06-26 02:13:33 +02:00
Vidar Holen
12d9c1b76d Clarify that SC1090 refers to ShellCheck, not sh 2020-06-24 11:50:27 -07:00
Onno Zweers
a2b5b6a500 Rephrase: *Shellcheck* can't follow non-constant source
The message "Can't follow non-constant source. Use a directive to specify location." set me off on the wrong foot. At first, I thought it meant "In bash, you can't source a script specified by a variable." It was only after reading the wiki page for this message https://github.com/koalaman/shellcheck/wiki/SC1090 that I understood that the problem is that *shellcheck* can't check the sourced file. So I would suggest to rephrase this message so that it is more clear that the problem is in the checking, not in the running of the script.
2020-06-03 11:37:53 +02:00
Vidar Holen
5cf2c00ff7 Warn about defining and using an alias in a single command (fixes #1807) 2020-05-25 23:24:33 -07:00
Vidar Holen
a08ad3bee9 Count $# as an argument reference in SC2120 2020-05-25 23:24:33 -07:00
Vidar Holen
417e13f129 Merge pull request #1950 from geeseven/aur-shellcheck-bin
update dependency free AUR package
2020-05-25 18:06:35 -07:00
geeseven
536cb584f4 update dependency free AUR package
The `shellcheck-static` AUR package has been removed and `shellcheck-bin` is an option for a dependency free AUR package.
2020-05-12 14:58:15 -05:00
Vidar Holen
c2a15ce8e9 Allow disabling SC1072/SC1073 with annotations (fixes #1931) 2020-05-03 21:57:16 -07:00
Vidar Holen
d6adbfde78 Improve SC2259/60/61 messages 2020-05-03 21:46:16 -07:00
Vidar Holen
2030b83607 Warn about duplicate uses of stdin/out/err 2020-05-03 11:54:25 -07:00
Vidar Holen
8aa40c43ed Merge pull request #1926 from scop/spelling
Spelling fixes
2020-05-02 18:39:44 -07:00
Vidar Holen
5a42f4b938 Merge pull request #1927 from scop/sc-prefix
Use SC prefix for disable= in man page
2020-04-25 16:37:55 -07:00
Vidar Holen
a7a406c43c Merge pull request #1925 from josephcsible/nofromright
Revert "Use fromRight instead of reimplementing it"
2020-04-25 16:37:22 -07:00
Ville Skyttä
1d126960f3 Use SC prefix for disable= in man page 2020-04-25 08:33:10 +03:00
Ville Skyttä
60e80e4ce1 Spelling fixes 2020-04-25 08:29:38 +03:00
Joseph C. Sible
e0daa936d2 Revert "Use fromRight instead of reimplementing it"
We still support GHC 8.0, which didn't have fromRight.

This reverts commit 64c31d9142.
2020-04-24 22:14:08 -04:00
Vidar Holen
75863a887e Merge pull request #1918 from josephcsible/getsuspiciousregexwildcard
Clean up and optimize getSuspiciousRegexWildcard
2020-04-12 15:32:00 -07:00
Vidar Holen
413f0048b8 Merge pull request #1917 from josephcsible/thenskip
Simplify thenSkip, and use in another location
2020-04-12 15:26:36 -07:00
Vidar Holen
e7b5fb9742 Merge pull request #1907 from josephcsible/formatters
Clean up formatters
2020-04-12 15:23:17 -07:00
Vidar Holen
30523555af Merge pull request #1906 from josephcsible/shellsupport
Simplify ShellSupport
2020-04-12 15:22:52 -07:00
Vidar Holen
58d3e50f43 Merge pull request #1905 from josephcsible/skiprepeating
Make skipRepeating lazier and faster
2020-04-12 15:21:39 -07:00
Vidar Holen
73cc11fd0a Merge pull request #1901 from josephcsible/bracedstring
Mostly get rid of bracedString
2020-04-12 15:14:50 -07:00
Joseph C. Sible
163c710ba7 Clean up and optimize getSuspiciousRegexWildcard 2020-04-12 16:15:45 -04:00
Vidar Holen
ab1610b004 Merge pull request #1903 from josephcsible/fixer
Only perform the comparisons once
2020-04-11 17:23:51 -07:00
Vidar Holen
148468be70 Merge pull request #1904 from josephcsible/commands
Simplify Commands
2020-04-11 17:23:39 -07:00
Vidar Holen
5eac721fcf Merge pull request #1902 from josephcsible/astlib
Clean up ASTLib
2020-04-11 17:23:08 -07:00
Joseph C. Sible
b58bb4ba9d Move bracedString to be local to its last use site 2020-04-11 19:24:11 -04:00
Joseph C. Sible
999b7e2596 Get rid of bracedString everywhere it's easy to 2020-04-11 19:24:11 -04:00
Joseph C. Sible
a9d564a8bc Combine bracedString into getSingleUnmodifiedVariable
Everywhere we used getSingleUnmodifiedVariable, we just called bracedString on
the result. Move this into that function instead, and rename it accordingly.
2020-04-11 19:23:13 -04:00
Joseph C. Sible
8a7497c4f0 Simplify checkVariableBraces 2020-04-11 19:23:13 -04:00
Vidar Holen
1eac0d7340 Merge pull request #1900 from josephcsible/analyzerlib
Clean up AnalyzerLib
2020-04-11 16:21:20 -07:00
Vidar Holen
f8c1ffb0dc Merge pull request #1898 from josephcsible/nameexpansion
Simplify nameExpansion
2020-04-11 16:18:54 -07:00
Joseph C. Sible
3e17a20965 Simplify thenSkip, and use in another location 2020-04-11 17:29:28 -04:00
Joseph C. Sible
1c6202dba4 Avoid some awkward parentheses with forM_ 2020-04-05 22:25:19 -04:00
Joseph C. Sible
64c31d9142 Use fromRight instead of reimplementing it 2020-04-05 22:23:22 -04:00
Joseph C. Sible
8a6679fd8a Remove unnecessary fromMaybe and when from bashism 2020-04-05 22:03:50 -04:00
Joseph C. Sible
facf0d1e27 Write getLiteralArgs with foldr and without fromMaybe or monads 2020-04-05 21:59:27 -04:00
Joseph C. Sible
cd38afce26 Make it slightly lazier still (and more clear) 2020-04-05 21:46:08 -04:00
Joseph C. Sible
5084ba8d7e Make skipRepeating lazier and faster 2020-04-05 21:39:14 -04:00
Joseph C. Sible
ed331b816b Simplify warnRedundant 2020-04-05 20:32:39 -04:00
Joseph C. Sible
cfa2a663af Simplify checkSetAssignment 2020-04-05 20:30:37 -04:00
Joseph C. Sible
df4928f4e3 Use MultiWayIf instead of case-matching on () 2020-04-05 20:30:37 -04:00
Joseph C. Sible
9747b1d5c3 Simplify checkArg 2020-04-05 20:10:56 -04:00
Joseph C. Sible
fa841cb270 Prefer pattern matching in undirected 2020-04-05 20:08:02 -04:00
Joseph C. Sible
e8501151dd Use a guard instead of unless 2020-04-05 20:04:54 -04:00
Joseph C. Sible
9027a9239f Use pattern matching instead of snd 2020-04-05 20:03:17 -04:00
Joseph C. Sible
773e98868d Use foldr in checkFindNameGlob 2020-04-05 19:53:40 -04:00
Joseph C. Sible
d45ab327b0 Only perform the comparisons once 2020-04-05 19:45:28 -04:00
Joseph C. Sible
0f9b0f18a4 Remove unnecessary cases from wordToPseudoGlob 2020-04-05 19:30:21 -04:00
Joseph C. Sible
322842b57e Remove unnecessary monadicity from wordToPseudoGlob 2020-04-05 19:29:40 -04:00
Joseph C. Sible
b6cff5ea0e Simplify getAssociativeArrays 2020-04-05 19:06:30 -04:00
Joseph C. Sible
8f105074fe Simplify getCommandNameAndToken 2020-04-05 19:01:56 -04:00
Joseph C. Sible
d22e0aa4a7 Simplify process
Note to self: This is a lot like foldr or traverse, and would be trivial to
implement as such if it didn't need to peek ahead when takesArg is true. I
wonder if there's a clean way to implement it in terms of one of them anyway.
2020-04-05 16:45:45 -04:00
Joseph C. Sible
fb55072302 Implement supportsArrays with pattern-matching 2020-04-05 16:30:59 -04:00
Joseph C. Sible
0cc5ed4563 Don't bother with asks if you're just immediately binding the result anyway 2020-04-05 16:25:43 -04:00
Joseph C. Sible
ca41440a67 Simplify getSpecial 2020-04-05 16:21:07 -04:00
Joseph C. Sible
1cf0aa25e9 Simplify dropPrefix 2020-04-05 16:19:18 -04:00
Joseph C. Sible
4604066c37 Use head instead of (!! 0) 2020-04-05 16:16:12 -04:00
Joseph C. Sible
2ebf522a52 Simplify isArrayFlag 2020-04-05 16:13:55 -04:00
Joseph C. Sible
e4eb2d157f Remove an unnecessary operator section 2020-04-05 16:13:55 -04:00
Joseph C. Sible
f109f9ab92 Remove unnecessary as-patterns 2020-04-05 16:13:55 -04:00
Joseph C. Sible
67e091674e Remove unnecessary maybeToList
The functions we use here are polymorphic enough to work in the [] monad,
so there's no point to use them in the Maybe monad and then convert.
2020-04-05 16:13:54 -04:00
Joseph C. Sible
f833ee3d5a Use a list comprehension instead of a concatMap with extra lists 2020-04-05 15:54:12 -04:00
Joseph C. Sible
f55d8c45e5 Simplify causesSubshell 2020-04-05 15:54:12 -04:00
Joseph C. Sible
14ee462ccd Use execState instead of reimplementing it 2020-04-05 15:50:42 -04:00
Joseph C. Sible
b3c04ce3d0 Implement findFirst in terms of foldr 2020-04-05 15:50:42 -04:00
Joseph C. Sible
b0dbc79f69 Remove unnecessary Maybe from isQuoteFreeElement 2020-04-05 15:07:36 -04:00
Joseph C. Sible
2a8170ba05 Use force instead of reimplementing it 2020-04-05 15:01:57 -04:00
Vidar Holen
01f4423465 Disable SC2257 about > $((i=42)) for Dash 2020-04-05 11:38:22 -07:00
Joseph C. Sible
d2fa88dd91 Simplify nameExpansion 2020-04-05 14:04:23 -04:00
Vidar Holen
a30e42ab05 Filter GitHub uploads by tag 2020-04-04 19:30:13 -07:00
Vidar Holen
84d6e53659 Update Changelog with new version 2020-04-04 19:29:49 -07:00
Luke Davis
00574dd1fc Add conda install instructions 2020-03-03 13:23:24 -07:00
Peter Gromov
a82e606e8d Don't trigger SC2154 (unassigned var) in -n/-z expressions #1583 2020-01-31 14:49:25 +01:00
Gandalf-
fdd02c94c0 Issue 1759 mapfile and process substition
https://github.com/koalaman/shellcheck/issues/1759

When a simple process substition is used, this tripped up
the getMapfileArray function by making the last argument
not a variable
2019-12-22 23:19:03 -08:00
Glen Mailer
9423691039 Mention the CircleCI shellcheck orb in the README. 2019-08-12 22:24:47 +01:00
46 changed files with 1976 additions and 768 deletions

View File

@@ -1,73 +0,0 @@
#!/bin/bash
build_linux() {
# Linux Docker image
name="$DOCKER_BASE"
DOCKER_BUILDS="$DOCKER_BUILDS $name"
docker build -t "$name:current" .
docker run "$name:current" --version
printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
docker run -v "$PWD:/mnt" "$name:current" myscript
# Copy static executable from docker image
id=$(docker create "$name:current")
docker cp "$id:/bin/shellcheck" "shellcheck"
docker rm "$id"
ls -l shellcheck
./shellcheck myscript
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-x86_64";
done
}
build_aarch64() {
# Linux aarch64 static executable
docker run -v "$PWD:/mnt" koalaman/aarch64-builder 'buildsc'
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-aarch64"
done
}
build_armv6hf() {
# Linux armv6hf static executable
docker run -v "$PWD:/mnt" koalaman/armv6hf-builder -c 'compile-shellcheck'
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-armv6hf";
done
}
build_windows() {
# Windows .exe
docker run -v "$PWD:/appdata" koalaman/winghc cuib
for tag in $TAGS
do
cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe";
done
}
build_osx() {
# Darwin x86_64 executable
brew update
brew install cabal-install pandoc gnu-tar
sudo ln -sf /usr/local/bin/gsha512sum /usr/local/bin/sha512sum
sudo ln -sf /usr/local/bin/gtar /usr/local/bin/tar
export PATH="/usr/local/bin:$PATH"
cabal update
cabal install --dependencies-only
cabal build shellcheck
# Cabal 3 no longer has a predictable output path
path="$(find . -name 'shellcheck' -type f -perm +111)"
[[ -e "$path" ]]
for tag in $TAGS
do
cp "$path" "deploy/shellcheck-$tag.darwin-x86_64";
done
}

View File

@@ -2,10 +2,10 @@
- Rule Id (if any, e.g. SC1000): - Rule Id (if any, e.g. SC1000):
- My shellcheck version (`shellcheck --version` or "online"): - My shellcheck version (`shellcheck --version` or "online"):
- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086) - [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
- [ ] I tried on shellcheck.net and verified that this is still a problem on the latest commit - [ ] I tried on https://www.shellcheck.net/ and verified that this is still a problem on the latest commit
#### For new checks and feature suggestions #### For new checks and feature suggestions
- [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this - [ ] https://www.shellcheck.net/ (i.e. the latest commit) currently gives no useful warnings about this
- [ ] I searched through https://github.com/koalaman/shellcheck/issues and didn't find anything related - [ ] I searched through https://github.com/koalaman/shellcheck/issues and didn't find anything related

124
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,124 @@
name: Build Lol
# Run this workflow every time a new commit pushed to your repository
on: push
jobs:
package_source:
name: Package Source Code
runs-on: ubuntu-latest
steps:
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-mark manual ghc # Don't bother installing ghc just to tar up source
sudo apt-get install cabal-install
- name: Checkout repository
uses: actions/checkout@v2
- name: Package Source
run: |
mkdir source
cabal sdist
mv dist/*.tar.gz source/source.tar.gz
- name: Deduce tags
run: |
exec > source/tags
echo "latest"
if tag=$(git describe --exact-match --tags)
then
echo "stable"
echo "$tag"
fi
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: source
path: source/
build_source:
name: Build Source Code
needs: package_source
strategy:
matrix:
build: [linux.x86_64, linux.aarch64, linux.armv6hf, darwin.x86_64, windows.x86_64]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Download artifacts
uses: actions/download-artifact@v2
- name: Build source
run: |
mkdir -p bin
mkdir -p bin/${{matrix.build}}
( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} )
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: bin
path: bin/
package_binary:
name: Package Binaries
needs: build_source
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Download artifacts
uses: actions/download-artifact@v2
- name: Work around GitHub permissions bug
run: chmod +x bin/*/shellcheck*
- name: Package binaries
run: |
export TAGS="$(cat source/tags)"
mkdir -p deploy
cp -r bin/* deploy
cd deploy
../.prepare_deploy
rm -rf */ README* LICENSE*
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: deploy
path: deploy/
deploy:
name: Deploy binaries
needs: package_binary
runs-on: ubuntu-latest
environment: Deploy
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Download artifacts
uses: actions/download-artifact@v2
- name: Upload to GitHub
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
export TAGS="$(cat source/tags)"
./.github_deploy
- name: Upload to Docker Hub
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_EMAIL: ${{ secrets.DOCKER_EMAIL }}
DOCKER_BASE: ${{ secrets.DOCKER_USERNAME }}/shellcheck
run: |
export TAGS="$(cat source/tags)"
( source ./.multi_arch_docker && set -eux && multi_arch_docker::main )

View File

@@ -2,39 +2,10 @@
set -x set -x
shopt -s extglob shopt -s extglob
if [[ "$TRAVIS_SECURE_ENV_VARS" != "true" ]]
then
echo >&2 "Missing TRAVIS_SECURE_ENV_VARS. Skipping GitHub deployment."
exit 0
fi
install_deps() {
version="2.7.0" # 2.14.1 fails to overwrite duplicates
case "$(uname)" in
Linux)
sudo apt-get update
sudo apt-get install curl
curl -L "https://github.com/github/hub/releases/download/v$version/hub-linux-amd64-$version.tgz" | tar xvz --strip-components=1 "hub-linux-amd64-$version/bin/hub"
;;
Darwin)
curl -L "https://github.com/github/hub/releases/download/v$version/hub-darwin-amd64-$version.tgz" | tar xvz --strip-components=1 "hub-darwin-amd64-$version/bin/hub"
;;
*)
echo "Unknown: $(uname)"
exit 1
;;
esac
hub_path="$PWD/bin/hub"
hub() {
"$hub_path" "$@"
}
}
install_deps
export EDITOR="touch" export EDITOR="touch"
# Sanity check # Sanity check
gh --version || exit 1
hub release show latest || exit 1 hub release show latest || exit 1
for tag in $TAGS for tag in $TAGS
@@ -50,8 +21,9 @@ do
for file in deploy/* for file in deploy/*
do do
[[ $file == *.@(xz|gz|zip) ]] || continue [[ $file == *.@(xz|gz|zip) ]] || continue
files+=(-a "$file") [[ $file == *"$tag"* ]] || continue
files+=("$file")
done done
hub release edit "${files[@]}" "$tag" || exit 1 gh release upload "$tag" "${files[@]}" --clobber || exit 1
done done

View File

@@ -1,12 +1,6 @@
#!/bin/bash #!/bin/bash
# This script builds and deploys multi-architecture docker images from the # This script builds and deploys multi-architecture docker images from the
# binaries previously built and deployed to GCS by the Travis pipeline. # binaries previously built and deployed to GitHub.
if [[ "$TRAVIS_SECURE_ENV_VARS" != "true" ]]
then
echo >&2 "Missing TRAVIS_SECURE_ENV_VARS. Skipping Docker builds."
exit 0
fi
function multi_arch_docker::install_docker_buildx() { function multi_arch_docker::install_docker_buildx() {
# Install up-to-date version of docker, with buildx support. # Install up-to-date version of docker, with buildx support.
@@ -108,6 +102,5 @@ function multi_arch_docker::main() {
multi_arch_docker::install_docker_buildx multi_arch_docker::install_docker_buildx
multi_arch_docker::login_to_docker_hub multi_arch_docker::login_to_docker_hub
multi_arch_docker::build_and_push_all multi_arch_docker::build_and_push_all
set +x
multi_arch_docker::test_all multi_arch_docker::test_all
} }

View File

@@ -1,8 +1,9 @@
#!/bin/bash #!/bin/bash
# This script packages up Travis compiled binaries # This script packages up compiled binaries
set -ex set -ex
shopt -s nullglob shopt -s nullglob extglob
cd deploy
ls -l
cp ../LICENSE LICENSE.txt cp ../LICENSE LICENSE.txt
sed -e $'s/$/\r/' > README.txt << END sed -e $'s/$/\r/' > README.txt << END
@@ -22,44 +23,32 @@ This binary was compiled on $(date -u).
$(git log -n 3) $(git log -n 3)
END END
for file in ./*.exe for dir in */
do do
zip "${file%.*}.zip" README.txt LICENSE.txt "$file" cp LICENSE.txt README.txt "$dir"
done done
for file in *.linux-x86_64 echo "Tags are $TAGS"
do
base="${file%.*}"
cp "$file" "shellcheck"
tar -cJf "$base.linux.x86_64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in *.linux-aarch64 for tag in $TAGS
do do
base="${file%.*}"
cp "$file" "shellcheck"
tar -cJf "$base.linux.aarch64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in *.linux-armv6hf for dir in windows.*/
do do
base="${file%.*}" ( cd "$dir" && zip "../shellcheck-$tag.zip" * )
cp "$file" "shellcheck" done
tar -cJf "$base.linux.armv6hf.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in *.darwin-x86_64 for dir in {linux,darwin}.*/
do do
base="${file%.*}" base="${dir%/}"
cp "$file" "shellcheck" ( cd "$dir" && tar -cJf "../shellcheck-$tag.$base.tar.xz" --transform="s:^:shellcheck-$tag/:" * )
tar -cJf "$base.darwin.x86_64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck done
rm "shellcheck"
done done
for file in ./* for file in ./*
do do
[[ -f "$file" ]] || continue
sha512sum "$file" > "$file.sha512sum" sha512sum "$file" > "$file.sha512sum"
done done
ls -l

View File

@@ -1,64 +0,0 @@
language: shell
os: linux
services:
- docker
jobs:
include:
- stage: Build
# This must weirdly not have a dash, otherwise an empty job is created
env: BUILD=linux
- env: BUILD=windows
- env: BUILD=armv6hf
- env: BUILD=aarch64
- env: BUILD=osx
os: osx
- stage: Deploy docker image
# Deploy only for pushes to master branch, not other branches, not PRs.
if: type = push
script:
- source ./.multi_arch_docker
- set -ex; multi_arch_docker::main; set +x
# This is in global context and runs for every stage that doesn't override it.
before_install: |
DOCKER_BASE="$DOCKER_USERNAME/shellcheck"
DOCKER_BUILDS=""
export TAGS=""
test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
echo "Tags are $TAGS"
# This is in global context and runs for every stage that doesn't override it.
script:
- mkdir -p deploy
- source ./.compile_binaries
- ./striptests
- set -ex; build_"$BUILD"; set +x;
- ./.prepare_deploy
- ./.github_deploy
# This is in global context and runs for every stage that doesn't override it.
after_failure: |
id
pwd
df -h
find . -name '*.log' -type f -exec grep "" /dev/null {} +
find . -ls
# This is in global context and runs for every stage that doesn't override it.
deploy:
provider: gcs
skip_cleanup: true
access_key_id: GOOG7MDN7WEH6IIGBDCA
secret_access_key:
secure: Bcx2cT0/E2ikj7sdamVq52xlLZF9dz9ojGPtoKfPyQhkkZa+McVI4xgUSuyyoSxyKj77sofx2y8m6PJYYumT4g5hREV1tfeUkl0J2DQFMbGDYEt7kxVkXCxojNvhHwTzLFv0ezstrxWWxQm81BfQQ4U9lggRXtndAP4czZnOeHPINPSiue1QNwRAEw05r5UoIUJXy/5xyUrjIxn381pAs+gJqP2COeN9kTKYH53nS/AAws29RprfZFnPlo7xxWmcjRcdS5KPdGXI/c6tQp5zl2iTh510VC1PN2w1Wvnn/oNWhiNdqPyVDsojIX5+sS3nejzJA+KFMxXSBlyXIY3wPpS/MdscU79X6Q5f9ivsFfsm7gNBmxHUPNn0HAvU4ROT/CCE9j6jSbs5PC7QBo3CK4++jxAwE/pd9HUc2rs3k0ofx3rgveJ7txpy5yPKfwIIBi98kVKlC4w7dLvNTOfjW1Imt2yH87XTfsE0UIG9st1WII6s4l/WgBx2GuwKdt6+3QUYiAlCFckkxWi+fAvpHZUEL43Qxub5fN+ZV7Zib1n7opchH4QKGBb6/y0WaDCmtCfu0lppoe/TH6saOTjDFj67NJSElK6ZDxGZ3uw4R+ret2gm6WRKT2Oeub8J33VzSa7VkmFpMPrAAfPa9N1Z4ewBLoTmvxSg2A0dDrCdJio=
bucket: shellcheck-private
local_dir: deploy
on:
repo: koalaman/shellcheck
condition: $TRAVIS_BUILD_STAGE_NAME = Build
all_branches: true

View File

@@ -1,3 +1,29 @@
## Git
### Added
- `disable` directives can now be a range, e.g. `disable=SC3000-SC4000`
- SC2259/SC2260: Warn when redirections override pipes
- SC2261: Warn about multiple competing redirections
- SC2262/SC2263: Warn about aliases declared and used in the same parsing unit
- SC2264: Warn about wrapper functions that blatantly recurse
- SC2265/SC2266: Warn when using & or | with test statements
- SC2267: Warn when using xargs -i instead of -I
- Optional avoid-x-comparisons: Style warning SC2268 for `[ x$var = xval ]`
### Fixed
- SC1072/SC1073 now respond to disable annotations, though ignoring parse errors
is still purely cosmetic and does not allow ShellCheck to continue.
- Improved error reporting for trailing tokens after ]/]] and compound commands
- `#!/usr/bin/env -S shell` is now handled correctly
- Here docs with \r are now parsed correctly and give better warnings
### Changed
- Assignments are now parsed to spec, without leniency for leading $ or spaces
- POSIX/dash unsupported feature warnings now have individual SC3xxx codes
- SC1090: A leading `$x/` or `$(x)/` is now treated as `./` when locating files
- SC2154: Variables appearing in -z/-n tests are no longer considered unassigned
- SC2270-SC2285: Improved warnings about misused =, e.g. `${var}=42`
## v0.7.1 - 2020-04-04 ## v0.7.1 - 2020-04-04
### Fixed ### Fixed
- `-f diff` no longer claims that it found more issues when it didn't - `-f diff` no longer claims that it found more issues when it didn't

View File

@@ -1,29 +0,0 @@
# Build-only image
FROM ubuntu:18.04 AS build
USER root
WORKDIR /opt/shellCheck
# Install OS deps
RUN apt-get update && apt-get install -y ghc cabal-install
# Install Haskell deps
# (This is a separate copy/run so that source changes don't require rebuilding)
COPY ShellCheck.cabal ./
RUN cabal update && cabal install --dependencies-only --ghc-options="-optlo-Os -split-sections"
# Copy source and build it
COPY LICENSE shellcheck.hs ./
COPY src src
RUN cabal build Paths_ShellCheck && \
ghc -optl-static -optl-pthread -isrc -idist/build/autogen --make shellcheck -split-sections -optc-Wl,--gc-sections -optlo-Os && \
strip --strip-all shellcheck
RUN mkdir -p /out/bin && \
cp shellcheck /out/bin/
# Resulting ShellCheck image
FROM scratch
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
WORKDIR /mnt
COPY --from=build /out /
ENTRYPOINT ["/bin/shellcheck"]

View File

@@ -109,7 +109,8 @@ Services and platforms that have ShellCheck pre-installed and ready to use:
* [Codacy](https://www.codacy.com/) * [Codacy](https://www.codacy.com/)
* [Code Climate](https://codeclimate.com/) * [Code Climate](https://codeclimate.com/)
* [Code Factor](https://www.codefactor.io/) * [Code Factor](https://www.codefactor.io/)
* [Github](https://github.com/features/actions)(only Linux) * [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck)
* [Github](https://github.com/features/actions) (only Linux)
Services and platforms with third party plugins: Services and platforms with third party plugins:
@@ -148,7 +149,7 @@ On Arch Linux based distros:
pacman -S shellcheck pacman -S shellcheck
or get the dependency free [shellcheck-static](https://aur.archlinux.org/packages/shellcheck-static/) from the AUR. or get the dependency free [shellcheck-bin](https://aur.archlinux.org/packages/shellcheck-bin/) from the AUR.
On Gentoo based distros: On Gentoo based distros:
@@ -167,10 +168,14 @@ On FreeBSD:
pkg install hs-ShellCheck pkg install hs-ShellCheck
On OS X with homebrew: On macOS (OS X) with Homebrew:
brew install shellcheck brew install shellcheck
Or with MacPorts:
sudo port install shellcheck
On OpenBSD: On OpenBSD:
pkg_add shellcheck pkg_add shellcheck
@@ -197,6 +202,10 @@ Or Windows (via [scoop](http://scoop.sh)):
C:\> scoop install shellcheck C:\> scoop install shellcheck
``` ```
From [conda-forge](https://anaconda.org/conda-forge/shellcheck):
conda install -c conda-forge shellcheck
From Snap Store: From Snap Store:
snap install --channel=edge shellcheck snap install --channel=edge shellcheck
@@ -220,7 +229,7 @@ Alternatively, you can download pre-compiled binaries for the latest release her
* [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked) * [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
* [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked) * [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
* [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked) * [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)
* [MacOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz) * [macOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz)
* [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip) * [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip)
or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
@@ -264,7 +273,7 @@ This section describes how to build ShellCheck from a source directory. ShellChe
ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `brew`, `emerge`, `yum`, or `zypper`). ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `brew`, `emerge`, `yum`, or `zypper`).
On MacOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source. On macOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source.
$ brew install cabal-install $ brew install cabal-install

13
build/README.md Normal file
View File

@@ -0,0 +1,13 @@
This directory contains Dockerfiles for all builds.
A build image will:
* Run on Linux x86\_64 with vanilla Docker (no exceptions)
* Not contain any software that would restrict easy modification or copying
* Take a `cabal sdist` style tar.gz of the ShellCheck directory on stdin
* Output a tar.gz of artifacts on stdout, in a directory named for the arch
This makes it simple to build any release without exotic hardware or software.
An image can be built and tagged using `build_builder`,
and run on a source tarball using `run_builder`.

12
build/build_builder Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
if [ $# -eq 0 ]
then
echo >&2 "No build image directories specified"
echo >&2 "Example: $0 build/*/"
exit 1
fi
for dir
do
( cd "$dir" && docker build -t "$(cat tag)" . ) || exit 1
done

View File

@@ -0,0 +1,31 @@
# DIGEST:sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76
FROM liushuyu/osxcross:latest
ENV TARGET x86_64-apple-darwin18
ENV TARGETNAME darwin.x86_64
# Build dependencies
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y ghc automake autoconf llvm curl
# Build GHC
WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
RUN make install
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/local/bin
# Due to an apparent cabal bug, we specify our options directly to cabal
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
ENV CABALOPTS "--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
# Prebuild the dependencies
RUN cabal update && IFS=';' && cabal install $CABALOPTS --lib Diff-0.4.0 base-compat-0.11.2 base-orphans-0.8.4 dlist-1.0 hashable-1.3.0.0 indexed-traversable-0.1.1 integer-logarithms-1.0.3.1 primitive-0.7.1.0 regex-base-0.94.0.0 splitmix-0.1.0.3 tagged-0.8.6.1 th-abstraction-0.4.2.0 transformers-compat-0.6.6 base-compat-batteries-0.11.2 time-compat-1.9.5 unordered-containers-0.2.13.0 data-fix-0.3.1 vector-0.12.2.0 scientific-0.3.6.2 regex-tdfa-1.3.1.0 random-1.2.0 distributive-0.6.2.1 attoparsec-0.13.2.5 uuid-types-1.0.3 comonad-5.0.8 bifunctors-5.5.10 assoc-1.0.2 these-1.1.1.1 strict-0.4.0.1 aeson-1.5.5.1
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

14
build/darwin.x86_64/build Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
set -xe
{
tar xzv --strip-components=1
./striptests
mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
"$TARGET-strip" -Sx "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
} >&2
tar czv "$TARGETNAME"

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

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

View File

@@ -0,0 +1,30 @@
FROM ubuntu:20.04
ENV TARGET aarch64-linux-gnu
ENV TARGETNAME linux.aarch64
# Build dependencies
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y ghc automake autoconf build-essential llvm curl qemu-user-static gcc-$TARGET
# Build GHC
WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
RUN make install
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/local/bin
# Due to an apparent cabal bug, we specify our options directly to cabal
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
# Prebuild the dependencies
RUN cabal update && IFS=';' && cabal install $CABALOPTS --lib Diff-0.4.0 base-compat-0.11.2 base-orphans-0.8.4 dlist-1.0 hashable-1.3.0.0 indexed-traversable-0.1.1 integer-logarithms-1.0.3.1 primitive-0.7.1.0 regex-base-0.94.0.0 splitmix-0.1.0.3 tagged-0.8.6.1 th-abstraction-0.4.2.0 transformers-compat-0.6.6 base-compat-batteries-0.11.2 time-compat-1.9.5 unordered-containers-0.2.13.0 data-fix-0.3.1 vector-0.12.2.0 scientific-0.3.6.2 regex-tdfa-1.3.1.0 random-1.2.0 distributive-0.6.2.1 attoparsec-0.13.2.5 uuid-types-1.0.3 comonad-5.0.8 bifunctors-5.5.10 assoc-1.0.2 these-1.1.1.1 strict-0.4.0.1 aeson-1.5.5.1
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

15
build/linux.aarch64/build Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
set -xe
{
tar xzv --strip-components=1
./striptests
mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
"$TARGET-strip" -s "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
qemu-aarch64-static "$TARGETNAME/shellcheck" --version
} >&2
tar czv "$TARGETNAME"

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

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

View File

@@ -0,0 +1,59 @@
# I've again spent days trying to get a working armv6hf compiler going.
# God only knows how many recompilations of GCC, GHC, libraries, and
# ShellCheck itself, has gone into it.
#
# I tried Debian's toolchain. I tried my custom one built according to
# RPi `gcc -v`. I tried GHC9, glibc, musl, registerised vs not, but
# nothing has yielded an armv6hf binary that does not immediately
# segfault on qemu-arm-static or the RPi itself.
#
# I then tried the same but with armv7hf. Same story.
#
# Emulating the entire userspace with balenalib again? Very strange build
# failures where programs would fail to execute with > ~100 arguments.
#
# Finally, creating our own appears to work when using a custom QEmu
# patched to follow execve calls.
#
# PS: $100 bounty for getting a RPi1 compatible static build going
# with cross-compilation, similar to what the aarch64 build does.
#
FROM ubuntu:20.04
ENV TARGETNAME linux.armv6hf
# Build QEmu with execve follow support
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
RUN apt-get install -y build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev
WORKDIR /build
RUN git clone --depth 1 https://github.com/koalaman/qemu
RUN cd qemu && ./configure --static && cd build && ninja qemu-arm
RUN cp qemu/build/qemu-arm /build/qemu-arm-static
ENV QEMU_EXECVE 1
# Set up an armv6 userspace
WORKDIR /
RUN apt-get install -y debootstrap qemu-user-static
# We expect this to fail if the host doesn't have binfmt qemu support
RUN qemu-debootstrap --arch armhf bullseye pi http://mirrordirector.raspbian.org/raspbian || [ -e /pi/etc/issue ]
RUN cp /build/qemu-arm-static /pi/usr/bin/qemu-arm-static
RUN printf > /bin/pirun '%s\n' '#!/bin/sh' 'chroot /pi /usr/bin/qemu-arm-static /usr/bin/env "$@"' && chmod +x /bin/pirun
# If the debootstrap process didn't finish, continue it
RUN [ ! -e /pi/debootstrap ] || pirun '/debootstrap/debootstrap' --second-stage
# Install deps in the chroot
RUN pirun apt-get update
RUN pirun apt-get install -y ghc cabal-install
# Finally we can build the current dependencies. This takes hours.
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--gcc-options;-Os -Wl,--gc-sections -ffunction-sections -fdata-sections"
RUN pirun cabal update
RUN IFS=";" && pirun cabal install --lib $CABALOPTS Diff-0.4.0 base-compat-0.11.2 base-orphans-0.8.4 dlist-1.0 hashable-1.3.1.0 indexed-traversable-0.1.1 integer-logarithms-1.0.3.1 primitive-0.7.1.0 regex-base-0.94.0.1 splitmix-0.1.0.3 tagged-0.8.6.1 th-abstraction-0.4.2.0 transformers-compat-0.6.6 base-compat-batteries-0.11.2 time-compat-1.9.5 unordered-containers-0.2.13.0 data-fix-0.3.1 vector-0.12.2.0 scientific-0.3.6.2 regex-tdfa-1.3.1.0 random-1.2.0 distributive-0.6.2.1 attoparsec-0.13.2.5 uuid-types-1.0.4 comonad-5.0.8 bifunctors-5.5.10 assoc-1.0.2 these-1.1.1.1 strict-0.4.0.1 aeson-1.5.6.0
# Copy the build script
WORKDIR /pi/scratch
COPY build /pi/usr/bin
ENTRYPOINT ["/bin/pirun", "/usr/bin/build"]

16
build/linux.armv6hf/build Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
set -xe
cd /scratch
{
tar xzv --strip-components=1
./striptests
mkdir "$TARGETNAME"
# This script does not cabal update because compiling anything new is slow
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
strip -s "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
"$TARGETNAME/shellcheck" --version
} >&2
tar czv "$TARGETNAME"

1
build/linux.armv6hf/tag Normal file
View File

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

View File

@@ -0,0 +1,30 @@
FROM ubuntu:20.04
ENV TARGET powerpc64le-linux-gnu
ENV TARGETNAME linux.ppc64le
# Build dependencies
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y ghc automake autoconf build-essential llvm curl qemu-user-static gcc-$TARGET
# Build GHC
WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" --enable-unregisterised
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
RUN make install
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/local/bin
# Due to an apparent cabal bug, we specify our options directly to cabal
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
ENV CABALOPTS "--ghc-options;-optl-Wl,-fuse-ld=bfd -split-sections -optc-Os -optc-Wl,--gc-sections;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
# Prebuild the dependencies
#RUN cabal update && IFS=';' && cabal install $CABALOPTS --lib Diff-0.4.0 base-compat-0.11.2 base-orphans-0.8.4 dlist-1.0 hashable-1.3.0.0 indexed-traversable-0.1.1 integer-logarithms-1.0.3.1 primitive-0.7.1.0 regex-base-0.94.0.0 splitmix-0.1.0.3 tagged-0.8.6.1 th-abstraction-0.4.2.0 transformers-compat-0.6.6 base-compat-batteries-0.11.2 time-compat-1.9.5 unordered-containers-0.2.13.0 data-fix-0.3.1 vector-0.12.2.0 scientific-0.3.6.2 regex-tdfa-1.3.1.0 random-1.2.0 distributive-0.6.2.1 attoparsec-0.13.2.5 uuid-types-1.0.3 comonad-5.0.8 bifunctors-5.5.10 assoc-1.0.2 these-1.1.1.1 strict-0.4.0.1 aeson-1.5.5.1
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

15
build/linux.ppc64le/build Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
set -xe
{
tar xzv --strip-components=1
./striptests
mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
"$TARGET-strip" -s "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
qemu-ppc64le-static "$TARGETNAME/shellcheck" --version
} >&2
tar czv "$TARGETNAME"

1
build/linux.ppc64le/tag Normal file
View File

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

View File

@@ -0,0 +1,26 @@
FROM ubuntu:20.04
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
# Use ld.bfd instead of ld.gold due to
# x86_64-linux-gnu/libpthread.a(pthread_cond_init.o)(.note.stapsdt+0x14): error:
# relocation refers to local symbol "" [2], which is defined in a discarded section
ENV CABALOPTS "--ghc-options;-optl-Wl,-fuse-ld=bfd -split-sections -optc-Os -optc-Wl,--gc-sections"
# Other archs pre-build dependencies here, but this one doesn't to detect ecosystem movement
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

15
build/linux.x86_64/build Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
set -xe
{
tar xzv --strip-components=1
./striptests
mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
strip -s "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
"$TARGETNAME/shellcheck" --version
} >&2
tar czv "$TARGETNAME"

1
build/linux.x86_64/tag Normal file
View File

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

30
build/run_builder Executable file
View File

@@ -0,0 +1,30 @@
#!/bin/bash
if [ $# -lt 2 ]
then
echo >&2 "This script builds a source archive (as produced by cabal sdist)"
echo >&2 "Usage: $0 sourcefile.tar.gz builddir..."
exit 1
fi
file=$(realpath "$1")
shift
if [ ! -e "$file" ]
then
echo >&2 "$file does not exist"
exit 1
fi
set -ex -o pipefail
for dir
do
tagfile="$dir/tag"
if [ ! -e "$tagfile" ]
then
echo >&2 "$tagfile does not exist"
exit 2
fi
docker run -i "$(< "$tagfile")" < "$file" | tar xz
done

View File

@@ -0,0 +1,27 @@
FROM ubuntu:20.04
ENV TARGETNAME windows.x86_64
# We don't need wine32, even though it complains
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y curl busybox wine
# Fetch Windows version, will be available under z:\haskell
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/* .
ENV WINEPATH /haskell/bin
# It's unknown whether Cabal on Windows suffers from the same issue
# that necessitated this but I don't care enough to find out
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections"
# Precompile some deps to speed up later builds. This list is just copied from `cabal build`
RUN wine /haskell/bin/cabal.exe update && IFS=';' && wine /haskell/bin/cabal.exe install $CABALOPTS --lib Diff-0.4.0 base-compat-0.11.2 base-orphans-0.8.4 dlist-1.0 hashable-1.3.0.0 indexed-traversable-0.1.1 integer-logarithms-1.0.3.1 primitive-0.7.1.0 regex-base-0.94.0.0 splitmix-0.1.0.3 tagged-0.8.6.1 th-abstraction-0.4.2.0 transformers-compat-0.6.6 base-compat-batteries-0.11.2 time-compat-1.9.5 unordered-containers-0.2.13.0 data-fix-0.3.1 vector-0.12.2.0 scientific-0.3.6.2 regex-tdfa-1.3.1.0 random-1.2.0 distributive-0.6.2.1 attoparsec-0.13.2.5 uuid-types-1.0.3 comonad-5.0.8 bifunctors-5.5.10 assoc-1.0.2 these-1.1.1.1 strict-0.4.0.1 aeson-1.5.5.1
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

19
build/windows.x86_64/build Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/sh
cabal() {
wine /haskell/bin/cabal.exe "$@"
}
set -xe
{
tar xzv --strip-components=1
./striptests
mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS )
find dist*/ -name shellcheck.exe -type f -ls -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
wine "/haskell/mingw/bin/strip.exe" -s "$TARGETNAME/shellcheck.exe"
ls -l "$TARGETNAME"
wine "$TARGETNAME/shellcheck.exe" --version
} >&2
tar czv "$TARGETNAME"

1
build/windows.x86_64/tag Normal file
View File

@@ -0,0 +1 @@
koalaman/scbuilder-windows-x86_64

View File

@@ -6,7 +6,7 @@ then
exit 1 exit 1
fi fi
for i in 1 2 for i in 1 2 3
do do
last=$(grep -hv "^prop" ./**/*.hs | grep -Ewo "${i}[0-9]{3}" | sort -n | tail -n 1) last=$(grep -hv "^prop" ./**/*.hs | grep -Ewo "${i}[0-9]{3}" | sort -n | tail -n 1)
echo "Next ${i}xxx: $((last+1))" echo "Next ${i}xxx: $((last+1))"

View File

@@ -232,7 +232,8 @@ Valid keys are:
**disable** **disable**
: Disables a comma separated list of error codes for the following command. : Disables a comma separated list of error codes for the following command.
The command can be a simple command like `echo foo`, or a compound command The command can be a simple command like `echo foo`, or a compound command
like a function definition, subshell block or loop. like a function definition, subshell block or loop. A range can be
be specified with a dash, e.g. `disable=SC3000-SC4000` to exclude 3xxx.
**enable** **enable**
: Enable an optional check by name, as listed with **--list-optional**. : Enable an optional check by name, as listed with **--list-optional**.
@@ -254,7 +255,7 @@ Valid keys are:
**shell** **shell**
: Overrides the shell detected from the shebang. This is useful for : Overrides the shell detected from the shebang. This is useful for
files meant to be included (and thus lacking a shebang), or possibly files meant to be included (and thus lacking a shebang), or possibly
as a more targeted alternative to 'disable=2039'. as a more targeted alternative to 'disable=SC2039'.
# RC FILES # RC FILES

View File

@@ -507,7 +507,7 @@ ioInterface options files = do
where where
find filename deflt = do find filename deflt = do
sources <- findM ((allowable inputs) `andM` doesFileExist) $ sources <- findM ((allowable inputs) `andM` doesFileExist) $
(adjustPath filename):(map (</> filename) $ map adjustPath $ sourcePathFlag ++ sourcePathAnnotation) (adjustPath filename):(map ((</> filename) . adjustPath) $ sourcePathFlag ++ sourcePathAnnotation)
case sources of case sources of
Nothing -> return deflt Nothing -> return deflt
Just first -> return first Just first -> return first

View File

@@ -31,6 +31,8 @@ apps:
shellcheck: shellcheck:
command: usr/bin/shellcheck command: usr/bin/shellcheck
plugs: [home, removable-media] plugs: [home, removable-media]
environment:
LANG: C.UTF-8
parts: parts:
shellcheck: shellcheck:

View File

@@ -145,7 +145,7 @@ data InnerToken t =
deriving (Show, Eq, Functor, Foldable, Traversable) deriving (Show, Eq, Functor, Foldable, Traversable)
data Annotation = data Annotation =
DisableComment Integer DisableComment Integer Integer -- [from, to)
| EnableComment String | EnableComment String
| SourceOverride String | SourceOverride String
| ShellOverride String | ShellOverride String

View File

@@ -28,6 +28,10 @@ import Data.Functor
import Data.Functor.Identity import Data.Functor.Identity
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import qualified Data.Map as Map
import Numeric (showHex)
arguments (T_SimpleCommand _ _ (cmd:args)) = args
-- Is this a type of loop? -- Is this a type of loop?
isLoop t = case t of isLoop t = case t of
@@ -134,13 +138,95 @@ isUnquotedFlag token = fromMaybe False $ do
str <- getLeadingUnquotedString token str <- getLeadingUnquotedString token
return $ "-" `isPrefixOf` str return $ "-" `isPrefixOf` str
-- Given a T_DollarBraced, return a simplified version of the string contents. -- getGnuOpts "erd:u:" will parse a list of arguments tokens like `read`
bracedString (T_DollarBraced _ _ l) = concat $ oversimplify l -- -re -d : -u 3 bar
bracedString _ = error "Internal shellcheck error, please report! (bracedString on non-variable)" -- into
-- Just [("r", (-re, -re)), ("e", (-re, -re)), ("d", (-d,:)), ("u", (-u,3)), ("", (bar,bar))]
--
-- Each string flag maps to a tuple of (flag, argument), where argument=flag if it
-- doesn't take a specific one.
--
-- Any unrecognized flag will result in Nothing. The exception is if arbitraryLongOpts
-- is set, in which case --anything will map to "anything".
getGnuOpts :: String -> [Token] -> Maybe [(String, (Token, Token))]
getGnuOpts str args = getOpts (True, False) str [] args
-- As above, except the first non-arg string will treat the rest as arguments
getBsdOpts :: String -> [Token] -> Maybe [(String, (Token, Token))]
getBsdOpts str args = getOpts (False, False) str [] args
-- Tests for this are in Commands.hs where it's more frequently used
getOpts ::
-- Behavioral config: gnu style, allow arbitrary long options
(Bool, Bool)
-- A getopts style string
-> String
-- List of long options and whether they take arguments
-> [(String, Bool)]
-- List of arguments (excluding command)
-> [Token]
-- List of flags to tuple of (optionToken, valueToken)
-> Maybe [(String, (Token, Token))]
getOpts (gnu, arbitraryLongOpts) string longopts args = process args
where
flagList (c:':':rest) = ([c], True) : flagList rest
flagList (c:rest) = ([c], False) : flagList rest
flagList [] = longopts
flagMap = Map.fromList $ ("", False) : flagList string
process [] = return []
process (token:rest) = do
case getLiteralStringDef "\0" token of
"--" -> return $ listToArgs rest
'-':'-':word -> do
let (name, arg) = span (/= '=') word
needsArg <-
if arbitraryLongOpts
then return $ Map.findWithDefault False name flagMap
else Map.lookup name flagMap
if needsArg && null arg
then
case rest of
(arg:rest2) -> do
more <- process rest2
return $ (name, (token, arg)) : more
_ -> fail "Missing arg"
else do
more <- process rest
-- Consider splitting up token to get arg
return $ (name, (token, token)) : more
'-':opts -> shortToOpts opts token rest
arg ->
if gnu
then do
more <- process rest
return $ ("", (token, token)):more
else return $ listToArgs (token:rest)
shortToOpts opts token args =
case opts of
c:rest -> do
needsArg <- Map.lookup [c] flagMap
case () of
_ | needsArg && null rest -> do
(next:restArgs) <- return args
more <- process restArgs
return $ ([c], (token, next)):more
_ | needsArg -> do
more <- process args
return $ ([c], (token, token)):more
_ -> do
more <- shortToOpts rest token args
return $ ([c], (token, token)):more
[] -> process args
listToArgs = map (\x -> ("", (x, x)))
-- Is this an expansion of multiple items of an array? -- Is this an expansion of multiple items of an array?
isArrayExpansion t@(T_DollarBraced _ _ _) = isArrayExpansion (T_DollarBraced _ _ l) =
let string = bracedString t in let string = concat $ oversimplify l in
"@" `isPrefixOf` string || "@" `isPrefixOf` string ||
not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string
isArrayExpansion _ = False isArrayExpansion _ = False
@@ -148,8 +234,8 @@ isArrayExpansion _ = False
-- Is it possible that this arg becomes multiple args? -- Is it possible that this arg becomes multiple args?
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
where where
f t@(T_DollarBraced _ _ _) = f (T_DollarBraced _ _ l) =
let string = bracedString t in let string = concat $ oversimplify l in
"!" `isPrefixOf` string "!" `isPrefixOf` string
f (T_DoubleQuoted _ parts) = any f parts f (T_DoubleQuoted _ parts) = any f parts
f (T_NormalWord _ parts) = any f parts f (T_NormalWord _ parts) = any f parts
@@ -193,6 +279,12 @@ getUnquotedLiteral (T_NormalWord _ list) =
str _ = Nothing str _ = Nothing
getUnquotedLiteral _ = Nothing getUnquotedLiteral _ = Nothing
isQuotes t =
case t of
T_DoubleQuoted {} -> True
T_SingleQuoted {} -> True
_ -> False
-- Get the last unquoted T_Literal in a word like "${var}foo"THIS -- Get the last unquoted T_Literal in a word like "${var}foo"THIS
-- or nothing if the word does not end in an unquoted literal. -- or nothing if the word does not end in an unquoted literal.
getTrailingUnquotedLiteral :: Token -> Maybe Token getTrailingUnquotedLiteral :: Token -> Maybe Token
@@ -211,8 +303,11 @@ getTrailingUnquotedLiteral t =
getLeadingUnquotedString :: Token -> Maybe String getLeadingUnquotedString :: Token -> Maybe String
getLeadingUnquotedString t = getLeadingUnquotedString t =
case t of case t of
T_NormalWord _ ((T_Literal _ s) : _) -> return s T_NormalWord _ ((T_Literal _ s) : rest) -> return $ s ++ from rest
_ -> Nothing _ -> Nothing
where
from ((T_Literal _ s):rest) = s ++ from rest
from _ = ""
-- Maybe get the literal string of this token and any globs in it. -- Maybe get the literal string of this token and any globs in it.
getGlobOrLiteralString = getLiteralStringExt f getGlobOrLiteralString = getLiteralStringExt f
@@ -273,6 +368,37 @@ getLiteralStringExt more = g
-- Is this token a string literal? -- Is this token a string literal?
isLiteral t = isJust $ getLiteralString t isLiteral t = isJust $ getLiteralString t
-- Escape user data for messages.
-- Messages generally avoid repeating user data, but sometimes it's helpful.
e4m = escapeForMessage
escapeForMessage :: String -> String
escapeForMessage str = concatMap f str
where
f '\\' = "\\\\"
f '\n' = "\\n"
f '\r' = "\\r"
f '\t' = "\\t"
f '\x1B' = "\\e"
f c =
if shouldEscape c
then
if ord c < 256
then "\\x" ++ (pad0 2 $ toHex c)
else "\\U" ++ (pad0 4 $ toHex c)
else [c]
shouldEscape c =
(not $ isPrint c)
|| (not (isAscii c) && not (isLetter c))
pad0 :: Int -> String -> String
pad0 n s =
let l = length s in
if l < n
then (replicate (n-l) '0') ++ s
else s
toHex :: Char -> String
toHex c = map toUpper $ showHex (ord c) ""
-- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz] -- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz]
getWordParts (T_NormalWord _ l) = concatMap getWordParts l getWordParts (T_NormalWord _ l) = concatMap getWordParts l
@@ -301,7 +427,7 @@ getCommand t =
-- Maybe get the command name string of a token representing a command -- Maybe get the command name string of a token representing a command
getCommandName :: Token -> Maybe String getCommandName :: Token -> Maybe String
getCommandName = fst . getCommandNameAndToken getCommandName = fst . getCommandNameAndToken False
-- Maybe get the name+arguments of a command. -- Maybe get the name+arguments of a command.
getCommandArgv t = do getCommandArgv t = do
@@ -311,20 +437,38 @@ getCommandArgv t = do
-- Get the command name token from a command, i.e. -- Get the command name token from a command, i.e.
-- the token representing 'ls' in 'ls -la 2> foo'. -- the token representing 'ls' in 'ls -la 2> foo'.
-- If it can't be determined, return the original token. -- If it can't be determined, return the original token.
getCommandTokenOrThis = snd . getCommandNameAndToken getCommandTokenOrThis = snd . getCommandNameAndToken False
getCommandNameAndToken :: Token -> (Maybe String, Token) -- Given a command, get the string and token that represents the command name.
getCommandNameAndToken t = fromMaybe (Nothing, t) $ do -- If direct, return the actual command (e.g. exec in 'exec ls')
(T_SimpleCommand _ _ (w:rest)) <- getCommand t -- If not, return the logical command (e.g. 'ls' in 'exec ls')
getCommandNameAndToken :: Bool -> Token -> (Maybe String, Token)
getCommandNameAndToken direct t = fromMaybe (Nothing, t) $ do
cmd@(T_SimpleCommand _ _ (w:rest)) <- getCommand t
s <- getLiteralString w s <- getLiteralString w
if "busybox" `isSuffixOf` s || "builtin" == s return $ fromMaybe (Just s, w) $ do
then guard $ not direct
case rest of actual <- getEffectiveCommandToken s cmd rest
(applet:_) -> return (getLiteralString applet, applet) return (getLiteralString actual, actual)
_ -> return (Just s, w) where
else getEffectiveCommandToken str cmd args =
return (Just s, w) let
firstArg = do
arg <- listToMaybe args
guard . not $ isFlag arg
return arg
in
case str of
"busybox" -> firstArg
"builtin" -> firstArg
"command" -> firstArg
"run" -> firstArg -- Used by bats
"exec" -> do
opts <- getBsdOpts "cla:" args
(_, (t, _)) <- find (null . fst) opts
return t
_ -> fail ""
-- If a command substitution is a single command, get its name. -- If a command substitution is a single command, get its name.
-- $(date +%s) = Just "date" -- $(date +%s) = Just "date"
@@ -341,8 +485,8 @@ getCommandNameFromExpansion t =
-- Get the basename of a token representing a command -- Get the basename of a token representing a command
getCommandBasename = fmap basename . getCommandName getCommandBasename = fmap basename . getCommandName
where
basename = reverse . takeWhile (/= '/') . reverse basename = reverse . takeWhile (/= '/') . reverse
isAssignment t = isAssignment t =
case t of case t of
@@ -400,10 +544,10 @@ getAssociativeArrays t =
f t@T_SimpleCommand {} = sequence_ $ do f t@T_SimpleCommand {} = sequence_ $ do
name <- getCommandName t name <- getCommandName t
let assocNames = ["declare","local","typeset"] let assocNames = ["declare","local","typeset"]
guard $ elem name assocNames guard $ name `elem` assocNames
let flags = getAllFlags t let flags = getAllFlags t
guard $ elem "A" $ map snd flags guard $ "A" `elem` map snd flags
let args = map fst . filter ((==) "" . snd) $ flags let args = [arg | (arg, "") <- flags]
let names = mapMaybe (getLiteralStringExt nameAssignments) args let names = mapMaybe (getLiteralStringExt nameAssignments) args
return $ tell names return $ tell names
f _ = return () f _ = return ()
@@ -421,38 +565,36 @@ data PseudoGlob = PGAny | PGMany | PGChar Char
-- Turn a word into a PG pattern, replacing all unknown/runtime values with -- Turn a word into a PG pattern, replacing all unknown/runtime values with
-- PGMany. -- PGMany.
wordToPseudoGlob :: Token -> Maybe [PseudoGlob] wordToPseudoGlob :: Token -> [PseudoGlob]
wordToPseudoGlob word = wordToPseudoGlob = fromMaybe [PGMany] . wordToPseudoGlob' False
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
where
f x = case x of
T_Literal _ s -> return $ map PGChar s
T_SingleQuoted _ s -> return $ map PGChar s
T_DollarBraced {} -> return [PGMany]
T_DollarExpansion {} -> return [PGMany]
T_Backticked {} -> return [PGMany]
T_Glob _ "?" -> return [PGAny]
T_Glob _ ('[':_) -> return [PGAny]
T_Glob {} -> return [PGMany]
T_Extglob {} -> return [PGMany]
_ -> return [PGMany]
-- Turn a word into a PG pattern, but only if we can preserve -- Turn a word into a PG pattern, but only if we can preserve
-- exact semantics. -- exact semantics.
wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob] wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob]
wordToExactPseudoGlob word = wordToExactPseudoGlob = wordToPseudoGlob' True
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
wordToPseudoGlob' :: Bool -> Token -> Maybe [PseudoGlob]
wordToPseudoGlob' exact word =
simplifyPseudoGlob <$> toGlob word
where where
toGlob :: Token -> Maybe [PseudoGlob]
toGlob word =
case word of
T_NormalWord _ (T_Literal _ ('~':str):rest) -> do
guard $ not exact
let this = (PGMany : (map PGChar $ dropWhile (/= '/') str))
tail <- concat <$> (mapM f $ concatMap getWordParts rest)
return $ this ++ tail
_ -> concat <$> (mapM f $ getWordParts word)
f x = case x of f x = case x of
T_Literal _ s -> return $ map PGChar s T_Literal _ s -> return $ map PGChar s
T_SingleQuoted _ s -> return $ map PGChar s T_SingleQuoted _ s -> return $ map PGChar s
T_Glob _ "?" -> return [PGAny] T_Glob _ "?" -> return [PGAny]
T_Glob _ "*" -> return [PGMany] T_Glob _ "*" -> return [PGMany]
_ -> fail "Unknown token type" T_Glob _ ('[':_) | not exact -> return [PGAny]
_ -> if exact then fail "" else return [PGMany]
-- Reorder a PseudoGlob for more efficient matching, e.g. -- Reorder a PseudoGlob for more efficient matching, e.g.
-- f?*?**g -> f??*g -- f?*?**g -> f??*g
@@ -502,8 +644,7 @@ pseudoGlobIsSuperSetof = matchable
matchable (PGMany : rest) [] = matchable rest [] matchable (PGMany : rest) [] = matchable rest []
matchable _ _ = False matchable _ _ = False
wordsCanBeEqual x y = fromMaybe True $ wordsCanBeEqual x y = pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
-- Is this an expansion that can be quoted, -- Is this an expansion that can be quoted,
-- e.g. $(foo) `foo` $foo (but not {foo,})? -- e.g. $(foo) `foo` $foo (but not {foo,})?
@@ -517,6 +658,11 @@ isCommandSubstitution t = case t of
T_Backticked {} -> True T_Backticked {} -> True
_ -> False _ -> False
-- Is this an expansion that results in a simple string?
isStringExpansion t = isCommandSubstitution t || case t of
T_DollarArithmetic {} -> True
T_DollarBraced {} -> not (isArrayExpansion t)
_ -> False
-- Is this a T_Annotation that ignores a specific code? -- Is this a T_Annotation that ignores a specific code?
isAnnotationIgnoringCode code t = isAnnotationIgnoringCode code t =
@@ -524,5 +670,5 @@ isAnnotationIgnoringCode code t =
T_Annotation _ anns _ -> any hasNum anns T_Annotation _ anns _ -> any hasNum anns
_ -> False _ -> False
where where
hasNum (DisableComment ts) = code == ts hasNum (DisableComment from to) = code >= from && code < to
hasNum _ = False hasNum _ = False

File diff suppressed because it is too large Load Diff

View File

@@ -163,6 +163,8 @@ err id code str = addComment $ makeComment ErrorC id code str
info id code str = addComment $ makeComment InfoC id code str info id code str = addComment $ makeComment InfoC id code str
style id code str = addComment $ makeComment StyleC id code str style id code str = addComment $ makeComment StyleC id code str
errWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
errWithFix = addCommentWithFix ErrorC
warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m () warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
warnWithFix = addCommentWithFix WarningC warnWithFix = addCommentWithFix WarningC
styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m () styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
@@ -178,7 +180,7 @@ makeCommentWithFix severity id code str fix =
withFix = comment { withFix = comment {
tcFix = Just fix tcFix = Just fix
} }
in withFix `deepseq` withFix in force withFix
makeParameters spec = makeParameters spec =
let params = Parameters { let params = Parameters {
@@ -236,6 +238,8 @@ prop_determineShell5 = determineShellTest "#shellcheck shell=sh\nfoo" == Sh
prop_determineShell6 = determineShellTest "#! /bin/sh" == Sh prop_determineShell6 = determineShellTest "#! /bin/sh" == Sh
prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh
prop_determineShell9 = determineShellTest "#!/bin/env -S dash -x" == Dash
prop_determineShell10 = determineShellTest "#!/bin/env --split-string= dash -x" == Dash
determineShellTest = determineShellTest' Nothing determineShellTest = determineShellTest' Nothing
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
@@ -254,17 +258,19 @@ determineShell fallbackShell t = fromMaybe Bash $
executableFromShebang :: String -> String executableFromShebang :: String -> String
executableFromShebang = shellFor executableFromShebang = shellFor
where where
shellFor s | "/env " `isInfixOf` s = headOrDefault "" (drop 1 $ words s) shellFor s | "/env " `isInfixOf` s = case matchRegex re s of
Just [flag, shell] -> shell
_ -> ""
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
shellFor s = reverse . takeWhile (/= '/') . reverse $ s shellFor s = reverse . takeWhile (/= '/') . reverse $ s
re = mkRegex "/env +(-S|--split-string=?)? *([^ ]*)"
-- Given a root node, make a map from Id to parent Token. -- Given a root node, make a map from Id to parent Token.
-- This is used to populate parentMap in Parameters -- This is used to populate parentMap in Parameters
getParentTree :: Token -> Map.Map Id Token getParentTree :: Token -> Map.Map Id Token
getParentTree t = getParentTree t =
snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty) snd $ execState (doStackAnalysis pre post t) ([], Map.empty)
where where
pre t = modify (first ((:) t)) pre t = modify (first ((:) t))
post t = do post t = do
@@ -293,15 +299,15 @@ isQuoteFree = isQuoteFreeNode False
isQuoteFreeNode strict tree t = isQuoteFreeNode strict tree t =
(isQuoteFreeElement t == Just True) || isQuoteFreeElement t ||
headOrDefault False (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t)) headOrDefault False (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t))
where where
-- Is this node self-quoting in itself? -- Is this node self-quoting in itself?
isQuoteFreeElement t = isQuoteFreeElement t =
case t of case t of
T_Assignment {} -> return True T_Assignment {} -> True
T_FdRedirect {} -> return True T_FdRedirect {} -> True
_ -> Nothing _ -> False
-- Are any subnodes inherently self-quoting? -- Are any subnodes inherently self-quoting?
isQuoteFreeContext t = isQuoteFreeContext t =
@@ -354,8 +360,8 @@ getClosestCommand tree t =
-- Like above, if koala_man knew Haskell when starting this project. -- Like above, if koala_man knew Haskell when starting this project.
getClosestCommandM t = do getClosestCommandM t = do
tree <- asks parentMap params <- ask
return $ getClosestCommand tree t return $ getClosestCommand (parentMap params) t
-- Is the token used as a command name (the first word in a T_SimpleCommand)? -- Is the token used as a command name (the first word in a T_SimpleCommand)?
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token) usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
@@ -364,8 +370,8 @@ usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
| currentId == getId word = go id rest | currentId == getId word = go id rest
go currentId (T_DoubleQuoted id [word]:rest) go currentId (T_DoubleQuoted id [word]:rest)
| currentId == getId word = go id rest | currentId == getId word = go id rest
go currentId (T_SimpleCommand _ _ (word:_):_) go currentId (t@(T_SimpleCommand _ _ (word:_)):_) =
| currentId == getId word = True getId word == currentId || getId (getCommandTokenOrThis t) == currentId
go _ _ = False go _ _ = False
-- A list of the element and all its parents up to the root node. -- A list of the element and all its parents up to the root node.
@@ -377,8 +383,8 @@ getPath tree t = t :
-- Version of the above taking the map from the current context -- Version of the above taking the map from the current context
-- Todo: give this the name "getPath" -- Todo: give this the name "getPath"
getPathM t = do getPathM t = do
map <- asks parentMap params <- ask
return $ getPath map t return $ getPath (parentMap params) t
isParentOf tree parent child = isParentOf tree parent child =
elem (getId parent) . map getId $ getPath tree child elem (getId parent) . map getId $ getPath tree child
@@ -388,14 +394,13 @@ parents params = getPath (parentMap params)
-- Find the first match in a list where the predicate is Just True. -- Find the first match in a list where the predicate is Just True.
-- Stops if it's Just False and ignores Nothing. -- Stops if it's Just False and ignores Nothing.
findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a
findFirst p l = findFirst p = foldr go Nothing
case l of where
[] -> Nothing go x acc =
(x:xs) -> case p x of
case p x of Just True -> return x
Just True -> return x Just False -> Nothing
Just False -> Nothing Nothing -> acc
Nothing -> findFirst p xs
-- Check whether a word is entirely output from a single command -- Check whether a word is entirely output from a single command
tokenIsJustCommandOutput t = case t of tokenIsJustCommandOutput t = case t of
@@ -410,8 +415,7 @@ tokenIsJustCommandOutput t = case t of
-- TODO: Replace this with a proper Control Flow Graph -- TODO: Replace this with a proper Control Flow Graph
getVariableFlow params t = getVariableFlow params t =
let (_, stack) = runState (doStackAnalysis startScope endScope t) [] reverse $ execState (doStackAnalysis startScope endScope t) []
in reverse stack
where where
startScope t = startScope t =
let scopeType = leadType params t let scopeType = leadType params t
@@ -462,28 +466,22 @@ leadType params t =
causesSubshell = do causesSubshell = do
(T_Pipeline _ _ list) <- parentPipeline (T_Pipeline _ _ list) <- parentPipeline
if length list <= 1 return $ case list of
then return False _:_:_ -> not (hasLastpipe params) || getId (last list) /= getId t
else if not $ hasLastpipe params _ -> False
then return True
else return . not $ (getId . head $ reverse list) == getId t
getModifiedVariables t = getModifiedVariables t =
case t of case t of
T_SimpleCommand _ vars [] -> T_SimpleCommand _ vars [] ->
concatMap (\x -> case x of [(x, x, name, dataTypeFrom DataString w) | x@(T_Assignment id _ name _ w) <- vars]
T_Assignment id _ name _ w -> T_SimpleCommand {} ->
[(x, x, name, dataTypeFrom DataString w)] getModifiedVariableCommand t
_ -> []
) vars
c@T_SimpleCommand {} ->
getModifiedVariableCommand c
TA_Unary _ "++|" v@(TA_Variable _ name _) -> TA_Unary _ "++|" v@(TA_Variable _ name _) ->
[(t, v, name, DataString $ SourceFrom [v])] [(t, v, name, DataString $ SourceFrom [v])]
TA_Unary _ "|++" v@(TA_Variable _ name _) -> TA_Unary _ "|++" v@(TA_Variable _ name _) ->
[(t, v, name, DataString $ SourceFrom [v])] [(t, v, name, DataString $ SourceFrom [v])]
TA_Assignment _ op (TA_Variable _ name _) rhs -> maybeToList $ do TA_Assignment _ op (TA_Variable _ name _) rhs -> do
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="] guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
return (t, t, name, DataString $ SourceFrom [rhs]) return (t, t, name, DataString $ SourceFrom [rhs])
@@ -495,26 +493,30 @@ getModifiedVariables t =
-- Count [[ -v foo ]] as an "assignment". -- Count [[ -v foo ]] as an "assignment".
-- This is to prevent [ -v foo ] being unassigned or unused. -- This is to prevent [ -v foo ] being unassigned or unused.
TC_Unary id _ "-v" token -> maybeToList $ do TC_Unary id _ "-v" token -> do
str <- fmap (takeWhile (/= '[')) $ -- Quoted index str <- fmap (takeWhile (/= '[')) $ -- Quoted index
flip getLiteralStringExt token $ \x -> flip getLiteralStringExt token $ \x ->
case x of case x of
T_Glob _ s -> return s -- Unquoted index T_Glob _ s -> return s -- Unquoted index
_ -> Nothing _ -> []
guard . not . null $ str guard . not . null $ str
return (t, token, str, DataString SourceChecked) return (t, token, str, DataString SourceChecked)
TC_Unary _ _ "-n" token -> markAsChecked t token
TC_Unary _ _ "-z" token -> markAsChecked t token
TC_Nullary _ _ token -> markAsChecked t token
T_DollarBraced _ _ l -> maybeToList $ do T_DollarBraced _ _ l -> maybeToList $ do
let string = bracedString t let string = concat $ oversimplify l
let modifier = getBracedModifier string let modifier = getBracedModifier string
guard $ any (`isPrefixOf` modifier) ["=", ":="] guard $ any (`isPrefixOf` modifier) ["=", ":="]
return (t, t, getBracedReference string, DataString $ SourceFrom [l]) return (t, t, getBracedReference string, DataString $ SourceFrom [l])
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo T_FdRedirect _ ('{':var) op -> -- {foo}>&2 modifies foo
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op] [(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
t@(T_CoProc _ name _) -> T_CoProc _ name _ ->
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)] [(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
--Points to 'for' rather than variable --Points to 'for' rather than variable
@@ -522,6 +524,14 @@ getModifiedVariables t =
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)] T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)] T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
_ -> [] _ -> []
where
markAsChecked place token = mapMaybe (f place) $ getWordParts token
f place t = case t of
T_DollarBraced _ _ l ->
let str = getBracedReference $ concat $ oversimplify l in do
guard $ isVariableName str
return (place, t, str, DataString SourceChecked)
_ -> Nothing
isClosingFileOp op = isClosingFileOp op =
case op of case op of
@@ -541,6 +551,9 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
(not $ any (`elem` flags) ["f", "F"]) (not $ any (`elem` flags) ["f", "F"])
then concatMap getReference rest then concatMap getReference rest
else [] else []
"local" -> if "x" `elem` flags
then concatMap getReference rest
else []
"trap" -> "trap" ->
case rest of case rest of
head:_ -> map (\x -> (base, head, x)) $ getVariablesFromLiteralToken head head:_ -> map (\x -> (base, head, x)) $ getVariablesFromLiteralToken head
@@ -569,10 +582,14 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
"builtin" -> "builtin" ->
getModifiedVariableCommand $ T_SimpleCommand id cmdPrefix rest getModifiedVariableCommand $ T_SimpleCommand id cmdPrefix rest
"read" -> "read" ->
let params = map getLiteral rest let fallback = catMaybes $ takeWhile isJust (reverse $ map getLiteral rest)
readArrayVars = getReadArrayVariables rest in fromMaybe fallback $ do
in parsed <- getGnuOpts flagsForRead rest
catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params case lookup "a" parsed of
Just (_, var) -> (:[]) <$> getLiteralArray var
Nothing -> return $ catMaybes $
map (getLiteral . snd . snd) $ filter (null . fst) parsed
"getopts" -> "getopts" ->
case rest of case rest of
opts:var:_ -> maybeToList $ getLiteral var opts:var:_ -> maybeToList $ getLiteral var
@@ -664,25 +681,28 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
f [] = fail "not found" f [] = fail "not found"
-- mapfile has some curious syntax allowing flags plus 0..n variable names -- mapfile has some curious syntax allowing flags plus 0..n variable names
-- where only the first non-option one is used if any. Here we cheat and -- where only the first non-option one is used if any.
-- just get the last one, if it's a variable name. getMapfileArray base rest = parseArgs `mplus` fallback
getMapfileArray base arguments = do where
lastArg <- listToMaybe (reverse arguments) parseArgs :: Maybe (Token, Token, String, DataType)
name <- getLiteralString lastArg parseArgs = do
guard $ isVariableName name args <- getGnuOpts "d:n:O:s:u:C:c:t" rest
return (base, lastArg, name, DataArray SourceExternal) case [y | ("",(_,y)) <- args] of
[] ->
-- get all the array variables used in read, e.g. read -a arr return (base, base, "MAPFILE", DataArray SourceExternal)
getReadArrayVariables args = first:_ -> do
map (getLiteralArray . snd) name <- getLiteralString first
(filter (isArrayFlag . fst) (zip args (tail args))) guard $ isVariableName name
return (base, first, name, DataArray SourceExternal)
isArrayFlag x = fromMaybe False $ do -- If arg parsing fails (due to bad or new flags), get the last variable name
str <- getLiteralString x fallback :: Maybe (Token, Token, String, DataType)
return $ case str of fallback = do
'-':'-':_ -> False (name, token) <- listToMaybe . mapMaybe f $ reverse rest
'-':str -> 'a' `elem` str return (base, token, name, DataArray SourceExternal)
_ -> False f arg = do
name <- getLiteralString arg
guard $ isVariableName name
return (name, arg)
-- get the FLAGS_ variable created by a shflags DEFINE_ call -- get the FLAGS_ variable created by a shflags DEFINE_ call
getFlagVariable (n:v:_) = do getFlagVariable (n:v:_) = do
@@ -713,7 +733,7 @@ getOffsetReferences mods = fromMaybe [] $ do
getReferencedVariables parents t = getReferencedVariables parents t =
case t of case t of
T_DollarBraced id _ l -> let str = bracedString t in T_DollarBraced id _ l -> let str = concat $ oversimplify l in
(t, t, getBracedReference str) : (t, t, getBracedReference str) :
map (\x -> (l, l, x)) ( map (\x -> (l, l, x)) (
getIndexReferences str getIndexReferences str
@@ -738,7 +758,7 @@ getReferencedVariables parents t =
(t, t, "output") (t, t, "output")
] ]
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo T_FdRedirect _ ('{':var) op -> -- {foo}>&- references and closes foo
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op] [(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
x -> getReferencedVariableCommand x x -> getReferencedVariableCommand x
where where
@@ -755,9 +775,9 @@ getReferencedVariables parents t =
literalizer t = case t of literalizer t = case t of
T_Glob _ s -> return s -- Also when parsed as globs T_Glob _ s -> return s -- Also when parsed as globs
_ -> Nothing _ -> []
getIfReference context token = maybeToList $ do getIfReference context token = do
str@(h:_) <- getLiteralStringExt literalizer token str@(h:_) <- getLiteralStringExt literalizer token
when (isDigit h) $ fail "is a number" when (isDigit h) $ fail "is a number"
return (context, token, getBracedReference str) return (context, token, getBracedReference str)
@@ -808,7 +828,7 @@ getVariablesFromLiteralToken token =
prop_getVariablesFromLiteral1 = prop_getVariablesFromLiteral1 =
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"] getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
getVariablesFromLiteral string = getVariablesFromLiteral string =
map (!! 0) $ matchAllSubgroups variableRegex string map head $ matchAllSubgroups variableRegex string
where where
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)" variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
@@ -824,27 +844,26 @@ prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo" prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
prop_getBracedReference10= getBracedReference "foo: -1" == "foo" prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
prop_getBracedReference11= getBracedReference "!os*" == "" prop_getBracedReference11= getBracedReference "!os*" == ""
prop_getBracedReference11b= getBracedReference "!os@" == ""
prop_getBracedReference12= getBracedReference "!os?bar**" == "" prop_getBracedReference12= getBracedReference "!os?bar**" == ""
prop_getBracedReference13= getBracedReference "foo[bar]" == "foo" prop_getBracedReference13= getBracedReference "foo[bar]" == "foo"
getBracedReference s = fromMaybe s $ getBracedReference s = fromMaybe s $
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
where where
noPrefix = dropPrefix s noPrefix = dropPrefix s
dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest dropPrefix (c:rest) | c `elem` "!#" = rest
dropPrefix "" = "" dropPrefix cs = cs
takeName s = do takeName s = do
let name = takeWhile isVariableChar s let name = takeWhile isVariableChar s
guard . not $ null name guard . not $ null name
return name return name
getSpecial (c:_) = getSpecial (c:_) | c `elem` "*@#?-$!" = return [c]
if c `elem` "*@#?-$!" then return [c] else fail "not special" getSpecial _ = fail "empty or not special"
getSpecial _ = fail "empty"
nameExpansion ('!':rest) = do -- e.g. ${!foo*bar*} nameExpansion ('!':next:rest) = do -- e.g. ${!foo*bar*}
let suffix = dropWhile isVariableChar rest guard $ isVariableChar next -- e.g. ${!@}
guard $ suffix /= rest -- e.g. ${!@} first <- find (not . isVariableChar) rest
first <- suffix !!! 0 guard $ first `elem` "*?@"
guard $ first `elem` "*?"
return "" return ""
nameExpansion _ = Nothing nameExpansion _ = Nothing
@@ -877,8 +896,8 @@ headOrDefault def _ = def
-- Run a command if the shell is in the given list -- Run a command if the shell is in the given list
whenShell l c = do whenShell l c = do
shell <- asks shellType params <- ask
when (shell `elem` l ) c when (shellType params `elem` l ) c
filterByAnnotation asSpec params = filterByAnnotation asSpec params =
@@ -907,45 +926,15 @@ isCountingReference _ = False
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"} -- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
isQuotedAlternativeReference t = isQuotedAlternativeReference t =
case t of case t of
T_DollarBraced _ _ _ -> T_DollarBraced _ _ l ->
getBracedModifier (bracedString t) `matches` re getBracedModifier (concat $ oversimplify l) `matches` re
_ -> False _ -> False
where where
re = mkRegex "(^|\\]):?\\+" re = mkRegex "(^|\\]):?\\+"
-- getGnuOpts "erd:u:" will parse a SimpleCommand like supportsArrays Bash = True
-- read -re -d : -u 3 bar supportsArrays Ksh = True
-- into supportsArrays _ = False
-- Just [("r", -re), ("e", -re), ("d", :), ("u", 3), ("", bar)]
-- where flags with arguments map to arguments, while others map to themselves.
-- Any unrecognized flag will result in Nothing.
getGnuOpts str t = getOpts str $ getAllFlags t
getBsdOpts str t = getOpts str $ getLeadingFlags t
getOpts :: String -> [(Token, String)] -> Maybe [(String, Token)]
getOpts string flags = process flags
where
flagList (c:':':rest) = ([c], True) : flagList rest
flagList (c:rest) = ([c], False) : flagList rest
flagList [] = []
flagMap = Map.fromList $ ("", False) : flagList string
process [] = return []
process [(token, flag)] = do
takesArg <- Map.lookup flag flagMap
guard $ not takesArg
return [(flag, token)]
process ((token1, flag1):rest2@((token2, flag2):rest)) = do
takesArg <- Map.lookup flag1 flagMap
if takesArg
then do
guard $ null flag2
more <- process rest
return $ (flag1, token2) : more
else do
more <- process rest2
return $ (flag1, token1) : more
supportsArrays shell = shell == Bash || shell == Ksh
-- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh) -- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh)
isBashLike :: Parameters -> Bool isBashLike :: Parameters -> Bool

View File

@@ -229,6 +229,12 @@ prop_cantSourceDynamic =
prop_cantSourceDynamic2 = prop_cantSourceDynamic2 =
[1090] == checkWithIncludes [("lib", "")] "source ~/foo" [1090] == checkWithIncludes [("lib", "")] "source ~/foo"
prop_canStripPrefixAndSource =
null $ checkWithIncludes [("./lib", "")] "source \"$MYDIR/lib\""
prop_canStripPrefixAndSource2 =
null $ checkWithIncludes [("./utils.sh", "")] "source \"$(dirname \"${BASH_SOURCE[0]}\")/utils.sh\""
prop_canSourceDynamicWhenRedirected = prop_canSourceDynamicWhenRedirected =
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\"" null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
@@ -270,7 +276,7 @@ prop_filewideAnnotation8 = null $
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1" check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source' prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh" 3046 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
prop_spinBug1413 = null $ check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n" prop_spinBug1413 = null $ check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n"
@@ -295,6 +301,13 @@ prop_canDisableShebangWarning = null $ result
csScript = "#shellcheck disable=SC2148\nfoo" csScript = "#shellcheck disable=SC2148\nfoo"
} }
prop_canDisableParseErrors = null $ result
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.sh",
csScript = "#shellcheck disable=SC1073,SC1072,SC2148\n()"
}
prop_shExtensionDoesntMatter = result == [2148] prop_shExtensionDoesntMatter = result == [2148]
where where
result = checkWithSpec [] emptyCheckSpec { result = checkWithSpec [] emptyCheckSpec {

View File

@@ -19,6 +19,7 @@
-} -}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiWayIf #-}
-- This module contains checks that examine specific commands by name. -- This module contains checks that examine specific commands by name.
module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where
@@ -52,8 +53,6 @@ verify :: CommandCheck -> String -> Bool
verify f s = producesComments (getChecker [f]) s == Just True verify f s = producesComments (getChecker [f]) s == Just True
verifyNot f s = producesComments (getChecker [f]) s == Just False verifyNot f s = producesComments (getChecker [f]) s == Just False
arguments (T_SimpleCommand _ _ (cmd:args)) = args
commandChecks :: [CommandCheck] commandChecks :: [CommandCheck]
commandChecks = [ commandChecks = [
checkTr checkTr
@@ -95,6 +94,7 @@ commandChecks = [
,checkSudoArgs ,checkSudoArgs
,checkSourceArgs ,checkSourceArgs
,checkChmodDashr ,checkChmodDashr
,checkXargsDashi
] ]
optionalChecks = map fst optionalCommandChecks optionalChecks = map fst optionalCommandChecks
@@ -115,6 +115,35 @@ prop_verifyOptionalExamples = all check optionalCommandChecks
verify check (cdPositive desc) verify check (cdPositive desc)
&& verifyNot check (cdNegative desc) && verifyNot check (cdNegative desc)
-- Run a check against the getopt parser. If it fails, the lists are empty.
checkGetOpts str flags args f =
flags == actualFlags && args == actualArgs
where
toTokens = map (T_Literal (Id 0)) . words
opts = fromMaybe [] $ f (toTokens str)
actualFlags = filter (not . null) $ map fst opts
actualArgs = [onlyLiteralString x | ("", (_, x)) <- opts]
-- Short options
prop_checkGetOptsS1 = checkGetOpts "-f x" ["f"] [] $ getOpts (True, True) "f:" []
prop_checkGetOptsS2 = checkGetOpts "-fx" ["f"] [] $ getOpts (True, True) "f:" []
prop_checkGetOptsS3 = checkGetOpts "-f -x" ["f", "x"] [] $ getOpts (True, True) "fx" []
prop_checkGetOptsS4 = checkGetOpts "-f -x" ["f"] [] $ getOpts (True, True) "f:" []
prop_checkGetOptsS5 = checkGetOpts "-fx" [] [] $ getOpts (True, True) "fx:" []
-- Long options
prop_checkGetOptsL1 = checkGetOpts "--foo=bar baz" ["foo"] ["baz"] $ getOpts (True, False) "" [("foo", True)]
prop_checkGetOptsL2 = checkGetOpts "--foo bar baz" ["foo"] ["baz"] $ getOpts (True, False) "" [("foo", True)]
prop_checkGetOptsL3 = checkGetOpts "--foo baz" ["foo"] ["baz"] $ getOpts (True, True) "" []
prop_checkGetOptsL4 = checkGetOpts "--foo baz" [] [] $ getOpts (True, False) "" []
-- Know when to terminate
prop_checkGetOptsT1 = checkGetOpts "-a x -b" ["a", "b"] ["x"] $ getOpts (True, True) "ab" []
prop_checkGetOptsT2 = checkGetOpts "-a x -b" ["a"] ["x","-b"] $ getOpts (False, True) "ab" []
prop_checkGetOptsT3 = checkGetOpts "-a -- -b" ["a"] ["-b"] $ getOpts (True, True) "ab" []
prop_checkGetOptsT4 = checkGetOpts "-a -- -b" ["a", "b"] [] $ getOpts (True, True) "a:b" []
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis) buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
buildCommandMap = foldl' addCheck Map.empty buildCommandMap = foldl' addCheck Map.empty
where where
@@ -199,12 +228,11 @@ prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
acceptsGlob s = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ] acceptsGlob s = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
f [] = return () f [] = return ()
f (x:xs) = g x xs f (x:xs) = foldr g (const $ return ()) xs x
g _ [] = return () g b acc a = do
g a (b:r) = do
forM_ (getLiteralString a) $ \s -> when (acceptsGlob s && isGlob b) $ forM_ (getLiteralString a) $ \s -> when (acceptsGlob s && isGlob b) $
warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it." warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."
g b r acc b
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)" prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
@@ -283,17 +311,11 @@ checkGrepRe = CommandCheck (Basename "grep") check where
candidates = candidates =
sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords
getSuspiciousRegexWildcard str = getSuspiciousRegexWildcard str = case matchRegex suspicious str of
if not $ str `matches` contra Just [[c]] | not (str `matches` contra) -> Just c
then do _ -> fail "looks good"
match <- matchRegex suspicious str suspicious = mkRegex "([A-Za-z1-9])\\*"
str <- match !!! 0 contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
str !!! 0
else
fail "looks good"
where
suspicious = mkRegex "([A-Za-z1-9])\\*"
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT" prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
@@ -462,8 +484,8 @@ checkMkdirDashPM = CommandCheck (Basename "mkdir") check
where where
check t = sequence_ $ do check t = sequence_ $ do
let flags = getAllFlags t let flags = getAllFlags t
dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags dashP <- find (\(_,f) -> f == "p" || f == "parents") flags
dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags dashM <- find (\(_,f) -> f == "m" || f == "mode") flags
-- mkdir -pm 0700 dir is fine, so is ../dir, but dir/subdir is not. -- mkdir -pm 0700 dir is fine, so is ../dir, but dir/subdir is not.
guard $ any couldHaveSubdirs (drop 1 $ arguments t) guard $ any couldHaveSubdirs (drop 1 $ arguments t)
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory." return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
@@ -483,7 +505,7 @@ prop_checkNonportableSignals7 = verifyNot checkNonportableSignals "trap 'stop' i
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments) checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
where where
f args = case args of f args = case args of
first:rest -> unless (isFlag first) $ mapM_ check rest first:rest | not $ isFlag first -> mapM_ check rest
_ -> return () _ -> return ()
check param = sequence_ $ do check param = sequence_ $ do
@@ -520,9 +542,9 @@ checkInteractiveSu = CommandCheck (Basename "su") f
info (getId cmd) 2117 info (getId cmd) 2117
"To run commands as another user, use su -c or sudo." "To run commands as another user, use su -c or sudo."
undirected (T_Pipeline _ _ l) = length l <= 1 undirected (T_Pipeline _ _ (_:_:_)) = False
-- This should really just be modifications to stdin, but meh -- This should really just be modifications to stdin, but meh
undirected (T_Redirecting _ list _) = null list undirected (T_Redirecting _ (_:_) _) = False
undirected _ = True undirected _ = True
@@ -539,9 +561,8 @@ checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
([], hostport:r@(_:_)) -> checkArg $ last r ([], hostport:r@(_:_)) -> checkArg $ last r
_ -> return () _ -> return ()
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) = checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
case filter (not . isConstant) parts of forM_ (find (not . isConstant) parts) $
[] -> return () \x -> info (getId x) 2029
(x:_) -> info (getId x) 2029
"Note that, unescaped, this expands on the client side." "Note that, unescaped, this expands on the client side."
checkArg _ = return () checkArg _ = return ()
@@ -567,6 +588,8 @@ prop_checkPrintfVar18= verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
prop_checkPrintfVar19= verifyNot checkPrintfVar "printf '%(%s)T'" prop_checkPrintfVar19= verifyNot checkPrintfVar "printf '%(%s)T'"
prop_checkPrintfVar20= verifyNot checkPrintfVar "printf '%d %(%s)T' 42" prop_checkPrintfVar20= verifyNot checkPrintfVar "printf '%d %(%s)T' 42"
prop_checkPrintfVar21= verify checkPrintfVar "printf '%d %(%s)T'" prop_checkPrintfVar21= verify checkPrintfVar "printf '%d %(%s)T'"
prop_checkPrintfVar22= verify checkPrintfVar "printf '%s\n%s' foo"
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
@@ -580,22 +603,21 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
let formatCount = length formats let formatCount = length formats
let argCount = length more let argCount = length more
return $ return $ if
case () of | argCount == 0 && formatCount == 0 ->
() | argCount == 0 && formatCount == 0 -> return () -- This is fine
return () -- This is fine | formatCount == 0 && argCount > 0 ->
() | formatCount == 0 && argCount > 0 -> err (getId format) 2182
err (getId format) 2182 "This printf format string has no variables. Other arguments are ignored."
"This printf format string has no variables. Other arguments are ignored." | any mayBecomeMultipleArgs more ->
() | any mayBecomeMultipleArgs more -> return () -- We don't know so trust the user
return () -- We don't know so trust the user | argCount < formatCount && onlyTrailingTs formats argCount ->
() | argCount < formatCount && onlyTrailingTs formats argCount -> return () -- Allow trailing %()Ts since they use the current time
return () -- Allow trailing %()Ts since they use the current time | argCount > 0 && argCount `mod` formatCount == 0 ->
() | argCount > 0 && argCount `mod` formatCount == 0 -> return () -- Great: a suitable number of arguments
return () -- Great: a suitable number of arguments | otherwise ->
() -> warn (getId format) 2183 $
warn (getId format) 2183 $ "This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments."
"This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments."
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $ unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
info (getId format) 2059 info (getId format) 2059
@@ -610,6 +632,8 @@ prop_checkGetPrintfFormats2 = getPrintfFormats "%0*s" == "*s"
prop_checkGetPrintfFormats3 = getPrintfFormats "%(%s)T" == "T" prop_checkGetPrintfFormats3 = getPrintfFormats "%(%s)T" == "T"
prop_checkGetPrintfFormats4 = getPrintfFormats "%d%%%(%s)T" == "dT" prop_checkGetPrintfFormats4 = getPrintfFormats "%d%%%(%s)T" == "dT"
prop_checkGetPrintfFormats5 = getPrintfFormats "%bPassed: %d, %bFailed: %d%b, Skipped: %d, %bErrored: %d%b\\n" == "bdbdbdbdb" prop_checkGetPrintfFormats5 = getPrintfFormats "%bPassed: %d, %bFailed: %d%b, Skipped: %d, %bErrored: %d%b\\n" == "bdbdbdbdb"
prop_checkGetPrintfFormats6 = getPrintfFormats "%s%s" == "ss"
prop_checkGetPrintfFormats7 = getPrintfFormats "%s\n%s" == "ss"
getPrintfFormats = getFormats getPrintfFormats = getFormats
where where
-- Get the arguments in the string as a string of type characters, -- Get the arguments in the string as a string of type characters,
@@ -628,17 +652,17 @@ getPrintfFormats = getFormats
regexBasedGetFormats rest = regexBasedGetFormats rest =
case matchRegex re rest of case matchRegex re rest of
Just [width, precision, typ, rest] -> Just [width, precision, typ, rest, _] ->
(if width == "*" then "*" else "") ++ (if width == "*" then "*" else "") ++
(if precision == "*" then "*" else "") ++ (if precision == "*" then "*" else "") ++
typ ++ getFormats rest typ ++ getFormats rest
Nothing -> take 1 rest ++ getFormats rest Nothing -> take 1 rest ++ getFormats rest
where where
-- constructed based on specifications in "man printf" -- constructed based on specifications in "man printf"
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)([diouxXfFeEgGaAcsbq])(.*)" re = mkRegex "#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)([diouxXfFeEgGaAcsbq])((\n|.)*)"
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \ / -- \____ _____/\___ ____/ \____ ____/\_________ _________/ \______ /
-- V V V V V -- V V V V V
-- flags field width precision format character rest -- 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 a '*' instead of a digit,
-- in which case printf will accept one more argument for each '*' used -- in which case printf will accept one more argument for each '*' used
@@ -663,17 +687,12 @@ prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'"
prop_checkSetAssignment6 = verifyNot checkSetAssignment "set" prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments) checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
where where
f (var:value:rest) = f (var:rest)
let str = literal var in | (not (null rest) && isVariableName str) || isAssignment str =
when (isVariableName str || isAssignment str) $ warn (getId var) 2121 "To assign a variable, use just 'var=value', no 'set ..'."
msg (getId var) where str = literal var
f (var:_) =
when (isAssignment $ literal var) $
msg (getId var)
f _ = return () f _ = return ()
msg id = warn id 2121 "To assign a variable, use just 'var=value', no 'set ..'."
isAssignment str = '=' `elem` str isAssignment str = '=' `elem` str
literal (T_NormalWord _ l) = concatMap literal l literal (T_NormalWord _ l) = concatMap literal l
literal (T_Literal _ str) = str literal (T_Literal _ str) = str
@@ -687,8 +706,7 @@ prop_checkExportedExpansions4 = verifyNot checkExportedExpansions "export ${foo?
checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments) checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
where where
check t = sequence_ $ do check t = sequence_ $ do
var <- getSingleUnmodifiedVariable t name <- getSingleUnmodifiedBracedString t
let name = bracedString var
return . warn (getId t) 2163 $ return . warn (getId t) 2163 $
"This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet." "This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
@@ -704,26 +722,25 @@ checkReadExpansions = CommandCheck (Exactly "read") check
where where
options = getGnuOpts flagsForRead options = getGnuOpts flagsForRead
getVars cmd = fromMaybe [] $ do getVars cmd = fromMaybe [] $ do
opts <- options cmd opts <- options $ arguments cmd
return [y | (x,y) <- opts, null x || x == "a"] return [y | (x,(_, y)) <- opts, null x || x == "a"]
check cmd = mapM_ warning $ getVars cmd check cmd = mapM_ warning $ getVars cmd
warning t = sequence_ $ do warning t = sequence_ $ do
var <- getSingleUnmodifiedVariable t name <- getSingleUnmodifiedBracedString t
let name = bracedString var
guard $ isVariableName name -- e.g. not $1 guard $ isVariableName name -- e.g. not $1
return . warn (getId t) 2229 $ return . warn (getId t) 2229 $
"This does not read '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet." "This does not read '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
-- Return the single variable expansion that makes up this word, if any. -- Return the single variable expansion that makes up this word, if any.
-- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing -- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing
getSingleUnmodifiedVariable :: Token -> Maybe Token getSingleUnmodifiedBracedString :: Token -> Maybe String
getSingleUnmodifiedVariable word = getSingleUnmodifiedBracedString word =
case getWordParts word of case getWordParts word of
[t@(T_DollarBraced {})] -> [T_DollarBraced _ _ l] ->
let contents = bracedString t let contents = concat $ oversimplify l
name = getBracedReference contents name = getBracedReference contents
in guard (contents == name) >> return t in guard (contents == name) >> return contents
_ -> Nothing _ -> Nothing
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'" prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
@@ -883,12 +900,12 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
notRequested = Map.difference handledMap requestedMap notRequested = Map.difference handledMap requestedMap
warnUnhandled optId caseId str = warnUnhandled optId caseId str =
warn caseId 2213 $ "getopts specified -" ++ str ++ ", but it's not handled by this 'case'." warn caseId 2213 $ "getopts specified -" ++ (e4m str) ++ ", but it's not handled by this 'case'."
warnRedundant (key, expr) = sequence_ $ do warnRedundant (Just str, expr)
str <- key | str `notElem` ["*", ":", "?"] =
guard $ str `notElem` ["*", ":", "?"] warn (getId expr) 2214 "This case is not specified by getopts."
return $ warn (getId expr) 2214 "This case is not specified by getopts." warnRedundant _ = return ()
getHandledStrings (_, globs, _) = getHandledStrings (_, globs, _) =
map (\x -> (literal x, x)) globs map (\x -> (literal x, x)) globs
@@ -899,7 +916,7 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
fromGlob t = fromGlob t =
case t of case t of
T_Glob _ ('[':c:']':[]) -> return [c] T_Glob _ ['[', c, ']'] -> return [c]
T_Glob _ "*" -> return "*" T_Glob _ "*" -> return "*"
T_Glob _ "?" -> return "?" T_Glob _ "?" -> return "?"
_ -> Nothing _ -> Nothing
@@ -934,7 +951,7 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
when (isRecursive t) $ when (isRecursive t) $
mapM_ (mapM_ checkWord . braceExpand) $ arguments t mapM_ (mapM_ checkWord . braceExpand) $ arguments t
where where
isRecursive = any (`elem` ["r", "R", "recursive"]) . map snd . getAllFlags isRecursive = any ((`elem` ["r", "R", "recursive"]) . snd) . getAllFlags
checkWord token = checkWord token =
case getLiteralString token of case getLiteralString token of
@@ -966,9 +983,9 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
f _ = return "" f _ = return ""
stripTrailing c = reverse . dropWhile (== c) . reverse stripTrailing c = reverse . dropWhile (== c) . reverse
skipRepeating c (a:b:rest) | a == b && b == c = skipRepeating c (b:rest) skipRepeating c = foldr go []
skipRepeating c (a:r) = a:skipRepeating c r where
skipRepeating _ [] = [] go a r = a : case r of b:rest | b == c && a == b -> rest; _ -> r
paths = [ paths = [
"", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local", "", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",
@@ -1081,8 +1098,8 @@ prop_checkSudoArgs7 = verifyNot checkSudoArgs "sudo docker export foo"
checkSudoArgs = CommandCheck (Basename "sudo") f checkSudoArgs = CommandCheck (Basename "sudo") f
where where
f t = sequence_ $ do f t = sequence_ $ do
opts <- parseOpts t opts <- parseOpts $ arguments t
let nonFlags = [x | ("",x) <- opts] let nonFlags = [x | ("",(x, _)) <- opts]
commandArg <- nonFlags !!! 0 commandArg <- nonFlags !!! 0
command <- getLiteralString commandArg command <- getLiteralString commandArg
guard $ command `elem` builtins guard $ command `elem` builtins
@@ -1113,5 +1130,18 @@ checkChmodDashr = CommandCheck (Basename "chmod") f
guard $ flag == "-r" guard $ flag == "-r"
return $ warn (getId t) 2253 "Use -R to recurse, or explicitly a-r to remove read permissions." return $ warn (getId t) 2253 "Use -R to recurse, or explicitly a-r to remove read permissions."
prop_checkXargsDashi1 = verify checkXargsDashi "xargs -i{} echo {}"
prop_checkXargsDashi2 = verifyNot checkXargsDashi "xargs -I{} echo {}"
prop_checkXargsDashi3 = verifyNot checkXargsDashi "xargs sed -i -e foo"
prop_checkXargsDashi4 = verify checkXargsDashi "xargs -e sed -i foo"
prop_checkXargsDashi5 = verifyNot checkXargsDashi "xargs -x sed -i foo"
checkXargsDashi = CommandCheck (Basename "xargs") f
where
f t = sequence_ $ do
opts <- parseOpts $ arguments t
(option, value) <- lookup "i" opts
return $ info (getId option) 2267 "GNU xargs -i is deprecated in favor of -I{}"
parseOpts = getBsdOpts "0oprtxadR:S:J:L:l:n:P:s:e:E:i:I:"
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -178,6 +178,9 @@ prop_checkBashisms93 = verify checkBashisms "#!/bin/sh\necho $(( 10#$(date +%m)
prop_checkBashisms94 = verify checkBashisms "#!/bin/sh\n[ -v var ]" prop_checkBashisms94 = verify checkBashisms "#!/bin/sh\n[ -v var ]"
prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_" prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_"
prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_" prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}"
prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}"
prop_checkBashisms99 = verifyNot checkBashisms "#!/bin/dash\necho [^f]oo"
checkBashisms = ForShell [Sh, Dash] $ \t -> do checkBashisms = ForShell [Sh, Dash] $ \t -> do
params <- ask params <- ask
kludge params t kludge params t
@@ -186,102 +189,102 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
kludge params = bashism kludge params = bashism
where where
isDash = shellType params == Dash isDash = shellType params == Dash
warnMsg id s = warnMsg id code s =
if isDash if isDash
then warn id 2169 $ "In dash, " ++ s ++ " not supported." then err id code $ "In dash, " ++ s ++ " not supported."
else warn id 2039 $ "In POSIX sh, " ++ s ++ " undefined." else warn id code $ "In POSIX sh, " ++ s ++ " undefined."
bashism (T_ProcSub id _ _) = warnMsg id "process substitution is" bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is"
bashism (T_Extglob id _ _) = warnMsg id "extglob is" bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is"
bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is" bashism (T_DollarSingleQuoted id _) = warnMsg id 3003 "$'..' is"
bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\" is" bashism (T_DollarDoubleQuoted id _) = warnMsg id 3004 "$\"..\" is"
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id "arithmetic for loops are" bashism (T_ForArithmetic id _ _ _ _) = warnMsg id 3005 "arithmetic for loops are"
bashism (T_Arithmetic id _) = warnMsg id "standalone ((..)) is" bashism (T_Arithmetic id _) = warnMsg id 3006 "standalone ((..)) is"
bashism (T_DollarBracket id _) = warnMsg id "$[..] in place of $((..)) is" bashism (T_DollarBracket id _) = warnMsg id 3007 "$[..] in place of $((..)) is"
bashism (T_SelectIn id _ _ _) = warnMsg id "select loops are" bashism (T_SelectIn id _ _ _) = warnMsg id 3008 "select loops are"
bashism (T_BraceExpansion id _) = warnMsg id "brace expansion is" bashism (T_BraceExpansion id _) = warnMsg id 3009 "brace expansion is"
bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is" bashism (T_Condition id DoubleBracket _) = warnMsg id 3010 "[[ ]] is"
bashism (T_HereString id _) = warnMsg id "here-strings are" bashism (T_HereString id _) = warnMsg id 3011 "here-strings are"
bashism (TC_Binary id SingleBracket op _ _) bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] = | op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
unless isDash $ warnMsg id $ "lexicographical " ++ op ++ " is" unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is"
bashism (TC_Binary id SingleBracket op _ _) bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "-ot", "-nt", "-ef" ] = | op `elem` [ "-ot", "-nt", "-ef" ] =
unless isDash $ warnMsg id $ op ++ " is" unless isDash $ warnMsg id 3013 $ op ++ " is"
bashism (TC_Binary id SingleBracket "==" _ _) = bashism (TC_Binary id SingleBracket "==" _ _) =
warnMsg id "== in place of = is" warnMsg id 3014 "== in place of = is"
bashism (TC_Binary id SingleBracket "=~" _ _) = bashism (TC_Binary id SingleBracket "=~" _ _) =
warnMsg id "=~ regex matching is" warnMsg id 3015 "=~ regex matching is"
bashism (TC_Unary id SingleBracket "-v" _) = bashism (TC_Unary id SingleBracket "-v" _) =
warnMsg id "unary -v (in place of [ -n \"${var+x}\" ]) is" warnMsg id 3016 "unary -v (in place of [ -n \"${var+x}\" ]) is"
bashism (TC_Unary id _ "-a" _) = bashism (TC_Unary id _ "-a" _) =
warnMsg id "unary -a in place of -e is" warnMsg id 3017 "unary -a in place of -e is"
bashism (TA_Unary id op _) bashism (TA_Unary id op _)
| op `elem` [ "|++", "|--", "++|", "--|"] = | op `elem` [ "|++", "|--", "++|", "--|"] =
warnMsg id $ filter (/= '|') op ++ " is" warnMsg id 3018 $ filter (/= '|') op ++ " is"
bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are" bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is" bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id 3020 "&> is"
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id ">& is" bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id 3021 ">& is"
bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are" bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are"
bashism (T_FdRedirect id num _) bashism (T_FdRedirect id num _)
| all isDigit num && length num > 1 = warnMsg id "FDs outside 0-9 are" | all isDigit num && length num > 1 = warnMsg id 3023 "FDs outside 0-9 are"
bashism (T_Assignment id Append _ _ _) = bashism (T_Assignment id Append _ _ _) =
warnMsg id "+= is" warnMsg id 3024 "+= is"
bashism (T_IoFile id _ word) | isNetworked = bashism (T_IoFile id _ word) | isNetworked =
warnMsg id "/dev/{tcp,udp} is" warnMsg id 3025 "/dev/{tcp,udp} is"
where where
file = onlyLiteralString word file = onlyLiteralString word
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"] isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
bashism (T_Glob id str) | "[^" `isInfixOf` str = bashism (T_Glob id str) | not isDash && "[^" `isInfixOf` str =
warnMsg id "^ in place of ! in glob bracket expressions is" warnMsg id 3026 "^ in place of ! in glob bracket expressions is"
bashism t@(TA_Variable id str _) | isBashVariable str = bashism t@(TA_Variable id str _) | isBashVariable str =
warnMsg id $ str ++ " is" warnMsg id 3028 $ str ++ " is"
bashism t@(T_DollarBraced id _ token) = do bashism t@(T_DollarBraced id _ token) = do
mapM_ check expansion mapM_ check expansion
when (isBashVariable var) $ when (isBashVariable var) $
warnMsg id $ var ++ " is" warnMsg id 3028 $ var ++ " is"
where where
str = bracedString t str = concat $ oversimplify token
var = getBracedReference str var = getBracedReference str
check (regex, feature) = check (regex, code, feature) =
when (isJust $ matchRegex regex str) $ warnMsg id feature when (isJust $ matchRegex regex str) $ warnMsg id code feature
bashism t@(T_Pipe id "|&") = bashism t@(T_Pipe id "|&") =
warnMsg id "|& in place of 2>&1 | is" warnMsg id 3029 "|& in place of 2>&1 | is"
bashism (T_Array id _) = bashism (T_Array id _) =
warnMsg id "arrays are" warnMsg id 3030 "arrays are"
bashism (T_IoFile id _ t) | isGlob t = bashism (T_IoFile id _ t) | isGlob t =
warnMsg id "redirecting to/from globs is" warnMsg id 3031 "redirecting to/from globs is"
bashism (T_CoProc id _ _) = bashism (T_CoProc id _ _) =
warnMsg id "coproc is" warnMsg id 3032 "coproc is"
bashism (T_Function id _ _ str _) | not (isVariableName str) = bashism (T_Function id _ _ str _) | not (isVariableName str) =
warnMsg id "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is" warnMsg id 3033 "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
bashism (T_DollarExpansion id [x]) | isOnlyRedirection x = bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
warnMsg id "$(<file) to read files is" warnMsg id 3034 "$(<file) to read files is"
bashism (T_Backticked id [x]) | isOnlyRedirection x = bashism (T_Backticked id [x]) | isOnlyRedirection x =
warnMsg id "`<file` to read files is" warnMsg id 3035 "`<file` to read files is"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "echo" && argString `matches` flagRegex = | t `isCommand` "echo" && argString `matches` flagRegex =
if isDash if isDash
then then
when (argString /= "-n") $ when (argString /= "-n") $
warnMsg (getId arg) "echo flags besides -n" warnMsg (getId arg) 3036 "echo flags besides -n"
else else
warnMsg (getId arg) "echo flags are" warnMsg (getId arg) 3037 "echo flags are"
where where
argString = concat $ oversimplify arg argString = concat $ oversimplify arg
flagRegex = mkRegex "^-[eEsn]+$" flagRegex = mkRegex "^-[eEsn]+$"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) = | getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
warnMsg (getId arg) "exec flags are" warnMsg (getId arg) 3038 "exec flags are"
bashism t@(T_SimpleCommand id _ _) bashism t@(T_SimpleCommand id _ _)
| t `isCommand` "let" = warnMsg id "'let' is" | t `isCommand` "let" = warnMsg id 3039 "'let' is"
bashism t@(T_SimpleCommand _ _ (cmd:args)) bashism t@(T_SimpleCommand _ _ (cmd:args))
| t `isCommand` "set" = unless isDash $ | t `isCommand` "set" = unless isDash $
checkOptions $ getLiteralArgs args checkOptions $ getLiteralArgs args
@@ -289,16 +292,17 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
-- Get the literal options from a list of arguments, -- Get the literal options from a list of arguments,
-- up until the first non-literal one -- up until the first non-literal one
getLiteralArgs :: [Token] -> [(Id, String)] getLiteralArgs :: [Token] -> [(Id, String)]
getLiteralArgs (first:rest) = fromMaybe [] $ do getLiteralArgs = foldr go []
str <- getLiteralString first where
return $ (getId first, str) : getLiteralArgs rest go first rest = case getLiteralString first of
getLiteralArgs [] = [] Just str -> (getId first, str) : rest
Nothing -> []
-- Check a flag-option pair (such as -o errexit) -- Check a flag-option pair (such as -o errexit)
checkOptions (flag@(fid,flag') : opt@(oid,opt') : rest) checkOptions (flag@(fid,flag') : opt@(oid,opt') : rest)
| flag' `matches` oFlagRegex = do | flag' `matches` oFlagRegex = do
when (opt' `notElem` longOptions) $ when (opt' `notElem` longOptions) $
warnMsg oid $ "set option " <> opt' <> " is" warnMsg oid 3040 $ "set option " <> opt' <> " is"
checkFlags (flag:rest) checkFlags (flag:rest)
| otherwise = checkFlags (flag:opt:rest) | otherwise = checkFlags (flag:opt:rest)
checkOptions (flag:rest) = checkFlags (flag:rest) checkOptions (flag:rest) = checkFlags (flag:rest)
@@ -311,10 +315,10 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
unless (flag' `matches` validFlagsRegex) $ unless (flag' `matches` validFlagsRegex) $
forM_ (tail flag') $ \letter -> forM_ (tail flag') $ \letter ->
when (letter `notElem` optionsSet) $ when (letter `notElem` optionsSet) $
warnMsg fid $ "set flag " <> ('-':letter:" is") warnMsg fid 3041 $ "set flag " <> ('-':letter:" is")
checkOptions rest checkOptions rest
| beginsWithDoubleDash flag' = do | beginsWithDoubleDash flag' = do
warnMsg fid $ "set flag " <> flag' <> " is" warnMsg fid 3042 $ "set flag " <> flag' <> " is"
checkOptions rest checkOptions rest
-- Either a word that doesn't start with a dash, or simply '--', -- Either a word that doesn't start with a dash, or simply '--',
-- so stop checking. -- so stop checking.
@@ -336,16 +340,19 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
let name = fromMaybe "" $ getCommandName t let name = fromMaybe "" $ getCommandName t
flags = getLeadingFlags t flags = getLeadingFlags t
in do in do
when (name == "local" && not isDash) $
-- This is so commonly accepted that we'll make it a special case
warnMsg id 3043 $ "'local' is"
when (name `elem` unsupportedCommands) $ when (name `elem` unsupportedCommands) $
warnMsg id $ "'" ++ name ++ "' is" warnMsg id 3044 $ "'" ++ name ++ "' is"
sequence_ $ do sequence_ $ do
allowed' <- Map.lookup name allowedFlags allowed' <- Map.lookup name allowedFlags
allowed <- allowed' allowed <- allowed'
(word, flag) <- find (word, flag) <- find
(\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is" return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is"
when (name == "source") $ warnMsg id "'source' in place of '.' is" when (name == "source") $ warnMsg id 3046 "'source' in place of '.' is"
when (name == "trap") $ when (name == "trap") $
let let
check token = sequence_ $ do check token = sequence_ $ do
@@ -353,12 +360,12 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
let upper = map toUpper str let upper = map toUpper str
return $ do return $ do
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $ when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
warnMsg (getId token) $ "trapping " ++ str ++ " is" warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is"
when ("SIG" `isPrefixOf` upper) $ when ("SIG" `isPrefixOf` upper) $
warnMsg (getId token) warnMsg (getId token) 3048
"prefixing signal names with 'SIG' is" "prefixing signal names with 'SIG' is"
when (not isDash && upper /= str) $ when (not isDash && upper /= str) $
warnMsg (getId token) warnMsg (getId token) 3049
"using lower/mixed case for signal names is" "using lower/mixed case for signal names is"
in in
mapM_ check (drop 1 rest) mapM_ check (drop 1 rest)
@@ -367,13 +374,13 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
format <- rest !!! 0 -- flags are covered by allowedFlags format <- rest !!! 0 -- flags are covered by allowedFlags
let literal = onlyLiteralString format let literal = onlyLiteralString format
guard $ "%q" `isInfixOf` literal guard $ "%q" `isInfixOf` literal
return $ warnMsg (getId format) "printf %q is" return $ warnMsg (getId format) 3050 "printf %q is"
where where
unsupportedCommands = [ unsupportedCommands = [
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown", "let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend", "enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
"typeset" "typeset"
] ++ if not isDash then ["local"] else [] ]
allowedFlags = Map.fromList [ allowedFlags = Map.fromList [
("cd", Just ["L", "P"]), ("cd", Just ["L", "P"]),
("exec", Just []), ("exec", Just []),
@@ -390,29 +397,35 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
("unset", Just ["f", "v"]), ("unset", Just ["f", "v"]),
("wait", Just []) ("wait", Just [])
] ]
bashism t@(T_SourceCommand id src _) = bashism t@(T_SourceCommand id src _)
let name = fromMaybe "" $ getCommandName src | getCommandName src == Just "source" = warnMsg id 3051 "'source' in place of '.' is"
in when (name == "source") $ warnMsg id "'source' in place of '.' is" bashism (TA_Expansion _ (T_Literal id str : _))
bashism (TA_Expansion _ (T_Literal id str : _)) | str `matches` radix = | str `matches` radix = warnMsg id 3052 "arithmetic base conversion is"
when (str `matches` radix) $ warnMsg id "arithmetic base conversion is"
where where
radix = mkRegex "^[0-9]+#" radix = mkRegex "^[0-9]+#"
bashism _ = return () bashism _ = return ()
varChars="_0-9a-zA-Z" varChars="_0-9a-zA-Z"
expansion = let re = mkRegex in [ expansion = let re = mkRegex in [
(re $ "^![" ++ varChars ++ "]", "indirect expansion is"), (re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"),
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"), (re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"),
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"), (re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"),
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"), (re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"),
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", "string indexing is"), (re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", 3057, "string indexing is"),
(re $ "^([*@][%#]|#[@*])", "string operations on $@/$* are"), (re $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is") (re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is")
] ]
bashVars = [ bashVars = [
-- This list deliberately excludes $BASH_VERSION as it's often used
-- for shell identification.
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS", "DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS",
"_" "_", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
"BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND",
"BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE",
"BASH_SUBSHELL", "BASH_VERSINFO", "EPOCHREALTIME", "EPOCHSECONDS",
"FUNCNAME", "GROUPS", "MACHTYPE", "MAPFILE"
] ]
bashDynamicVars = [ "RANDOM", "SECONDS" ] bashDynamicVars = [ "RANDOM", "SECONDS" ]
dashVars = [ "_" ] dashVars = [ "_" ]
@@ -506,13 +519,13 @@ checkMultiDimensionalArrays = ForShell [Bash] f
case token of case token of
T_Assignment _ _ name (first:second:_) _ -> about second T_Assignment _ _ name (first:second:_) _ -> about second
T_IndexedElement _ (first:second:_) _ -> about second T_IndexedElement _ (first:second:_) _ -> about second
T_DollarBraced {} -> T_DollarBraced _ _ l ->
when (isMultiDim token) $ about token when (isMultiDim l) $ about token
_ -> return () _ -> return ()
about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays." about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays."
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
isMultiDim t = getBracedModifier (bracedString t) `matches` re isMultiDim l = getBracedModifier (concat $ oversimplify l) `matches` re
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '" 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\\$ '"

View File

@@ -273,10 +273,10 @@ getPrefixSum = f 0
where where
f sum _ PSLeaf = sum f sum _ PSLeaf = sum
f sum target (PSBranch pivot left right cumulative) = f sum target (PSBranch pivot left right cumulative) =
case () of case target `compare` pivot of
_ | target < pivot -> f sum target left LT -> f sum target left
_ | target > pivot -> f (sum+cumulative) target right GT -> f (sum+cumulative) target right
_ -> sum+cumulative EQ -> sum+cumulative
-- Add a value to the Prefix Sum tree at the given index. -- Add a value to the Prefix Sum tree at the given index.
-- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5 -- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5
@@ -285,10 +285,10 @@ addPSValue key value tree = if value == 0 then tree else f tree
where where
f PSLeaf = PSBranch key PSLeaf PSLeaf value f PSLeaf = PSBranch key PSLeaf PSLeaf value
f (PSBranch pivot left right sum) = f (PSBranch pivot left right sum) =
case () of case key `compare` pivot of
_ | key < pivot -> PSBranch pivot (f left) right (sum + value) LT -> PSBranch pivot (f left) right (sum + value)
_ | key > pivot -> PSBranch pivot left (f right) sum GT -> PSBranch pivot left (f right) sum
_ -> PSBranch pivot left right (sum + value) EQ -> PSBranch pivot left right (sum + value)
prop_pstreeSumsCorrectly kvs targets = prop_pstreeSumsCorrectly kvs targets =
let let

View File

@@ -127,7 +127,7 @@ outputForFile color sys comments = do
let lineCount = length fileLinesList let lineCount = length fileLinesList
let fileLines = listArray (1, lineCount) fileLinesList let fileLines = listArray (1, lineCount) fileLinesList
let groups = groupWith lineNo comments let groups = groupWith lineNo comments
mapM_ (\commentsForLine -> do forM_ groups $ \commentsForLine -> do
let lineNum = fromIntegral $ lineNo (head commentsForLine) let lineNum = fromIntegral $ lineNo (head commentsForLine)
let line = if lineNum < 1 || lineNum > lineCount let line = if lineNum < 1 || lineNum > lineCount
then "" then ""
@@ -136,10 +136,9 @@ outputForFile color sys comments = do
putStrLn $ color "message" $ putStrLn $ color "message" $
"In " ++ fileName ++" line " ++ show lineNum ++ ":" "In " ++ fileName ++" line " ++ show lineNum ++ ":"
putStrLn (color "source" line) putStrLn (color "source" line)
mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) commentsForLine forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c
putStrLn "" putStrLn ""
showFixedString color commentsForLine (fromIntegral lineNum) fileLines showFixedString color commentsForLine (fromIntegral lineNum) fileLines
) groups
-- Pick out only the lines necessary to show a fix in action -- Pick out only the lines necessary to show a fix in action
sliceFile :: Fix -> Array Int String -> (Fix, Array Int String) sliceFile :: Fix -> Array Int String -> (Fix, Array Int String)

View File

@@ -123,8 +123,10 @@ readUnicodeQuote = do
return $ T_Literal id [c] return $ T_Literal id [c]
carriageReturn = do carriageReturn = do
parseNote ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ." pos <- getPosition
char '\r' char '\r'
parseProblemAt pos ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ."
return '\r'
almostSpace = almostSpace =
choice [ choice [
@@ -209,8 +211,13 @@ startSpan = IncompleteInterval <$> getPosition
endSpan (IncompleteInterval start) = do endSpan (IncompleteInterval start) = do
endPos <- getPosition endPos <- getPosition
id <- getNextIdBetween start endPos getNextIdBetween start endPos
return id
getSpanPositionsFor m = do
start <- getPosition
m
end <- getPosition
return (start, end)
addToHereDocMap id list = do addToHereDocMap id list = do
state <- getState state <- getState
@@ -253,16 +260,22 @@ ignoreProblemsOf p = do
shouldIgnoreCode code = do shouldIgnoreCode code = do
context <- getCurrentContexts context <- getCurrentContexts
checkSourced <- Mr.asks checkSourced checkSourced <- Mr.asks checkSourced
return $ any (disabling checkSourced) context return $ any (contextItemDisablesCode checkSourced code) context
-- Does this item on the context stack disable warnings for 'code'?
contextItemDisablesCode :: Bool -> Integer -> Context -> Bool
contextItemDisablesCode alsoCheckSourced code = disabling alsoCheckSourced
where where
disabling checkSourced item = disabling checkSourced item =
case item of case item of
ContextAnnotation list -> any disabling' list ContextAnnotation list -> any disabling' list
ContextSource _ -> not $ checkSourced ContextSource _ -> not $ checkSourced
_ -> False _ -> False
disabling' (DisableComment n) = code == n disabling' (DisableComment n m) = code >= n && code < m
disabling' _ = False disabling' _ = False
getCurrentAnnotations includeSource = getCurrentAnnotations includeSource =
concatMap get . takeWhile (not . isBoundary) <$> getCurrentContexts concatMap get . takeWhile (not . isBoundary) <$> getCurrentContexts
where where
@@ -373,16 +386,15 @@ parseNoteAtId id c l a = do
parseNoteAtWithEnd start end c l a = addParseNote $ ParseNote start end c l a parseNoteAtWithEnd start end c l a = addParseNote $ ParseNote start end c l a
--------- Convenient combinators --------- Convenient combinators
thenSkip main follow = do thenSkip main follow = main <* optional follow
r <- main
optional follow
return r
unexpecting s p = try $ unexpecting s p = try $
(try p >> fail ("Unexpected " ++ s)) <|> return () (try p >> fail ("Unexpected " ++ s)) <|> return ()
notFollowedBy2 = unexpecting "" notFollowedBy2 = unexpecting ""
isFollowedBy p = (lookAhead . try $ p $> True) <|> return False
reluctantlyTill p end = reluctantlyTill p end =
(lookAhead (void (try end) <|> eof) >> return []) <|> do (lookAhead (void (try end) <|> eof) >> return []) <|> do
x <- p x <- p
@@ -420,7 +432,7 @@ acceptButWarn parser level code note =
parsecBracket before after op = do parsecBracket before after op = do
val <- before val <- before
(op val <* optional (after val)) <|> (after val *> fail "") op val `thenSkip` after val <|> (after val *> fail "")
swapContext contexts p = swapContext contexts p =
parsecBracket (getCurrentContexts <* setCurrentContexts contexts) parsecBracket (getCurrentContexts <* setCurrentContexts contexts)
@@ -914,8 +926,9 @@ prop_readCondition20 = isOk readCondition "[[ echo_rc -eq 0 ]]"
prop_readCondition21 = isOk readCondition "[[ $1 =~ ^(a\\ b)$ ]]" prop_readCondition21 = isOk readCondition "[[ $1 =~ ^(a\\ b)$ ]]"
prop_readCondition22 = isOk readCondition "[[ $1 =~ \\.a\\.(\\.b\\.)\\.c\\. ]]" prop_readCondition22 = isOk readCondition "[[ $1 =~ \\.a\\.(\\.b\\.)\\.c\\. ]]"
prop_readCondition23 = isOk readCondition "[[ -v arr[$var] ]]" prop_readCondition23 = isOk readCondition "[[ -v arr[$var] ]]"
prop_readCondition24 = isWarning readCondition "[[ 1 == 2 ]]]"
prop_readCondition25 = isOk readCondition "[[ lex.yy.c -ot program.l ]]" prop_readCondition25 = isOk readCondition "[[ lex.yy.c -ot program.l ]]"
prop_readCondition26 = isOk readScript "[[ foo ]]\\\n && bar"
prop_readCondition27 = not $ isOk readConditionCommand "[[ x ]] foo"
readCondition = called "test expression" $ do readCondition = called "test expression" $ do
opos <- getPosition opos <- getPosition
start <- startSpan start <- startSpan
@@ -942,15 +955,9 @@ readCondition = called "test expression" $ do
cpos <- getPosition cpos <- getPosition
close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here (don't wrap commands in []/[[]])" close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here (don't wrap commands in []/[[]])"
id <- endSpan start id <- endSpan start
when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Did you mean ]] ?" when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Test expression was opened with double [[ but closed with single ]. Make sure they match."
when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?" when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Test expression was opened with single [ but closed with double ]]. Make sure they match."
optional $ lookAhead $ do
pos <- getPosition
notFollowedBy2 readCmdWord <|>
parseProblemAt pos ErrorC 1136
("Unexpected characters after terminating " ++ close ++ ". Missing semicolon/linefeed?")
spacing spacing
many readCmdWord -- Read and throw away remainders to get then/do warnings. Fixme?
return $ T_Condition id typ condition return $ T_Condition id typ condition
readAnnotationPrefix = do readAnnotationPrefix = do
@@ -964,6 +971,7 @@ prop_readAnnotation3 = isOk readAnnotation "# shellcheck disable=SC1234 source=/
prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=SC1234\n" prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=SC1234\n"
prop_readAnnotation5 = isOk readAnnotation "# shellcheck disable=SC2002 # All cats are precious\n" prop_readAnnotation5 = isOk readAnnotation "# shellcheck disable=SC2002 # All cats are precious\n"
prop_readAnnotation6 = isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\n" prop_readAnnotation6 = isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\n"
prop_readAnnotation7 = isOk readAnnotation "# shellcheck disable=SC1000,SC2000-SC3000,SC1001\n"
readAnnotation = called "shellcheck directive" $ do readAnnotation = called "shellcheck directive" $ do
try readAnnotationPrefix try readAnnotationPrefix
many1 linewhitespace many1 linewhitespace
@@ -984,12 +992,16 @@ readAnnotationWithoutPrefix = do
key <- many1 (letter <|> char '-') key <- many1 (letter <|> char '-')
char '=' <|> fail "Expected '=' after directive key" char '=' <|> fail "Expected '=' after directive key"
annotations <- case key of annotations <- case key of
"disable" -> readCode `sepBy` char ',' "disable" -> readRange `sepBy` char ','
where where
readRange = do
from <- readCode
to <- choice [ char '-' *> readCode, return $ from+1 ]
return $ DisableComment from to
readCode = do readCode = do
optional $ string "SC" optional $ string "SC"
int <- many1 digit int <- many1 digit
return $ DisableComment (read int) return $ read int
"enable" -> readName `sepBy` char ',' "enable" -> readName `sepBy` char ','
where where
@@ -1554,7 +1566,7 @@ readDollarExpression = do
readDollarExp = arithmetic <|> readDollarExpansion <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarVariable readDollarExp = arithmetic <|> readDollarExpansion <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarVariable
where where
arithmetic = readAmbiguous "$((" readDollarArithmetic readDollarExpansion (\pos -> arithmetic = readAmbiguous "$((" readDollarArithmetic readDollarExpansion (\pos ->
parseNoteAt pos ErrorC 1102 "Shells disambiguate $(( differently or not at all. For $(command substition), add space after $( . For $((arithmetics)), fix parsing errors.") parseNoteAt pos ErrorC 1102 "Shells disambiguate $(( differently or not at all. For $(command substitution), add space after $( . For $((arithmetics)), fix parsing errors.")
prop_readDollarSingleQuote = isOk readDollarSingleQuote "$'foo\\\'lol'" prop_readDollarSingleQuote = isOk readDollarSingleQuote "$'foo\\\'lol'"
readDollarSingleQuote = called "$'..' expression" $ do readDollarSingleQuote = called "$'..' expression" $ do
@@ -1603,6 +1615,7 @@ readArithmeticExpression = called "((..)) command" $ do
c <- readArithmeticContents c <- readArithmeticContents
string "))" string "))"
id <- endSpan start id <- endSpan start
spacing
return (T_Arithmetic id c) return (T_Arithmetic id c)
-- If the next characters match prefix, try two different parsers and warn if the alternate parser had to be used -- If the next characters match prefix, try two different parsers and warn if the alternate parser had to be used
@@ -1749,6 +1762,8 @@ prop_readHereDoc17= isWarning readScript "cat <<- ' foo'\nbar\n foo\n foo\n"
prop_readHereDoc18= isOk readScript "cat <<'\"foo'\nbar\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_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_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 readHereDoc = called "here document" $ do
pos <- getPosition pos <- getPosition
try $ string "<<" try $ string "<<"
@@ -1777,7 +1792,9 @@ readHereDoc = called "here document" $ do
-- Fun fact: bash considers << foo"" quoted, but not << <("foo"). -- Fun fact: bash considers << foo"" quoted, but not << <("foo").
readToken = do readToken = do
str <- readStringForParser readNormalWord str <- readStringForParser readNormalWord
return $ unquote str -- A here doc actually works with \r\n because the \r becomes part of the token
crstr <- (carriageReturn >> (return $ str ++ "\r")) <|> return str
return $ unquote crstr
readPendingHereDocs = do readPendingHereDocs = do
docs <- popPendingHereDocs docs <- popPendingHereDocs
@@ -1892,14 +1909,14 @@ readPendingHereDocs = do
debugHereDoc tokenId endToken doc debugHereDoc tokenId endToken doc
| endToken `isInfixOf` doc = | endToken `isInfixOf` doc =
let lookAt line = when (endToken `isInfixOf` line) $ let lookAt line = when (endToken `isInfixOf` line) $
parseProblemAtId tokenId ErrorC 1042 ("Close matches include '" ++ line ++ "' (!= '" ++ endToken ++ "').") parseProblemAtId tokenId ErrorC 1042 ("Close matches include '" ++ (e4m line) ++ "' (!= '" ++ (e4m endToken) ++ "').")
in do in do
parseProblemAtId tokenId ErrorC 1041 ("Found '" ++ endToken ++ "' further down, but not on a separate line.") parseProblemAtId tokenId ErrorC 1041 ("Found '" ++ (e4m endToken) ++ "' further down, but not on a separate line.")
mapM_ lookAt (lines doc) mapM_ lookAt (lines doc)
| map toLower endToken `isInfixOf` map toLower doc = | map toLower endToken `isInfixOf` map toLower doc =
parseProblemAtId tokenId ErrorC 1043 ("Found " ++ endToken ++ " further down, but with wrong casing.") parseProblemAtId tokenId ErrorC 1043 ("Found " ++ (e4m endToken) ++ " further down, but with wrong casing.")
| otherwise = | otherwise =
parseProblemAtId tokenId ErrorC 1044 ("Couldn't find end token `" ++ endToken ++ "' in the here document.") parseProblemAtId tokenId ErrorC 1044 ("Couldn't find end token `" ++ (e4m endToken) ++ "' in the here document.")
readFilename = readNormalWord readFilename = readNormalWord
@@ -1955,8 +1972,6 @@ readIoRedirect = do
spacing spacing
return $ T_FdRedirect id n redir return $ T_FdRedirect id n redir
readRedirectList = many1 readIoRedirect
prop_readHereString = isOk readHereString "<<< \"Hello $world\"" prop_readHereString = isOk readHereString "<<< \"Hello $world\""
readHereString = called "here string" $ do readHereString = called "here string" $ do
start <- startSpan start <- startSpan
@@ -2079,10 +2094,6 @@ readSimpleCommand = called "simple command" $ do
then action then action
else getParser def cmd rest else getParser def cmd rest
cStyleComment cmd =
case cmd of
_ -> False
validateCommand cmd = validateCommand cmd =
case cmd of case cmd of
(T_NormalWord _ [T_Literal _ "//"]) -> commentWarning (getId cmd) (T_NormalWord _ [T_Literal _ "//"]) -> commentWarning (getId cmd)
@@ -2119,14 +2130,14 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
let file = getFile file' rest' let file = getFile file' rest'
override <- getSourceOverride override <- getSourceOverride
let literalFile = do let literalFile = do
name <- override `mplus` getLiteralString file name <- override `mplus` getLiteralString file `mplus` stripDynamicPrefix file
-- Hack to avoid 'source ~/foo' trying to read from literal tilde -- Hack to avoid 'source ~/foo' trying to read from literal tilde
guard . not $ "~/" `isPrefixOf` name guard . not $ "~/" `isPrefixOf` name
return name return name
case literalFile of case literalFile of
Nothing -> do Nothing -> do
parseNoteAtId (getId file) WarningC 1090 parseNoteAtId (getId file) WarningC 1090
"Can't follow non-constant source. Use a directive to specify location." "ShellCheck can't follow non-constant source. Use a directive to specify location."
return t return t
Just filename -> do Just filename -> do
proceed <- shouldFollow filename proceed <- shouldFollow filename
@@ -2179,6 +2190,16 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
SourcePath x -> Just x SourcePath x -> Just x
_ -> Nothing _ -> Nothing
-- If the word has a single expansion as the directory, try stripping it
-- This affects `$foo/bar` but not `${foo}-dir/bar` or `/foo/$file`
stripDynamicPrefix word =
case getWordParts word of
exp : rest | isStringExpansion exp -> do
str <- getLiteralString (T_NormalWord (Id 0) rest)
guard $ "/" `isPrefixOf` str
return $ "." ++ str
_ -> Nothing
subRead name script = subRead name script =
withContext (ContextSource name) $ withContext (ContextSource name) $
inSeparateContext $ inSeparateContext $
@@ -2276,6 +2297,7 @@ readPipe = do
readCommand = choice [ readCommand = choice [
readCompoundCommand, readCompoundCommand,
readConditionCommand,
readCoProc, readCoProc,
readSimpleCommand readSimpleCommand
] ]
@@ -2388,6 +2410,7 @@ readSubshell = called "explicit subshell" $ do
allspacing allspacing
char ')' <|> fail "Expected ) closing the subshell" char ')' <|> fail "Expected ) closing the subshell"
id <- endSpan start id <- endSpan start
spacing
return $ T_Subshell id list return $ T_Subshell id list
prop_readBraceGroup = isOk readBraceGroup "{ a; b | c | d; e; }" prop_readBraceGroup = isOk readBraceGroup "{ a; b | c | d; e; }"
@@ -2408,6 +2431,7 @@ readBraceGroup = called "brace group" $ do
parseProblem ErrorC 1056 "Expected a '}'. If you have one, try a ; or \\n in front of it." parseProblem ErrorC 1056 "Expected a '}'. If you have one, try a ; or \\n in front of it."
fail "Missing '}'" fail "Missing '}'"
id <- endSpan start id <- endSpan start
spacing
return $ T_BraceGroup id list return $ T_BraceGroup id list
prop_readBatsTest = isOk readBatsTest "@test 'can parse' {\n true\n}" prop_readBatsTest = isOk readBatsTest "@test 'can parse' {\n true\n}"
@@ -2460,6 +2484,11 @@ readDoGroup kwId = do
parseProblemAtId (getId doKw) ErrorC 1061 "Couldn't find 'done' for this 'do'." parseProblemAtId (getId doKw) ErrorC 1061 "Couldn't find 'done' for this 'do'."
parseProblem ErrorC 1062 "Expected 'done' matching previously mentioned 'do'." parseProblem ErrorC 1062 "Expected 'done' matching previously mentioned 'do'."
return "Expected 'done'" return "Expected 'done'"
optional . lookAhead $ do
pos <- getPosition
try $ string "<("
parseProblemAt pos ErrorC 1142 "Use 'done < <(cmd)' to redirect from process substitution (currently missing one '<')."
return commands return commands
@@ -2482,19 +2511,31 @@ readForClause = called "for loop" $ do
readArithmetic id <|> readRegular id readArithmetic id <|> readRegular id
where where
readArithmetic id = called "arithmetic for condition" $ do readArithmetic id = called "arithmetic for condition" $ do
try $ string "((" readArithmeticDelimiter '(' "Missing second '(' to start arithmetic for ((;;)) loop"
x <- readArithmeticContents x <- readArithmeticContents
char ';' >> spacing char ';' >> spacing
y <- readArithmeticContents y <- readArithmeticContents
char ';' >> spacing char ';' >> spacing
z <- readArithmeticContents z <- readArithmeticContents
spacing spacing
string "))" readArithmeticDelimiter ')' "Missing second ')' to terminate 'for ((;;))' loop condition"
spacing spacing
optional $ readSequentialSep >> spacing optional $ readSequentialSep >> spacing
group <- readBraced <|> readDoGroup id group <- readBraced <|> readDoGroup id
return $ T_ForArithmetic id x y z group return $ T_ForArithmetic id x y z group
-- For c='(' read "((" and be lenient about spaces
readArithmeticDelimiter c msg = do
char c
startPos <- getPosition
sp <- spacing
endPos <- getPosition
char c <|> do
parseProblemAt startPos ErrorC 1137 msg
fail ""
unless (null sp) $
parseProblemAtWithEnd startPos endPos ErrorC 1138 $ "Remove spaces between " ++ [c,c] ++ " in arithmetic for loop."
readBraced = do readBraced = do
(T_BraceGroup _ list) <- readBraceGroup (T_BraceGroup _ list) <- readBraceGroup
return list return list
@@ -2625,7 +2666,7 @@ readFunctionDefinition = called "function" $ do
readWithoutFunction = try $ do readWithoutFunction = try $ do
name <- many1 functionChars name <- many1 functionChars
guard $ name /= "time" -- Interfers with time ( foo ) guard $ name /= "time" -- Interferes with time ( foo )
spacing spacing
readParens readParens
return $ \id -> T_Function id (FunctionKeyword False) (FunctionParentheses True) name return $ \id -> T_Function id (FunctionKeyword False) (FunctionParentheses True) name
@@ -2665,9 +2706,38 @@ readCoProc = called "coproc" $ do
id <- endSpan start id <- endSpan start
return $ T_CoProcBody id body return $ T_CoProcBody id body
readPattern = (readPatternWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing) readPattern = (readPatternWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing)
prop_readConditionCommand = isOk readConditionCommand "[[ x ]] > foo 2>&1"
readConditionCommand = do
cmd <- readCondition
redirs <- many readIoRedirect
id <- getNextIdSpanningTokenList (cmd:redirs)
pos <- getPosition
hasDashAo <- isFollowedBy $ do
c <- choice $ try . string <$> ["-o", "-a", "or", "and"]
posEnd <- getPosition
parseProblemAtWithEnd pos posEnd ErrorC 1139 $
"Use " ++ alt c ++ " instead of '" ++ c ++ "' between test commands."
-- If the next word is a keyword, readNormalWord will trigger a warning
hasKeyword <- isFollowedBy readKeyword
hasWord <- isFollowedBy readNormalWord
when (hasWord && not (hasKeyword || hasDashAo)) $ do
-- We have other words following, and no error has been emitted.
posEnd <- getPosition
parseProblemAtWithEnd pos posEnd ErrorC 1140 "Unexpected parameters after condition. Missing &&/||, or bad expression?"
return $ T_Redirecting id redirs cmd
where
alt "or" = "||"
alt "-o" = "||"
alt "and" = "&&"
alt "-a" = "&&"
alt _ = "|| or &&"
prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null" prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null"
readCompoundCommand = do readCompoundCommand = do
cmd <- choice [ cmd <- choice [
@@ -2675,7 +2745,6 @@ readCompoundCommand = do
readAmbiguous "((" readArithmeticExpression readSubshell (\pos -> readAmbiguous "((" readArithmeticExpression readSubshell (\pos ->
parseNoteAt pos ErrorC 1105 "Shells disambiguate (( differently or not at all. For subshell, add spaces around ( . For ((, fix parsing errors."), parseNoteAt pos ErrorC 1105 "Shells disambiguate (( differently or not at all. For subshell, add spaces around ( . For ((, fix parsing errors."),
readSubshell, readSubshell,
readCondition,
readWhileClause, readWhileClause,
readUntilClause, readUntilClause,
readIfClause, readIfClause,
@@ -2685,15 +2754,15 @@ readCompoundCommand = do
readBatsTest, readBatsTest,
readFunctionDefinition readFunctionDefinition
] ]
spacing
redirs <- many readIoRedirect redirs <- many readIoRedirect
id <- getNextIdSpanningTokenList (cmd:redirs) id <- getNextIdSpanningTokenList (cmd:redirs)
unless (null redirs) $ optional $ do optional . lookAhead $ do
lookAhead $ try (spacing >> needsSeparator) notFollowedBy2 $ choice [readKeyword, g_Lbrace]
parseProblem WarningC 1013 "Bash requires ; or \\n here, after redirecting nested compound commands." pos <- getPosition
many1 readNormalWord
posEnd <- getPosition
parseProblemAtWithEnd pos posEnd ErrorC 1141 "Unexpected tokens after compound command. Bad redirection or missing ;/&&/||/|?"
return $ T_Redirecting id redirs cmd return $ T_Redirecting id redirs cmd
where
needsSeparator = choice [ g_Then, g_Else, g_Elif, g_Fi, g_Do, g_Done, g_Esac, g_Rbrace ]
readCompoundList = readTerm readCompoundList = readTerm
@@ -2763,17 +2832,13 @@ readLiteralForParser parser = do
prop_readAssignmentWord = isOk readAssignmentWord "a=42" prop_readAssignmentWord = isOk readAssignmentWord "a=42"
prop_readAssignmentWord2 = isOk readAssignmentWord "b=(1 2 3)" prop_readAssignmentWord2 = isOk readAssignmentWord "b=(1 2 3)"
prop_readAssignmentWord3 = isWarning readAssignmentWord "$b = 13"
prop_readAssignmentWord4 = isWarning readAssignmentWord "b = $(lol)"
prop_readAssignmentWord5 = isOk readAssignmentWord "b+=lol" prop_readAssignmentWord5 = isOk readAssignmentWord "b+=lol"
prop_readAssignmentWord6 = isWarning readAssignmentWord "b += (1 2 3)"
prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42" prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42"
prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42" prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42"
prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= " prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "
prop_readAssignmentWord9a= isOk readAssignmentWord "foo=" prop_readAssignmentWord9a= isOk readAssignmentWord "foo="
prop_readAssignmentWord9b= isOk readAssignmentWord "foo= " prop_readAssignmentWord9b= isOk readAssignmentWord "foo= "
prop_readAssignmentWord9c= isOk readAssignmentWord "foo= #bar" prop_readAssignmentWord9c= isOk readAssignmentWord "foo= #bar"
prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42"
prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )" prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'" prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'"
prop_readAssignmentWord13= isOk readAssignmentWord "var=( (1 2) (3 4) )" prop_readAssignmentWord13= isOk readAssignmentWord "var=( (1 2) (3 4) )"
@@ -2781,52 +2846,63 @@ prop_readAssignmentWord14= isOk readAssignmentWord "var=( 1 [2]=(3 4) )"
prop_readAssignmentWord15= isOk readAssignmentWord "var=(1 [2]=(3 4))" prop_readAssignmentWord15= isOk readAssignmentWord "var=(1 [2]=(3 4))"
readAssignmentWord = readAssignmentWordExt True readAssignmentWord = readAssignmentWordExt True
readWellFormedAssignment = readAssignmentWordExt False readWellFormedAssignment = readAssignmentWordExt False
readAssignmentWordExt lenient = try $ do readAssignmentWordExt lenient = called "variable assignment" $ do
start <- startSpan -- Parse up to and including the = in a 'try'
pos <- getPosition (id, variable, op, indices) <- try $ do
when lenient $ start <- startSpan
optional (char '$' >> parseNote ErrorC 1066 "Don't use $ on the left side of assignments.") pos <- getPosition
variable <- readVariableName -- Check for a leading $ at parse time, to warn for $foo=(bar) which
when lenient $ -- would otherwise cause a parse failure so it can't be checked later.
optional (readNormalDollar >> parseNoteAt pos ErrorC leadingDollarPos <-
1067 "For indirection, use arrays, declare \"var$n=value\", or (for sh) read/eval.") if lenient
indices <- many readArrayIndex then optionMaybe $ getSpanPositionsFor (char '$')
hasLeftSpace <- fmap (not . null) spacing else return Nothing
pos <- getPosition variable <- readVariableName
id <- endSpan start indices <- many readArrayIndex
op <- readAssignmentOp hasLeftSpace <- fmap (not . null) spacing
opStart <- getPosition
id <- endSpan start
op <- readAssignmentOp
opEnd <- getPosition
when (isJust leadingDollarPos || hasLeftSpace) $ do
hasParen <- isFollowedBy (spacing >> char '(')
when hasParen $
sequence_ $ do
(l, r) <- leadingDollarPos
return $ parseProblemAtWithEnd l r ErrorC 1066 "Don't use $ on the left side of assignments."
-- Fail so that this is not parsed as an assignment.
fail ""
-- At this point we know for sure.
return (id, variable, op, indices)
rightPosStart <- getPosition
hasRightSpace <- fmap (not . null) spacing hasRightSpace <- fmap (not . null) spacing
rightPosEnd <- getPosition
isEndOfCommand <- fmap isJust $ optionMaybe (try . lookAhead $ (void (oneOf "\r\n;&|)") <|> eof)) isEndOfCommand <- fmap isJust $ optionMaybe (try . lookAhead $ (void (oneOf "\r\n;&|)") <|> eof))
if not hasLeftSpace && (hasRightSpace || isEndOfCommand)
if hasRightSpace || isEndOfCommand
then do then do
when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $ when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $ do
parseNoteAt pos WarningC 1007 parseProblemAtWithEnd rightPosStart rightPosEnd WarningC 1007
"Remove space after = if trying to assign a value (for empty string, use var='' ... )." "Remove space after = if trying to assign a value (for empty string, use var='' ... )."
value <- readEmptyLiteral value <- readEmptyLiteral
return $ T_Assignment id op variable indices value return $ T_Assignment id op variable indices value
else do else do
when (hasLeftSpace || hasRightSpace) $ optional $ do
parseNoteAt pos ErrorC 1068 $ lookAhead $ char '='
"Don't put spaces around the " parseProblem ErrorC 1097 "Unexpected ==. For assignment, use =. For comparison, use [/[[. Or quote for literal string."
++ (if op == Append
then "+= when appending"
else "= in assignments")
++ " (or quote to make it literal)."
value <- readArray <|> readNormalWord value <- readArray <|> readNormalWord
spacing spacing
return $ T_Assignment id op variable indices value return $ T_Assignment id op variable indices value
where where
readAssignmentOp = do readAssignmentOp = do
pos <- getPosition -- This is probably some kind of ascii art border
unexpecting "" $ string "===" unexpecting "===" (string "===")
choice [ choice [
string "+=" >> return Append, string "+=" >> return Append,
do
try (string "==")
parseProblemAt pos ErrorC 1097
"Unexpected ==. For assignment, use =. For comparison, use [/[[."
return Assign,
string "=" >> return Assign string "=" >> return Assign
] ]
@@ -3093,7 +3169,7 @@ readConfigFile filename = do
let line = "line " ++ (show . sourceLine $ errorPos err) let line = "line " ++ (show . sourceLine $ errorPos err)
suggestion = getStringFromParsec $ errorMessages err suggestion = getStringFromParsec $ errorMessages err
in in
"Failed to process " ++ filename ++ ", " ++ line ++ ": " "Failed to process " ++ (e4m filename) ++ ", " ++ line ++ ": "
++ suggestion ++ suggestion
prop_readConfigKVs1 = isOk readConfigKVs "disable=1234" prop_readConfigKVs1 = isOk readConfigKVs "disable=1234"
@@ -3114,6 +3190,7 @@ prop_readScript2 = isWarning readScript "#!/bin/bash\r\necho hello world\n"
prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world" prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world"
prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=(" prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=("
prop_readScript5 = isOk readScript "#!/bin/bash\n#This is an empty script\n\n" 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"
readScriptFile sourced = do readScriptFile sourced = do
start <- startSpan start <- startSpan
pos <- getPosition pos <- getPosition
@@ -3155,13 +3232,14 @@ readScriptFile sourced = do
where where
basename s = reverse . takeWhile (/= '/') . reverse $ s basename s = reverse . takeWhile (/= '/') . reverse $ s
skipFlags = dropWhile ("-" `isPrefixOf`)
getShell sb = getShell sb =
case words sb of case words sb of
[] -> "" [] -> ""
[x] -> basename x [x] -> basename x
(first:second:_) -> (first:args) ->
if basename first == "env" if basename first == "env"
then second then fromMaybe "" $ find (notElem '=') $ skipFlags args
else basename first else basename first
verifyShebang pos s = do verifyShebang pos s = do
@@ -3316,16 +3394,21 @@ parseShell env name contents = do
prRoot = Just $ prRoot = Just $
reattachHereDocs script (hereDocMap userstate) reattachHereDocs script (hereDocMap userstate)
} }
Left err -> Left err -> do
let context = contextStack state
return newParseResult { return newParseResult {
prComments = prComments =
map toPositionedComment $ map toPositionedComment $
notesForContext (contextStack state) (filter (not . isIgnored context) $
++ [makeErrorFor err] notesForContext context
++ [makeErrorFor err])
++ parseProblems state, ++ parseProblems state,
prTokenPositions = Map.empty, prTokenPositions = Map.empty,
prRoot = Nothing prRoot = Nothing
} }
where
-- A final pass for ignoring parse errors after failed parsing
isIgnored stack note = any (contextItemDisablesCode False (codeForParseNote note)) stack
notesForContext list = zipWith ($) [first, second] $ filter isName list notesForContext list = zipWith ($) [first, second] $ filter isName list
where where

View File

@@ -9,7 +9,7 @@ fail() {
if git diff | grep -q "" if git diff | grep -q ""
then then
fail "There are uncommited changes" fail "There are uncommitted changes"
fi fi
current=$(git tag --points-at) current=$(git tag --points-at)