658 Commits

Author SHA1 Message Date
Vidar Holen
cff3e22911 Stable version v0.7.2
This release is dedicated to ethanol, for keeping
COVID-19 off both our hands and our minds.
2021-04-19 14:44:27 -07:00
Vidar Holen
5669eb2203 Make x-comparison warning default 2021-04-11 15:52:13 -07:00
Vidar Holen
b68df1882d Merge pull request #2181 from matthiasdiener/patch-1
Clarify 'which'
2021-03-31 20:03:23 -07:00
Matthias Diener
087865c680 Clarify 'which' 2021-03-20 20:43:18 -05:00
Vidar Holen
19c6f22c3f Merge branch 'm-ildefons-comment-backslash' 2021-03-20 18:13:50 -07:00
Vidar Holen
98952df35b Improve warnings on backslashes in comments 2021-03-20 18:12:39 -07:00
Vidar Holen
a277efdbb1 Merge branch 'comment-backslash' of https://github.com/m-ildefons/shellcheck into m-ildefons-comment-backslash 2021-03-20 13:34:40 -07:00
Vidar Holen
45687b0548 Merge pull request #2166 from avoidik/patch-1
suppress ntlm error messages in Windows build
2021-03-20 13:31:04 -07:00
Vidar Holen
ecdc21b0b7 Merge pull request #2112 from pepeiborra/patch-1
Add Haddock markup to SystemInterface
2021-03-14 13:16:29 -07:00
Vidar Holen
4eb42fa3c1 Merge branch 'austin987-busybox' 2021-03-14 13:02:41 -07:00
Vidar Holen
f02c297fdd Merge parser and analyzer shebang parsing 2021-03-11 23:04:17 -08:00
Vidar Holen
ea83b602d7 Merge branch 'busybox' of https://github.com/austin987/shellcheck into austin987-busybox 2021-03-11 21:44:17 -08:00
Vidar Holen
88cd21fd0f Fix missing +x with new cabal and use previous release deps for caching 2021-03-08 14:01:48 -08:00
Vidar Holen
83435c4f2e Merge pull request #2134 from kolyshkin/podman-sc2016
Whitelist podman for SC2016 about '$var'
2021-03-07 20:48:58 -08:00
Vidar Holen
4324b4a213 Merge pull request #2122 from freddii/master
fixed typing mistakes in changelog
2021-03-07 20:48:24 -08:00
Vidar Holen
a69d6cb661 Merge pull request #2117 from brother/patch-1
Change error 2076 to a warning.
2021-03-07 20:47:53 -08:00
Viacheslav Vasilyev
8442695b73 suppress ntlm error messages in Windows build
when building for windows there are many error message like below

```
003a:err:winediag:SECUR32_initNTLMSP ntlm_auth was not found or is outdated. Make sure that ntlm_auth >= 3.0.25 is in your path. Usually, you can find it in the winbind package of your distribution.
```
2021-03-06 14:19:08 +02: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
Moritz Röhrich
d6bb8fc0d8 Error on backslash in comment #2132
- Report error in case of a backspace in a comment

Backspaces in comments are no good. In most cases they are the result of
commenting out a longer line, that was broken down. This usually results
in the shell treating the following lines as their own commands on their
own lines instead of as parts of the longer, broken down line.
2021-02-14 19:13:29 +01: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
Austin English
2e59eba6eb add support for /bin/busybox sh shebang 2021-02-05 19:56:44 -06:00
Vidar Holen
15ff87cf80 Merge pull request #2119 from josephcsible/refactors
Various refactorings
2021-02-02 18:14:27 -08:00
Kir Kolyshkin
99e9d5c54b Whitelist podman for SC2016 about '$var'
Same as 08d2eef411 but for podman.

Fixes https://github.com/koalaman/shellcheck/issues/2057

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
2021-01-27 16:21:44 -08:00
Vidar Holen
dff8f9492a Improve SC2283 message and position 2021-01-05 10:07:39 -08:00
freddii
c5756760cb fixed typing mistakes in changelog 2021-01-05 14:08:03 +01: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
Martin Bagge / brother
19355226e1 Change error 2076 to a warning.
Implementing the suggestion by @pixarbuff #1985.
2020-12-27 00:27:36 +01:00
Pepe Iborra
4e7e3f9456 Add Haddock markup to SystemInterface 2020-12-22 09:15:57 +00: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
Vidar Holen
f7547c9a5a Stable version v0.7.1
This release is dedicated to the board game Pandemic, for teaching us
relevant survival skills like how to stay inside and play board games.
2020-04-04 18:38:39 -07:00
Vidar Holen
bd717c9d1b Don't warn about [ 0 -ne $FOO ] || [ 0 -ne $BAR ] (fixes #1891) 2020-04-01 22:09:00 -07:00
Vidar Holen
da0931740f Merge pull request #1876 from fork-graveyard/master
recognize `: ${parameter=word}` as assignment
2020-04-01 18:52:53 -07:00
Vidar Holen
555f8a80dd Merge pull request #1896 from ArturKlauser/travis-deploy-stage-fix
Run "deploy" step only for "Build" stages
2020-04-01 18:45:37 -07:00
Vidar Holen
a9c04e8a37 Merge pull request #1897 from ArturKlauser/use-shellcheck-on-yourself
Use shellcheck on yourself
2020-04-01 18:43:26 -07:00
Artur Klauser
9378227570 Use shellcheck on yourself
Fixing shellcheck warnings on shell scripts in this repo.
2020-04-01 09:03:38 +02:00
Artur Klauser
a128796c0c Run "deploy" step only for "Build" stages 2020-04-01 07:52:16 +02:00
Vidar Holen
a0005bfa5a Merge pull request #1885 from ArturKlauser/travis-pr-fix
Don't try to deploy docker images on PR runs
2020-03-31 19:12:14 -07:00
Vidar Holen
37a72d05ec Merge pull request #1880 from josephcsible/patch-1
Mark that base >= 4.8.0.0 is required
2020-03-31 19:11:15 -07:00
Vidar Holen
c60323fb25 Merge pull request #1873 from josephcsible/checkwhilereadpitfalls
Simplify checkWhileReadPitfalls
2020-03-31 19:10:53 -07:00
Vidar Holen
db11e2f663 Merge pull request #1872 from josephcsible/checkforinquoted
Simplify checkForInQuoted
2020-03-31 19:10:25 -07:00
Vidar Holen
aac1d05a7e Merge pull request #1893 from josephcsible/pattern-synonyms
Fix #1892: Use pattern synonyms to clean up AST
2020-03-31 18:13:22 -07:00
Vidar Holen
67f0dc4fd5 Update distro tests to support newer Cabal 2020-03-30 22:28:56 -07:00
Joseph C. Sible
8cf037fe5e Fix #1892: Use pattern synonyms to clean up AST 2020-03-28 18:38:07 -04:00
Artur Klauser
615063a9c3 Don't try to deploy docker images on PR runs
For security reasons, PR runs don't have access to Travis secrets. However,
Docker deployment depends on the secret DOCKER_PASSWORD. Thus we shouldn't try
Docker deployment when running PRs since it will fail for lack of access.
2020-03-28 09:27:17 +01:00
Vidar Holen
37e78141bd Stop deploying artifacts to GCS 2020-03-26 17:02:08 -07:00
Joseph C. Sible
9f833770b0 Mark that base >= 4.8.0.0 is required
We've actually already required base >= 4.8.0.0 since commit a8376a0 (in which we first used `<$>` without an import, which wasn't in the Prelude prior to this version). Since then, we've also made use of other more substantial features that de-facto require base >= 4.8.0.0 since they require GHC 7.10, such as `DeriveAnyClass`.
2020-03-19 21:31:10 -04:00
Vidar Holen
7963eeab9d Include shebang in AST traversal (fixes #1858) 2020-03-16 21:36:41 -07:00
girst
7a5e261d03 recognize : ${parameter=word} as assignment 2020-03-16 23:04:54 +01:00
Joseph C. Sible
9d5363377e Simplify checkWhileReadPitfalls
* Clean up usage of not
* Use a case match instead of sequence_ and a do block
2020-03-16 00:14:22 -04:00
Joseph C. Sible
86d470c74f Simplify checkForInQuoted
* Avoid some unnecessary fmaps
* Reuse an identical pattern-match for two guards
* Apply De Morgan's law
* Use forM_ to avoid an unnecessary where
2020-03-15 16:05:55 -04:00
Vidar Holen
acee69676b Try to make TravisCI not fail on deployment of Docker stage 2020-03-15 12:54:54 -07:00
Vidar Holen
a57f6d2886 Improve detection of for loops with single values 2020-03-15 11:30:56 -07:00
Vidar Holen
d28c8f883f Merge pull request #1865 from josephcsible/patch-1
Use headOrDefault instead of fromMaybe and listToMaybe
2020-03-14 21:21:13 -07:00
Vidar Holen
c43b19f897 Make SC2095 (ssh in while read loops) more robust and suggest fixes 2020-03-14 21:15:47 -07:00
Joseph C. Sible
45a67e7c64 Use headOrDefault instead of fromMaybe and listToMaybe 2020-03-10 13:27:52 -04:00
Vidar Holen
68a03e05e5 Refer to GitHub rather than GCS for release builds 2020-03-08 17:41:46 -07:00
Vidar Holen
014a66f3f6 Fix TravisCI condition 2020-03-07 18:06:14 -08:00
Vidar Holen
fee13732a4 Merge pull request #1862 from austin987/sc2148-shell-directive
src/ShellCheck/Analytics.hs: suggest using a shell directive for SC2148
2020-03-07 17:43:44 -08:00
Austin English
741d499b3d src/ShellCheck/Analytics.hs: suggest using a shell directive for SC2148 2020-03-07 19:23:35 -06:00
Vidar Holen
9b66bc2f13 Upload to assets to GitHub 2020-03-07 17:13:01 -08:00
Luke Davis
00574dd1fc Add conda install instructions 2020-03-03 13:23:24 -07:00
Vidar Holen
7b998239af SC2257: Warn when changing arithmetic variables in redirections 2020-02-17 18:16:57 -08:00
Vidar Holen
4c9210af79 Inspect 'alias' commands for referenced variables (Fixes #1832) 2020-02-17 14:20:21 -08:00
Vidar Holen
a75219e525 Remove unused instance Ord Replacement (fixes #1829) 2020-02-17 12:44:38 -08:00
Vidar Holen
99d6df8a08 Bump SC1102/SC1105 about ambiguous $(( to Error (fixes #1836) 2020-02-17 12:27:24 -08:00
Vidar Holen
106f321cf0 Parse keywords with case sensitivity (fixes #1809) 2020-02-17 11:13:29 -08:00
Vidar Holen
1da0becb0f Rename 'Test' stage 2020-02-15 19:24:16 -08:00
Vidar Holen
472579052b Don't try to deploy on PRs 2020-02-15 16:56:20 -08:00
Vidar Holen
c735bbf30a Merge pull request #1831 from josephcsible/checkfindnameglob
Improve checkFindNameGlob
2020-02-15 16:49:22 -08:00
Joseph C. Sible
eecd003e2d Optimize patterns in checkFindNameGlob
1. Instead of pattern-matching the same list multiple times, do it only
   once and then pass the pieces separately.
2. Don't reconstruct an object equivalent to one we just deconstructed.
2020-02-11 01:04:49 -05:00
Joseph C. Sible
440d0038aa Remove a partial pattern match equivalent to fromJust from checkFindNameGlob 2020-02-11 01:03:10 -05:00
Vidar Holen
12bc7a750c Merge pull request #1785 from ArturKlauser/multi-arch-docker
Add multi-architecture Docker image build
2020-02-10 20:28:33 -08:00
Vidar Holen
c2d67c15f8 Merge pull request #1802 from szydell/master
SC2016, repair false error for M language parser
2020-02-10 18:25:28 -08:00
Vidar Holen
6043deb8f2 Merge pull request #1824 from josephcsible/patch-1
Simplify literalEquals
2020-02-10 18:20:05 -08:00
Vidar Holen
83d329c8da Merge pull request #1825 from josephcsible/nofilterm
Use findM instead of filterM
2020-02-10 18:10:15 -08:00
Vidar Holen
d0beac6d0b Merge pull request #1826 from josephcsible/nofromjust
Use the Identity monad to avoid unnecessary uses of fromJust
2020-02-10 18:05:36 -08:00
Vidar Holen
b88b253cad Merge pull request #1827 from josephcsible/nofromjust2
Remove more unnecessary uses of fromJust
2020-02-10 18:01:38 -08:00
Vidar Holen
a8f9f25ec9 Merge pull request #1828 from josephcsible/cleanups
Another round of cleanups/refactoring
2020-02-10 18:01:12 -08:00
Joseph C. Sible
85c49a8af9 Simplify mockedSystemInterface 2020-02-09 23:50:48 -05:00
Joseph C. Sible
42abcb7ae2 Simplify shellFromFilename 2020-02-09 23:18:09 -05:00
Joseph C. Sible
d5c5128115 Use isJust instead of reimplementing it 2020-02-09 23:18:09 -05:00
Joseph C. Sible
6d06103cab Remove unnecessary uses of head 2020-02-09 23:18:09 -05:00
Joseph C. Sible
c95914f9b3 Simplify determineShell 2020-02-09 23:18:09 -05:00
Joseph C. Sible
ea24e25efd Use Map.member instead of isJust and Map.lookup 2020-02-09 23:18:09 -05:00
Joseph C. Sible
8f0448133c Use isNothing instead of reimplementing it 2020-02-09 23:18:08 -05:00
Joseph C. Sible
7fc9496320 Use forM_ instead of reimplementing it 2020-02-09 23:18:08 -05:00
Joseph C. Sible
962fad038c Avoid a zip that breaks fusion 2020-02-09 23:18:08 -05:00
Joseph C. Sible
a223a7a5a5 Remove unnecessary fromMaybes 2020-02-09 23:18:08 -05:00
Joseph C. Sible
8e9290badb Do toLower earlier 2020-02-09 23:17:53 -05:00
Joseph C. Sible
292b0840d9 Simplify a double negative 2020-02-09 23:17:53 -05:00
Joseph C. Sible
43c24cf79c Use Map.! instead of reimplementing it 2020-02-09 23:17:53 -05:00
Joseph C. Sible
21ad4196db Simplify findFunction 2020-02-09 23:17:53 -05:00
Joseph C. Sible
172aa7c4fc Avoid unnecessary use of when and unless 2020-02-09 23:17:53 -05:00
Joseph C. Sible
c290eace54 Inline an uncurry 2020-02-09 23:17:53 -05:00
Joseph C. Sible
a6efd02807 Simplify <> for SpaceStatus 2020-02-09 23:17:53 -05:00
Joseph C. Sible
057cc714b3 Simplify matchToken 2020-02-09 23:17:52 -05:00
Joseph C. Sible
0e00249eae Use void instead of do and return () 2020-02-09 23:17:52 -05:00
Joseph C. Sible
0ca50159ec Use head instead of reimplementing it
Normally I wouldn't use head, but this code is partial anyway.
2020-02-09 23:17:52 -05:00
Joseph C. Sible
7e6a556ef1 Get rid of potentially
This already exists as sequence_.
2020-02-09 23:17:52 -05:00
Joseph C. Sible
4bfe6496d9 Simplify check and checkTranslatedStringVariable
Avoid the "potentially" and "Maybe" business, and just use regular guards.
2020-02-09 23:17:52 -05:00
Joseph C. Sible
ffbbfcfe25 Use mapM_ and sequence_ instead of reimplementing them 2020-02-09 23:17:52 -05:00
Joseph C. Sible
cc424bac11 Use find instead of take 1 and filter 2020-02-09 23:17:52 -05:00
Joseph C. Sible
cb01cbf7eb Use mapM instead of implementing a slower version of it 2020-02-09 23:17:52 -05:00
Joseph C. Sible
1e32139f66 Replace mapMaybe and concatMap with list comprehensions 2020-02-09 23:17:52 -05:00
Joseph C. Sible
4d92a2e15c Add getLiteralStringDef and simplify with it 2020-02-09 21:36:38 -05:00
Joseph C. Sible
f8648e5465 Switch getLiteralStringExt to Identity where it can never be Nothing 2020-02-09 21:26:42 -05:00
Joseph C. Sible
4fd8de058b Remove more unnecessary uses of fromJust 2020-02-08 23:48:36 -05:00
Joseph C. Sible
aaffe38198 Use the Identity monad to avoid unnecessary uses of fromJust 2020-02-08 23:20:54 -05:00
Joseph C. Sible
bd116f252b Use findM instead of filterM
Using filterM makes us run the monadic predicate on every list element.
Use findM instead so that we can stop once we find a matching one.
2020-02-08 22:55:45 -05:00
Joseph C. Sible
ef51ed3950 Simplify literalEquals 2020-02-08 14:09:17 -05:00
Vidar Holen
61b073d507 Merge pull request #1817 from furkanpham/master
README.md: Fix pre-compiled binary URL for aarch64
2020-02-08 10:48:37 -08:00
Vidar Holen
9d604ae732 Merge pull request #1822 from yetamrra/arrayindex
SC2191: Tighten index checks
2020-02-08 10:48:07 -08:00
Vidar Holen
1ca0b72329 Merge pull request #1816 from josephcsible/cleanups
Various cleanups and refactorings
2020-02-08 10:38:27 -08:00
Benjamin Gordon
474b23d6e7 SC2191: Tighten index checks
When adding a value containing an equals sign to an indexed array, the
left side is treated as an index if it looks like [N]=val and N is
numeric.  SC2191 currently warns about anything that looks like key=val
even though non-numeric values of key will never be treated as an index.
This causes spurious warnings for the common pattern of building up
program arguments in an array, such as:
  args=(
    --dry-run
    --in="${my_var}"
    --out=/some/path
    -f
  )
  /bin/program "${args[@]}"

Since only numeric expressions can be a valid index for an indexed
array, only emit SC2191 if the left side of a literal string containing
an equals looks numeric.  Other more complicated constructs should still
warn because shellcheck doesn't know if they may evaluate to a numeric
result.  Associative arrays still warn because a non-numeric left side
is a valid subscript.
2020-02-05 16:50:32 -07:00
Furkan Pham
fe2b4b5079 Fix pre-compiled binary URL for aarch64 2020-02-03 13:00:10 +01:00
Joseph C. Sible
e820a5642b Adjust a pattern to get rid of a fromJust 2020-02-02 00:40:22 -05:00
Joseph C. Sible
392b57b8e8 Use maybe instead of isJust and fromJust 2020-02-02 00:27:05 -05:00
Joseph C. Sible
6595e14d25 Adjust a pattern to avoid tail 2020-02-02 00:24:24 -05:00
Joseph C. Sible
115ef29079 Use pattern matching instead of head 2020-02-02 00:16:59 -05:00
Joseph C. Sible
76b798394f Use case matching instead of null
Using null followed by a head, tail, or a partial pattern match is
an anti-pattern. Use case matching instead.
2020-02-01 23:07:16 -05:00
Joseph C. Sible
8a005526cc Use drop instead of splitAt since we only use the second half 2020-02-01 23:04:04 -05:00
Joseph C. Sible
c29b6afa56 Use null instead of comparing with empty lists 2020-02-01 23:04:04 -05:00
Joseph C. Sible
e6e89d68fd Use list comprehensions instead of clunky combinations of map and filter 2020-02-01 23:04:04 -05:00
Joseph C. Sible
f25b8bd03a Use gets instead of fmapping the result of get 2020-02-01 22:50:20 -05:00
Joseph C. Sible
d7278b95f2 Remove unnecessary "map snd" 2020-02-01 22:50:19 -05:00
Joseph C. Sible
5487b3f229 Use sortOn instead of sortBy and comparing 2020-02-01 22:50:18 -05:00
Joseph C. Sible
28978a8b65 Use maybe instead of fromMaybe and fmap 2020-02-01 22:50:17 -05:00
Joseph C. Sible
f5c6771016 Use find instead of listToMaybe and filter 2020-02-01 22:50:16 -05:00
Joseph C. Sible
0f48bb78a5 Remove incorrect otherwise
You're supposed to use otherwise where you need a Boolean, not a pattern
match. This is misleadingly shadowing the real otherwise. Use _ instead.
2020-02-01 22:50:14 -05:00
Joseph C. Sible
93be86f988 Use "drop 1" instead of clumsily rewriting it 2020-02-01 22:50:14 -05:00
Joseph C. Sible
3449e6be21 Get rid of our getOpt, as it already exists as lookup 2020-02-01 22:50:13 -05:00
Joseph C. Sible
2e52c2b56a Use notElem instead of not on the result of elem 2020-02-01 22:50:11 -05:00
Vidar Holen
1696296c0a Make SC2141 trigger more broadly 2020-02-01 16:51:40 -08:00
Peter Gromov
a82e606e8d Don't trigger SC2154 (unassigned var) in -n/-z expressions #1583 2020-01-31 14:49:25 +01:00
Marcin Szydelski
93486ed6ac SC2016: disable for mumps -run %XCMD and LOOP%XCMD 2020-01-21 12:43:27 +01:00
Artur Klauser
499e0ceaba Add multi-architecture Docker image build
* Adds a shell script with functions to install multi-architecture docker
  support, as well as build, deploy, and test the shellcheck docker images for
  the same set of architectures for which binaries were already built and
  deployed as tarballs.
* Hooks up the multi-architecture docker build, deploy, and test to the existing
  Travis CI/CD pipeline. It is organized as a separate stage which only runs if
  all previous steps in the already existing test stage succeed.
2020-01-07 21:05:17 +01:00
Vidar Holen
ff5f29f661 Merge pull request #1784 from ArturKlauser/travis-warnings
Fix Travis warnings
2020-01-05 12:53:42 -08:00
Vidar Holen
c7bf1fd96e Merge pull request #1783 from ArturKlauser/fix-osx-travis-build
Fix OSX build on Travis
2020-01-05 12:53:28 -08:00
Artur Klauser
b96b7f35f4 Fix Travis warnings
Fixing the following Travis build config validation warnings:
  * root: deprecated key sudo (The key `sudo` has no effect anymore.)
  * root: missing os, using the default linux
  * deploy: key local-dir is not underscored, using local_dir
  * language: value sh is an alias for shell, using shell
  * root: key matrix is an alias for jobs, using jobs
2019-12-28 14:24:23 +01:00
Artur Klauser
926ee54036 Fix OSX build on Travis
Symlink was failing due to pre-existing target file:
  sudo ln -s /usr/local/bin/gsha512sum /usr/local/bin/sha512sum
  ln: /usr/local/bin/sha512sum: File exists
2019-12-28 09:51:11 +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
Vidar Holen
9008a6833b Merge pull request #1711 from renatoassis01/master
add github actions link
2019-12-21 18:53:16 -08:00
Vidar Holen
ce60a1764f Merge branch 'jabberabbe-iss1724-builtin-support' 2019-12-21 18:50:52 -08:00
Vidar Holen
cbcca528ae Merge branch 'iss1724-builtin-support' of https://github.com/jabberabbe/shellcheck into jabberabbe-iss1724-builtin-support 2019-12-21 18:13:07 -08:00
Vidar Holen
83187dafd7 Added a unit test for parsing shell keyword case branches 2019-12-21 17:59:09 -08:00
Vidar Holen
d919aaa847 Merge branch 'Gandalf--issue_1731_case_pattern_literals' 2019-12-21 17:56:26 -08:00
Gandalf-
3f296a08c1 Issue 1731 Literals in case patterns
https://github.com/koalaman/shellcheck/issues/1731

Any literal except esac is valid pattern in a case statement
2019-12-18 20:23:48 -08:00
Vidar Holen
0f15fa49ba Make SC2230 optional 2019-12-07 16:11:49 -08:00
Vidar Holen
0a4580e234 Mention that ShellCheck is now compatible with Cabal 3 2019-12-07 16:11:49 -08:00
Vidar Holen
5c7d8129ad Try to search for binary on macOS/Cabal3 2019-11-18 17:12:25 -08:00
Vidar Holen
e075cde357 Revert docker image to 18.04 since ld fails on later versions 2019-11-16 11:46:58 -08:00
Vidar Holen
9f578f41a1 Explicitly add 'mappend' for old GHC versions 2019-11-16 11:16:15 -08:00
Vidar Holen
2c026f1ec7 Support Cabal 3. Man page no longer autobuilds. 2019-11-16 11:06:18 -08:00
Vidar Holen
874bdcb514 Merge pull request #1728 from mgttlinger/patch-1
Nix install instructions
2019-11-15 21:03:01 -08:00
Vidar Holen
fa3eb47193 Merge pull request #1716 from ryantig/ryantig-patch-1
Update README.md
2019-11-15 21:01:24 -08:00
Vidar Holen
989ac32625 Merge pull request #1734 from gabrielelana/braced-regular-for
Parse regular `for` with body in curly braces
2019-11-15 20:59:51 -08:00
Vidar Holen
2bbfd0570d Merge pull request #1735 from gabrielelana/quoted-heredoc
Support for heredoc quoted token like `'"FOO"`
2019-11-15 20:27:10 -08:00
Vidar Holen
9b1befadc1 Update brew before building on macOS due to incompatible Ruby 2019-11-15 09:26:01 -08:00
Vidar Holen
f44624a9c0 Hide <> from Writer to not conflict with Semigroup 2019-11-14 20:02:25 -08:00
Vidar Holen
c75bbcbd60 Include missing Semigroup import 2019-11-13 22:10:27 -08:00
Vidar Holen
daa9c08dd5 Merge pull request #1749 from lvjp/simple-docker-build
Make image build process a bit simpler
2019-11-13 22:04:14 -08:00
Vidar Holen
4da34fbc64 Merge branch 'translatedVars' 2019-11-13 21:48:53 -08:00
Vidar Holen
4a63a3a8bd For SC2256, make sure the complete string is a variable name 2019-11-13 21:48:01 -08:00
Benjamin Gordon
2341a4c683 SC2256: Check for translated strings matching known variables
SC2247 already warns about translated strings that look like $"(foo)" or
$"{foo}".  Since typical use of translated strings is to translate whole
messages, a string like $"foo" is likely to be a similar mistake if foo
is the name of an existing variable.  Conversely, a string like
$"foo bar" is potentially meant to be a message id even if foo is a
known variable.

Add a warning for the $"foo" case, but make it separate from the
existing warning so that projects that reuse variable names as their
message ids can separately disable the new warning.
2019-11-13 16:41:16 -07:00
Laurent VERDOÏA
7eb6b35cb0 Make image build process a bit simpler
Take full leverage of multi-stage docker build.
2019-11-09 10:26:59 +01:00
Vidar Holen
93eca1cb8e Only trigger SC1014 when command is a complete word (fixes #1737) 2019-11-03 13:26:23 -08:00
Vidar Holen
e701cf6fad Warn about [ x -ot y ] in POSIX mode 2019-11-03 13:25:35 -08:00
Vidar Holen
5962b01816 Correctly handle empty variables for SC2086 (fixes #1722) 2019-11-03 12:46:25 -08:00
Tito Sacchi
5becc673b2 Modify CHANGELOG.md 2019-11-01 14:36:15 +01:00
Tito Sacchi
84ca7711c4 Make command-specific checks act on builtin ...
Now if shellchecks encounters a command like `builtin cmd ...`
it applies the same check that would be applied to `cmd ...`.
2019-11-01 14:28:00 +01:00
Tito Sacchi
0e0de94045 Fix issue #1724
(bash: missing support for 'builtin' keyword)
Now shellcheck looks for the arguments to 'builtin' to determine
read/written variables. A change in the parser makes sure that
assignments are parsed correctly in commands that start with 'builtin'.
2019-11-01 13:49:17 +01:00
gabriele.lana
699aac589a Support for heredoc quoted token like '"FOO"
Fixes #1650
2019-10-26 17:36:32 +02:00
gabriele.lana
30c75340e6 Parse regular for with body in curly braces
Fixes #1694
2019-10-26 15:41:46 +02:00
Vidar Holen
4dfd7eb1cf Use single quotes for the format string example in SC2059 2019-10-24 10:33:17 -07:00
Merlin Göttlinger
79ba67dbd3 Nix install instructions 2019-10-21 08:04:59 +02:00
Vidar Holen
60f75e5b8a Warn about unexpected characters after ]/]] (fixes #1680) 2019-10-13 20:26:40 -07:00
Vidar Holen
f042b0ebd1 Merge branch 'iboss-ptk-read-t-0' 2019-10-12 20:55:32 -07:00
Vidar Holen
764fdcb260 Move failing test to correct check 2019-10-12 20:50:55 -07:00
Vidar Holen
7473d4a743 Make read -t 0 test more forgiving towards other flags 2019-10-12 20:45:36 -07:00
Vidar Holen
91abd979f2 Merge branch 'read-t-0' of https://github.com/iboss-ptk/shellcheck into iboss-ptk-read-t-0 2019-10-12 20:23:13 -07:00
Vidar Holen
afea62de4e Suggest using $((..)) in [ 2*3 -eq 6 ] (fixes #1641) 2019-10-12 19:55:20 -07:00
ryantig
fa0f88c106 Update README.md
Repair link to #installing-a-pre-compiled-binary (was pointing to #installing-the-shellcheck-binary)
2019-10-04 11:11:21 -07:00
Supanat Pothivarakorn
7fb399528c Allow read -t 0 to not require -r flag
since it has specific purpose for checking only
2019-10-02 22:34:43 +07:00
Vidar Holen
de9ab4e6ef Fix glob range duplicate warning in [!!] (fixes #1706) 2019-09-28 14:03:11 -07:00
Renato Assis
ff1eab286c add github 2019-09-25 19:20:25 -03:00
Vidar Holen
e01c470598 Suggest quoting case patterns, as for SC2053 (fixes #1682) 2019-09-08 20:08:43 -07:00
Glen Mailer
9423691039 Mention the CircleCI shellcheck orb in the README. 2019-08-12 22:24:47 +01:00
Vidar Holen
71a4053e8c Remove _cleanup now that builds don't run in sequence 2019-07-31 21:32:13 -07:00
Vidar Holen
3fdc6babb2 Update TravisCI config for new winghc docker image 2019-07-31 21:16:36 -07:00
Vidar Holen
c175971bf0 Make -f diff stop saying it found more issues when it didn't. 2019-07-28 20:50:50 -07:00
Vidar Holen
b7b4d5d29e Stable version 0.7.0
This release is dedicated to RetroArch: the second best way to make your
PC feel like a 16bit system (right after building ShellCheck with GHC)
2019-07-28 18:12:25 -07:00
Vidar Holen
9cc9a575b2 Tweak man page 2019-07-28 18:12:25 -07:00
Vidar Holen
b2dd00e4ee Mention aarch64 and macOS binaries in CHANGELOG 2019-07-28 17:26:31 -07:00
Vidar Holen
2053ac8882 Add a release checklist script 2019-07-28 17:25:30 -07:00
Vidar Holen
e4cbf59fda Update distrotest with new image names 2019-07-28 17:10:20 -07:00
Vidar Holen
f9c8a255be Set up Travis build matrix 2019-07-24 22:23:47 -07:00
Vidar Holen
bfb2d79e54 Merge branch 'master' of github.com:koalaman/shellcheck 2019-07-24 22:00:57 -07:00
Vidar Holen
fbb571811f Merge branch 'Luizm-master' 2019-07-24 21:41:24 -07:00
Vidar Holen
0eaef95db8 THIS COMMIT WILL BE FORCE PUSHED AWAY (Help I'm not good with computers) 2019-07-24 21:20:27 -07:00
Vidar Holen
f4deac6e43 Merge branch 'master' of https://github.com/Luizm/shellcheck into Luizm-master 2019-07-24 21:02:00 -07:00
Vidar Holen
49aa600c85 Merge pull request #1639 from shak-mar/master
Fix syntax and indentation in shellcheck.1.md
2019-07-24 20:55:03 -07:00
Vidar Holen
25b5b77240 Add automated linux-aarch64 build 2019-07-24 20:03:23 -07:00
LuizMuller
ded04820b8 Merge branch 'master' into master 2019-07-24 15:28:03 -03:00
Luizm
7a1fb2523d Add support to compiling a binary for macOS 2019-07-24 14:33:03 -03:00
Vidar Holen
38bb156a1c Warn about $_ in POSIX sh (fixes #1647) 2019-07-21 21:22:16 -07:00
Vidar Holen
023ae5dfda Don't warn about printf '%()T' without corresponding argument 2019-07-20 15:10:41 -07:00
shak-mar
e280116ef0 Fix syntax and indentation in shellcheck.1.md
Out of interest, I ran the command

    pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1

locally, but that produces warnings (previous to this commit).  Checking
the generated manpage, I found the diff to be rendered very badly.
(Broken at terminal width like a normal paragraph).  This commit fixes
the problem.
2019-07-12 16:52:39 +02:00
Vidar Holen
788cf17076 Fix bad advice for SC2251 (fixes #1588) 2019-07-04 19:10:14 -07:00
Vidar Holen
78b8e76066 Also mention globbing in SC2206 (fixes #1626) 2019-07-04 17:43:18 -07:00
Vidar Holen
914974bd4f Don't consider .* a glob-like regex (fixes #1214) 2019-07-04 17:41:23 -07:00
Vidar Holen
c0d3a98fcd Add warning for chmod -r (fixes #1321) 2019-07-04 16:54:42 -07:00
Vidar Holen
380221a02c Recognize read -ra foo as arrays (fixes #1636) 2019-07-03 20:40:41 -07:00
Vidar Holen
ba2c20a08a Improve message for SC1067 2019-07-03 20:02:14 -07:00
Vidar Holen
4d56852b9f Allow SCRIPTDIR in source directives (fixes #1617) 2019-07-03 19:49:47 -07:00
Vidar Holen
be1f1c1ab7 Don't count 'readonly x' as a reference to x (fixes #1573) 2019-07-02 20:58:08 -07:00
Vidar Holen
bee4303c32 Add an empty Custom.hs to simplify site-specific patching 2019-07-02 20:07:05 -07:00
Vidar Holen
ef764b60ca Fix botched variable usage 2019-07-01 23:47:13 -07:00
Vidar Holen
9e6b07dbba Merge branch 'yetamrra-shflags' 2019-07-01 23:23:16 -07:00
Vidar Holen
3e3e4fd0cd Avoid defining flags for non-literal parameters 2019-07-01 23:22:09 -07:00
Vidar Holen
561075ea79 Merge branch 'shflags' of https://github.com/yetamrra/shellcheck into yetamrra-shflags 2019-07-01 20:00:28 -07:00
Vidar Holen
42f0dce467 Merge pull request #1627 from Avi-D-coder/master
Bump stack snapshot
2019-07-01 19:58:25 -07:00
Vidar Holen
9702f1ff9c Handle diffs for files without trailing linefeed 2019-06-30 20:19:10 -07:00
Vidar Holen
544047c5af Warn about ending double quotes just to make $ literal 2019-06-30 18:43:42 -07:00
Vidar Holen
321afa427e Remove unused parse-time AST warnings 2019-06-30 17:38:17 -07:00
Vidar Holen
c381c5746f Remove unnecessary lookahead in readDollarLonely 2019-06-30 17:28:15 -07:00
Vidar Holen
eeb7ea01c9 Allow SC2103 to be silenced (fixes #1591) 2019-06-30 16:36:45 -07:00
Vidar Holen
3116ed3ae5 Filter warnings by annotations in unit tests 2019-06-30 16:36:03 -07:00
Avi ד
e95d8dd14e Bump stack snapshot 2019-06-29 03:40:05 -04:00
Benjamin Gordon
f6ba500d6b Add support for basic shflags semantics
The shflags command-line flags library creates variables at runtime
with a few well-defined functions.  This causes shellcheck to spit out
lots of warnings about unassigned variables, as well as miss warnings
about unused flag variables.

We can address this with two parts:

1. Pretend that the shflags global variables are predefined like other
   shell variables so that shellcheck doesn't expect users to set them.
2. Treat DEFINE_string, DEFINE_int, etc. as new commands that create
   variables, similar to the existing read, local, mapfile, etc.

Part 1 can theoretically be addresssed without this by following sourced
files, but that doesn't help if people are otherwise not following
external sources.

The new behavior is on by default, similar to automatic bats test
behavior.

Addresses #1597
2019-06-25 12:14:54 -06:00
Vidar Holen
c5aa171a5f Use mappend over <> for compatibility 2019-06-24 09:02:35 -07:00
Vidar Holen
b1aeee564c Add a Diff output format 2019-06-23 20:02:01 -07:00
Vidar Holen
b8b4a11348 Update JSON1 docs in man page 2019-06-23 19:19:00 -07:00
Vidar Holen
e099625e7d Remove unused ioref 2019-06-23 15:50:48 -07:00
Vidar Holen
5242e384a1 Fix error spans for shebang warnings (fixes #1620) 2019-06-23 13:49:08 -07:00
Vidar Holen
7e77bfae49 Improve message for SC2055 2019-06-23 13:48:43 -07:00
Vidar Holen
9059024de6 Merge pull request #1592 from hugopeixoto/fix/SC2016-false-positive
SC2016: Don't trigger when using empty backticks
2019-06-19 19:55:34 -07:00
Vidar Holen
0eebb50563 Merge pull request #1608 from oleg-andreyev/issue-1607
#1607 fixing brew command
2019-06-19 19:03:02 -07:00
Vidar Holen
3b5aa84757 Merge pull request #1616 from blueyed/dockerignore
Add .dockerignore
2019-06-19 18:55:49 -07:00
Daniel Hahler
200aabb63c Add .dockerignore
This explicitly defines included/copied files, to reduce the context
being sent to the Docker daemon initially.
2019-06-18 23:18:26 +02:00
Oleg Andreyev
c6dcb4127a #1607 fixing brew command 2019-06-09 17:00:51 +03:00
Vidar Holen
61d2112e71 Add missing JSON1.hs 2019-06-02 13:00:38 -07:00
Vidar Holen
9f0ef5983a Optionally check for unassigned uppercase variables 2019-06-02 10:29:04 -07:00
Vidar Holen
1297ef46d7 Add JSON1 as a separate format, wrap result in an object 2019-06-02 10:28:37 -07:00
Vidar Holen
f4be53eb19 Warn about [ -v var ] for POSIX sh 2019-06-02 10:28:20 -07:00
Vidar Holen
3e7c2bfec0 Warn about [ $a != x ] || [ $a != y ] 2019-06-02 09:26:54 -07:00
Hugo Peixoto
07ffcb626e SC2016: Don't trigger when using empty backticks
When using '``' or '```', it should not suggest using double quotes.
2019-05-27 11:03:24 +01:00
Vidar Holen
36bb1e7858 Mention that "-" is supported as a filename. (Fixes #1586) 2019-05-22 17:35:41 -07:00
Vidar Holen
95b1185882 Inform about ineffectual ! on commands (fixes #1531) 2019-05-22 17:14:28 -07:00
Vidar Holen
8efbecd64a Don't suggest removing braces from $((${x+1})) (fixes #1533) 2019-05-19 15:29:47 -07:00
Vidar Holen
52a9d90e1a Merge pull request #1580 from virgilwashere/copyright
Update Copyright to year 2019; README linting
2019-05-19 12:03:15 -07:00
Virgil
f5892f2d0d (docs)Fix typo in yaml markdown
Was aligned as per TOC 2nd.  Reverted to -
2019-05-17 07:41:12 +10:00
Vidar Holen
de7541e656 Merge branch 'yetamrra-require-braces' 2019-05-14 18:57:44 -07:00
Vidar Holen
861b63aa77 Specify 'variable' in require-braces 2019-05-14 18:48:41 -07:00
Benjamin Gordon
64c9c83cc8 SC2250: New optional check for braces around variable references
Always using braces makes it harder to accidentally change a variable by
pasting other text next to it, but the warning is off by default because
it's definitely a style preference.  Omit special and positional
variables from the check because appending additional characters to them
already doesn't change parsing.
2019-05-14 11:01:38 -06:00
Benjamin Gordon
aa3b709b5d Track whether braces were present in T_DollarBraced
References of the form $var and ${var} both map to the same structure in
the AST, which prevents any later analysis functions from distinguishing
them.  In preparation for adding checks that need this info, add a Bool
to T_DollarBraced that tracks whether the braces were seen at parsing
time and update all references so that this change is a no-op.
2019-05-14 11:01:38 -06:00
Benjamin Gordon
0358090b3c Refactor definition of special variables.
This ensures that the parser and other places that refer to special
variables can use the same list.
2019-05-14 08:57:56 -06:00
Virgil
ea05271fa3 📝 Update Copyright to year 2019 and Markdown linting
- [x] 📝 Update Copyright to year 2019

- [x] 📝 MD009/no-trailing-spaces: Trailing spaces [Expected: 0 or 2; Actual: 1]
- [x] 📝 MD034/no-bare-urls: Bare URL used

- [ ] 📝 ~MD004/ul-style: Unordered list style [Expected: dash; Actual: asterisk]~
- [ ] ~add missing TOC entries~
2019-05-14 20:12:34 +10:00
Vidar Holen
50116e8aee Don't suggest [[..]] for sh in SC2081 (fixes #1562) 2019-05-13 20:45:53 -07:00
Vidar Holen
5ccaddbcc2 Promote json1 as the primary JSON format 2019-05-13 19:31:55 -07:00
Vidar Holen
80b7e1e099 Merge pull request #1578 from yetamrra/tabstops
Allow variable tabstop widths
2019-05-13 19:25:01 -07:00
Benjamin Gordon
50af8aba29 Add json1 format that ignores tabs
The new json1 format works just like json except that it treats tabs as
single characters instead of 8-character tabstops.

The main use case is to allow editors to pass -fjson1 so that they can
consume the json output in a character-oriented way without breaking
backwards compatibility.

Also addresses #1048.
2019-05-13 10:55:16 -06:00
Vidar Holen
5fb1da6814 Replace verbose checks with optional checks 2019-05-12 19:14:04 -07:00
Vidar Holen
58205a3573 Emit resolved rather than apparent filename for 'source' (fixes #1579) 2019-05-12 15:55:37 -07:00
Vidar Holen
5b177d62cb Simplify docker instructions 2019-05-09 20:22:10 -07:00
Vidar Holen
0ab3a726d3 Merge branch 'efx-adjust-docker-usage' 2019-05-09 20:21:41 -07:00
Vidar Holen
2791a48444 Merge branch 'adjust-docker-usage' of https://github.com/efx/shellcheck into efx-adjust-docker-usage 2019-05-09 20:21:21 -07:00
Vidar Holen
bb63d66f7c Delete trailing spaces 2019-05-09 20:17:35 -07:00
Vidar Holen
d9e419d60f Add support for source-path directives (fixes #1577) 2019-05-09 19:54:41 -07:00
Vidar Holen
aa4b24e458 Merge pull request #1570 from virgilwashere/readme-pandoc
docs: README: Update pandoc command to match `Setup.hs`
2019-05-09 17:38:21 -07:00
Virgil
1c7a9f8a2f Merge branch 'master' into readme-pandoc 2019-05-09 20:32:07 +10:00
Vidar Holen
2521c1cf56 Tweak README 2019-05-08 18:04:51 -07:00
Vidar Holen
65e7f2059d Merge pull request #1571 from virgilwashere/master
docs: add Chocolatey installation method
2019-05-08 17:53:48 -07:00
Vidar Holen
248858c13e Merge pull request #1549 from Lin-Buo-Ren/patch/snap/improve-packaging
Improve snap packaging
2019-05-08 17:35:05 -07:00
Eli Flanagan
c0d4c5a106 ensure docker invocation is ephemeral
Also adjust the tag to use the `:stable` tag mentioned in the prior
line.
2019-05-07 07:48:43 -04:00
Virgil
ec25fb4052 📝 add Chocolatey installation method
- [x]  add Chocolatey for Windows installation
- [x] 🚨 add language types to code blocks
2019-05-05 22:59:35 +10:00
Vidar Holen
a3cd5979a2 Update message for SC2171 2019-05-04 12:54:59 -07:00
Vidar Holen
37b24cc129 Don't warn about "a"b"c" in =~ regex (fixes #1565) 2019-05-04 12:18:45 -07:00
Virgil
d72a5faa1f 📝 docs: Update pandoc to match Setup.hs
The sdist hook in [Setup.hs](Setup.hs) disables the `smart` extension
when creating man page.
2019-05-04 16:27:44 +10:00
Vidar Holen
e2e65e1350 Warn about arithmetic base conversation in sh (fixes #1547) 2019-04-29 18:02:44 -07:00
Vidar Holen
6ccf9d6af1 Mention in manual that 'sh' means POSIX and not system 2019-04-27 17:25:20 -07:00
Vidar Holen
9470b9dc31 Don't mention arrays in SC2089 in sh/dash (fixes #1014) 2019-04-27 16:22:01 -07:00
Vidar Holen
bf1003eae3 Auto-disable SC2119 when disabling SC2120 (fixes #703) 2019-04-27 15:20:07 -07:00
Vidar Holen
301705edea Merge branch 'epontan-root-option' 2019-04-24 18:52:04 -07:00
Vidar Holen
c6c12f52bd Expand root paths into source paths 2019-04-24 18:51:24 -07:00
Pontus Andersson
af46758ff1 Add option to look for sources in alternate root paths
Add a new optional flag "-r|--root ROOTPATHS", where ROOTPATHS is a
colon separated list of paths, that will look for external sources in
alternate roots.

This is particular useful when the run-time environment does not fully
match the development environment. The #shellcheck source=file directive
is useful, but has its limitations in certain scenarios. Also, in many
cases the directive could be removed from scripts when the root flag is
used.

Script example.bash:
  #!/bin/bash
  source /etc/foo/config

Example usage where etc/foo/config exists in skel/foo:
  # shellcheck -x -r skel/foo:skel/core example.bash
2019-04-22 17:54:42 +02:00
林博仁(Buo-ren Lin)
025c380b84 snap: Migrate to core18 base
This patch migrates the snap to core18 base, which should make the cabal
build work again.

Signed-off-by: 林博仁(Buo-ren Lin) <Buo.Ren.Lin@gmail.com>
2019-04-16 20:14:37 +08:00
林博仁(Buo-ren Lin)
10955a143c snap: Replace deprecated build and install keyword
These keywords has been obsoleted in Snapcraft 3.

Refer-to: The 'build' keyword has been replaced by 'override-build' <https://github.com/canonical-web-and-design/snappy-docs/blob/master/deprecation-notices/dn8.md>
Refer-to: The 'install' keyword has been replaced by 'override-build' <https://github.com/canonical-web-and-design/snappy-docs/blob/master/deprecation-notices/dn9.md>
Signed-off-by: 林博仁(Buo-ren Lin) <Buo.Ren.Lin@gmail.com>
2019-04-16 18:10:49 +08:00
林博仁(Buo-ren Lin)
67dbbcbd89 snap: Drop unneeded trailing slash in the source property
Signed-off-by: 林博仁(Buo-ren Lin) <Buo.Ren.Lin@gmail.com>
2019-04-16 17:53:01 +08:00
林博仁(Buo-ren Lin)
cef4c1a0bc snap: Flip grade property to stable
This allows the snap to be promoted to the `candidate` and the `stable` release channels

Signed-off-by: 林博仁(Buo-ren Lin) <Buo.Ren.Lin@gmail.com>
2019-04-16 17:48:34 +08:00
Vidar Holen
b824294961 Limit SC2032 to likely command args (fixes #1537) 2019-04-14 20:58:01 -07:00
Vidar Holen
5b7354918f SC2249: When verbose, warn about missing default case (fixes #997) 2019-04-14 16:58:17 -07:00
Vidar Holen
b76c0a8221 SC2248: Warn about unquoted variables without special chars 2019-04-13 20:19:13 -07:00
Vidar Holen
c860b74505 Set SC2243/SC2244 level to "verbose" 2019-04-13 13:40:18 -07:00
Vidar Holen
9652ccfdbd Add a verbose mode: -S verbose 2019-04-13 13:16:41 -07:00
Vidar Holen
f514f5f735 Warn about flipped $ and " in $"(cmd)" (fixes #1517) 2019-03-20 22:10:04 -07:00
Vidar Holen
c53c8a5ead Allow using 'source -- file' (fixes #1518) 2019-03-17 19:37:35 -07:00
Vidar Holen
b456987b84 Add the minimum version of 'directory' 2019-03-06 19:19:00 -08:00
Vidar Holen
ed92fe501f Fix internal error for --format (fixes #1507) 2019-03-06 17:44:15 -08:00
Vidar Holen
bbe5155e63 Use less modern APIs to support more GHC versions 2019-03-04 18:18:58 -08:00
Vidar Holen
4dfb3fce9c Add missing backtick in man page 2019-03-03 19:00:22 -08:00
Vidar Holen
581bcc3907 Add support for .shellcheckrc files 2019-03-03 18:57:13 -08:00
Vidar Holen
293c3b27b8 Continue on parse errors in backticks (fixes #1475) 2019-03-03 13:37:32 -08:00
Vidar Holen
25ea405468 Fix typo in man page (fixes #1486) 2019-03-02 13:50:07 -08:00
Vidar Holen
e45d81c8fa Update README.md with more CI and build info 2019-03-02 13:36:55 -08:00
Vidar Holen
05e657e130 Merge pull request #1499 from jabberabbe/iss896-printf-v-arrays
Fix issues #896 and #433: printf -v and arrays
2019-03-02 12:05:50 -08:00
Tito Sacchi
bd19ab4fa9 Fix issues #896 and #433: printf -v and arrays 2019-02-24 09:45:31 +01:00
Vidar Holen
8aa44bf529 Merge pull request #1494 from cclauss/patch-2
README.md: pipe wget | tar to reduce duplication
2019-02-23 13:15:32 -08:00
Vidar Holen
45021a9b40 Merge pull request #1488 from contivero/wait-flags
Check wait flags in dash & POSIX sh
2019-02-23 12:56:44 -08:00
cclauss
d31d31df23 wget -qO- "https://storage.googleapis.com/shellcheck/shellcheck-"${scversion}".linux.x86_64.tar.xz" | tar -xJv 2019-02-18 10:18:21 +01:00
cclauss
3a276bd336 README.md: pipe wget | tar to reduce duplication 2019-02-18 09:36:36 +01:00
Cristian Adrián Ontivero
d3f6e045e2 Check wait flags in dash & POSIX sh
Flags for the wait builtin are undefined under both POSIX sh and dash.
Bash though, accepts [-fn].
2019-02-10 12:44:22 +01:00
Vidar Holen
abe6afc09f Merge branch 'contivero-set-flags' 2019-02-08 22:37:21 -08:00
Vidar Holen
d984f8cbe7 Don't look at 'set' options after a non-literal. 2019-02-08 22:36:22 -08:00
Cristian Adrián Ontivero
acef53be9c Check set flags under dash & POSIX sh (fixes #990)
The set builtin accepts certain flags, and some longer synonyms (for
instance set -e is the same as set -o errexit) under POSIX sh. This
makes ShellCheck warn if any of the used flags and options are
undefined when targeting POSIX sh.

This fixes #990, while adding general flag-support checking for set in
the process.
2019-02-03 17:27:36 +01:00
Vidar Holen
2ea2293154 Update SC1008 to suggest using directive. 2019-01-27 15:02:15 -08:00
Vidar Holen
d40d376bf4 Merge pull request #1477 from contivero/hash-flags
Check hash flags under dash and POSIX sh
2019-01-27 13:53:57 -08:00
Cristian Adrián Ontivero
a669e1684b Check hash flags under dash and POSIX sh
Flags for the hash builtin other than [-r] are undefined under POSIX sh.
Dash also accepts [-v], while bash adds [-l] [-p filename] [-dt] aside
from [-r].
2019-01-27 08:22:37 +01:00
Vidar Holen
420d913bbf Merge branch 'issue_1330_unsupported_echo_flags' of https://github.com/Gandalf-/shellcheck 2019-01-26 20:16:34 -08:00
Vidar Holen
fb7ac3f57e Merge pull request #1474 from contivero/unset-flags
Check unset flags under dash and POSIX sh
2019-01-26 20:11:03 -08:00
Gandalf-
112a7d8b9b Issue 1330 unsupported echo flags
Issue https://github.com/koalaman/shellcheck/issues/1330

Addresses false positives when quoted arguments to echo begin with what
looks like a flag. Now, warn only when the first argument is a
recognized echo flag when flags are unsupported.
2019-01-24 19:00:19 -08:00
Cristian Adrián Ontivero
31c5601c5e Check unset flags under dash and POSIX sh
The only acceptable flags for the unset builtin under POSIX sh and dash
are [-fv]. Bash though, accepts [-n] too. This commits makes shellcheck
warn about this.
2019-01-23 06:35:08 +01:00
Vidar Holen
2737496b3a Fix grammatical error in comments 2019-01-22 19:47:40 -08:00
Vidar Holen
a404efab65 Merge branch 'issue_837_opposite_of_exclude_option' of https://github.com/Gandalf-/shellcheck 2019-01-22 19:39:55 -08:00
Vidar Holen
3c94d8b3eb Merge branch 'issue_1393_quiet_flag' of https://github.com/Gandalf-/shellcheck 2019-01-22 19:15:28 -08:00
Gandalf-
a89403f09b Issue 1393 quiet flag
Issue https://github.com/koalaman/shellcheck/issues/1393

Provide '-q' and '--quiet' flags that suppress all normal output, but keep the
return status, similar to 'grep -q'.
2019-01-21 18:25:41 -08:00
Vidar Holen
6dcf4b8e64 Mention extension in changelog and man page 2019-01-21 16:55:01 -08:00
Vidar Holen
d4d219affd Don't warn that cd ../.. and similar can fail in SC2164 2019-01-21 16:55:01 -08:00
Vidar Holen
489c3a4ddf Fix SC2164 always saying 'cd' even when using 'pushd' 2019-01-21 16:55:01 -08:00
Vidar Holen
1507e92c44 Merge pull request #1472 from contivero/type-flags
Check type flags under dash and POSIX sh (fixes #1471)
2019-01-21 16:44:39 -08:00
Cristian Adrián Ontivero
63a259e5be Check type flags under dash and POSIX sh (fixes #1471)
There are no flags for the type builtin defined under POSIX sh, nor does
dash define any. Bash, however, allows [-aftpP]. We check this now under
POSIX and dash.
2019-01-21 19:49:14 +01:00
Gandalf-
59c47f2266 Issue 837 flag to include only certain warnings
Issue https://github.com/koalaman/shellcheck/issues/837

Add an --include option, which creates a whitelist of warnings to report
on, the opposite of --exclude.
2019-01-20 16:42:27 -08:00
Vidar Holen
a621eba6d3 Merge pull request #1456 from contivero/issue-667
Silence SC2103 when using 'set -e' (fixes #667)
2019-01-20 14:54:29 -08:00
Vidar Holen
978bfdd5da Merge pull request #1462 from contivero/trap-flags
Check trap flags under dash & POSIX sh (fixes #1461)
2019-01-20 14:48:01 -08:00
Vidar Holen
a03d94c0b2 Merge pull request #1468 from Gandalf-/issue_824_grep_fixed_strings
Issue 824 grep fixed strings and SC2063
2019-01-20 14:06:40 -08:00
Vidar Holen
e1fe9be7af Fix minor details in new Bats support 2019-01-20 14:02:42 -08:00
Vidar Holen
c97cb8cf54 Merge branch 'bats-support' of https://github.com/damienrg/shellcheck 2019-01-20 13:35:12 -08:00
Vidar Holen
a504ca6b57 Add some unit tests for extension detection 2019-01-20 13:24:31 -08:00
Vidar Holen
437f73c001 Merge branch 'iss1369-shell-from-file-extension' of https://github.com/jabberabbe/shellcheck 2019-01-20 12:50:06 -08:00
Vidar Holen
f187382a0c Add bats support
This is motivated by the fact that the popularity of bats is increasing
since the creation of bats-core/bats-core.

The code is a cherry-pick of koalaman/shellcheck/bats branch.

Fix koalaman/shellcheck#417.
2019-01-20 14:59:37 +01:00
Gandalf-
661be056f1 Issue 824 grep fixed strings and SC2063
Issue https://github.com/koalaman/shellcheck/issues/824

Fix up to original change to include '--fixed-strings' in the grep +
regex special cases.
2019-01-19 08:49:26 -08:00
Tito Sacchi
9f45dc4c8b Not determine the shell from .sh extension
See discussion on issue #1369 for details.
2019-01-18 09:21:07 +01:00
Cristian Adrián Ontivero
8e31e86cc4 Check trap flags under dash & POSIX sh (fixes #1461) 2019-01-16 08:44:41 +01:00
Vidar Holen
c6c615217b Allow specifying that flags should not be checked for support.
This was motivated by the fact that `-a` was missing from Dash's
long list.
2019-01-15 19:50:23 -08:00
Vidar Holen
c1adc588fb Merge pull request #1460 from contivero/umask-flags
Check umask flags under dash & POSIX sh (fixes #1459)
2019-01-15 19:18:21 -08:00
Cristian Adrián Ontivero
3107a1bae0 Check umask flags under dash & POSIX sh (fixes #1459) 2019-01-15 08:24:23 +01:00
Vidar Holen
73859039dd Merge branch 'cd-flags' of https://github.com/contivero/shellcheck 2019-01-14 17:47:25 -08:00
Tito Sacchi
1e6a30905a Make ShellCheck not emit warnings about the shebang if the shell
type is determined from the extension
2019-01-14 14:32:25 +01:00
Tito Sacchi
a4b9cec9f0 Fix #1369 (Use file extension to detect shell)
The precedence order that is used to determine the shell
is the following:
1. ShellCheck directive
2. Shebang
3. File extension
A new field `asFallbackShell` has been
added to the record type `AnalysisSpec`.
2019-01-14 14:32:25 +01:00
Cristian Adrián Ontivero
c3a56659f4 Check cd flags under dash & POSIX sh (fixes #1457) 2019-01-14 08:18:17 +01:00
Vidar Holen
e0a4241baa Warn if a shebang's interpreter ends in / (fixes #373) 2019-01-13 17:32:25 -08:00
Vidar Holen
1835ebd3a0 SC2245: Warn that Ksh [ -f * ] only applies to first (Fixes #1452) 2019-01-13 16:41:08 -08:00
Cristian Adrián Ontivero
b34f4c1f4b Silence SC2103 when using 'set -e' (fixes #667) 2019-01-13 16:20:41 +01:00
Vidar Holen
ec6f9e4d49 Merge pull request #1449 from contivero/readonly-flags
Check readonly flags in dash/POSIX sh (fixes #1448)
2019-01-10 18:56:40 -08:00
Cristian Adrián Ontivero
3760e7945f Check readonly flags in dash/POSIX sh (fixes #1448) 2019-01-10 11:05:37 +01:00
Vidar Holen
fcdd6055df Add new replacement format to the JSON 2019-01-09 18:35:36 -08:00
Vidar Holen
fd2beaadfa Make Fixer responsible for realigning tab stops 2019-01-09 18:08:59 -08:00
Vidar Holen
df7f00eaed Remove duplicate pathTo and unused replaceMultiLines 2019-01-09 17:51:43 -08:00
Vidar Holen
e45b679d58 Merge pull request #1445 from Gandalf-/issue_1318_single_comma_array
Issue 1318 single comma array delimiter
2019-01-09 17:42:35 -08:00
Gandalf-
263401cfcb Issue 1318 single comma array delimiter
Issue https://github.com/koalaman/shellcheck/issues/1318

The case in which a single comma, with no spaces, used in an array
assignment is now caught for SC2054.
2019-01-08 19:56:34 -08:00
Vidar Holen
0e21f91c07 Merge pull request #1433 from contivero/fix-export-p
Fix 'export -p' being undefined under POSIX sh
2019-01-08 19:33:03 -08:00
Vidar Holen
4ecdc10599 Merge branch 'shellcheck_sed_herestring_130' of https://github.com/Gandalf-/shellcheck 2019-01-08 19:19:44 -08:00
Vidar Holen
baa4d2e555 Let checkGrepRe only parse flags once 2019-01-08 19:06:00 -08:00
Vidar Holen
26c55750cf Merge branch 'issue_1404_grep_globs' of https://github.com/Gandalf-/shellcheck 2019-01-08 18:53:51 -08:00
Vidar Holen
9c42d43e90 Merge branch 'sc2093-exec-in-loops' of https://github.com/jabberabbe/shellcheck 2019-01-08 18:31:59 -08:00
Vidar Holen
434b904746 Process replacements according to AST depth (fixes #1431) 2019-01-08 18:25:37 -08:00
Tito Sacchi
ab2b0e11a3 Fix #1340 (SC2093 about removing "exec" should trigger in loops) 2019-01-08 20:20:26 +01:00
Vidar Holen
394f4d6505 Make quicktest interpret test/shellcheck.hs directly 2019-01-08 01:01:31 -08:00
Gandalf-
4a2b2c7396 Issue 1404 grep glob false positives
https://github.com/koalaman/shellcheck/issues/1404

Some grep flags support globs; these are now all checked prevent false
positives.
2019-01-06 17:45:29 -08:00
Vidar Holen
97cb753d21 Recognize --help (fixes #1441) 2019-01-05 11:37:05 -08:00
Vidar Holen
98266a1878 Merge branch 'issue_1039_case_pattern_context' of https://github.com/Gandalf-/shellcheck 2019-01-01 16:50:59 -08:00
Vidar Holen
6138206ce5 Merge branch 'autofix-tab' of https://github.com/ngzhian/shellcheck 2019-01-01 13:06:20 -08:00
Gandalf-
6debd59f02 Add context to case pattern warnings
https://github.com/koalaman/shellcheck/issues/1039
2018-12-31 18:52:30 -08:00
Gandalf-
9425654a42 Expand echo + sed style warning to herestrings
https://github.com/koalaman/shellcheck/issues/130
2018-12-31 15:33:37 -08:00
Ng Zhi An
461be74976 Realign virtual tabs when applying fix
Fix an off-by-one error, in the case that is commented `should never happen`.
It happens when the end of a range is the at the end of a line.
In that case we should update the real column count (probably just by +1)
instead of returning it.

I modified makeNonVirtual to use a helper, realign, that works on
Ranged. That way we can share the code to realign a PositionedComment
and also a Replacement.

Fixes #1420
2018-12-29 17:16:29 +08:00
Vidar Holen
278ce56650 Merge branch 'ngzhian-1416-encourage-n' 2018-12-28 19:02:40 -08:00
Vidar Holen
73822c3588 Allow SC2243 and SC2244 to trigger with quotes, add fix 2018-12-28 19:02:06 -08:00
Cristian Adrián Ontivero
29dedbdc9c Fix 'export -p' being undefined under POSIX sh
Fixes #1432
2018-12-28 21:23:49 -03:00
Vidar Holen
f6bc009331 Merge branch '1416-encourage-n' of https://github.com/ngzhian/shellcheck into ngzhian-1416-encourage-n 2018-12-28 15:56:15 -08:00
Vidar Holen
ef811995fa Merge pull request #1430 from contivero/posix-sh-jobs-flags
Check jobs flags in dash/POSIX sh (fixes #1429)
2018-12-28 15:53:17 -08:00
Cristian Adrián Ontivero
73a41cdd2f Check jobs flags in dash/POSIX sh (fixes #1429) 2018-12-28 10:04:19 -03:00
Vidar Holen
1b4c486748 Merge pull request #1426 from ngzhian/dash-1406
Update supported ulimit flags for dash
2018-12-27 12:19:30 -08:00
Ng Zhi An
95a8cf93c9 Add check for ambiguous nullary test
Given an input like `if [[ $(a) ]]; then ...`, this is a implicit `-n` test,
so it works like `if [[ -n $(a) ]]; then ...`. Users might confuse this for
a check for the exit code of the command a, which should be tested with:

    if a; then
        ...

We warn the user to be more explicity and specifity the `-n`.

Fixes #1416
2018-12-25 17:14:21 +08:00
Ng Zhi An
bd04af0769 Update supported ulimit flags for dash
Values are retrieved from https://linux.die.net/man/1/dash, search for
ulimit.

Fixes #1406
2018-12-25 09:33:58 +08:00
Vidar Holen
9acc8fcb53 Fix semigroup incompatibility 2018-12-23 11:08:48 -08:00
Vidar Holen
897f019353 Move Ranged definition to Fixer to avoid overpromising 2018-12-22 10:04:00 -08:00
Ng Zhi An
0636e7023c Fix applying multiple fixes per line
Fixes #1421
2018-12-21 14:34:03 +08:00
Vidar Holen
08ca1ee6e9 Remove unnecessary Regex constraint 2018-12-17 20:15:39 -08:00
Vidar Holen
eb3e6fe8e1 Add ShellCheck.Fixer to the cabal file 2018-12-17 20:14:49 -08:00
Vidar Holen
ecd61bfc68 Merge pull request #1376 from ngzhian/autofix
Add method to apply a multi-line replacement
2018-12-17 17:24:59 -08:00
Ng Zhi An
a8d88dfe98 Fix calculation of changed lines 2018-12-17 00:20:50 -08:00
Ng Zhi An
7d2c519d64 Remove spurious new line in fix message 2018-12-17 00:20:50 -08:00
Ng Zhi An
3403f8d75b Fix bug in overlap check 2018-12-17 00:20:50 -08:00
Ng Zhi An
408a3b99d8 Remove overlaps before applying replacements 2018-12-17 00:20:50 -08:00
Ng Zhi An
bc111141f8 Move fix application logic to separate module 2018-12-17 00:20:50 -08:00
Ng Zhi An
3471ad45b1 Smarter sorting and application of fix to handle multiple replacements 2018-12-17 00:20:50 -08:00
Ng Zhi An
d5ba41035b Add method to apply a multi-line replacement 2018-12-16 21:53:48 -08:00
Vidar Holen
88aef838f1 SC1068 (var = x) now alternatively suggests quoting (fixes #1412) 2018-12-16 15:45:52 -08:00
Vidar Holen
3d61b73e91 Be more specific about why you should read the wiki page 2018-12-16 15:14:29 -08:00
Vidar Holen
138080bdc7 Fix infinite loop on annotations for SC2188 (fixes #1413) 2018-12-16 14:42:19 -08:00
Vidar Holen
5b3f17c29d Allow tests to access token positions for fixes 2018-12-16 13:17:59 -08:00
Vidar Holen
b47e083ee3 Fix 'does not support multiple targets at once' error 2018-12-16 10:15:32 -08:00
Vidar Holen
3cba76dc7d Update CHANGELOG with new release and autofix merge 2018-12-09 15:01:08 -08:00
Vidar Holen
eb588f62f6 Enable autofix support. It's still preliminary. 2018-12-09 15:01:08 -08:00
Vidar Holen
bcd13614eb Improve Fix memory usage 2018-12-09 15:01:08 -08:00
Vidar Holen
a8376a09a9 Minor renaming and output fixes 2018-12-09 15:01:08 -08:00
Ng Zhi An
5ed89d2241 Change definition of Replacement, add ToJSON instance for it 2018-12-09 15:01:08 -08:00
Ng Zhi An
4a87d2a3de Expose token positions in params, use that to construct fixes 2018-12-09 15:01:08 -08:00
Ng Zhi An
41613babd9 Prototype fix 2018-12-09 15:01:08 -08:00
Vidar Holen
cb57b4a74f Stable version 0.6.0
This release is dedicated to Factorio. If this is how much fun it is to
build factories and oppress natives, then history makes a lot of sense.
2018-12-02 19:08:06 -08:00
Vidar Holen
e3f0243c0e Add 'striptests' script to Cabal package 2018-12-02 19:08:06 -08:00
Vidar Holen
66b5f13c6f Make wiki links fit in 80 columns 2018-12-02 19:08:06 -08:00
Vidar Holen
a7a404a5a8 Fill in missing bits in CHANGELOG 2018-12-02 14:49:04 -08:00
Vidar Holen
0761f5c923 Merge pull request #1397 from ngzhian/man-en-dash
Disable smart typography extension for markdown input
2018-12-02 13:56:14 -08:00
Vidar Holen
b55149b22d Add man page instructions (fixes #1347) 2018-12-02 12:29:42 -08:00
Ng Zhi An
4097bb5154 Disable smart typography extension for markdown input
Fixes #1392
2018-11-29 23:18:20 -08:00
Vidar Holen
1b207b3d43 Preemptively fix possible '-- |' breakage 2018-11-26 20:43:15 -08:00
Vidar Holen
135b4aa485 Add stack builds to distro test 2018-11-26 20:41:29 -08:00
Vidar Holen
cb76951ad2 Add warnings for 'exit' similar to 'return' (fixes #1388) 2018-11-24 23:05:40 -08:00
Vidar Holen
705e476e4c Merge pull request #1389 from romanzolotarev/patch-1
Add OpenBSD to "Installing" section of README.md
2018-11-15 19:58:30 +11:00
Roman Zolotarev
e705552c97 Update README.html
Add OpenBSD to "Installing" section.
2018-11-15 08:49:26 +00:00
Vidar Holen
198aa4fc3d Merge pull request #1383 from l2dy/master
Fix typo in CHANGELOG
2018-11-08 11:13:55 +08:00
Zero King
f4044fbcc7 Fix typo in CHANGELOG 2018-11-08 03:07:52 +00:00
Vidar Holen
2827b35696 SC2240: Warn about . script args.. in sh/dash (fixes #1373) 2018-11-07 18:04:18 -08:00
Vidar Holen
de95c376ea Merge pull request #1380 from PeterDaveHello/Update-Dockerfile
Update Docker build-only image to Ubuntu 18.04
2018-11-08 08:34:21 +08:00
Peter Dave Hello
5e1b1e010a Update Docker build-only image to Ubuntu 18.04
Ref:
> Ubuntu 17.10 (Artful Aardvark) End of Life reached on July 19 2018
https://fridge.ubuntu.com/2018/07/19/ubuntu-17-10-artful-aardvark-end-of-life-reached-on-july-19-2018/
2018-11-02 13:47:22 +08:00
Vidar Holen
620c9c2023 Also warn about glob matching with [ a != b* ] (fixes #1374) 2018-11-01 04:47:44 -07:00
Vidar Holen
359b1467a2 Work around snap's old cabal + new snapcraft proxy. 2018-10-24 21:33:42 -07:00
Vidar Holen
df0a0d41fa Add SC1133: Warn when a line starts with |/||/&& (fixes #1359) 2018-10-21 17:46:46 -07:00
Vidar Holen
b815242506 Improve regex parsing (fixes #1367) 2018-10-21 15:25:35 -07:00
Vidar Holen
07b5aa2971 Add SC2239: shebang is not absolute path. 2018-10-17 20:38:21 -07:00
Vidar Holen
f7b82658f4 Add $# to list of variables not containing spaces (fixes #1362) 2018-10-17 09:00:52 -07:00
Vidar Holen
e0e46e979a Add wiki links to output, and a -W controlling it. (Fixes #920) 2018-10-10 21:53:43 -07:00
Vidar Holen
79319558a5 Merge pull request #1350 from peti/master
Fix build with ghc 8.6.1
2018-10-04 20:11:40 -07:00
Vidar Holen
8d13add1ed Add automated cross-distro testing via Docker 2018-10-01 17:10:43 -07:00
Peter Simons
8940e60300 ShellCheck.cabal: our Setup.hs works fine with Cabal 2.4.x 2018-09-27 17:32:45 +02:00
Peter Simons
5f1c969546 getParentTree: avoid pattern matching in do notation
Pattern matching in "do" requires a MonadFail context, which we don't have in
pure code. Instead, we'll use "case-of" to bind the part of the state that
we're interested in.
2018-09-27 17:30:41 +02:00
Vidar Holen
dadfdfde97 Don't suggest subshells for cd ..; foo; cd.. 2018-09-21 21:08:41 -07:00
Vidar Holen
3e2cb26119 Add SC2238 about redirections to command names 2018-09-17 17:46:49 -07:00
Vidar Holen
1a6ae4f19e Add plug for shfmt 2018-09-16 10:56:53 -07:00
Vidar Holen
95a376aad1 Minor script cleanup 2018-09-16 10:11:03 -07:00
Vidar Holen
a06d7c1841 Merge pull request #1324 from ngzhian/679
Understand array variable declaration in read (fixes #679)
2018-09-15 12:33:58 -07:00
Vidar Holen
5202072a34 Add employer mandated disclaimer 2018-09-15 11:10:27 -07:00
Vidar Holen
72af1cfd59 Merge pull request #1331 from federicotdn/patch-1
Add link to flymake-shellcheck under Emacs section
2018-09-15 11:09:04 -07:00
Vidar Holen
228af7df54 Merge pull request #1337 from dimo414/master
Expand "rhs"; this abbreviation seems needlessly obfuscating.
2018-09-12 16:01:43 -07:00
Michael Diamond
6db392511b Expand "rhs"; this abbreviation seems needlessly obfuscating. 2018-09-12 14:22:40 -07:00
Ng Zhi An
07f04e13ce Understand array variable declaration in read (fixes #679 fixes #1272)
It used to only treat all trailing variables in read as varaible
declarings, but an array variable can be declared in other positions:

    read -a foo -r

foo is a declared variable, and multiple such variables can be declared.
2018-09-08 09:19:02 -07:00
Federico T
493ecd6f73 Add link to flymake-shellcheck under Emacs section
I've recently created the flymake-shellcheck package for Emacs, which allows using ShellCheck with the built-in Flymake package.
2018-09-07 12:37:16 -03:00
Vidar Holen
f0a2e688c4 Don't warn about LINENO since it's now POSIX. Fixes #644 2018-09-03 12:36:25 -07:00
Vidar Holen
0cee8a993d Suggest reading the wiki page in the issue template 2018-08-28 20:40:57 -07:00
Vidar Holen
3d03b0ab3b Suggest -z/-n instead of ! -n/-z (fixes #1326). 2018-08-28 20:15:54 -07:00
Vidar Holen
488d6dcb41 Improve find leading flag detection (fixes #1312) 2018-08-26 18:18:57 -07:00
Vidar Holen
d02a9bbcce Account for &&/||/{}/() in SC2233&co (fixes #1320). 2018-08-26 17:36:10 -07:00
Vidar Holen
165e408114 Merge branch 'ngzhian-opqaque-interface' 2018-08-18 20:33:14 -07:00
Vidar Holen
932e2b3538 Merge branch 'opqaque-interface' of https://github.com/ngzhian/shellcheck into ngzhian-opqaque-interface 2018-08-18 20:32:27 -07:00
Vidar Holen
76b1482f64 Avoid using error for option parsing failure 2018-08-18 20:06:44 -07:00
Vidar Holen
49250eadae Add --severity to CHANGELOG 2018-08-18 20:06:31 -07:00
Martin Schwenke
3fe11927bb SQUASH: --severity specifies *minimum* severity to be handled
Signed-off-by: Martin Schwenke <martin@meltin.net>
2018-08-18 20:05:56 -07:00
Martin Schwenke
b16da4b242 Add command-line option -S/--severity
Specifies the maximum severity of errors to handle.  For example,
specifying "-S warning" means that errors of severity "info" and
"style" are ignored.

Signed-off-by: Martin Schwenke <martin@meltin.net>
2018-08-18 20:05:56 -07:00
Ng Zhi An
c8e0797350 Make data in Interface more opaque 2018-08-17 22:10:18 -07:00
Vidar Holen
15aaacf715 Add test for parsing bitwise not 2018-08-15 18:30:10 -07:00
Vidar Holen
5ef4229f61 Modernize SC2028 echo escape test 2018-08-07 19:31:28 -07:00
Vidar Holen
afada43978 Merge pull request #1311 from ngzhian/1310
Use regex to match special flags for printf
2018-08-07 18:45:44 -07:00
Ng Zhi An
8be76b13b9 Use regex to match special flags for printf
Fixes #1310
2018-08-05 22:45:24 -07:00
Vidar Holen
581be5878b Suggest 'cat' when piping/redirecting to echo (fixes #1292) 2018-07-28 17:38:53 -07:00
Vidar Holen
0f835a5a2c Don't trigger SC2222 for fallthrough case branches (fixes #1044) 2018-07-28 12:30:06 -07:00
Vidar Holen
4b0a35d4c9 Merge pull request #1302 from pjeby/fix949
Fix #949 (failing on @ in function names)
2018-07-26 21:07:48 -07:00
Vidar Holen
51e0c1be62 Use three instead of two dots in 2006 message 2018-07-26 19:59:42 -07:00
Vidar Holen
d8a32da07f Retire SC1117 (unknown quoted escapes) due to noise 2018-07-26 19:23:53 -07:00
PJ Eby
0d1a34a291 Fix #949 (failing on @ in function names)
'@' was previously mentioned in 5005dc0fa1 as a
character needed to fix #909, but was not included
in the actual change at that time.
2018-07-23 16:18:35 -04:00
Vidar Holen
5005dc0fa1 Allow directive/-s to override shebang blacklist (fixes #974) 2018-07-22 12:43:51 -07:00
Vidar Holen
b8ee7436e5 Add a test for 03ce3b15 2018-07-21 13:51:21 -07:00
Vidar Holen
da8e450386 Realign =s 2018-07-21 13:51:08 -07:00
Vidar Holen
c3ac4c3d87 Merge pull request #1298 from ngzhian/1268
Fix false positive when indexing into array in cond
2018-07-21 13:43:45 -07:00
Ng Zhi An
03ce3b15b6 Fix false positive when indexing into array in cond
Fixes #1268
2018-07-18 22:31:58 -07:00
Vidar Holen
10edba3ab8 Minimize build size with -Os and -split-sections 2018-07-12 09:33:59 -07:00
Vidar Holen
797b424917 Add armv6hf link for Raspberry Pi 2018-07-08 20:47:05 -07:00
Vidar Holen
84e678e9ff TravisCI armv6hf build (aka Raspberry Pi build) 2018-07-08 20:13:33 -07:00
Vidar Holen
3a672968f3 Merge pull request #1282 from kenden/patch-1
Add instructions to install linux binary
2018-07-07 13:32:37 -07:00
Quentin Nerden
8c7efae393 Add instructions to install linux binary 2018-07-05 15:30:16 +02:00
Vidar Holen
f91b5bc270 Merge pull request #1256 from ngzhian/mv-stdin
Do not warn on mv -i (fixes #1251)
2018-06-24 11:47:53 -07:00
Vidar Holen
b01f1128c7 Make SC1012 "printf '\t'" suggestion use single quotes 2018-06-24 11:47:00 -07:00
Vidar Holen
db33294838 Merge pull request #1257 from ngzhian/trailing-comma-exclude
Allow trailing comma in exclude flag
2018-06-24 11:46:26 -07:00
Vidar Holen
75fb4da387 Don't warn about tr '[=e=]' equivalence classes 2018-06-23 16:55:35 -07:00
Vidar Holen
366262af18 Update CHANGELOG to mention end positions 2018-06-23 16:53:44 -07:00
Vidar Holen
6869c2fa18 Merge pull request #1261 from ngzhian/1188
Do not warn find --help (fixes #1188)
2018-06-23 16:07:23 -07:00
Vidar Holen
868a7be33e Improve spans for some warnings 2018-06-17 19:19:18 -07:00
Vidar Holen
7138abff4b Expose (some) span information in TTY output 2018-06-17 17:44:31 -07:00
Vidar Holen
9d3e79b576 Require all Ids to be constructed with a span 2018-06-16 17:33:08 -07:00
Vidar Holen
402e635f86 Warn about & followed by letters, e.g. http://foo/?a=b&c=d 2018-06-16 12:30:19 -07:00
Ng Zhi An
91cbcddd9d Do not warn find --help (fixes #1188) 2018-06-14 22:40:26 -07:00
Ng Zhi An
963b39b002 Allow trailing comma in exclude flag 2018-06-13 22:31:51 -07:00
Ng Zhi An
0cc45447d3 Do not warn on mv -i (fixes #1251) 2018-06-13 21:39:37 -07:00
Vidar Holen
32a53f21b5 Merge pull request #1239 from ngzhian/end_column
End column
2018-06-13 19:29:55 -07:00
Vidar Holen
12b8720bd8 Merge pull request #1255 from ngzhian/popd-n
Check popd flags for -n (fixes #1252)
2018-06-13 19:22:39 -07:00
Ng Zhi An
7adeaccd11 Check popd flags for -n (fixes #1252) 2018-06-12 22:53:41 -07:00
Ng Zhi An
b63483d44c Remove unused import 2018-06-12 22:50:02 -07:00
Ng Zhi An
4111ce8fde Make end pos non-optional 2018-06-12 22:39:06 -07:00
Ng Zhi An
b9a9eb2529 Change getNextId to create a zero width span at new id 2018-06-12 22:17:35 -07:00
Ng Zhi An
e717802de1 Change usage of endPosOfStartId to startSpan and endSpan 2018-06-12 22:11:11 -07:00
Ng Zhi An
1699c9e9ba Add api to begin and end a span of source code 2018-06-12 21:56:53 -07:00
Vidar Holen
bfc32200e2 Correctly consider $'..' a literal (fixes #1242) 2018-06-10 20:23:10 -07:00
Vidar Holen
52e8a42d9d Merge pull request #1253 from sblondon/master
Remove unnecessary dot
2018-06-09 13:03:24 -07:00
sblondon
00360af672 Remove unnecessary dot
The dot after the screenshot is strange so this commit remove it.
2018-06-08 10:57:41 +02:00
Ng Zhi An
8ff35fb4af Add end pos to readSingleQuoted 2018-06-07 23:09:59 -07:00
Ng Zhi An
29e8c0a16e Add end pos to readDollarBraced 2018-06-07 22:25:16 -07:00
Ng Zhi An
3848788c2d Add end pos to readDollarVariable 2018-06-07 21:55:41 -07:00
Ng Zhi An
0c459ae2cb Add function to set end pos of start id 2018-06-07 21:55:41 -07:00
Ng Zhi An
e496b413bd Remove usage of withNextId 2018-06-07 21:55:41 -07:00
Ng Zhi An
48ac654a93 Merge end pos map into start pos map 2018-06-07 21:55:41 -07:00
Russell Harmon
4470fe715c Support emitting a correct end column on SC2086
This does the necessary work to emit end columns on AST analyses. SC2086
is made to emit a correct end column as an illustrative example.

For example:
```
$ shellcheck -s bash -f json /dev/stdin <<< 'echo $1'
[{"file":"/dev/stdin","line":1,"endLine":1,"column":6,"endColumn":8,"level":"info","code":2086,"message":"Double quote to prevent globbing and word splitting."}]
```

This change deprecates the parser's getNextId and getNextIdAt, replacing
it with a new withNextId function. This function has the type signature:

withNextId :: Monad m => ParsecT s UserState (SCBase m) (Id -> b) -> ParsecT s UserState (SCBase m) b

Specifically, it should be used to wrap read* functions and will pass in
a newly generated Id which should be used to represent that node.
Sub-parsers will need their own call to withNextId in order to get a
unique Id.

In doing this, withNextId can now track both the entry and exit position
of every read* parser which uses it, enabling the tracking of end
columns throughout the application.
2018-06-07 21:55:41 -07:00
Vidar Holen
379321d1f3 Show tags in travis output 2018-06-07 20:40:44 -07:00
Vidar Holen
0adea473fd Update CHANGELOG after release 2018-06-07 20:19:00 -07:00
69 changed files with 7049 additions and 2151 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
*
!LICENSE
!Setup.hs
!ShellCheck.cabal
!shellcheck.hs
!src

View File

@@ -1,11 +1,11 @@
#### For bugs #### For bugs
- 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"):
- [ ] I tried on shellcheck.net and verified that this is still a problem on the latest commit - [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
- [ ] It's not reproducible on shellcheck.net, but I think that's because it's an OS, configuration or encoding issue - [ ] 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 ShellCheck
# 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-newstyle/sdist/*.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 )

29
.github_deploy Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/bash
set -x
shopt -s extglob
export EDITOR="touch"
# Sanity check
gh --version || exit 1
hub release show latest || exit 1
for tag in $TAGS
do
if ! hub release show "$tag"
then
echo "Creating new release $tag"
git show --no-patch --format='format:%B' > description
hub release create -F description "$tag"
fi
files=()
for file in deploy/*
do
[[ $file == *.@(xz|gz|zip) ]] || continue
[[ $file == *"$tag"* ]] || continue
files+=("$file")
done
gh release upload "$tag" "${files[@]}" --clobber || exit 1
done

106
.multi_arch_docker Executable file
View File

@@ -0,0 +1,106 @@
#!/bin/bash
# This script builds and deploys multi-architecture docker images from the
# binaries previously built and deployed to GitHub.
function multi_arch_docker::install_docker_buildx() {
# Install up-to-date version of docker, with buildx support.
local -r docker_apt_repo='https://download.docker.com/linux/ubuntu'
curl -fsSL "${docker_apt_repo}/gpg" | sudo apt-key add -
local -r os="$(lsb_release -cs)"
sudo add-apt-repository "deb [arch=amd64] $docker_apt_repo $os stable"
sudo apt-get update
sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce
# Enable docker daemon experimental support (for 'pull --platform').
local -r config='/etc/docker/daemon.json'
if [[ -e "$config" ]]; then
sudo sed -i -e 's/{/{ "experimental": true, /' "$config"
else
echo '{ "experimental": true }' | sudo tee "$config"
fi
sudo systemctl restart docker
# Install QEMU multi-architecture support for docker buildx.
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
# Instantiate docker buildx builder with multi-architecture support.
export DOCKER_CLI_EXPERIMENTAL=enabled
docker buildx create --name mybuilder
docker buildx use mybuilder
# Start up buildx and verify that all is OK.
docker buildx inspect --bootstrap
}
# Log in to Docker Hub for deployment.
function multi_arch_docker::login_to_docker_hub() {
echo "$DOCKER_PASSWORD" | docker login -u="$DOCKER_USERNAME" --password-stdin
}
# Run buildx build and push. Passed in arguments augment the command line.
function multi_arch_docker::buildx() {
mkdir -p /tmp/empty
docker buildx build \
--platform "${DOCKER_PLATFORMS// /,}" \
--push \
--progress plain \
-f Dockerfile.multi-arch \
"$@" \
/tmp/empty
rmdir /tmp/empty
}
# Build and push plain and alpine docker images for all tags.
function multi_arch_docker::build_and_push_all() {
for tag in $TAGS; do
multi_arch_docker::buildx -t "$DOCKER_BASE:$tag" --build-arg "tag=$tag"
multi_arch_docker::buildx -t "$DOCKER_BASE-alpine:$tag" \
--build-arg "tag=$tag" --target alpine
done
}
# Test all pushed docker images.
function multi_arch_docker::test_all() {
printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
for platform in $DOCKER_PLATFORMS; do
for tag in $TAGS; do
for ext in '-alpine' ''; do
image="${DOCKER_BASE}${ext}:${tag}"
msg="Testing docker image $image on platform $platform"
line="${msg//?/=}"
printf '\n%s\n%s\n%s\n' "${line}" "${msg}" "${line}"
docker pull -q --platform "$platform" "$image"
if [ -n "$ext" ]; then
echo -n "Image architecture: "
docker run --rm --entrypoint /bin/sh "$image" -c 'uname -m'
version=$(docker run --rm "$image" shellcheck --version \
| grep 'version:')
else
version=$(docker run --rm "$image" --version | grep 'version:')
fi
version=${version/#version: /v}
echo "shellcheck version: $version"
if [[ ! ("$tag" =~ ^(latest|stable)$) && "$tag" != "$version" ]]; then
echo "Version mismatch: shellcheck $version tagged as $tag"
exit 1
fi
if [ -n "$ext" ]; then
docker run --rm -v "$PWD:/mnt" -w /mnt "$image" shellcheck myscript
else
docker run --rm -v "$PWD:/mnt" "$image" myscript
fi
done
done
done
}
function multi_arch_docker::main() {
export DOCKER_PLATFORMS='linux/amd64'
DOCKER_PLATFORMS+=' linux/arm64'
DOCKER_PLATFORMS+=' linux/arm/v6'
multi_arch_docker::install_docker_buildx
multi_arch_docker::login_to_docker_hub
multi_arch_docker::build_and_push_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,21 +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 echo "Tags are $TAGS"
for tag in $TAGS
do do
base="${file%.*}"
cp "$file" "shellcheck" for dir in windows.*/
tar -cJf "$base.linux.x86_64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck do
rm "shellcheck" ( cd "$dir" && zip "../shellcheck-$tag.zip" * )
done
for dir in {linux,darwin}.*/
do
base="${dir%/}"
( cd "$dir" && tar -cJf "../shellcheck-$tag.$base.tar.xz" --transform="s:^:shellcheck-$tag/:" * )
done
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

14
.snapsquid.conf Normal file
View File

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

View File

@@ -1,76 +0,0 @@
sudo: required
language: sh
services:
- docker
before_install:
- DOCKER_BASE="$DOCKER_USERNAME/shellcheck"
- DOCKER_BUILDS=""
- TAGS=""
- test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
- test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
script:
- mkdir deploy
# Remove all tests to reduce binary size
- ./striptests
# 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"; done
# Linux Alpine based Docker image
- name="$DOCKER_BASE-alpine"
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
- sed -e '/DELETE-MARKER/,$d' Dockerfile > Dockerfile.alpine
- docker build -f Dockerfile.alpine -t "$name:current" .
- docker run "$name:current" sh -c 'shellcheck --version'
# Windows .exe
- docker pull koalaman/winghc
- docker run --user="$UID" --rm -v "$PWD:/appdata" koalaman/winghc cuib
- for tag in $TAGS; do cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe"; done
- rm -rf dist || true
# Misc packaging
- ./.prepare_deploy
after_success:
- docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
- for repo in $DOCKER_BUILDS;
do
for tag in $TAGS;
do
echo "Deploying $repo:current as $repo:$tag...";
docker tag "$repo:current" "$repo:$tag" || exit 1;
docker push "$repo:$tag" || exit 1;
done;
done;
after_failure:
- id
- pwd
- df -h
- find . -name '*.log' -type f -exec grep "" /dev/null {} +
- find . -ls
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
local-dir: deploy
on:
repo: koalaman/shellcheck
all_branches: true

View File

@@ -1,3 +1,109 @@
## v0.7.2 - 2021-04-19
### Added
- `disable` directives can now be a range, e.g. `disable=SC3000-SC4000`
- SC1143: Warn about line continuations in comments
- 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
- SC2268: Warn about unnecessary x-comparisons like `[ 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
### Fixed
- `-f diff` no longer claims that it found more issues when it didn't
- Known empty variables now correctly trigger SC2086
- ShellCheck should now be compatible with Cabal 3
- SC2154 and all command-specific checks now trigger for builtins
called with `builtin`
### Added
- SC1136: Warn about unexpected characters after ]/]]
- SC2254: Suggest quoting expansions in case statements
- SC2255: Suggest using `$((..))` in `[ 2*3 -eq 6 ]`
- SC2256: Warn about translated strings that are known variables
- SC2257: Warn about arithmetic mutation in redirections
- SC2258: Warn about trailing commas in for loop elements
### Changed
- SC2230: 'command -v' suggestion is now off by default (-i deprecate-which)
- SC1081: Keywords are now correctly parsed case sensitively, with a warning
## v0.7.0 - 2019-07-28
### Added
- Precompiled binaries for macOS and Linux aarch64
- Preliminary support for fix suggestions
- New `-f diff` unified diff format for auto-fixes
- Files containing Bats tests can now be checked
- Directory wide directives can now be placed in a `.shellcheckrc`
- Optional checks: Use `--list-optional` to show a list of tests,
Enable with `-o` flags or `enable=name` directives
- Source paths: Use `-P dir1:dir2` or a `source-path=dir1` directive
to specify search paths for sourced files.
- json1 format like --format=json but treats tabs as single characters
- Recognize FLAGS variables created by the shflags library.
- Site-specific changes can now be made in Custom.hs for ease of patching
- SC2154: Also warn about unassigned uppercase variables (optional)
- SC2252: Warn about `[ $a != x ] || [ $a != y ]`, similar to SC2055
- SC2251: Inform about ineffectual ! in front of commands
- SC2250: Warn about variable references without braces (optional)
- SC2249: Warn about `case` with missing default case (optional)
- SC2248: Warn about unquoted variables without special chars (optional)
- SC2247: Warn about $"(cmd)" and $"{var}"
- SC2246: Warn if a shebang's interpreter ends with /
- SC2245: Warn that Ksh ignores all but the first glob result in `[`
- SC2243/SC2244: Suggest using explicit -n for `[ $foo ]` (optional)
- SC1135: Suggest not ending double quotes just to make $ literal
### Changed
- If a directive or shebang is not specified, a `.bash/.bats/.dash/.ksh`
extension will be used to infer the shell type when present.
- Disabling SC2120 on a function now disables SC2119 on call sites
### Fixed
- SC2183 no longer warns about missing printf args for `%()T`
## v0.6.0 - 2018-12-02
### Added
- Command line option --severity/-S for filtering by minimum severity
- Command line option --wiki-link-count/-W for showing wiki links
- SC2152/SC2151: Warn about bad `exit` values like `1234` and `"foo"`
- SC2236/SC2237: Suggest -n/-z instead of ! -z/-n
- SC2238: Warn when redirecting to a known command name, e.g. ls > rm
- SC2239: Warn if the shebang is not an absolute path, e.g. #!bin/sh
- SC2240: Warn when passing additional arguments to dot (.) in sh/dash
- SC1133: Better diagnostics when starting a line with |/||/&&
### Changed
- Most warnings now have useful end positions
- SC1117 about unknown double-quoted escape sequences has been retired
### Fixed
- SC2021 no longer triggers for equivalence classes like `[=e=]`
- SC2221/SC2222 no longer mistriggers on fall-through case branches
- SC2081 about glob matches in `[ .. ]` now also triggers for `!=`
- SC2086 no longer warns about spaces in `$#`
- SC2164 no longer suggests subshells for `cd ..; cmd; cd ..`
- `read -a` is now correctly considered an array assignment
- SC2039 no longer warns about LINENO now that it's POSIX
## v0.5.0 - 2018-05-31 ## v0.5.0 - 2018-05-31
### Added ### Added
- SC2233/SC2234/SC2235: Suggest removing or replacing (..) around tests - SC2233/SC2234/SC2235: Suggest removing or replacing (..) around tests
@@ -59,7 +165,7 @@
- SC2204/SC2205: Warn about `( -z foo )` and `( foo -eq bar )` - SC2204/SC2205: Warn about `( -z foo )` and `( foo -eq bar )`
- SC2200/SC2201: Warn about brace expansion in [/[[ - SC2200/SC2201: Warn about brace expansion in [/[[
- SC2198/SC2199: Warn about arrays in [/[[ - SC2198/SC2199: Warn about arrays in [/[[
- SC2196/SC2197: Warn about deprected egrep/fgrep - SC2196/SC2197: Warn about deprecated egrep/fgrep
- SC2195: Warn about unmatchable case branches - SC2195: Warn about unmatchable case branches
- SC2194: Warn about constant 'case' statements - SC2194: Warn about constant 'case' statements
- SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables - SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables
@@ -76,7 +182,7 @@
### Fixed ### Fixed
- `-c` no longer suggested when using `grep -o | wc` - `-c` no longer suggested when using `grep -o | wc`
- Comments and whitespace are now allowed before filewide directives - Comments and whitespace are now allowed before filewide directives
- Here doc delimters with esoteric quoting like `foo""` are now handled - Here doc delimiters with esoteric quoting like `foo""` are now handled
- SC2095 about `ssh` in while read loops is now suppressed when using `-n` - SC2095 about `ssh` in while read loops is now suppressed when using `-n`
- `%(%Y%M%D)T` now recognized as a single formatter in `printf` checks - `%(%Y%M%D)T` now recognized as a single formatter in `printf` checks
- `grep -F` now suppresses regex related suggestions - `grep -F` now suppresses regex related suggestions

View File

@@ -1,36 +0,0 @@
# Build-only image
FROM ubuntu:17.10 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
# Copy source and build it
COPY LICENSE Setup.hs shellcheck.hs ./
COPY src src
RUN cabal build Paths_ShellCheck && \
ghc -optl-static -optl-pthread -isrc -idist/build/autogen --make shellcheck && \
strip --strip-all shellcheck
RUN mkdir -p /out/bin && \
cp shellcheck /out/bin/
# Resulting Alpine image
FROM alpine:latest
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
COPY --from=build /out /
# DELETE-MARKER (Remove everything below to keep the alpine image)
# Resulting ShellCheck image
FROM scratch
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
WORKDIR /mnt
COPY --from=build /out /
ENTRYPOINT ["/bin/shellcheck"]

26
Dockerfile.multi-arch Normal file
View File

@@ -0,0 +1,26 @@
# Alpine image
FROM alpine:latest AS alpine
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
ARG tag
# Put the right binary for each architecture into place for the
# multi-architecture docker image.
RUN set -x; \
arch="$(uname -m)"; \
echo "arch is $arch"; \
if [ "${arch}" = 'armv7l' ]; then \
arch='armv6hf'; \
fi; \
url_base='https://github.com/koalaman/shellcheck/releases/download/'; \
tar_file="${tag}/shellcheck-${tag}.linux.${arch}.tar.xz"; \
wget "${url_base}${tar_file}" -O - | tar xJf -; \
mv "shellcheck-${tag}/shellcheck" /bin/; \
rm -rf "shellcheck-${tag}"; \
ls -laF /bin/shellcheck
# ShellCheck image
FROM scratch
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
WORKDIR /mnt
COPY --from=alpine /bin/shellcheck /bin/
ENTRYPOINT ["/bin/shellcheck"]

10
LICENSE
View File

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

219
README.md
View File

@@ -4,49 +4,49 @@
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts: ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts:
![Screenshot of a terminal showing problematic shell script lines highlighted](doc/terminal.png). ![Screenshot of a terminal showing problematic shell script lines highlighted](doc/terminal.png)
The goals of ShellCheck are The goals of ShellCheck are
- To point out and clarify typical beginner's syntax issues that cause a shell * To point out and clarify typical beginner's syntax issues that cause a shell
to give cryptic error messages. to give cryptic error messages.
- To point out and clarify typical intermediate level semantic problems that * To point out and clarify typical intermediate level semantic problems that
cause a shell to behave strangely and counter-intuitively. cause a shell to behave strangely and counter-intuitively.
- To point out subtle caveats, corner cases and pitfalls that may cause an * To point out subtle caveats, corner cases and pitfalls that may cause an
advanced user's otherwise working script to fail under future circumstances. advanced user's otherwise working script to fail under future circumstances.
See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for examples of what ShellCheck can help you identify! See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for examples of what ShellCheck can help you identify!
## Table of Contents ## Table of Contents
- [How to use](#how-to-use) * [How to use](#how-to-use)
- [On the web](#on-the-web) * [On the web](#on-the-web)
- [From your terminal](#from-your-terminal) * [From your terminal](#from-your-terminal)
- [In your editor](#in-your-editor) * [In your editor](#in-your-editor)
- [In your build or test suites](#in-your-build-or-test-suites) * [In your build or test suites](#in-your-build-or-test-suites)
- [Installing](#installing) * [Installing](#installing)
- [Travis CI](#travis-ci) * [Compiling from source](#compiling-from-source)
- [Compiling from source](#compiling-from-source) * [Installing Cabal](#installing-cabal)
- [Installing Cabal](#installing-cabal) * [Compiling ShellCheck](#compiling-shellcheck)
- [Compiling ShellCheck](#compiling-shellcheck) * [Running tests](#running-tests)
- [Running tests](#running-tests) * [Gallery of bad code](#gallery-of-bad-code)
- [Gallery of bad code](#gallery-of-bad-code) * [Quoting](#quoting)
- [Quoting](#quoting) * [Conditionals](#conditionals)
- [Conditionals](#conditionals) * [Frequently misused commands](#frequently-misused-commands)
- [Frequently misused commands](#frequently-misused-commands) * [Common beginner's mistakes](#common-beginners-mistakes)
- [Common beginner's mistakes](#common-beginners-mistakes) * [Style](#style)
- [Style](#style) * [Data and typing errors](#data-and-typing-errors)
- [Data and typing errors](#data-and-typing-errors) * [Robustness](#robustness)
- [Robustness](#robustness) * [Portability](#portability)
- [Portability](#portability) * [Miscellaneous](#miscellaneous)
- [Miscellaneous](#miscellaneous) * [Testimonials](#testimonials)
- [Testimonials](#testimonials) * [Ignoring issues](#ignoring-issues)
- [Ignoring issues](#ignoring-issues) * [Reporting bugs](#reporting-bugs)
- [Reporting bugs](#reporting-bugs) * [Contributing](#contributing)
- [Contributing](#contributing) * [Copyright](#copyright)
- [Copyright](#copyright) * [Other Resources](#other-resources)
## How to use ## How to use
@@ -54,7 +54,7 @@ There are a number of ways to use ShellCheck!
### On the web ### On the web
Paste a shell script on https://www.shellcheck.net for instant feedback. Paste a shell script on <https://www.shellcheck.net> for instant feedback.
[ShellCheck.net](https://www.shellcheck.net) is always synchronized to the latest git commit, and is the easiest way to give ShellCheck a go. Tell your friends! [ShellCheck.net](https://www.shellcheck.net) is always synchronized to the latest git commit, and is the easiest way to give ShellCheck a go. Tell your friends!
@@ -70,7 +70,7 @@ You can see ShellCheck suggestions directly in a variety of editors.
![Screenshot of Vim showing inlined shellcheck feedback](doc/vim-syntastic.png). ![Screenshot of Vim showing inlined shellcheck feedback](doc/vim-syntastic.png).
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck): * Emacs, through [Flycheck](https://github.com/flycheck/flycheck) or [Flymake](https://github.com/federicotdn/flymake-shellcheck):
![Screenshot of emacs showing inlined shellcheck feedback](doc/emacs-flycheck.png). ![Screenshot of emacs showing inlined shellcheck feedback](doc/emacs-flycheck.png).
@@ -85,8 +85,47 @@ You can see ShellCheck suggestions directly in a variety of editors.
### In your build or test suites ### In your build or test suites
While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites. While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites.
It makes canonical use of exit codes, so you can just add a `shellcheck` command as part of the process.
ShellCheck makes canonical use of exit codes, and can output simple JSON, CheckStyle compatible XML, GCC compatible warnings as well as human readable text (with or without ANSI colors). See the [Integration](https://github.com/koalaman/shellcheck/wiki/Integration) wiki page for more documentation. For example, in a Makefile:
```Makefile
check-scripts:
# Fail if any of these files have warnings
shellcheck myscripts/*.sh
```
or in a Travis CI `.travis.yml` file:
```yaml
script:
# Fail if any of these files have warnings
- shellcheck myscripts/*.sh
```
Services and platforms that have ShellCheck pre-installed and ready to use:
* [Travis CI](https://travis-ci.org/)
* [Codacy](https://www.codacy.com/)
* [Code Climate](https://codeclimate.com/)
* [Code Factor](https://www.codefactor.io/)
* [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:
* [SonarQube](https://www.sonarqube.org/) through [sonar-shellcheck-plugin](https://github.com/emerald-squad/sonar-shellcheck-plugin)
Most other services, including [GitLab](https://about.gitlab.com/), let you install
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
or by downloading and unpacking a [binary release](#installing-a-pre-compiled-binary).
It's a good idea to manually install a specific ShellCheck version regardless. This avoids
any surprise build breaks when a new version with new warnings is published.
For customized filtering or reporting, ShellCheck can output simple JSON, CheckStyle compatible XML,
GCC compatible warnings as well as human readable text (with or without ANSI colors). See the
[Integration](https://github.com/koalaman/shellcheck/wiki/Integration) wiki page for more documentation.
## Installing ## Installing
@@ -110,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:
@@ -129,23 +168,43 @@ 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:
pkg_add shellcheck
On openSUSE On openSUSE
zypper in ShellCheck zypper in ShellCheck
Or use OneClickInstall - https://software.opensuse.org/package/ShellCheck Or use OneClickInstall - <https://software.opensuse.org/package/ShellCheck>
On Solus: On Solus:
eopkg install shellcheck eopkg install shellcheck
On Windows (via [scoop](http://scoop.sh)):
scoop install shellcheck On Windows (via [chocolatey](https://chocolatey.org/packages/shellcheck)):
```cmd
C:\> choco install shellcheck
```
Or Windows (via [scoop](http://scoop.sh)):
```cmd
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:
@@ -154,36 +213,57 @@ From Snap Store:
From Docker Hub: From Docker Hub:
```sh ```sh
docker pull koalaman/shellcheck:stable # Or :v0.4.7 for that version, or :latest for daily builds docker run --rm -v "$PWD:/mnt" koalaman/shellcheck:stable myscript
docker run -v "$PWD:/mnt" koalaman/shellcheck myscript # Or :v0.4.7 for that version, or :latest for daily builds
``` ```
or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend. It works exactly like a regular Alpine image, but has shellcheck preinstalled. or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend. It works exactly like a regular Alpine image, but has shellcheck preinstalled.
Using the [nix package manager](https://nixos.org/nix):
```sh
nix-env -iA nixpkgs.shellcheck
```
Alternatively, you can download pre-compiled binaries for the latest release here: Alternatively, you can download pre-compiled binaries for the latest release here:
* [Linux, x86_64](https://storage.googleapis.com/shellcheck/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)
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-stable.zip) * [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)
* [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)
or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds. or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
(including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds).
## Travis CI Distro packages already come with a `man` page. If you are building from source, it can be installed with:
```console
pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1
sudo mv shellcheck.1 /usr/share/man/man1
```
### Travis CI
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it. Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
If you still want to do so in order to upgrade at your leisure or ensure the latest release: If you still want to do so in order to upgrade at your leisure or ensure you're
using the latest release, follow the steps below to install a binary version.
install: ### Installing a pre-compiled binary
# Install a custom version of shellcheck instead of Travis CI's default The pre-compiled binaries come in `tar.xz` files. To decompress them, make sure
- scversion="stable" # or "v0.4.7", or "latest" `xz` is installed.
- wget "https://storage.googleapis.com/shellcheck/shellcheck-$scversion.linux.x86_64.tar.xz" On Debian/Ubuntu/Mint, you can `apt install xz-utils`.
- tar --xz -xvf "shellcheck-$scversion.linux.x86_64.tar.xz" On Redhat/Fedora/CentOS, `yum -y install xz`.
- shellcheck() { "shellcheck-$scversion/shellcheck" "$@"; }
- shellcheck --version
script: A simple installer may do something like:
- shellcheck *.sh
```bash
scversion="stable" # or "v0.4.7", or "latest"
wget -qO- "https://github.com/koalaman/shellcheck/releases/download/${scversion?}/shellcheck-${scversion?}.linux.x86_64.tar.xz" | tar -xJv
cp "shellcheck-${scversion}/shellcheck" /usr/bin/
shellcheck --version
```
## Compiling from source ## Compiling from source
@@ -193,13 +273,11 @@ 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 cask $ brew install cabal-install
brew cask install haskell-platform
cabal install cabal-install
On MacPorts, the package is instead called `hs-cabal-install`, while native Windows users should install the latest version of the Haskell platform from https://www.haskell.org/platform/ On MacPorts, the package is instead called `hs-cabal-install`, while native Windows users should install the latest version of the Haskell platform from <https://www.haskell.org/platform/>
Verify that `cabal` is installed and update its dependency list with Verify that `cabal` is installed and update its dependency list with
@@ -235,12 +313,15 @@ may use a legacy codepage. In `cmd.exe`, `powershell.exe` and Powershell ISE,
make sure to use a TrueType font, not a Raster font, and set the active make sure to use a TrueType font, not a Raster font, and set the active
codepage to UTF-8 (65001) with `chcp`: codepage to UTF-8 (65001) with `chcp`:
> chcp 65001 ```cmd
Active code page: 65001 chcp 65001
```
In Powershell ISE, you may need to additionally update the output encoding: In Powershell ISE, you may need to additionally update the output encoding:
> [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 ```powershell
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
```
### Running tests ### Running tests
@@ -418,13 +499,13 @@ Alexander Tarasikov,
Issues can be ignored via environmental variable, command line, individually or globally within a file: Issues can be ignored via environmental variable, command line, individually or globally within a file:
https://github.com/koalaman/shellcheck/wiki/Ignore <https://github.com/koalaman/shellcheck/wiki/Ignore>
## Reporting bugs ## Reporting bugs
Please use the GitHub issue tracker for any bugs or feature suggestions: Please use the GitHub issue tracker for any bugs or feature suggestions:
https://github.com/koalaman/shellcheck/issues <https://github.com/koalaman/shellcheck/issues>
## Contributing ## Contributing
@@ -439,6 +520,12 @@ The contributor retains the copyright.
ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE). ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE).
Copyright 2012-2018, Vidar 'koala_man' Holen and contributors. Copyright 2012-2019, [Vidar 'koala_man' Holen](https://github.com/koalaman/) and contributors.
Happy ShellChecking! Happy ShellChecking!
## Other Resources
* The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221).
* ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)!

View File

@@ -1,36 +0,0 @@
import Distribution.PackageDescription (
HookedBuildInfo,
emptyHookedBuildInfo )
import Distribution.Simple (
Args,
UserHooks ( preSDist ),
defaultMainWithHooks,
simpleUserHooks )
import Distribution.Simple.Setup ( SDistFlags )
import System.Process ( system )
main = defaultMainWithHooks myHooks
where
myHooks = simpleUserHooks { preSDist = myPreSDist }
-- | This hook will be executed before e.g. @cabal sdist@. It runs
-- pandoc to create the man page from shellcheck.1.md. If the pandoc
-- command is not found, this will fail with an error message:
--
-- /bin/sh: pandoc: command not found
--
-- Since the man page is listed in the Extra-Source-Files section of
-- our cabal file, a failure here should result in a failure to
-- create the distribution tarball (that's a good thing).
--
myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo
myPreSDist _ _ = do
putStrLn "Building the man page (shellcheck.1) with pandoc..."
putStrLn pandoc_cmd
result <- system pandoc_cmd
putStrLn $ "pandoc exited with " ++ show result
return emptyHookedBuildInfo
where
pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"

View File

@@ -1,5 +1,5 @@
Name: ShellCheck Name: ShellCheck
Version: 0.5.0 Version: 0.7.2
Synopsis: Shell script analysis tool Synopsis: Shell script analysis tool
License: GPL-3 License: GPL-3
License-file: LICENSE License-file: LICENSE
@@ -7,7 +7,7 @@ Category: Static Analysis
Author: Vidar Holen Author: Vidar Holen
Maintainer: vidar@vidarholen.net Maintainer: vidar@vidarholen.net
Homepage: https://www.shellcheck.net/ Homepage: https://www.shellcheck.net/
Build-Type: Custom Build-Type: Simple
Cabal-Version: >= 1.8 Cabal-Version: >= 1.8
Bug-reports: https://github.com/koalaman/shellcheck/issues Bug-reports: https://github.com/koalaman/shellcheck/issues
Description: Description:
@@ -26,17 +26,13 @@ Extra-Source-Files:
-- documentation -- documentation
README.md README.md
shellcheck.1.md shellcheck.1.md
-- built with a cabal sdist hook -- A script to build the man page using pandoc
shellcheck.1 manpage
-- convenience script for stripping tests
striptests
-- tests -- tests
test/shellcheck.hs test/shellcheck.hs
custom-setup
setup-depends:
base >= 4 && <5,
process >= 1.0 && <1.7,
Cabal >= 1.10 && <2.3
source-repository head source-repository head
type: git type: git
location: git://github.com/koalaman/shellcheck.git location: git://github.com/koalaman/shellcheck.git
@@ -47,14 +43,16 @@ library
build-depends: build-depends:
semigroups semigroups
build-depends: build-depends:
-- GHC 7.6.3 (base 4.6.0.1) is buggy (#1131, #1119) in optimized mode.
-- Just disable that version entirely to fail fast.
aeson, aeson,
base > 4.6.0.1 && < 5, array,
base >= 4.8.0.0 && < 5,
bytestring, bytestring,
containers >= 0.5, containers >= 0.5,
directory, deepseq >= 1.4.0.0,
Diff >= 0.2.0,
directory >= 1.2.3.0,
mtl >= 2.2.1, mtl >= 2.2.1,
filepath,
parsec, parsec,
regex-tdfa, regex-tdfa,
QuickCheck >= 2.7.4, QuickCheck >= 2.7.4,
@@ -68,13 +66,18 @@ library
ShellCheck.AnalyzerLib ShellCheck.AnalyzerLib
ShellCheck.Checker ShellCheck.Checker
ShellCheck.Checks.Commands ShellCheck.Checks.Commands
ShellCheck.Checks.Custom
ShellCheck.Checks.ShellSupport ShellCheck.Checks.ShellSupport
ShellCheck.Data ShellCheck.Data
ShellCheck.Fixer
ShellCheck.Formatter.Format ShellCheck.Formatter.Format
ShellCheck.Formatter.CheckStyle ShellCheck.Formatter.CheckStyle
ShellCheck.Formatter.Diff
ShellCheck.Formatter.GCC ShellCheck.Formatter.GCC
ShellCheck.Formatter.JSON ShellCheck.Formatter.JSON
ShellCheck.Formatter.JSON1
ShellCheck.Formatter.TTY ShellCheck.Formatter.TTY
ShellCheck.Formatter.Quiet
ShellCheck.Interface ShellCheck.Interface
ShellCheck.Parser ShellCheck.Parser
ShellCheck.Regex ShellCheck.Regex
@@ -87,29 +90,37 @@ executable shellcheck
semigroups semigroups
build-depends: build-depends:
aeson, aeson,
array,
base >= 4 && < 5, base >= 4 && < 5,
bytestring, bytestring,
ShellCheck,
containers, containers,
directory, deepseq >= 1.4.0.0,
Diff >= 0.2.0,
directory >= 1.2.3.0,
mtl >= 2.2.1, mtl >= 2.2.1,
filepath,
parsec >= 3.0, parsec >= 3.0,
QuickCheck >= 2.7.4, QuickCheck >= 2.7.4,
regex-tdfa regex-tdfa,
ShellCheck
main-is: shellcheck.hs main-is: shellcheck.hs
test-suite test-shellcheck test-suite test-shellcheck
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0
build-depends: build-depends:
aeson, aeson,
array,
base >= 4 && < 5, base >= 4 && < 5,
bytestring, bytestring,
ShellCheck,
containers, containers,
directory, deepseq >= 1.4.0.0,
Diff >= 0.2.0,
directory >= 1.2.3.0,
mtl >= 2.2.1, mtl >= 2.2.1,
filepath,
parsec, parsec,
QuickCheck >= 2.7.4, QuickCheck >= 2.7.4,
regex-tdfa regex-tdfa,
ShellCheck
main-is: test/shellcheck.hs main-is: test/shellcheck.hs

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 --dependencies-only $CABALOPTS ShellCheck
# 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
chmod +x striptests && ./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 --dependencies-only $CABALOPTS ShellCheck
# 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
chmod +x striptests && ./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 --dependencies-only $CABALOPTS ShellCheck
# 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
chmod +x striptests && ./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,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
chmod +x striptests && ./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 winbind
# 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
RUN wine /haskell/bin/cabal.exe update && IFS=';' && wine /haskell/bin/cabal.exe install --lib --dependencies-only $CABALOPTS ShellCheck
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
chmod +x striptests && ./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

4
manpage Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
echo >&2 "Generating man page using pandoc"
pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1 || exit
echo >&2 "Done. You can read it with: man ./shellcheck.1"

View File

@@ -6,8 +6,8 @@ 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))"
done done

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
# quickrun runs ShellCheck in an interpreted mode. # quickrun runs ShellCheck in an interpreted mode.
# This allows testing changes without recompiling. # This allows testing changes without recompiling.

View File

@@ -1,18 +1,11 @@
#!/bin/bash #!/usr/bin/env bash
# quicktest runs the ShellCheck unit tests in an interpreted mode. # quicktest runs the ShellCheck unit tests in an interpreted mode.
# This allows running tests without compiling, which can be faster. # This allows running tests without compiling, which can be faster.
# 'cabal test' remains the source of truth. # 'cabal test' remains the source of truth.
( (
var=$(echo 'liftM and $ sequence [ var=$(echo 'main' | ghci test/shellcheck.hs 2>&1 | tee /dev/stderr)
ShellCheck.Analytics.runTests if [[ $var == *ExitSuccess* ]]
,ShellCheck.Parser.runTests
,ShellCheck.Checker.runTests
,ShellCheck.Checks.Commands.runTests
,ShellCheck.Checks.ShellSupport.runTests
,ShellCheck.AnalyzerLib.runTests
]' | tr -d '\n' | cabal repl 2>&1 | tee /dev/stderr)
if [[ $var == *$'\nTrue'* ]]
then then
exit 0 exit 0
else else

View File

@@ -29,14 +29,13 @@ will warn that decimals are not supported.
+ For scripts starting with `#!/bin/ksh` (or using `-s ksh`), ShellCheck will + For scripts starting with `#!/bin/ksh` (or using `-s ksh`), ShellCheck will
not warn at all, as `ksh` supports decimals in arithmetic contexts. not warn at all, as `ksh` supports decimals in arithmetic contexts.
# OPTIONS # OPTIONS
**-a**,\ **--check-sourced** **-a**,\ **--check-sourced**
: Emit warnings in sourced files. Normally, `shellcheck` will only warn : Emit warnings in sourced files. Normally, `shellcheck` will only warn
about issues in the specified files. With this option, any issues in about issues in the specified files. With this option, any issues in
sourced files files will also be reported. sourced files will also be reported.
**-C**[*WHEN*],\ **--color**[=*WHEN*] **-C**[*WHEN*],\ **--color**[=*WHEN*]
@@ -44,6 +43,13 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
is *auto*. **--color** without an argument is equivalent to is *auto*. **--color** without an argument is equivalent to
**--color=always**. **--color=always**.
**-i**\ *CODE1*[,*CODE2*...],\ **--include=***CODE1*[,*CODE2*...]
: Explicitly include only the specified codes in the report. Subsequent **-i**
options are cumulative, but all the codes can be specified at once,
comma-separated as a single argument. Include options override any provided
exclude options.
**-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...] **-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...]
: Explicitly exclude the specified codes from the report. Subsequent **-e** : Explicitly exclude the specified codes from the report. Subsequent **-e**
@@ -56,23 +62,60 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
standard output. Subsequent **-f** options are ignored, see **FORMATS** standard output. Subsequent **-f** options are ignored, see **FORMATS**
below for more information. below for more information.
**--list-optional**
: Output a list of known optional checks. These can be enabled with **-o**
flags or **enable** directives.
**--norc**
: Don't try to look for .shellcheckrc configuration files.
**-o**\ *NAME1*[,*NAME2*...],\ **--enable=***NAME1*[,*NAME2*...]
: Enable optional checks. The special name *all* enables all of them.
Subsequent **-o** options accumulate. This is equivalent to specifying
**enable** directives.
**-P**\ *SOURCEPATH*,\ **--source-path=***SOURCEPATH*
: Specify paths to search for sourced files, separated by `:` on Unix and
`;` on Windows. This is equivalent to specifying `search-path`
directives.
**-s**\ *shell*,\ **--shell=***shell* **-s**\ *shell*,\ **--shell=***shell*
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*. : Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
The default is to use the file's shebang, or *bash* if the target shell The default is to deduce the shell from the file's `shell` directive,
can't be determined. shebang, or `.bash/.bats/.dash/.ksh` extension, in that order. *sh* refers to
POSIX `sh` (not the system's), and will warn of portability issues.
**-S**\ *SEVERITY*,\ **--severity=***severity*
: Specify minimum severity of errors to consider. Valid values in order of
severity are *error*, *warning*, *info* and *style*.
The default is *style*.
**-V**,\ **--version** **-V**,\ **--version**
: Print version information and exit. : Print version information and exit.
**-W** *NUM*,\ **--wiki-link-count=NUM**
: For TTY output, show *NUM* wiki links to more information about mentioned
warnings. Set to 0 to disable them entirely.
**-x**,\ **--external-sources** **-x**,\ **--external-sources**
: Follow 'source' statements even when the file is not specified as input. : Follow `source` statements even when the file is not specified as input.
By default, `shellcheck` will only follow files specified on the command By default, `shellcheck` will only follow files specified on the command
line (plus `/dev/null`). This option allows following any file the script line (plus `/dev/null`). This option allows following any file the script
may `source`. may `source`.
**FILES...**
: One or more script files to check, or "-" for standard input.
# FORMATS # FORMATS
@@ -109,27 +152,59 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
... ...
</checkstyle> </checkstyle>
**json** **diff**
: Auto-fixes in unified diff format. Can be piped to `git apply` or `patch -p1`
to automatically apply fixes.
--- a/test.sh
+++ b/test.sh
@@ -2,6 +2,6 @@
## Example of a broken script.
for f in $(ls *.m3u)
do
- grep -qi hq.*mp3 $f \
+ grep -qi hq.*mp3 "$f" \
&& echo -e 'Playlist $f contains a HQ file in mp3 format'
done
**json1**
: Json is a popular serialization format that is more suitable for web : Json is a popular serialization format that is more suitable for web
applications. ShellCheck's json is compact and contains only the bare applications. ShellCheck's json is compact and contains only the bare
minimum. minimum. Tabs are counted as 1 character.
{
comments: [
{
"file": "filename",
"line": lineNumber,
"column": columnNumber,
"level": "severitylevel",
"code": errorCode,
"message": "warning message"
},
...
]
}
**json**
: This is a legacy version of the **json1** format. It's a raw array of
comments, and all offsets have a tab stop of 8.
**quiet**
: Suppress all normal output. Exit with zero if no issues are found,
otherwise exit with one. Stops processing after the first issue.
[
{
"file": "filename",
"line": lineNumber,
"column": columnNumber,
"level": "severitylevel",
"code": errorCode,
"message": "warning message"
},
...
]
# DIRECTIVES # DIRECTIVES
ShellCheck directives can be specified as comments in the shell script
before a command or block: ShellCheck directives can be specified as comments in the shell script.
If they appear before the first command, they are considered file-wide.
Otherwise, they apply to the immediately following command or block:
# shellcheck key=value key=value # shellcheck key=value key=value
command-or-structure command-or-structure
@@ -157,19 +232,67 @@ 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 an optional check by name, as listed with **--list-optional**.
Only file-wide `enable` directives are considered.
**source** **source**
: Overrides the filename included by a `source`/`.` statement. This can be : Overrides the filename included by a `source`/`.` statement. This can be
used to tell shellcheck where to look for a file whose name is determined used to tell shellcheck where to look for a file whose name is determined
at runtime, or to skip a source by telling it to use `/dev/null`. at runtime, or to skip a source by telling it to use `/dev/null`.
**source-path**
: Add a directory to the search path for `source`/`.` statements (by default,
only ShellCheck's working directory is included). Absolute paths will also
be rooted in these paths. The special path `SCRIPTDIR` can be used to
specify the currently checked script's directory, as in
`source-path=SCRIPTDIR` or `source-path=SCRIPTDIR/../libs`. Multiple
paths accumulate, and `-P` takes precedence over them.
**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
Unless `--norc` is used, ShellCheck will look for a file `.shellcheckrc` or
`shellcheckrc` in the script's directory and each parent directory. If found,
it will read `key=value` pairs from it and treat them as file-wide directives.
Here is an example `.shellcheckrc`:
# Look for 'source'd files relative to the checked script,
# and also look for absolute paths in /mnt/chroot
source-path=SCRIPTDIR
source-path=/mnt/chroot
# Turn on warnings for unquoted variables with safe values
enable=quote-safe-variables
# Turn on warnings for unassigned uppercase variables
enable=check-unassigned-uppercase
# Allow [ ! -z foo ] instead of suggesting -n
disable=SC2236
If no `.shellcheckrc` is found in any of the parent directories, ShellCheck
will look in `~/.shellcheckrc` followed by the XDG config directory
(usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on
Windows. Only the first file found will be used.
Note for Snap users: the Snap sandbox disallows access to hidden files.
Use `shellcheckrc` without the dot instead.
Note for Docker users: ShellCheck will only be able to look for files that
are mounted in the container, so `~/.shellcheckrc` will not be read.
# ENVIRONMENT VARIABLES # ENVIRONMENT VARIABLES
The environment variable `SHELLCHECK_OPTS` can be set with default flags: The environment variable `SHELLCHECK_OPTS` can be set with default flags:
export SHELLCHECK_OPTS='--shell=bash --exclude=SC2016' export SHELLCHECK_OPTS='--shell=bash --exclude=SC2016'
@@ -179,7 +302,7 @@ invocation.
# RETURN VALUES # RETURN VALUES
ShellCheck uses the follow exit codes: ShellCheck uses the following exit codes:
+ 0: All files successfully scanned with no issues. + 0: All files successfully scanned with no issues.
+ 1: All files successfully scanned with some issues. + 1: All files successfully scanned with some issues.
@@ -188,6 +311,7 @@ ShellCheck uses the follow exit codes:
+ 4: ShellCheck was invoked with bad options (e.g. unknown formatter). + 4: ShellCheck was invoked with bad options (e.g. unknown formatter).
# LOCALE # LOCALE
This version of ShellCheck is only available in English. All files are This version of ShellCheck is only available in English. All files are
leniently decoded as UTF-8, with a fallback of ISO-8859-1 for invalid leniently decoded as UTF-8, with a fallback of ISO-8859-1 for invalid
sequences. `LC_CTYPE` is respected for output, and defaults to UTF-8 for sequences. `LC_CTYPE` is respected for output, and defaults to UTF-8 for
@@ -196,20 +320,23 @@ locales where encoding is unspecified (such as the `C` locale).
Windows users seeing `commitBuffer: invalid argument (invalid character)` Windows users seeing `commitBuffer: invalid argument (invalid character)`
should set their terminal to use UTF-8 with `chcp 65001`. should set their terminal to use UTF-8 with `chcp 65001`.
# AUTHOR # AUTHORS
ShellCheck is written and maintained by Vidar Holen.
ShellCheck is developed and maintained by Vidar Holen, with assistance from a
long list of wonderful contributors.
# REPORTING BUGS # REPORTING BUGS
Bugs and issues can be reported on GitHub: Bugs and issues can be reported on GitHub:
https://github.com/koalaman/shellcheck/issues https://github.com/koalaman/shellcheck/issues
# COPYRIGHT # COPYRIGHT
Copyright 2012-2015, Vidar Holen.
Copyright 2012-2019, Vidar Holen and contributors.
Licensed under the GNU General Public License version 3 or later, Licensed under the GNU General Public License version 3 or later,
see https://gnu.org/licenses/gpl.html see https://gnu.org/licenses/gpl.html
# SEE ALSO # SEE ALSO
sh(1) bash(1) sh(1) bash(1)

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -17,6 +17,7 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
import qualified ShellCheck.Analyzer
import ShellCheck.Checker import ShellCheck.Checker
import ShellCheck.Data import ShellCheck.Data
import ShellCheck.Interface import ShellCheck.Interface
@@ -24,9 +25,12 @@ import ShellCheck.Regex
import qualified ShellCheck.Formatter.CheckStyle import qualified ShellCheck.Formatter.CheckStyle
import ShellCheck.Formatter.Format import ShellCheck.Formatter.Format
import qualified ShellCheck.Formatter.Diff
import qualified ShellCheck.Formatter.GCC import qualified ShellCheck.Formatter.GCC
import qualified ShellCheck.Formatter.JSON import qualified ShellCheck.Formatter.JSON
import qualified ShellCheck.Formatter.JSON1
import qualified ShellCheck.Formatter.TTY import qualified ShellCheck.Formatter.TTY
import qualified ShellCheck.Formatter.Quiet
import Control.Exception import Control.Exception
import Control.Monad import Control.Monad
@@ -46,6 +50,7 @@ import System.Console.GetOpt
import System.Directory import System.Directory
import System.Environment import System.Environment
import System.Exit import System.Exit
import System.FilePath
import System.IO import System.IO
data Flag = Flag String String data Flag = Flag String String
@@ -67,15 +72,19 @@ instance Monoid Status where
data Options = Options { data Options = Options {
checkSpec :: CheckSpec, checkSpec :: CheckSpec,
externalSources :: Bool, externalSources :: Bool,
formatterOptions :: FormatterOptions sourcePaths :: [FilePath],
formatterOptions :: FormatterOptions,
minSeverity :: Severity
} }
defaultOptions = Options { defaultOptions = Options {
checkSpec = emptyCheckSpec, checkSpec = emptyCheckSpec,
externalSources = False, externalSources = False,
formatterOptions = FormatterOptions { sourcePaths = [],
formatterOptions = newFormatterOptions {
foColorOption = ColorAuto foColorOption = ColorAuto
} },
minSeverity = StyleC
} }
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..." usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
@@ -85,19 +94,40 @@ options = [
Option "C" ["color"] Option "C" ["color"]
(OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN") (OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN")
"Use color (auto, always, never)", "Use color (auto, always, never)",
Option "i" ["include"]
(ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings",
Option "e" ["exclude"] Option "e" ["exclude"]
(ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings", (ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings",
Option "f" ["format"] Option "f" ["format"]
(ReqArg (Flag "format") "FORMAT") $ (ReqArg (Flag "format") "FORMAT") $
"Output format (" ++ formatList ++ ")", "Output format (" ++ formatList ++ ")",
Option "" ["list-optional"]
(NoArg $ Flag "list-optional" "true") "List checks disabled by default",
Option "" ["norc"]
(NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc files",
Option "o" ["enable"]
(ReqArg (Flag "enable") "check1,check2..")
"List of optional checks to enable (or 'all')",
Option "P" ["source-path"]
(ReqArg (Flag "source-path") "SOURCEPATHS")
"Specify path when looking for sourced files (\"SCRIPTDIR\" for script's dir)",
Option "s" ["shell"] Option "s" ["shell"]
(ReqArg (Flag "shell") "SHELLNAME") (ReqArg (Flag "shell") "SHELLNAME")
"Specify dialect (sh, bash, dash, ksh)", "Specify dialect (sh, bash, dash, ksh)",
Option "S" ["severity"]
(ReqArg (Flag "severity") "SEVERITY")
"Minimum severity of errors to consider (error, warning, info, style)",
Option "V" ["version"] Option "V" ["version"]
(NoArg $ Flag "version" "true") "Print version information", (NoArg $ Flag "version" "true") "Print version information",
Option "W" ["wiki-link-count"]
(ReqArg (Flag "wiki-link-count") "NUM")
"The number of wiki links to show, when applicable",
Option "x" ["external-sources"] Option "x" ["external-sources"]
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES" (NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES",
Option "" ["help"]
(NoArg $ Flag "help" "true") "Show this usage summary and exit"
] ]
getUsageInfo = usageInfo usageHeader options
printErr = lift . hPutStrLn stderr printErr = lift . hPutStrLn stderr
@@ -106,15 +136,18 @@ parseArguments argv =
case getOpt Permute options argv of case getOpt Permute options argv of
(opts, files, []) -> return (opts, files) (opts, files, []) -> return (opts, files)
(_, _, errors) -> do (_, _, errors) -> do
printErr $ concat errors ++ "\n" ++ usageInfo usageHeader options printErr $ concat errors ++ "\n" ++ getUsageInfo
throwError SyntaxFailure throwError SyntaxFailure
formats :: FormatterOptions -> Map.Map String (IO Formatter) formats :: FormatterOptions -> Map.Map String (IO Formatter)
formats options = Map.fromList [ formats options = Map.fromList [
("checkstyle", ShellCheck.Formatter.CheckStyle.format), ("checkstyle", ShellCheck.Formatter.CheckStyle.format),
("diff", ShellCheck.Formatter.Diff.format options),
("gcc", ShellCheck.Formatter.GCC.format), ("gcc", ShellCheck.Formatter.GCC.format),
("json", ShellCheck.Formatter.JSON.format), ("json", ShellCheck.Formatter.JSON.format),
("tty", ShellCheck.Formatter.TTY.format options) ("json1", ShellCheck.Formatter.JSON1.format),
("tty", ShellCheck.Formatter.TTY.format options),
("quiet", ShellCheck.Formatter.Quiet.format options)
] ]
formatList = intercalate ", " names formatList = intercalate ", " names
@@ -137,12 +170,6 @@ split char str =
else split' rest (a:element) else split' rest (a:element)
split' [] element = [reverse element] split' [] element = [reverse element]
getExclusions options =
let elements = concatMap (split ',') $ getOptions options "exclude"
clean = dropWhile (not . isDigit)
in
map (Prelude.read . clean) elements :: [Int]
toStatus = fmap (either id id) . runExceptT toStatus = fmap (either id id) . runExceptT
getEnvArgs = do getEnvArgs = do
@@ -222,12 +249,28 @@ runFormatter sys format options files = do
then NoProblems then NoProblems
else SomeProblems else SomeProblems
parseColorOption colorOption = parseEnum name value list =
case colorOption of case filter ((== value) . fst) list of
"auto" -> ColorAuto [(name, value)] -> return value
"always" -> ColorAlways [] -> do
"never" -> ColorNever printErr $ "Unknown value for --" ++ name ++ ". " ++
_ -> error $ "Bad value for --color `" ++ colorOption ++ "'" "Valid options are: " ++ (intercalate ", " $ map fst list)
throwError SupportFailure
parseColorOption value =
parseEnum "color" value [
("auto", ColorAuto),
("always", ColorAlways),
("never", ColorNever)
]
parseSeverityOption value =
parseEnum "severity" value [
("error", ErrorC),
("warning", WarningC),
("info", InfoC),
("style", StyleC)
]
parseOption flag options = parseOption flag options =
case flag of case flag of
@@ -241,7 +284,7 @@ parseOption flag options =
} }
Flag "exclude" str -> do Flag "exclude" str -> do
new <- mapM parseNum $ split ',' str new <- mapM parseNum $ filter (not . null) $ split ',' str
let old = csExcludedWarnings . checkSpec $ options let old = csExcludedWarnings . checkSpec $ options
return options { return options {
checkSpec = (checkSpec options) { checkSpec = (checkSpec options) {
@@ -249,22 +292,49 @@ parseOption flag options =
} }
} }
Flag "include" str -> do
new <- mapM parseNum $ filter (not . null) $ split ',' str
let old = csIncludedWarnings . checkSpec $ options
return options {
checkSpec = (checkSpec options) {
csIncludedWarnings =
if null new
then old
else Just new `mappend` old
}
}
Flag "version" _ -> do Flag "version" _ -> do
liftIO printVersion liftIO printVersion
throwError NoProblems throwError NoProblems
Flag "list-optional" _ -> do
liftIO printOptional
throwError NoProblems
Flag "help" _ -> do
liftIO $ putStrLn getUsageInfo
throwError NoProblems
Flag "externals" _ -> Flag "externals" _ ->
return options { return options {
externalSources = True externalSources = True
} }
Flag "color" color -> Flag "color" color -> do
option <- parseColorOption color
return options { return options {
formatterOptions = (formatterOptions options) { formatterOptions = (formatterOptions options) {
foColorOption = parseColorOption color foColorOption = option
} }
} }
Flag "source-path" str -> do
let paths = splitSearchPath str
return options {
sourcePaths = (sourcePaths options) ++ paths
}
Flag "sourced" _ -> Flag "sourced" _ ->
return options { return options {
checkSpec = (checkSpec options) { checkSpec = (checkSpec options) {
@@ -272,7 +342,42 @@ parseOption flag options =
} }
} }
_ -> return options Flag "severity" severity -> do
option <- parseSeverityOption severity
return options {
checkSpec = (checkSpec options) {
csMinSeverity = option
}
}
Flag "wiki-link-count" countString -> do
count <- parseNum countString
return options {
formatterOptions = (formatterOptions options) {
foWikiLinkCount = count
}
}
Flag "norc" _ ->
return options {
checkSpec = (checkSpec options) {
csIgnoreRC = True
}
}
Flag "enable" value ->
let cs = checkSpec options in return options {
checkSpec = cs {
csOptionalChecks = (csOptionalChecks cs) ++ split ',' value
}
}
-- This flag is handled specially in 'process'
Flag "format" _ -> return options
Flag str _ -> do
printErr $ "Internal error for --" ++ str ++ ". Please file a bug :("
return options
where where
die s = do die s = do
printErr s printErr s
@@ -280,19 +385,23 @@ parseOption flag options =
parseNum ('S':'C':str) = parseNum str parseNum ('S':'C':str) = parseNum str
parseNum num = do parseNum num = do
unless (all isDigit num) $ do unless (all isDigit num) $ do
printErr $ "Bad exclusion: " ++ num printErr $ "Invalid number: " ++ num
throwError SyntaxFailure throwError SyntaxFailure
return (Prelude.read num :: Integer) return (Prelude.read num :: Integer)
ioInterface options files = do ioInterface options files = do
inputs <- mapM normalize files inputs <- mapM normalize files
cache <- newIORef emptyCache cache <- newIORef emptyCache
configCache <- newIORef ("", Nothing)
return SystemInterface { return SystemInterface {
siReadFile = get cache inputs siReadFile = get cache inputs,
siFindSource = findSourceFile inputs (sourcePaths options),
siGetConfig = getConfig configCache
} }
where where
emptyCache :: Map.Map FilePath String emptyCache :: Map.Map FilePath String
emptyCache = Map.empty emptyCache = Map.empty
get cache inputs file = do get cache inputs file = do
map <- readIORef cache map <- readIORef cache
case Map.lookup file map of case Map.lookup file map of
@@ -309,7 +418,6 @@ ioInterface options files = do
return $ Right contents return $ Right contents
) `catch` handler ) `catch` handler
else return $ Left (file ++ " was not specified as input (see shellcheck -x).") else return $ Left (file ++ " was not specified as input (see shellcheck -x).")
where where
handler :: IOException -> IO (Either ErrorMessage String) handler :: IOException -> IO (Either ErrorMessage String)
handler ex = return . Left $ show ex handler ex = return . Left $ show ex
@@ -327,6 +435,88 @@ ioInterface options files = do
fallback :: FilePath -> IOException -> IO FilePath fallback :: FilePath -> IOException -> IO FilePath
fallback path _ = return path fallback path _ = return path
-- Returns the name and contents of .shellcheckrc for the given file
getConfig cache filename = do
path <- normalize filename
let dir = takeDirectory path
(previousPath, result) <- readIORef cache
if dir == previousPath
then return result
else do
paths <- getConfigPaths dir
result <- findConfig paths
writeIORef cache (dir, result)
return result
findConfig paths =
case paths of
(file:rest) -> do
contents <- readConfig file
if isJust contents
then return contents
else findConfig rest
[] -> return Nothing
-- Get a list of candidate filenames. This includes .shellcheckrc
-- in all parent directories, plus the user's home dir and xdg dir.
-- The dot is optional for Windows and Snap users.
getConfigPaths dir = do
let next = takeDirectory dir
rest <- if next /= dir
then getConfigPaths next
else defaultPaths `catch`
((const $ return []) :: IOException -> IO [FilePath])
return $ (dir </> ".shellcheckrc") : (dir </> "shellcheckrc") : rest
defaultPaths = do
home <- getAppUserDataDirectory "shellcheckrc"
xdg <- getXdgDirectory XdgConfig "shellcheckrc"
return [home, xdg]
readConfig file = do
exists <- doesFileExist file
if exists
then do
(contents, _) <- inputFile file `catch` handler file
return $ Just (file, contents)
else
return Nothing
where
handler :: FilePath -> IOException -> IO (String, Bool)
handler file err = do
putStrLn $ file ++ ": " ++ show err
return ("", True)
andM a b arg = do
first <- a arg
if not first then return False else b arg
findM p = foldr go (pure Nothing)
where
go x acc = do
b <- p x
if b then pure (Just x) else acc
findSourceFile inputs sourcePathFlag currentScript sourcePathAnnotation original =
if isAbsolute original
then
let (_, relative) = splitDrive original
in find relative original
else
find original original
where
find filename deflt = do
sources <- findM ((allowable inputs) `andM` doesFileExist) $
(adjustPath filename):(map ((</> filename) . adjustPath) $ sourcePathFlag ++ sourcePathAnnotation)
case sources of
Nothing -> return deflt
Just first -> return first
scriptdir = dropFileName currentScript
adjustPath str =
case (splitDirectories str) of
("SCRIPTDIR":rest) -> joinPath (scriptdir:rest)
_ -> str
inputFile file = do inputFile file = do
(handle, shouldCache) <- (handle, shouldCache) <-
if file == "-" if file == "-"
@@ -383,3 +573,14 @@ printVersion = do
putStrLn $ "version: " ++ shellcheckVersion putStrLn $ "version: " ++ shellcheckVersion
putStrLn "license: GNU General Public License, version 3" putStrLn "license: GNU General Public License, version 3"
putStrLn "website: https://www.shellcheck.net" putStrLn "website: https://www.shellcheck.net"
printOptional = do
mapM f list
where
list = sortOn cdName ShellCheck.Analyzer.optionalChecks
f item = do
putStrLn $ "name: " ++ cdName item
putStrLn $ "desc: " ++ cdDescription item
putStrLn $ "example: " ++ cdPositive item
putStrLn $ "fix: " ++ cdNegative item
putStrLn ""

View File

@@ -23,24 +23,34 @@ description: |
# snap connect shellcheck:removable-media # snap connect shellcheck:removable-media
version: git version: git
grade: devel base: core18
grade: stable
confinement: strict confinement: strict
apps: 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:
plugin: dump plugin: dump
source: ./ source: .
build-packages: build-packages:
- cabal-install - cabal-install
build: | - squid
override-build: |
# See comments in .snapsquid.conf
[ "$http_proxy" ] && {
squid3 -f .snapsquid.conf
export http_proxy="http://localhost:8888"
sleep 3
}
cabal sandbox init cabal sandbox init
cabal update cabal update || cat /var/log/squid/*
cabal install -j cabal install -j
install: |
install -d $SNAPCRAFT_PART_INSTALL/usr/bin install -d $SNAPCRAFT_PART_INSTALL/usr/bin
install .cabal-sandbox/bin/shellcheck $SNAPCRAFT_PART_INSTALL/usr/bin install .cabal-sandbox/bin/shellcheck $SNAPCRAFT_PART_INSTALL/usr/bin

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -17,14 +17,17 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE DeriveGeneric, DeriveAnyClass, DeriveTraversable, PatternSynonyms #-}
module ShellCheck.AST where module ShellCheck.AST where
import GHC.Generics (Generic)
import Control.Monad.Identity import Control.Monad.Identity
import Control.DeepSeq
import Text.Parsec import Text.Parsec
import qualified ShellCheck.Regex as Re import qualified ShellCheck.Regex as Re
import Prelude hiding (id) import Prelude hiding (id)
newtype Id = Id Int deriving (Show, Eq, Ord) newtype Id = Id Int deriving (Show, Eq, Ord, Generic, NFData)
data Quoted = Quoted | Unquoted deriving (Show, Eq) data Quoted = Quoted | Unquoted deriving (Show, Eq)
data Dashed = Dashed | Undashed deriving (Show, Eq) data Dashed = Dashed | Undashed deriving (Show, Eq)
@@ -34,349 +37,241 @@ newtype FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq) data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
newtype Root = Root Token newtype Root = Root Token
data Token = data Token = OuterToken Id (InnerToken Token) deriving (Show)
TA_Binary Id String Token Token
| TA_Assignment Id String Token Token data InnerToken t =
| TA_Variable Id String [Token] Inner_TA_Binary String t t
| TA_Expansion Id [Token] | Inner_TA_Assignment String t t
| TA_Sequence Id [Token] | Inner_TA_Variable String [t]
| TA_Trinary Id Token Token Token | Inner_TA_Expansion [t]
| TA_Unary Id String Token | Inner_TA_Sequence [t]
| TC_And Id ConditionType String Token Token | Inner_TA_Trinary t t t
| TC_Binary Id ConditionType String Token Token | Inner_TA_Unary String t
| TC_Group Id ConditionType Token | Inner_TC_And ConditionType String t t
| TC_Nullary Id ConditionType Token | Inner_TC_Binary ConditionType String t t
| TC_Or Id ConditionType String Token Token | Inner_TC_Group ConditionType t
| TC_Unary Id ConditionType String Token | Inner_TC_Nullary ConditionType t
| TC_Empty Id ConditionType | Inner_TC_Or ConditionType String t t
| T_AND_IF Id | Inner_TC_Unary ConditionType String t
| T_AndIf Id Token Token | Inner_TC_Empty ConditionType
| T_Arithmetic Id Token | Inner_T_AND_IF
| T_Array Id [Token] | Inner_T_AndIf t t
| T_IndexedElement Id [Token] Token | Inner_T_Arithmetic t
| Inner_T_Array [t]
| Inner_T_IndexedElement [t] t
-- Store the index as string, and parse as arithmetic or string later -- Store the index as string, and parse as arithmetic or string later
| T_UnparsedIndex Id SourcePos String | Inner_T_UnparsedIndex SourcePos String
| T_Assignment Id AssignmentMode String [Token] Token | Inner_T_Assignment AssignmentMode String [t] t
| T_Backgrounded Id Token | Inner_T_Backgrounded t
| T_Backticked Id [Token] | Inner_T_Backticked [t]
| T_Bang Id | Inner_T_Bang
| T_Banged Id Token | Inner_T_Banged t
| T_BraceExpansion Id [Token] | Inner_T_BraceExpansion [t]
| T_BraceGroup Id [Token] | Inner_T_BraceGroup [t]
| T_CLOBBER Id | Inner_T_CLOBBER
| T_Case Id | Inner_T_Case
| T_CaseExpression Id Token [(CaseType, [Token], [Token])] | Inner_T_CaseExpression t [(CaseType, [t], [t])]
| T_Condition Id ConditionType Token | Inner_T_Condition ConditionType t
| T_DGREAT Id | Inner_T_DGREAT
| T_DLESS Id | Inner_T_DLESS
| T_DLESSDASH Id | Inner_T_DLESSDASH
| T_DSEMI Id | Inner_T_DSEMI
| T_Do Id | Inner_T_Do
| T_DollarArithmetic Id Token | Inner_T_DollarArithmetic t
| T_DollarBraced Id Token | Inner_T_DollarBraced Bool t
| T_DollarBracket Id Token | Inner_T_DollarBracket t
| T_DollarDoubleQuoted Id [Token] | Inner_T_DollarDoubleQuoted [t]
| T_DollarExpansion Id [Token] | Inner_T_DollarExpansion [t]
| T_DollarSingleQuoted Id String | Inner_T_DollarSingleQuoted String
| T_DollarBraceCommandExpansion Id [Token] | Inner_T_DollarBraceCommandExpansion [t]
| T_Done Id | Inner_T_Done
| T_DoubleQuoted Id [Token] | Inner_T_DoubleQuoted [t]
| T_EOF Id | Inner_T_EOF
| T_Elif Id | Inner_T_Elif
| T_Else Id | Inner_T_Else
| T_Esac Id | Inner_T_Esac
| T_Extglob Id String [Token] | Inner_T_Extglob String [t]
| T_FdRedirect Id String Token | Inner_T_FdRedirect String t
| T_Fi Id | Inner_T_Fi
| T_For Id | Inner_T_For
| T_ForArithmetic Id Token Token Token [Token] | Inner_T_ForArithmetic t t t [t]
| T_ForIn Id String [Token] [Token] | Inner_T_ForIn String [t] [t]
| T_Function Id FunctionKeyword FunctionParentheses String Token | Inner_T_Function FunctionKeyword FunctionParentheses String t
| T_GREATAND Id | Inner_T_GREATAND
| T_Glob Id String | Inner_T_Glob String
| T_Greater Id | Inner_T_Greater
| T_HereDoc Id Dashed Quoted String [Token] | Inner_T_HereDoc Dashed Quoted String [t]
| T_HereString Id Token | Inner_T_HereString t
| T_If Id | Inner_T_If
| T_IfExpression Id [([Token],[Token])] [Token] | Inner_T_IfExpression [([t],[t])] [t]
| T_In Id | Inner_T_In
| T_IoFile Id Token Token | Inner_T_IoFile t t
| T_IoDuplicate Id Token String | Inner_T_IoDuplicate t String
| T_LESSAND Id | Inner_T_LESSAND
| T_LESSGREAT Id | Inner_T_LESSGREAT
| T_Lbrace Id | Inner_T_Lbrace
| T_Less Id | Inner_T_Less
| T_Literal Id String | Inner_T_Literal String
| T_Lparen Id | Inner_T_Lparen
| T_NEWLINE Id | Inner_T_NEWLINE
| T_NormalWord Id [Token] | Inner_T_NormalWord [t]
| T_OR_IF Id | Inner_T_OR_IF
| T_OrIf Id Token Token | Inner_T_OrIf t t
| T_ParamSubSpecialChar Id String -- e.g. '%' in ${foo%bar} or '/' in ${foo/bar/baz} | Inner_T_ParamSubSpecialChar String -- e.g. '%' in ${foo%bar} or '/' in ${foo/bar/baz}
| T_Pipeline Id [Token] [Token] -- [Pipe separators] [Commands] | Inner_T_Pipeline [t] [t] -- [Pipe separators] [Commands]
| T_ProcSub Id String [Token] | Inner_T_ProcSub String [t]
| T_Rbrace Id | Inner_T_Rbrace
| T_Redirecting Id [Token] Token | Inner_T_Redirecting [t] t
| T_Rparen Id | Inner_T_Rparen
| T_Script Id String [Token] | Inner_T_Script t [t] -- Shebang T_Literal, followed by script.
| T_Select Id | Inner_T_Select
| T_SelectIn Id String [Token] [Token] | Inner_T_SelectIn String [t] [t]
| T_Semi Id | Inner_T_Semi
| T_SimpleCommand Id [Token] [Token] | Inner_T_SimpleCommand [t] [t]
| T_SingleQuoted Id String | Inner_T_SingleQuoted String
| T_Subshell Id [Token] | Inner_T_Subshell [t]
| T_Then Id | Inner_T_Then
| T_Until Id | Inner_T_Until
| T_UntilExpression Id [Token] [Token] | Inner_T_UntilExpression [t] [t]
| T_While Id | Inner_T_While
| T_WhileExpression Id [Token] [Token] | Inner_T_WhileExpression [t] [t]
| T_Annotation Id [Annotation] Token | Inner_T_Annotation [Annotation] t
| T_Pipe Id String | Inner_T_Pipe String
| T_CoProc Id (Maybe String) Token | Inner_T_CoProc (Maybe String) t
| T_CoProcBody Id Token | Inner_T_CoProcBody t
| T_Include Id Token | Inner_T_Include t
| T_SourceCommand Id Token Token | Inner_T_SourceCommand t t
deriving (Show) | Inner_T_BatsTest t t
deriving (Show, Eq, Functor, Foldable, Traversable)
data Annotation = data Annotation =
DisableComment Integer DisableComment Integer Integer -- [from, to)
| EnableComment String
| SourceOverride String | SourceOverride String
| ShellOverride String | ShellOverride String
| SourcePath String
deriving (Show, Eq) deriving (Show, Eq)
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq) data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
-- This is an abomination. pattern T_AND_IF id = OuterToken id Inner_T_AND_IF
tokenEquals :: Token -> Token -> Bool pattern T_Bang id = OuterToken id Inner_T_Bang
tokenEquals a b = kludge a == kludge b pattern T_Case id = OuterToken id Inner_T_Case
where kludge s = Re.subRegex (Re.mkRegex "\\(Id [0-9]+\\)") (show s) "(Id 0)" pattern TC_Empty id typ = OuterToken id (Inner_TC_Empty typ)
pattern T_CLOBBER id = OuterToken id Inner_T_CLOBBER
pattern T_DGREAT id = OuterToken id Inner_T_DGREAT
pattern T_DLESS id = OuterToken id Inner_T_DLESS
pattern T_DLESSDASH id = OuterToken id Inner_T_DLESSDASH
pattern T_Do id = OuterToken id Inner_T_Do
pattern T_DollarSingleQuoted id str = OuterToken id (Inner_T_DollarSingleQuoted str)
pattern T_Done id = OuterToken id Inner_T_Done
pattern T_DSEMI id = OuterToken id Inner_T_DSEMI
pattern T_Elif id = OuterToken id Inner_T_Elif
pattern T_Else id = OuterToken id Inner_T_Else
pattern T_EOF id = OuterToken id Inner_T_EOF
pattern T_Esac id = OuterToken id Inner_T_Esac
pattern T_Fi id = OuterToken id Inner_T_Fi
pattern T_For id = OuterToken id Inner_T_For
pattern T_Glob id str = OuterToken id (Inner_T_Glob str)
pattern T_GREATAND id = OuterToken id Inner_T_GREATAND
pattern T_Greater id = OuterToken id Inner_T_Greater
pattern T_If id = OuterToken id Inner_T_If
pattern T_In id = OuterToken id Inner_T_In
pattern T_Lbrace id = OuterToken id Inner_T_Lbrace
pattern T_Less id = OuterToken id Inner_T_Less
pattern T_LESSAND id = OuterToken id Inner_T_LESSAND
pattern T_LESSGREAT id = OuterToken id Inner_T_LESSGREAT
pattern T_Literal id str = OuterToken id (Inner_T_Literal str)
pattern T_Lparen id = OuterToken id Inner_T_Lparen
pattern T_NEWLINE id = OuterToken id Inner_T_NEWLINE
pattern T_OR_IF id = OuterToken id Inner_T_OR_IF
pattern T_ParamSubSpecialChar id str = OuterToken id (Inner_T_ParamSubSpecialChar str)
pattern T_Pipe id str = OuterToken id (Inner_T_Pipe str)
pattern T_Rbrace id = OuterToken id Inner_T_Rbrace
pattern T_Rparen id = OuterToken id Inner_T_Rparen
pattern T_Select id = OuterToken id Inner_T_Select
pattern T_Semi id = OuterToken id Inner_T_Semi
pattern T_SingleQuoted id str = OuterToken id (Inner_T_SingleQuoted str)
pattern T_Then id = OuterToken id Inner_T_Then
pattern T_UnparsedIndex id pos str = OuterToken id (Inner_T_UnparsedIndex pos str)
pattern T_Until id = OuterToken id Inner_T_Until
pattern T_While id = OuterToken id Inner_T_While
pattern TA_Assignment id op t1 t2 = OuterToken id (Inner_TA_Assignment op t1 t2)
pattern TA_Binary id op t1 t2 = OuterToken id (Inner_TA_Binary op t1 t2)
pattern TA_Expansion id t = OuterToken id (Inner_TA_Expansion t)
pattern T_AndIf id t u = OuterToken id (Inner_T_AndIf t u)
pattern T_Annotation id anns t = OuterToken id (Inner_T_Annotation anns t)
pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c)
pattern T_Array id t = OuterToken id (Inner_T_Array t)
pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)
pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value)
pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3)
pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1)
pattern TA_Variable id str t = OuterToken id (Inner_TA_Variable str t)
pattern T_Backgrounded id l = OuterToken id (Inner_T_Backgrounded l)
pattern T_Backticked id list = OuterToken id (Inner_T_Backticked list)
pattern T_Banged id l = OuterToken id (Inner_T_Banged l)
pattern T_BatsTest id name t = OuterToken id (Inner_T_BatsTest name t)
pattern T_BraceExpansion id list = OuterToken id (Inner_T_BraceExpansion list)
pattern T_BraceGroup id l = OuterToken id (Inner_T_BraceGroup l)
pattern TC_And id typ str t1 t2 = OuterToken id (Inner_TC_And typ str t1 t2)
pattern T_CaseExpression id word cases = OuterToken id (Inner_T_CaseExpression word cases)
pattern TC_Binary id typ op lhs rhs = OuterToken id (Inner_TC_Binary typ op lhs rhs)
pattern TC_Group id typ token = OuterToken id (Inner_TC_Group typ token)
pattern TC_Nullary id typ token = OuterToken id (Inner_TC_Nullary typ token)
pattern T_Condition id typ token = OuterToken id (Inner_T_Condition typ token)
pattern T_CoProcBody id t = OuterToken id (Inner_T_CoProcBody t)
pattern T_CoProc id var body = OuterToken id (Inner_T_CoProc var body)
pattern TC_Or id typ str t1 t2 = OuterToken id (Inner_TC_Or typ str t1 t2)
pattern TC_Unary id typ op token = OuterToken id (Inner_TC_Unary typ op token)
pattern T_DollarArithmetic id c = OuterToken id (Inner_T_DollarArithmetic c)
pattern T_DollarBraceCommandExpansion id list = OuterToken id (Inner_T_DollarBraceCommandExpansion list)
pattern T_DollarBraced id braced op = OuterToken id (Inner_T_DollarBraced braced op)
pattern T_DollarBracket id c = OuterToken id (Inner_T_DollarBracket c)
pattern T_DollarDoubleQuoted id list = OuterToken id (Inner_T_DollarDoubleQuoted list)
pattern T_DollarExpansion id list = OuterToken id (Inner_T_DollarExpansion list)
pattern T_DoubleQuoted id list = OuterToken id (Inner_T_DoubleQuoted list)
pattern T_Extglob id str l = OuterToken id (Inner_T_Extglob str l)
pattern T_FdRedirect id v t = OuterToken id (Inner_T_FdRedirect v t)
pattern T_ForArithmetic id a b c group = OuterToken id (Inner_T_ForArithmetic a b c group)
pattern T_ForIn id v w l = OuterToken id (Inner_T_ForIn v w l)
pattern T_Function id a b name body = OuterToken id (Inner_T_Function a b name body)
pattern T_HereDoc id d q str l = OuterToken id (Inner_T_HereDoc d q str l)
pattern T_HereString id word = OuterToken id (Inner_T_HereString word)
pattern T_IfExpression id conditions elses = OuterToken id (Inner_T_IfExpression conditions elses)
pattern T_Include id script = OuterToken id (Inner_T_Include script)
pattern T_IndexedElement id indices t = OuterToken id (Inner_T_IndexedElement indices t)
pattern T_IoDuplicate id op num = OuterToken id (Inner_T_IoDuplicate op num)
pattern T_IoFile id op file = OuterToken id (Inner_T_IoFile op file)
pattern T_NormalWord id list = OuterToken id (Inner_T_NormalWord list)
pattern T_OrIf id t u = OuterToken id (Inner_T_OrIf t u)
pattern T_Pipeline id l1 l2 = OuterToken id (Inner_T_Pipeline l1 l2)
pattern T_ProcSub id typ l = OuterToken id (Inner_T_ProcSub typ l)
pattern T_Redirecting id redirs cmd = OuterToken id (Inner_T_Redirecting redirs cmd)
pattern T_Script id shebang list = OuterToken id (Inner_T_Script shebang list)
pattern T_SelectIn id v w l = OuterToken id (Inner_T_SelectIn v w l)
pattern T_SimpleCommand id vars cmds = OuterToken id (Inner_T_SimpleCommand vars cmds)
pattern T_SourceCommand id includer t_include = OuterToken id (Inner_T_SourceCommand includer t_include)
pattern T_Subshell id l = OuterToken id (Inner_T_Subshell l)
pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l)
pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l)
{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-}
instance Eq Token where instance Eq Token where
(==) = tokenEquals OuterToken _ a == OuterToken _ b = a == b
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> m Token) -> Token -> m Token analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> m Token) -> Token -> m Token
analyze f g i = analyze f g i =
round round
where where
round t = do round t@(OuterToken id it) = do
f t f t
newT <- delve t newIt <- traverse round it
g t g t
i newT i (OuterToken id newIt)
roundAll = mapM round
dl l v = do
x <- roundAll l
return $ v x
dll l m v = do
x <- roundAll l
y <- roundAll m
return $ v x y
d1 t v = do
x <- round t
return $ v x
d2 t1 t2 v = do
x <- round t1
y <- round t2
return $ v x y
delve (T_NormalWord id list) = dl list $ T_NormalWord id
delve (T_DoubleQuoted id list) = dl list $ T_DoubleQuoted id
delve (T_DollarDoubleQuoted id list) = dl list $ T_DollarDoubleQuoted id
delve (T_DollarExpansion id list) = dl list $ T_DollarExpansion id
delve (T_DollarBraceCommandExpansion id list) = dl list $ T_DollarBraceCommandExpansion id
delve (T_BraceExpansion id list) = dl list $ T_BraceExpansion id
delve (T_Backticked id list) = dl list $ T_Backticked id
delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id
delve (T_DollarBracket id c) = d1 c $ T_DollarBracket id
delve (T_IoFile id op file) = d2 op file $ T_IoFile id
delve (T_IoDuplicate id op num) = d1 op $ \x -> T_IoDuplicate id x num
delve (T_HereString id word) = d1 word $ T_HereString id
delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v
delve (T_Assignment id mode var indices value) = do
a <- roundAll indices
b <- round value
return $ T_Assignment id mode var a b
delve (T_Array id t) = dl t $ T_Array id
delve (T_IndexedElement id indices t) = do
a <- roundAll indices
b <- round t
return $ T_IndexedElement id a b
delve (T_Redirecting id redirs cmd) = do
a <- roundAll redirs
b <- round cmd
return $ T_Redirecting id a b
delve (T_SimpleCommand id vars cmds) = dll vars cmds $ T_SimpleCommand id
delve (T_Pipeline id l1 l2) = dll l1 l2 $ T_Pipeline id
delve (T_Banged id l) = d1 l $ T_Banged id
delve (T_AndIf id t u) = d2 t u $ T_AndIf id
delve (T_OrIf id t u) = d2 t u $ T_OrIf id
delve (T_Backgrounded id l) = d1 l $ T_Backgrounded id
delve (T_Subshell id l) = dl l $ T_Subshell id
delve (T_ProcSub id typ l) = dl l $ T_ProcSub id typ
delve (T_Arithmetic id c) = d1 c $ T_Arithmetic id
delve (T_IfExpression id conditions elses) = do
newConds <- mapM (\(c, t) -> do
x <- mapM round c
y <- mapM round t
return (x,y)
) conditions
newElses <- roundAll elses
return $ T_IfExpression id newConds newElses
delve (T_BraceGroup id l) = dl l $ T_BraceGroup id
delve (T_WhileExpression id c l) = dll c l $ T_WhileExpression id
delve (T_UntilExpression id c l) = dll c l $ T_UntilExpression id
delve (T_ForIn id v w l) = dll w l $ T_ForIn id v
delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v
delve (T_CaseExpression id word cases) = do
newWord <- round word
newCases <- mapM (\(o, c, t) -> do
x <- mapM round c
y <- mapM round t
return (o, x,y)
) cases
return $ T_CaseExpression id newWord newCases
delve (T_ForArithmetic id a b c group) = do
x <- round a
y <- round b
z <- round c
list <- mapM round group
return $ T_ForArithmetic id x y z list
delve (T_Script id s l) = dl l $ T_Script id s
delve (T_Function id a b name body) = d1 body $ T_Function id a b name
delve (T_Condition id typ token) = d1 token $ T_Condition id typ
delve (T_Extglob id str l) = dl l $ T_Extglob id str
delve (T_DollarBraced id op) = d1 op $ T_DollarBraced id
delve (T_HereDoc id d q str l) = dl l $ T_HereDoc id d q str
delve (TC_And id typ str t1 t2) = d2 t1 t2 $ TC_And id typ str
delve (TC_Or id typ str t1 t2) = d2 t1 t2 $ TC_Or id typ str
delve (TC_Group id typ token) = d1 token $ TC_Group id typ
delve (TC_Binary id typ op lhs rhs) = d2 lhs rhs $ TC_Binary id typ op
delve (TC_Unary id typ op token) = d1 token $ TC_Unary id typ op
delve (TC_Nullary id typ token) = d1 token $ TC_Nullary id typ
delve (TA_Binary id op t1 t2) = d2 t1 t2 $ TA_Binary id op
delve (TA_Assignment id op t1 t2) = d2 t1 t2 $ TA_Assignment id op
delve (TA_Unary id op t1) = d1 t1 $ TA_Unary id op
delve (TA_Sequence id l) = dl l $ TA_Sequence id
delve (TA_Trinary id t1 t2 t3) = do
a <- round t1
b <- round t2
c <- round t3
return $ TA_Trinary id a b c
delve (TA_Expansion id t) = dl t $ TA_Expansion id
delve (TA_Variable id str t) = dl t $ TA_Variable id str
delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns
delve (T_CoProc id var body) = d1 body $ T_CoProc id var
delve (T_CoProcBody id t) = d1 t $ T_CoProcBody id
delve (T_Include id script) = d1 script $ T_Include id
delve (T_SourceCommand id includer t_include) = d2 includer t_include $ T_SourceCommand id
delve t = return t
getId :: Token -> Id getId :: Token -> Id
getId t = case t of getId (OuterToken id _) = id
T_AND_IF id -> id
T_OR_IF id -> id
T_DSEMI id -> id
T_Semi id -> id
T_DLESS id -> id
T_DGREAT id -> id
T_LESSAND id -> id
T_GREATAND id -> id
T_LESSGREAT id -> id
T_DLESSDASH id -> id
T_CLOBBER id -> id
T_If id -> id
T_Then id -> id
T_Else id -> id
T_Elif id -> id
T_Fi id -> id
T_Do id -> id
T_Done id -> id
T_Case id -> id
T_Esac id -> id
T_While id -> id
T_Until id -> id
T_For id -> id
T_Select id -> id
T_Lbrace id -> id
T_Rbrace id -> id
T_Lparen id -> id
T_Rparen id -> id
T_Bang id -> id
T_In id -> id
T_NEWLINE id -> id
T_EOF id -> id
T_Less id -> id
T_Greater id -> id
T_SingleQuoted id _ -> id
T_Literal id _ -> id
T_NormalWord id _ -> id
T_DoubleQuoted id _ -> id
T_DollarExpansion id _ -> id
T_DollarBraced id _ -> id
T_DollarArithmetic id _ -> id
T_BraceExpansion id _ -> id
T_ParamSubSpecialChar id _ -> id
T_DollarBraceCommandExpansion id _ -> id
T_IoFile id _ _ -> id
T_IoDuplicate id _ _ -> id
T_HereDoc id _ _ _ _ -> id
T_HereString id _ -> id
T_FdRedirect id _ _ -> id
T_Assignment id _ _ _ _ -> id
T_Array id _ -> id
T_IndexedElement id _ _ -> id
T_Redirecting id _ _ -> id
T_SimpleCommand id _ _ -> id
T_Pipeline id _ _ -> id
T_Banged id _ -> id
T_AndIf id _ _ -> id
T_OrIf id _ _ -> id
T_Backgrounded id _ -> id
T_IfExpression id _ _ -> id
T_Subshell id _ -> id
T_BraceGroup id _ -> id
T_WhileExpression id _ _ -> id
T_UntilExpression id _ _ -> id
T_ForIn id _ _ _ -> id
T_SelectIn id _ _ _ -> id
T_CaseExpression id _ _ -> id
T_Function id _ _ _ _ -> id
T_Arithmetic id _ -> id
T_Script id _ _ -> id
T_Condition id _ _ -> id
T_Extglob id _ _ -> id
T_Backticked id _ -> id
TC_And id _ _ _ _ -> id
TC_Or id _ _ _ _ -> id
TC_Group id _ _ -> id
TC_Binary id _ _ _ _ -> id
TC_Unary id _ _ _ -> id
TC_Nullary id _ _ -> id
TA_Binary id _ _ _ -> id
TA_Assignment id _ _ _ -> id
TA_Unary id _ _ -> id
TA_Sequence id _ -> id
TA_Trinary id _ _ _ -> id
TA_Expansion id _ -> id
T_ProcSub id _ _ -> id
T_Glob id _ -> id
T_ForArithmetic id _ _ _ _ -> id
T_DollarSingleQuoted id _ -> id
T_DollarDoubleQuoted id _ -> id
T_DollarBracket id _ -> id
T_Annotation id _ _ -> id
T_Pipe id _ -> id
T_CoProc id _ _ -> id
T_CoProcBody id _ -> id
T_Include id _ -> id
T_SourceCommand id _ _ -> id
T_UnparsedIndex id _ _ -> id
TC_Empty id _ -> id
TA_Variable id _ _ -> id
blank :: Monad m => Token -> m () blank :: Monad m => Token -> m ()
blank = const $ return () blank = const $ return ()

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -17,15 +17,25 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.ASTLib where module ShellCheck.ASTLib where
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.Regex
import Control.Monad.Writer import Control.Monad.Writer
import Control.Monad import Control.Monad
import Data.Char
import Data.Functor import Data.Functor
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)
import Test.QuickCheck
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
@@ -45,6 +55,7 @@ willSplit x =
T_BraceExpansion {} -> True T_BraceExpansion {} -> True
T_Glob {} -> True T_Glob {} -> True
T_Extglob {} -> True T_Extglob {} -> True
T_DoubleQuoted _ l -> any willBecomeMultipleArgs l
T_NormalWord _ l -> any willSplit l T_NormalWord _ l -> any willSplit l
_ -> False _ -> False
@@ -80,7 +91,7 @@ oversimplify token =
(T_NormalWord _ l) -> [concat (concatMap oversimplify l)] (T_NormalWord _ l) -> [concat (concatMap oversimplify l)]
(T_DoubleQuoted _ l) -> [concat (concatMap oversimplify l)] (T_DoubleQuoted _ l) -> [concat (concatMap oversimplify l)]
(T_SingleQuoted _ s) -> [s] (T_SingleQuoted _ s) -> [s]
(T_DollarBraced _ _) -> ["${VAR}"] (T_DollarBraced _ _ _) -> ["${VAR}"]
(T_DollarArithmetic _ _) -> ["${VAR}"] (T_DollarArithmetic _ _) -> ["${VAR}"]
(T_DollarExpansion _ _) -> ["${VAR}"] (T_DollarExpansion _ _) -> ["${VAR}"]
(T_Backticked _ _) -> ["${VAR}"] (T_Backticked _ _) -> ["${VAR}"]
@@ -131,13 +142,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
@@ -145,8 +238,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
@@ -174,9 +267,13 @@ willConcatInAssignment token =
getLiteralString :: Token -> Maybe String getLiteralString :: Token -> Maybe String
getLiteralString = getLiteralStringExt (const Nothing) getLiteralString = getLiteralStringExt (const Nothing)
-- Definitely get a literal string, with a given default for all non-literals
getLiteralStringDef :: String -> Token -> String
getLiteralStringDef x = runIdentity . getLiteralStringExt (const $ return x)
-- Definitely get a literal string, skipping over all non-literals -- Definitely get a literal string, skipping over all non-literals
onlyLiteralString :: Token -> String onlyLiteralString :: Token -> String
onlyLiteralString = fromJust . getLiteralStringExt (const $ return "") onlyLiteralString = getLiteralStringDef ""
-- Maybe get a literal string, but only if it's an unquoted argument. -- Maybe get a literal string, but only if it's an unquoted argument.
getUnquotedLiteral (T_NormalWord _ list) = getUnquotedLiteral (T_NormalWord _ list) =
@@ -186,6 +283,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
@@ -204,8 +307,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
@@ -215,7 +321,7 @@ getGlobOrLiteralString = getLiteralStringExt f
-- Maybe get the literal value of a token, using a custom function -- Maybe get the literal value of a token, using a custom function
-- to map unrecognized Tokens into strings. -- to map unrecognized Tokens into strings.
getLiteralStringExt :: (Token -> Maybe String) -> Token -> Maybe String getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String
getLiteralStringExt more = g getLiteralStringExt more = g
where where
allInList = fmap concat . mapM g allInList = fmap concat . mapM g
@@ -226,11 +332,77 @@ getLiteralStringExt more = g
g (T_SingleQuoted _ s) = return s g (T_SingleQuoted _ s) = return s
g (T_Literal _ s) = return s g (T_Literal _ s) = return s
g (T_ParamSubSpecialChar _ s) = return s g (T_ParamSubSpecialChar _ s) = return s
g (T_DollarSingleQuoted _ s) = return $ decodeEscapes s
g x = more x g x = more x
-- Bash style $'..' decoding
decodeEscapes ('\\':c:cs) =
case c of
'a' -> '\a' : rest
'b' -> '\b' : rest
'e' -> '\x1B' : rest
'f' -> '\f' : rest
'n' -> '\n' : rest
'r' -> '\r' : rest
't' -> '\t' : rest
'v' -> '\v' : rest
'\'' -> '\'' : rest
'"' -> '"' : rest
'\\' -> '\\' : rest
'x' ->
case cs of
(x:y:more) ->
if isHexDigit x && isHexDigit y
then chr (16*(digitToInt x) + (digitToInt y)) : rest
else '\\':c:rest
_ | isOctDigit c ->
let digits = take 3 $ takeWhile isOctDigit (c:cs)
num = parseOct digits
in (if num < 256 then chr num else '?') : rest
_ -> '\\' : c : rest
where
rest = decodeEscapes cs
parseOct = f 0
where
f n "" = n
f n (c:rest) = f (n * 8 + digitToInt c) rest
decodeEscapes (c:cs) = c : decodeEscapes cs
decodeEscapes [] = []
-- 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
@@ -257,17 +429,50 @@ getCommand t =
T_Annotation _ _ t -> getCommand t T_Annotation _ _ t -> getCommand t
_ -> Nothing _ -> Nothing
-- Maybe get the command name of a token representing a command -- Maybe get the command name string of a token representing a command
getCommandName t = do getCommandName :: Token -> Maybe String
(T_SimpleCommand _ _ (w:rest)) <- getCommand t getCommandName = fst . getCommandNameAndToken False
-- Maybe get the name+arguments of a command.
getCommandArgv t = do
(T_SimpleCommand _ _ args@(_:_)) <- getCommand t
return args
-- Get the command name token from a command, i.e.
-- the token representing 'ls' in 'ls -la 2> foo'.
-- If it can't be determined, return the original token.
getCommandTokenOrThis = snd . getCommandNameAndToken False
-- Given a command, get the string and token that represents the command name.
-- If direct, return the actual command (e.g. exec in 'exec ls')
-- 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:_) -> getLiteralString applet return (getLiteralString actual, actual)
_ -> return s where
else getEffectiveCommandToken str cmd args =
return s 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"
@@ -284,8 +489,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
@@ -305,22 +510,34 @@ isOnlyRedirection t =
isFunction t = case t of T_Function {} -> True; _ -> False isFunction t = case t of T_Function {} -> True; _ -> False
-- Bats tests are functions for the purpose of 'local' and such
isFunctionLike t =
case t of
T_Function {} -> True
T_BatsTest {} -> True
_ -> False
isBraceExpansion t = case t of T_BraceExpansion {} -> True; _ -> False isBraceExpansion t = case t of T_BraceExpansion {} -> True; _ -> False
-- Get the lists of commands from tokens that contain them, such as -- Get the lists of commands from tokens that contain them, such as
-- the body of while loops or branches of if statements. -- the conditions and bodies of while loops or branches of if statements.
getCommandSequences :: Token -> [[Token]] getCommandSequences :: Token -> [[Token]]
getCommandSequences t = getCommandSequences t =
case t of case t of
T_Script _ _ cmds -> [cmds] T_Script _ _ cmds -> [cmds]
T_BraceGroup _ cmds -> [cmds] T_BraceGroup _ cmds -> [cmds]
T_Subshell _ cmds -> [cmds] T_Subshell _ cmds -> [cmds]
T_WhileExpression _ _ cmds -> [cmds] T_WhileExpression _ cond cmds -> [cond, cmds]
T_UntilExpression _ _ cmds -> [cmds] T_UntilExpression _ cond cmds -> [cond, cmds]
T_ForIn _ _ _ cmds -> [cmds] T_ForIn _ _ _ cmds -> [cmds]
T_ForArithmetic _ _ _ _ cmds -> [cmds] T_ForArithmetic _ _ _ _ cmds -> [cmds]
T_IfExpression _ thens elses -> map snd thens ++ [elses] T_IfExpression _ thens elses -> (concatMap (\(a,b) -> [a,b]) thens) ++ [elses]
T_Annotation _ _ t -> getCommandSequences t T_Annotation _ _ t -> getCommandSequences t
T_DollarExpansion _ cmds -> [cmds]
T_DollarBraceCommandExpansion _ cmds -> [cmds]
T_Backticked _ cmds -> [cmds]
_ -> [] _ -> []
-- Get a list of names of associative arrays -- Get a list of names of associative arrays
@@ -328,13 +545,13 @@ getAssociativeArrays t =
nub . execWriter $ doAnalysis f t nub . execWriter $ doAnalysis f t
where where
f :: Token -> Writer [String] () f :: Token -> Writer [String] ()
f t@T_SimpleCommand {} = fromMaybe (return ()) $ 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 ()
@@ -352,38 +569,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
@@ -433,14 +648,71 @@ 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,})?
isQuoteableExpansion t = case t of isQuoteableExpansion t = case t of
T_DollarBraced {} -> True
_ -> isCommandSubstitution t
isCommandSubstitution t = case t of
T_DollarExpansion {} -> True T_DollarExpansion {} -> True
T_DollarBraceCommandExpansion {} -> True T_DollarBraceCommandExpansion {} -> True
T_Backticked {} -> True T_Backticked {} -> True
T_DollarBraced {} -> 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?
isAnnotationIgnoringCode code t =
case t of
T_Annotation _ anns _ -> any hasNum anns
_ -> False
where
hasNum (DisableComment from to) = code >= from && code < to
hasNum _ = False
prop_executableFromShebang1 = executableFromShebang "/bin/sh" == "sh"
prop_executableFromShebang2 = executableFromShebang "/bin/bash" == "bash"
prop_executableFromShebang3 = executableFromShebang "/usr/bin/env ksh" == "ksh"
prop_executableFromShebang4 = executableFromShebang "/usr/bin/env -S foo=bar bash -x" == "bash"
prop_executableFromShebang5 = executableFromShebang "/usr/bin/env --split-string=bash -x" == "bash"
prop_executableFromShebang6 = executableFromShebang "/usr/bin/env --split-string=foo=bar bash -x" == "bash"
prop_executableFromShebang7 = executableFromShebang "/usr/bin/env --split-string bash -x" == "bash"
prop_executableFromShebang8 = executableFromShebang "/usr/bin/env --split-string foo=bar bash -x" == "bash"
prop_executableFromShebang9 = executableFromShebang "/usr/bin/env foo=bar dash" == "dash"
prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "ash"
prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "ash"
-- Get the shell executable from a string like '/usr/bin/env bash'
executableFromShebang :: String -> String
executableFromShebang = shellFor
where
re = mkRegex "/env +(-S|--split-string=?)? *(.*)"
shellFor s | s `matches` re =
case matchRegex re s of
Just [flag, shell] -> fromEnvArgs (words shell)
_ -> ""
shellFor sb =
case words sb of
[] -> ""
[x] -> basename x
(first:second:args) | basename first == "busybox" ->
case basename second of
"sh" -> "ash" -- busybox sh is ash
x -> x
(first:args) | basename first == "env" ->
fromEnvArgs args
(first:_) -> basename first
fromEnvArgs args = fromMaybe "" $ find (notElem '=') $ skipFlags args
basename s = reverse . takeWhile (/= '/') . reverse $ s
skipFlags = dropWhile ("-" `isPrefixOf`)
return []
runTests = $quickCheckAll

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -17,7 +17,7 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
module ShellCheck.Analyzer (analyzeScript) where module ShellCheck.Analyzer (analyzeScript, ShellCheck.Analyzer.optionalChecks) where
import ShellCheck.Analytics import ShellCheck.Analytics
import ShellCheck.AnalyzerLib import ShellCheck.AnalyzerLib
@@ -25,21 +25,28 @@ import ShellCheck.Interface
import Data.List import Data.List
import Data.Monoid import Data.Monoid
import qualified ShellCheck.Checks.Commands import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.Custom
import qualified ShellCheck.Checks.ShellSupport import qualified ShellCheck.Checks.ShellSupport
-- TODO: Clean up the cruft this is layered on -- TODO: Clean up the cruft this is layered on
analyzeScript :: AnalysisSpec -> AnalysisResult analyzeScript :: AnalysisSpec -> AnalysisResult
analyzeScript spec = AnalysisResult { analyzeScript spec = newAnalysisResult {
arComments = arComments =
filterByAnnotation spec params . nub $ filterByAnnotation spec params . nub $
runAnalytics spec runAnalytics spec
++ runChecker params (checkers params) ++ runChecker params (checkers spec params)
} }
where where
params = makeParameters spec params = makeParameters spec
checkers params = mconcat $ map ($ params) [ checkers spec params = mconcat $ map ($ params) [
ShellCheck.Checks.Commands.checker, ShellCheck.Checks.Commands.checker spec,
ShellCheck.Checks.Custom.checker,
ShellCheck.Checks.ShellSupport.checker ShellCheck.Checks.ShellSupport.checker
] ]
optionalChecks = mconcat $ [
ShellCheck.Analytics.optionalChecks,
ShellCheck.Checks.Commands.optionalChecks
]

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -20,26 +20,28 @@
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
module ShellCheck.AnalyzerLib where module ShellCheck.AnalyzerLib where
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Regex
import Control.Arrow (first) import ShellCheck.AST
import Control.Monad.Identity import ShellCheck.ASTLib
import Control.Monad.RWS import ShellCheck.Data
import Control.Monad.State import ShellCheck.Interface
import Control.Monad.Writer import ShellCheck.Parser
import Data.Char import ShellCheck.Regex
import Data.List
import qualified Data.Map as Map
import Data.Maybe
import Data.Semigroup
import Test.QuickCheck.All (forAllProperties) import Control.Arrow (first)
import Test.QuickCheck.Test (maxSuccess, quickCheckWithResult, stdArgs) import Control.DeepSeq
import Control.Monad.Identity
import Control.Monad.RWS
import Control.Monad.State
import Control.Monad.Writer
import Data.Char
import Data.List
import Data.Maybe
import Data.Semigroup
import qualified Data.Map as Map
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (maxSuccess, quickCheckWithResult, stdArgs)
type Analysis = AnalyzerM () type Analysis = AnalyzerM ()
type AnalyzerM a = RWS Parameters [TokenComment] Cache a type AnalyzerM a = RWS Parameters [TokenComment] Cache a
@@ -75,14 +77,23 @@ composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis
composeAnalyzers f g x = f x >> g x composeAnalyzers f g x = f x >> g x
data Parameters = Parameters { data Parameters = Parameters {
hasLastpipe :: Bool, -- Whether this script has the 'lastpipe' option set/default. -- Whether this script has the 'lastpipe' option set/default.
hasSetE :: Bool, -- Whether this script has 'set -e' anywhere. hasLastpipe :: Bool,
variableFlow :: [StackData], -- A linear (bad) analysis of data flow -- Whether this script has 'set -e' anywhere.
parentMap :: Map.Map Id Token, -- A map from Id to parent Token hasSetE :: Bool,
shellType :: Shell, -- The shell type, such as Bash or Ksh -- A linear (bad) analysis of data flow
shellTypeSpecified :: Bool, -- True if shell type was forced via flags variableFlow :: [StackData],
rootNode :: Token -- The root node of the AST -- A map from Id to parent Token
} parentMap :: Map.Map Id Token,
-- The shell type, such as Bash or Ksh
shellType :: Shell,
-- True if shell type was forced via flags
shellTypeSpecified :: Bool,
-- The root node of the AST
rootNode :: Token,
-- map from token id to start and end position
tokenPositions :: Map.Map Id (Position, Position)
} deriving (Show)
-- TODO: Cache results of common AST ops here -- TODO: Cache results of common AST ops here
data Cache = Cache {} data Cache = Cache {}
@@ -109,35 +120,42 @@ data DataSource =
data VariableState = Dead Token String | Alive deriving (Show) data VariableState = Dead Token String | Alive deriving (Show)
defaultSpec root = AnalysisSpec { defaultSpec pr = spec {
asScript = root,
asShellType = Nothing, asShellType = Nothing,
asCheckSourced = False, asCheckSourced = False,
asExecutionMode = Executed asExecutionMode = Executed,
} asTokenPositions = prTokenPositions pr
} where spec = newAnalysisSpec (fromJust $ prRoot pr)
pScript s = pScript s =
let let
pSpec = ParseSpec { pSpec = newParseSpec {
psFilename = "script", psFilename = "script",
psScript = s, psScript = s
psCheckSourced = False
} }
in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec in runIdentity $ parseScript (mockedSystemInterface []) pSpec
-- For testing. If parsed, returns whether there are any comments -- For testing. If parsed, returns whether there are any comments
producesComments :: Checker -> String -> Maybe Bool producesComments :: Checker -> String -> Maybe Bool
producesComments c s = do producesComments c s = do
root <- pScript s let pr = pScript s
let spec = defaultSpec root prRoot pr
let spec = defaultSpec pr
let params = makeParameters spec let params = makeParameters spec
return . not . null $ runChecker params c return . not . null $ runChecker params c
makeComment :: Severity -> Id -> Code -> String -> TokenComment makeComment :: Severity -> Id -> Code -> String -> TokenComment
makeComment severity id code note = makeComment severity id code note =
TokenComment id $ Comment severity code note newTokenComment {
tcId = id,
tcComment = newComment {
cSeverity = severity,
cCode = code,
cMessage = note
}
}
addComment note = tell [note] addComment note = note `deepseq` tell [note]
warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m () warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m ()
warn id code str = addComment $ makeComment WarningC id code str warn id code str = addComment $ makeComment WarningC id code str
@@ -145,10 +163,29 @@ 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 = addCommentWithFix WarningC
styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
styleWithFix = addCommentWithFix StyleC
addCommentWithFix :: MonadWriter [TokenComment] m => Severity -> Id -> Code -> String -> Fix -> m ()
addCommentWithFix severity id code str fix =
addComment $ makeCommentWithFix severity id code str fix
makeCommentWithFix :: Severity -> Id -> Code -> String -> Fix -> TokenComment
makeCommentWithFix severity id code str fix =
let comment = makeComment severity id code str
withFix = comment {
tcFix = Just fix
}
in force withFix
makeParameters spec = makeParameters spec =
let params = Parameters { let params = Parameters {
rootNode = root, rootNode = root,
shellType = fromMaybe (determineShell root) $ asShellType spec, shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec,
hasSetE = containsSetE root, hasSetE = containsSetE root,
hasLastpipe = hasLastpipe =
case shellType params of case shellType params of
@@ -157,9 +194,10 @@ makeParameters spec =
Sh -> False Sh -> False
Ksh -> True, Ksh -> True,
shellTypeSpecified = isJust $ asShellType spec, shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
parentMap = getParentTree root, parentMap = getParentTree root,
variableFlow = getVariableFlow params root variableFlow = getVariableFlow params root,
tokenPositions = asTokenPositions spec
} in params } in params
where root = asScript spec where root = asScript spec
@@ -170,7 +208,7 @@ containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
where where
isSetE t = isSetE t =
case t of case t of
T_Script _ str _ -> str `matches` re T_Script _ (T_Literal _ str) _ -> str `matches` re
T_SimpleCommand {} -> T_SimpleCommand {} ->
t `isUnqualifiedCommand` "set" && t `isUnqualifiedCommand` "set" &&
("errexit" `elem` oversimplify t || ("errexit" `elem` oversimplify t ||
@@ -191,53 +229,44 @@ containsLastpipe root =
_ -> False _ -> False
prop_determineShell0 = determineShell (fromJust $ pScript "#!/bin/sh") == Sh prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh
prop_determineShell1 = determineShell (fromJust $ pScript "#!/usr/bin/env ksh") == Ksh prop_determineShell1 = determineShellTest "#!/usr/bin/env ksh" == Ksh
prop_determineShell2 = determineShell (fromJust $ pScript "") == Bash prop_determineShell2 = determineShellTest "" == Bash
prop_determineShell3 = determineShell (fromJust $ pScript "#!/bin/sh -e") == Sh prop_determineShell3 = determineShellTest "#!/bin/sh -e" == Sh
prop_determineShell4 = determineShell (fromJust $ pScript prop_determineShell4 = determineShellTest "#!/bin/ksh\n#shellcheck shell=sh\nfoo" == Sh
"#!/bin/ksh\n#shellcheck shell=sh\nfoo") == Sh prop_determineShell5 = determineShellTest "#shellcheck shell=sh\nfoo" == Sh
prop_determineShell5 = determineShell (fromJust $ pScript prop_determineShell6 = determineShellTest "#! /bin/sh" == Sh
"#shellcheck shell=sh\nfoo") == Sh prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
prop_determineShell6 = determineShell (fromJust $ pScript "#! /bin/sh") == Sh prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh
prop_determineShell7 = determineShell (fromJust $ pScript "#! /bin/ash") == Dash prop_determineShell9 = determineShellTest "#!/bin/env -S dash -x" == Dash
determineShell t = fromMaybe Bash $ do prop_determineShell10 = determineShellTest "#!/bin/env --split-string= dash -x" == Dash
shellString <- foldl mplus Nothing $ getCandidates t prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == Dash -- busybox sh is a specific shell, not posix sh
shellForExecutable shellString prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == Dash
determineShellTest = determineShellTest' Nothing
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
determineShell fallbackShell t = fromMaybe Bash $
shellForExecutable shellString `mplus` fallbackShell
where where
forAnnotation t = shellString = getCandidate t
case t of getCandidate :: Token -> String
(ShellOverride s) -> return s getCandidate t@T_Script {} = fromShebang t
_ -> fail "" getCandidate (T_Annotation _ annotations s) =
getCandidates :: Token -> [Maybe String] headOrDefault (fromShebang s) [s | ShellOverride s <- annotations]
getCandidates t@T_Script {} = [Just $ fromShebang t] fromShebang (T_Script _ (T_Literal _ s) _) = executableFromShebang s
getCandidates (T_Annotation _ annotations s) =
map forAnnotation annotations ++
[Just $ fromShebang s]
fromShebang (T_Script _ s t) = executableFromShebang s
-- Given a string like "/bin/bash" or "/usr/bin/env dash",
-- return the shell basename like "bash" or "dash"
executableFromShebang :: String -> String
executableFromShebang = shellFor
where
shellFor s | "/env " `isInfixOf` s = head (drop 1 (words s)++[""])
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
-- 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
(_:rest, map) <- get (x, map) <- get
case rest of [] -> put (rest, map) case x of
(x:_) -> put (rest, Map.insert (getId t) x map) _:rest -> case rest of [] -> put (rest, map)
(x:_) -> put (rest, Map.insert (getId t) x map)
-- Given a root node, make a map from Id to Token -- Given a root node, make a map from Id to Token
getTokenMap :: Token -> Map.Map Id Token getTokenMap :: Token -> Map.Map Id Token
@@ -259,15 +288,15 @@ isQuoteFree = isQuoteFreeNode False
isQuoteFreeNode strict tree t = isQuoteFreeNode strict tree t =
(isQuoteFreeElement t == Just True) || isQuoteFreeElement t ||
head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False]) 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 =
@@ -320,8 +349,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)
@@ -330,8 +359,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.
@@ -343,29 +372,24 @@ 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
parents params = getPath (parentMap params) parents params = getPath (parentMap params)
pathTo t = do
parents <- reader parentMap
return $ getPath parents t
-- 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
@@ -380,8 +404,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
@@ -398,6 +421,7 @@ getVariableFlow params t =
assignFirst T_ForIn {} = True assignFirst T_ForIn {} = True
assignFirst T_SelectIn {} = True assignFirst T_SelectIn {} = True
assignFirst (T_BatsTest {}) = True
assignFirst _ = False assignFirst _ = False
setRead t = setRead t =
@@ -415,9 +439,10 @@ leadType params t =
T_Backticked _ _ -> SubshellScope "`..` expansion" T_Backticked _ _ -> SubshellScope "`..` expansion"
T_Backgrounded _ _ -> SubshellScope "backgrounding &" T_Backgrounded _ _ -> SubshellScope "backgrounding &"
T_Subshell _ _ -> SubshellScope "(..) group" T_Subshell _ _ -> SubshellScope "(..) group"
T_BatsTest {} -> SubshellScope "@bats test"
T_CoProcBody _ _ -> SubshellScope "coproc" T_CoProcBody _ _ -> SubshellScope "coproc"
T_Redirecting {} -> T_Redirecting {} ->
if fromMaybe False causesSubshell if causesSubshell == Just True
then SubshellScope "pipeline" then SubshellScope "pipeline"
else NoneScope else NoneScope
_ -> NoneScope _ -> NoneScope
@@ -430,53 +455,57 @@ 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])
T_BatsTest {} -> [
(t, t, "lines", DataArray SourceExternal),
(t, t, "status", DataString SourceInteger),
(t, t, "output", DataString SourceExternal)
]
-- 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)
T_DollarBraced _ l -> maybeToList $ do TC_Unary _ _ "-n" token -> markAsChecked t token
let string = bracedString t TC_Unary _ _ "-z" token -> markAsChecked t token
TC_Nullary _ _ token -> markAsChecked t token
T_DollarBraced _ _ l -> maybeToList $ do
let string = concat $ oversimplify l
let modifier = getBracedModifier string let modifier = getBracedModifier string
guard $ ":=" `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
@@ -484,6 +513,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
@@ -503,14 +540,14 @@ 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 []
"readonly" -> "local" -> if "x" `elem` flags
if any (`elem` flags) ["f", "p"] then concatMap getReference rest
then [] else []
else concatMap getReference rest
"trap" -> "trap" ->
case rest of case rest of
head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head head:_ -> map (\x -> (base, head, x)) $ getVariablesFromLiteralToken head
_ -> [] _ -> []
"alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token]
_ -> [] _ -> []
where where
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)] getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
@@ -520,12 +557,28 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
getReferencedVariableCommand _ = [] getReferencedVariableCommand _ = []
getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) = -- The function returns a tuple consisting of four items describing an assignment.
-- Given e.g. declare foo=bar
-- (
-- BaseCommand :: Token, -- The command/structure assigning the variable, i.e. declare foo=bar
-- AssignmentToken :: Token, -- The specific part that assigns this variable, i.e. foo=bar
-- VariableName :: String, -- The variable name, i.e. foo
-- VariableValue :: DataType -- A description of the value being assigned, i.e. "Literal string with value foo"
-- )
getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T_Literal _ x:_):rest)) =
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $ filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
case x of case x of
"builtin" ->
getModifiedVariableCommand $ T_SimpleCommand id cmdPrefix rest
"read" -> "read" ->
let params = map getLiteral rest in let fallback = catMaybes $ takeWhile isJust (reverse $ map getLiteral rest)
catMaybes . takeWhile isJust . reverse $ params in fromMaybe fallback $ do
parsed <- getGnuOpts flagsForRead rest
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
@@ -553,11 +606,15 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
"mapfile" -> maybeToList $ getMapfileArray base rest "mapfile" -> maybeToList $ getMapfileArray base rest
"readarray" -> maybeToList $ getMapfileArray base rest "readarray" -> maybeToList $ getMapfileArray base rest
"DEFINE_boolean" -> maybeToList $ getFlagVariable rest
"DEFINE_float" -> maybeToList $ getFlagVariable rest
"DEFINE_integer" -> maybeToList $ getFlagVariable rest
"DEFINE_string" -> maybeToList $ getFlagVariable rest
_ -> [] _ -> []
where where
flags = map snd $ getAllFlags base flags = map snd $ getAllFlags base
stripEquals s = let rest = dropWhile (/= '=') s in stripEquals s = drop 1 $ dropWhile (/= '=') s
if rest == "" then "" else tail rest
stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) = stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) =
T_NormalWord id1 (T_Literal id2 (stripEquals s):rs) T_NormalWord id1 (T_Literal id2 (stripEquals s):rs)
stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) = stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) =
@@ -568,10 +625,14 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
where where
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
getLiteral t = do getLiteralOfDataType t d = do
s <- getLiteralString t s <- getLiteralString t
when ("-" `isPrefixOf` s) $ fail "argument" when ("-" `isPrefixOf` s) $ fail "argument"
return (base, t, s, DataString SourceExternal) return (base, t, s, d)
getLiteral t = getLiteralOfDataType t (DataString SourceExternal)
getLiteralArray t = getLiteralOfDataType t (DataArray SourceExternal)
getModifierParamString = getModifierParam DataString getModifierParamString = getModifierParam DataString
@@ -584,7 +645,7 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
getModifierParam _ _ = [] getModifierParam _ _ = []
letParamToLiteral token = letParamToLiteral token =
if var == "" if null var
then [] then []
else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])] else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])]
where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token
@@ -600,18 +661,43 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
where where
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, var, DataString $ SourceFrom list) f ((_, Just "-v") : (t, Just var) : _) = return (base, t, varName, varType $ SourceFrom list)
where
(varName, varType) = case elemIndex '[' var of
Just i -> (take i var, DataArray)
Nothing -> (var, DataString)
f (_:rest) = f rest f (_:rest) = f rest
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
[] ->
return (base, base, "MAPFILE", DataArray SourceExternal)
first:_ -> do
name <- getLiteralString first
guard $ isVariableName name
return (base, first, name, DataArray SourceExternal)
-- If arg parsing fails (due to bad or new flags), get the last variable name
fallback :: Maybe (Token, Token, String, DataType)
fallback = do
(name, token) <- listToMaybe . mapMaybe f $ reverse rest
return (base, token, name, DataArray SourceExternal)
f arg = do
name <- getLiteralString arg
guard $ isVariableName name
return (name, arg)
-- get the FLAGS_ variable created by a shflags DEFINE_ call
getFlagVariable (n:v:_) = do
name <- getLiteralString n
return (base, n, "FLAGS_" ++ name, DataString $ SourceExternal)
getFlagVariable _ = Nothing
getModifiedVariableCommand _ = [] getModifiedVariableCommand _ = []
@@ -636,7 +722,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
@@ -655,7 +741,13 @@ getReferencedVariables parents t =
then concatMap (getIfReference t) [lhs, rhs] then concatMap (getIfReference t) [lhs, rhs]
else [] else []
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo T_BatsTest {} -> [ -- pretend @test references vars to avoid warnings
(t, t, "lines"),
(t, t, "status"),
(t, t, "output")
]
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
@@ -672,12 +764,11 @@ 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 <- getLiteralStringExt literalizer token str@(h:_) <- getLiteralStringExt literalizer token
guard . not $ null str when (isDigit h) $ fail "is a number"
when (isDigit $ head str) $ fail "is a number"
return (context, token, getBracedReference str) return (context, token, getBracedReference str)
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"]) isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
@@ -697,15 +788,15 @@ isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `
-- Compare a command to a literal. Like above, but checks full path. -- Compare a command to a literal. Like above, but checks full path.
isUnqualifiedCommand token str = isCommandMatch token (== str) isUnqualifiedCommand token str = isCommandMatch token (== str)
isCommandMatch token matcher = fromMaybe False $ isCommandMatch token matcher = maybe False
fmap matcher (getCommandName token) matcher (getCommandName token)
-- Does this regex look like it was intended as a glob? -- Does this regex look like it was intended as a glob?
-- True: *foo* -- True: *foo*
-- False: .*foo.* -- False: .*foo.*
isConfusedGlobRegex :: String -> Bool isConfusedGlobRegex :: String -> Bool
isConfusedGlobRegex ('*':_) = True isConfusedGlobRegex ('*':_) = True
isConfusedGlobRegex [x,'*'] | x /= '\\' = True isConfusedGlobRegex [x,'*'] | x `notElem` "\\." = True
isConfusedGlobRegex _ = False isConfusedGlobRegex _ = False
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
@@ -719,14 +810,14 @@ isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
isVariableName _ = False isVariableName _ = False
getVariablesFromLiteralToken token = getVariablesFromLiteralToken token =
getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token) getVariablesFromLiteral (getLiteralStringDef " " token)
-- Try to get referenced variables from a literal string like "$foo" -- Try to get referenced variables from a literal string like "$foo"
-- Ignores tons of cases like arithmetic evaluation and array indices. -- Ignores tons of cases like arithmetic evaluation and array indices.
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_]+)"
@@ -742,34 +833,33 @@ 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
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz" prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo" prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]" prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
getBracedModifier s = fromMaybe "" . listToMaybe $ do getBracedModifier s = headOrDefault "" $ do
let var = getBracedReference s let var = getBracedReference s
a <- dropModifier s a <- dropModifier s
dropPrefix var a dropPrefix var a
@@ -783,15 +873,6 @@ getBracedModifier s = fromMaybe "" . listToMaybe $ do
-- Useful generic functions. -- Useful generic functions.
-- Run an action in a Maybe (or do nothing).
-- Example:
-- potentially $ do
-- s <- getLiteralString cmd
-- guard $ s `elem` ["--recursive", "-r"]
-- return $ warn .. "Something something recursive"
potentially :: Monad m => Maybe (m ()) -> m ()
potentially = fromMaybe (return ())
-- Get element 0 or a default. Like `head` but safe. -- Get element 0 or a default. Like `head` but safe.
headOrDefault _ (a:_) = a headOrDefault _ (a:_) = a
headOrDefault def _ = def headOrDefault def _ = def
@@ -804,30 +885,28 @@ 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 =
filter (not . shouldIgnore) filter (not . shouldIgnore)
where where
token = asScript asSpec token = asScript asSpec
idFor (TokenComment id _) = id
shouldIgnore note = shouldIgnore note =
any (shouldIgnoreFor (getCode note)) $ any (shouldIgnoreFor (getCode note)) $
getPath parents (T_Bang $ idFor note) getPath parents (T_Bang $ tcId note)
shouldIgnoreFor num (T_Annotation _ anns _) =
any hasNum anns
where
hasNum (DisableComment ts) = num == ts
hasNum _ = False
shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec
shouldIgnoreFor _ _ = False shouldIgnoreFor code t = isAnnotationIgnoringCode code t
parents = parentMap params parents = parentMap params
getCode (TokenComment _ (Comment _ c _)) = c getCode = cCode . tcComment
shouldIgnoreCode params code t =
any (isAnnotationIgnoringCode code) $
getPath (parentMap params) t
-- Is this a ${#anything}, to get string length or array count? -- Is this a ${#anything}, to get string length or array count?
isCountingReference (T_DollarBraced id token) = isCountingReference (T_DollarBraced id _ token) =
case concat $ oversimplify token of case concat $ oversimplify token of
'#':_ -> True '#':_ -> True
_ -> False _ -> False
@@ -836,44 +915,24 @@ 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 = getOpts getAllFlags
getBsdOpts = getOpts getLeadingFlags
getOpts :: (Token -> [(Token, String)]) -> String -> Token -> Maybe [(String, Token)]
getOpts flagTokenizer string cmd = process flags
where
flags = flagTokenizer cmd
flagList (c:':':rest) = ([c], True) : flagList rest
flagList (c:rest) = ([c], False) : flagList rest
flagList [] = []
flagMap = Map.fromList $ ("", False) : flagList string
process [] = return [] -- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh)
process [(token, flag)] = do isBashLike :: Parameters -> Bool
takesArg <- Map.lookup flag flagMap isBashLike params =
guard $ not takesArg case shellType params of
return [(flag, token)] Bash -> True
process ((token1, flag1):rest2@((token2, flag2):rest)) = do Ksh -> True
takesArg <- Map.lookup flag1 flagMap Dash -> False
if takesArg Sh -> False
then do
guard $ flag2 == ""
more <- process rest
return $ (flag1, token2) : more
else do
more <- process rest2
return $ (flag1, token1) : more
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -37,56 +37,91 @@ import Control.Monad
import Test.QuickCheck.All import Test.QuickCheck.All
tokenToPosition map (TokenComment id c) = fromMaybe fail $ do tokenToPosition startMap t = fromMaybe fail $ do
position <- Map.lookup id map span <- Map.lookup (tcId t) startMap
return $ PositionedComment position position c return $ newPositionedComment {
pcStartPos = fst span,
pcEndPos = snd span,
pcComment = tcComment t,
pcFix = tcFix t
}
where where
fail = error "Internal shellcheck error: id doesn't exist. Please report!" fail = error "Internal shellcheck error: id doesn't exist. Please report!"
shellFromFilename filename = listToMaybe candidates
where
shellExtensions = [(".ksh", Ksh)
,(".bash", Bash)
,(".bats", Bash)
,(".dash", Dash)]
-- The `.sh` is too generic to determine the shell:
-- We fallback to Bash in this case and emit SC2148 if there is no shebang
candidates =
[sh | (ext,sh) <- shellExtensions, ext `isSuffixOf` filename]
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
checkScript sys spec = do checkScript sys spec = do
results <- checkScript (csScript spec) results <- checkScript (csScript spec)
return CheckResult { return emptyCheckResult {
crFilename = csFilename spec, crFilename = csFilename spec,
crComments = results crComments = results
} }
where where
checkScript contents = do checkScript contents = do
result <- parseScript sys ParseSpec { result <- parseScript sys newParseSpec {
psFilename = csFilename spec, psFilename = csFilename spec,
psScript = contents, psScript = contents,
psCheckSourced = csCheckSourced spec psCheckSourced = csCheckSourced spec,
psIgnoreRC = csIgnoreRC spec,
psShellTypeOverride = csShellTypeOverride spec
} }
let parseMessages = prComments result let parseMessages = prComments result
let tokenPositions = prTokenPositions result
let analysisSpec root =
as {
asScript = root,
asShellType = csShellTypeOverride spec,
asFallbackShell = shellFromFilename $ csFilename spec,
asCheckSourced = csCheckSourced spec,
asExecutionMode = Executed,
asTokenPositions = tokenPositions,
asOptionalChecks = csOptionalChecks spec
} where as = newAnalysisSpec root
let analysisMessages = let analysisMessages =
fromMaybe [] $ maybe []
(arComments . analyzeScript . analysisSpec) (arComments . analyzeScript . analysisSpec)
<$> prRoot result $ prRoot result
let translator = tokenToPosition (prTokenPositions result) let translator = tokenToPosition tokenPositions
return . nub . sortMessages . filter shouldInclude $ return . nub . sortMessages . filter shouldInclude $
(parseMessages ++ map translator analysisMessages) (parseMessages ++ map translator analysisMessages)
shouldInclude (PositionedComment _ _ (Comment _ code _)) = shouldInclude pc =
code `notElem` csExcludedWarnings spec severity <= csMinSeverity spec &&
case csIncludedWarnings spec of
Nothing -> code `notElem` csExcludedWarnings spec
Just includedWarnings -> code `elem` includedWarnings
where
code = cCode (pcComment pc)
severity = cSeverity (pcComment pc)
sortMessages = sortBy (comparing order) sortMessages = sortOn order
order (PositionedComment pos _ (Comment severity code message)) = order pc =
(posFile pos, posLine pos, posColumn pos, severity, code, message) let pos = pcStartPos pc
getPosition (PositionedComment pos _ _) = pos comment = pcComment pc in
(posFile pos,
posLine pos,
posColumn pos,
cSeverity comment,
cCode comment,
cMessage comment)
getPosition = pcStartPos
analysisSpec root =
AnalysisSpec {
asScript = root,
asShellType = csShellTypeOverride spec,
asCheckSourced = csCheckSourced spec,
asExecutionMode = Executed
}
getErrors sys spec = getErrors sys spec =
sort . map getCode . crComments $ sort . map getCode . crComments $
runIdentity (checkScript sys spec) runIdentity (checkScript sys spec)
where where
getCode (PositionedComment _ _ (Comment _ code _)) = code getCode = cCode . pcComment
check = checkWithIncludes [] check = checkWithIncludes []
@@ -106,6 +141,21 @@ checkRecursive includes src =
csCheckSourced = True csCheckSourced = True
} }
checkOptionIncludes includes src =
checkWithSpec [] emptyCheckSpec {
csScript = src,
csIncludedWarnings = includes,
csCheckSourced = True
}
checkWithRc rc = getErrors
(mockRcFile rc $ mockedSystemInterface [])
checkWithIncludesAndSourcePath includes mapper = getErrors
(mockedSystemInterface includes) {
siFindSource = mapper
}
prop_findsParseIssue = check "echo \"$12\"" == [1037] prop_findsParseIssue = check "echo \"$12\"" == [1037]
prop_commentDisablesParseIssue1 = prop_commentDisablesParseIssue1 =
@@ -136,8 +186,23 @@ prop_optionDisablesIssue2 =
csExcludedWarnings = [2148, 1037] csExcludedWarnings = [2148, 1037]
} }
prop_wontParseBadShell =
[1071] == check "#!/usr/bin/python\ntrue $1\n"
prop_optionDisablesBadShebang =
null $ getErrors
(mockedSystemInterface [])
emptyCheckSpec {
csScript = "#!/usr/bin/python\ntrue\n",
csShellTypeOverride = Just Sh
}
prop_annotationDisablesBadShebang =
null $ check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
prop_canParseDevNull = prop_canParseDevNull =
[] == check "source /dev/null" null $ check "source /dev/null"
prop_failsWhenNotSourcing = prop_failsWhenNotSourcing =
[1091, 2154] == check "source lol; echo \"$bar\"" [1091, 2154] == check "source lol; echo \"$bar\""
@@ -145,11 +210,15 @@ prop_failsWhenNotSourcing =
prop_worksWhenSourcing = prop_worksWhenSourcing =
null $ checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\"" null $ checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\""
prop_worksWhenSourcingWithDashDash =
null $ checkWithIncludes [("lib", "bar=1")] "source -- lib; echo \"$bar\""
prop_worksWhenDotting = prop_worksWhenDotting =
null $ checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\"" null $ checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\""
-- FIXME: This should really be giving [1093], "recursively sourced"
prop_noInfiniteSourcing = prop_noInfiniteSourcing =
[] == checkWithIncludes [("lib", "source lib")] "source lib" null $ checkWithIncludes [("lib", "source lib")] "source lib"
prop_canSourceBadSyntax = prop_canSourceBadSyntax =
[1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1" [1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
@@ -160,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\""
@@ -169,6 +244,12 @@ prop_recursiveAnalysis =
prop_recursiveParsing = prop_recursiveParsing =
[1037] == checkRecursive [("lib", "echo \"$10\"")] "source lib" [1037] == checkRecursive [("lib", "echo \"$10\"")] "source lib"
prop_nonRecursiveAnalysis =
null $ checkWithIncludes [("lib", "echo $1")] "source lib"
prop_nonRecursiveParsing =
null $ checkWithIncludes [("lib", "echo \"$10\"")] "source lib"
prop_sourceDirectiveDoesntFollowFile = prop_sourceDirectiveDoesntFollowFile =
null $ checkWithIncludes null $ checkWithIncludes
[("foo", "source bar"), ("bar", "baz=3")] [("foo", "source bar"), ("bar", "baz=3")]
@@ -180,7 +261,7 @@ prop_filewideAnnotation1 = null $
prop_filewideAnnotation2 = null $ prop_filewideAnnotation2 = null $
check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1" check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation3 = null $ prop_filewideAnnotation3 = null $
check "#!/bin/sh\n#unerlated\n# shellcheck disable=2086\ntrue\necho $1" check "#!/bin/sh\n#unrelated\n# shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation4 = null $ prop_filewideAnnotation4 = null $
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1" check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
prop_filewideAnnotation5 = null $ prop_filewideAnnotation5 = null $
@@ -195,8 +276,139 @@ 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_deducesTypeFromExtension = null result
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.ksh",
csScript = "(( 3.14 ))"
}
prop_deducesTypeFromExtension2 = result == [2079]
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.bash",
csScript = "(( 3.14 ))"
}
prop_canDisableShebangWarning = null $ result
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.sh",
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]
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.sh",
csScript = "echo 'hello world'"
}
prop_sourcedFileUsesOriginalShellExtension = result == [2079]
where
result = checkWithSpec [("file.ksh", "(( 3.14 ))")] emptyCheckSpec {
csFilename = "file.bash",
csScript = "source file.ksh",
csCheckSourced = True
}
prop_canEnableOptionalsWithSpec = result == [2244]
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.sh",
csScript = "#!/bin/sh\n[ \"$1\" ]",
csOptionalChecks = ["avoid-nullary-conditions"]
}
prop_optionIncludes1 =
-- expect 2086, but not included, so nothing reported
null $ checkOptionIncludes (Just [2080]) "#!/bin/sh\n var='a b'\n echo $var"
prop_optionIncludes2 =
-- expect 2086, included, so it is reported
[2086] == checkOptionIncludes (Just [2086]) "#!/bin/sh\n var='a b'\n echo $var"
prop_optionIncludes3 =
-- expect 2086, no inclusions provided, so it is reported
[2086] == checkOptionIncludes Nothing "#!/bin/sh\n var='a b'\n echo $var"
prop_optionIncludes4 =
-- expect 2086 & 2154, only 2154 included, so only that's reported
[2154] == checkOptionIncludes (Just [2154]) "#!/bin/sh\n var='a b'\n echo $var\n echo $bar"
prop_readsRcFile = null result
where
result = checkWithRc "disable=2086" emptyCheckSpec {
csScript = "#!/bin/sh\necho $1",
csIgnoreRC = False
}
prop_canUseNoRC = result == [2086]
where
result = checkWithRc "disable=2086" emptyCheckSpec {
csScript = "#!/bin/sh\necho $1",
csIgnoreRC = True
}
prop_NoRCWontLookAtFile = result == [2086]
where
result = checkWithRc (error "Fail") emptyCheckSpec {
csScript = "#!/bin/sh\necho $1",
csIgnoreRC = True
}
prop_brokenRcGetsWarning = result == [1134, 2086]
where
result = checkWithRc "rofl" emptyCheckSpec {
csScript = "#!/bin/sh\necho $1",
csIgnoreRC = False
}
prop_canEnableOptionalsWithRc = result == [2244]
where
result = checkWithRc "enable=avoid-nullary-conditions" emptyCheckSpec {
csScript = "#!/bin/sh\n[ \"$1\" ]"
}
prop_sourcePathRedirectsName = result == [2086]
where
f "dir/myscript" _ "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource lib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_sourcePathAddsAnnotation = result == [2086]
where
f "dir/myscript" ["mypath"] "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source-path=mypath\nsource lib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_sourcePathRedirectsDirective = result == [2086]
where
f "dir/myscript" _ "lib" = return "foo/lib"
f _ _ _ = return "/dev/null"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source=lib\nsource kittens",
csFilename = "dir/myscript",
csCheckSourced = True
}
return [] return []
runTests = $quickCheckAll runTests = $quickCheckAll

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -19,9 +19,10 @@
-} -}
{-# 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 , ShellCheck.Checks.Commands.runTests) where module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.ASTLib import ShellCheck.ASTLib
@@ -34,6 +35,7 @@ import ShellCheck.Regex
import Control.Monad import Control.Monad
import Control.Monad.RWS import Control.Monad.RWS
import Data.Char import Data.Char
import Data.Functor.Identity
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
@@ -51,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
@@ -61,6 +61,7 @@ commandChecks = [
,checkGrepRe ,checkGrepRe
,checkTrapQuotes ,checkTrapQuotes
,checkReturn ,checkReturn
,checkExit
,checkFindExecWithSingleArgument ,checkFindExecWithSingleArgument
,checkUnusedEchoEscapes ,checkUnusedEchoEscapes
,checkInjectableFindSh ,checkInjectableFindSh
@@ -89,11 +90,60 @@ commandChecks = [
,checkMvArguments, checkCpArguments, checkLnArguments ,checkMvArguments, checkCpArguments, checkLnArguments
,checkFindRedirections ,checkFindRedirections
,checkReadExpansions ,checkReadExpansions
,checkWhich
,checkSudoRedirect ,checkSudoRedirect
,checkSudoArgs ,checkSudoArgs
,checkSourceArgs
,checkChmodDashr
,checkXargsDashi
] ]
optionalChecks = map fst optionalCommandChecks
optionalCommandChecks :: [(CheckDescription, CommandCheck)]
optionalCommandChecks = [
(newCheckDescription {
cdName = "deprecate-which",
cdDescription = "Suggest 'command -v' instead of 'which'",
cdPositive = "which javac",
cdNegative = "command -v javac"
}, checkWhich)
]
optionalCheckMap = Map.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks
prop_verifyOptionalExamples = all check optionalCommandChecks
where
check (desc, check) =
verify check (cdPositive 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
@@ -102,12 +152,16 @@ buildCommandMap = foldl' addCheck Map.empty
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
checkCommand map t@(T_SimpleCommand id _ (cmd:rest)) = fromMaybe (return ()) $ do checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do
name <- getLiteralString cmd name <- getLiteralString cmd
return $ return $
if '/' `elem` name if '/' `elem` name
then then
Map.findWithDefault nullCheck (Basename $ basename name) map t Map.findWithDefault nullCheck (Basename $ basename name) map t
else if name == "builtin" && not (null rest) then
let t' = T_SimpleCommand id cmdPrefix rest
selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest
in Map.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
else do else do
Map.findWithDefault nullCheck (Exactly name) map t Map.findWithDefault nullCheck (Exactly name) map t
Map.findWithDefault nullCheck (Basename name) map t Map.findWithDefault nullCheck (Basename name) map t
@@ -125,8 +179,14 @@ getChecker list = Checker {
map = buildCommandMap list map = buildCommandMap list
checker :: Parameters -> Checker checker :: AnalysisSpec -> Parameters -> Checker
checker params = getChecker commandChecks checker spec params = getChecker $ commandChecks ++ optionals
where
keys = asOptionalChecks spec
optionals =
if "all" `elem` keys
then map snd optionalCommandChecks
else mapMaybe (\x -> Map.lookup x optionalCheckMap) keys
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]" prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'" prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
@@ -141,6 +201,7 @@ prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
prop_checkTr9 = verifyNot checkTr "a-z n-za-m" prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr" prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'" prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
prop_checkTr12= verifyNot checkTr "tr '[=e=]' 'e'"
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments) checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
where where
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme? f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
@@ -152,7 +213,7 @@ checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
Just s -> do -- Eliminate false positives by only looking for dupes in SET2? Just s -> do -- Eliminate false positives by only looking for dupes in SET2?
when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $ when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $
info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)." info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)."
unless ("[:" `isPrefixOf` s) $ unless ("[:" `isPrefixOf` s || "[=" `isPrefixOf` s) $
when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $ when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $
info (getId word) 2021 "Don't use [] around classes in tr, it replaces literal square brackets." info (getId word) 2021 "Don't use [] around classes in tr, it replaces literal square brackets."
Nothing -> return () Nothing -> return ()
@@ -165,15 +226,13 @@ prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php"
prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)" prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)"
prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'" prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
acceptsGlob (Just 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" ]
acceptsGlob _ = False
f [] = return () f [] = return ()
f [x] = return () f (x:xs) = foldr g (const $ return ()) xs x
f (a:b:r) = do g b acc a = do
when (acceptsGlob (getLiteralString a) && isGlob b) $ do forM_ (getLiteralString a) $ \s -> when (acceptsGlob s && isGlob b) $
let (Just s) = getLiteralString a
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."
f (b:r) acc b
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)" prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
@@ -183,7 +242,7 @@ prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)
checkNeedlessExpr = CommandCheck (Basename "expr") f where checkNeedlessExpr = CommandCheck (Basename "expr") f where
f t = f t =
when (all (`notElem` exceptions) (words $ arguments t)) $ when (all (`notElem` exceptions) (words $ arguments t)) $
style (getId t) 2003 style (getId $ getCommandTokenOrThis t) 2003
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]." "expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
-- These operators are hard to replicate in POSIX -- These operators are hard to replicate in POSIX
exceptions = [ ":", "<", ">", "<=", ">=" ] exceptions = [ ":", "<", ">", "<=", ">=" ]
@@ -205,17 +264,24 @@ prop_checkGrepRe12= verifyNot checkGrepRe "grep -F 'Foo*' file"
prop_checkGrepRe13= verifyNot checkGrepRe "grep -- -foo bar*" prop_checkGrepRe13= verifyNot checkGrepRe "grep -- -foo bar*"
prop_checkGrepRe14= verifyNot checkGrepRe "grep -e -foo bar*" prop_checkGrepRe14= verifyNot checkGrepRe "grep -e -foo bar*"
prop_checkGrepRe15= verifyNot checkGrepRe "grep --regex -foo bar*" prop_checkGrepRe15= verifyNot checkGrepRe "grep --regex -foo bar*"
prop_checkGrepRe16= verifyNot checkGrepRe "grep --include 'Foo*' file"
prop_checkGrepRe17= verifyNot checkGrepRe "grep --exclude 'Foo*' file"
prop_checkGrepRe18= verifyNot checkGrepRe "grep --exclude-dir 'Foo*' file"
prop_checkGrepRe19= verify checkGrepRe "grep -- 'Foo*' file"
prop_checkGrepRe20= verifyNot checkGrepRe "grep --fixed-strings 'Foo*' file"
prop_checkGrepRe21= verifyNot checkGrepRe "grep -o 'x*' file"
prop_checkGrepRe22= verifyNot checkGrepRe "grep --only-matching 'x*' file"
prop_checkGrepRe23= verifyNot checkGrepRe "grep '.*' file"
checkGrepRe = CommandCheck (Basename "grep") check where checkGrepRe = CommandCheck (Basename "grep") check where
check cmd = f cmd (arguments cmd) check cmd = f cmd (arguments cmd)
-- --regex=*(extglob) doesn't work. Fixme? -- --regex=*(extglob) doesn't work. Fixme?
skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s skippable s = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s
skippable _ = False
f _ [] = return () f _ [] = return ()
f cmd (x:r) = f cmd (x:r) =
let str = getLiteralStringExt (const $ return "_") x let str = getLiteralStringDef "_" x
in in
if str `elem` [Just "--", Just "-e", Just "--regex"] if str `elem` ["--", "-e", "--regex"]
then checkRE cmd r -- Regex is *after* this then checkRE cmd r -- Regex is *after* this
else else
if skippable str if skippable str
@@ -227,32 +293,29 @@ checkGrepRe = CommandCheck (Basename "grep") check where
when (isGlob re) $ when (isGlob re) $
warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it." warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it."
unless (cmd `hasFlag` "F") $ do unless (any (`elem` flags) grepGlobFlags) $ do
let string = concat $ oversimplify re let string = concat $ oversimplify re
if isConfusedGlobRegex string then if isConfusedGlobRegex string then
warn (getId re) 2063 "Grep uses regex, but this looks like a glob." warn (getId re) 2063 "Grep uses regex, but this looks like a glob."
else potentially $ do else sequence_ $ do
char <- getSuspiciousRegexWildcard string char <- getSuspiciousRegexWildcard string
return $ info (getId re) 2022 $ return $ info (getId re) 2022 $
"Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'." "Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'."
where
flags = map snd $ getAllFlags cmd
grepGlobFlags = ["fixed-strings", "F", "include", "exclude", "exclude-dir", "o", "only-matching"]
wordStartingWith c = wordStartingWith c =
head . filter ([c] `isPrefixOf`) $ candidates headOrDefault (c:"test") . filter ([c] `isPrefixOf`) $ candidates
where where
candidates = candidates =
sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords ++ [c:"test"] 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"
@@ -267,7 +330,7 @@ checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
warning id = warn id 2064 "Use single quotes, otherwise this expands now rather than when signalled." warning id = warn id 2064 "Use single quotes, otherwise this expands now rather than when signalled."
checkExpansions (T_DollarExpansion id _) = warning id checkExpansions (T_DollarExpansion id _) = warning id
checkExpansions (T_Backticked id _) = warning id checkExpansions (T_Backticked id _) = warning id
checkExpansions (T_DollarBraced id _) = warning id checkExpansions (T_DollarBraced id _ _) = warning id
checkExpansions (T_DollarArithmetic id _) = warning id checkExpansions (T_DollarArithmetic id _) = warning id
checkExpansions _ = return () checkExpansions _ = return ()
@@ -279,21 +342,34 @@ prop_checkReturn4 = verifyNot checkReturn "return $((a|b))"
prop_checkReturn5 = verify checkReturn "return -1" prop_checkReturn5 = verify checkReturn "return -1"
prop_checkReturn6 = verify checkReturn "return 1000" prop_checkReturn6 = verify checkReturn "return 1000"
prop_checkReturn7 = verify checkReturn "return 'hello world'" prop_checkReturn7 = verify checkReturn "return 'hello world'"
checkReturn = CommandCheck (Exactly "return") (f . arguments) checkReturn = CommandCheck (Exactly "return") (returnOrExit
(\c -> err c 2151 "Only one integer 0-255 can be returned. Use stdout for other data.")
(\c -> err c 2152 "Can only return 0-255. Other data should be written to stdout."))
prop_checkExit1 = verifyNot checkExit "exit"
prop_checkExit2 = verifyNot checkExit "exit 1"
prop_checkExit3 = verifyNot checkExit "exit $var"
prop_checkExit4 = verifyNot checkExit "exit $((a|b))"
prop_checkExit5 = verify checkExit "exit -1"
prop_checkExit6 = verify checkExit "exit 1000"
prop_checkExit7 = verify checkExit "exit 'hello world'"
checkExit = CommandCheck (Exactly "exit") (returnOrExit
(\c -> err c 2241 "The exit status can only be one integer 0-255. Use stdout for other data.")
(\c -> err c 2242 "Can only exit with status 0-255. Other data should be written to stdout/stderr."))
returnOrExit multi invalid = (f . arguments)
where where
f (first:second:_) = f (first:second:_) =
err (getId second) 2151 multi (getId first)
"Only one integer 0-255 can be returned. Use stdout for other data."
f [value] = f [value] =
when (isInvalid $ literal value) $ when (isInvalid $ literal value) $
err (getId value) 2152 invalid (getId value)
"Can only return 0-255. Other data should be written to stdout."
f _ = return () f _ = return ()
isInvalid s = s == "" || any (not . isDigit) s || length s > 5 isInvalid s = null s || any (not . isDigit) s || length s > 5
|| let value = (read s :: Integer) in value > 255 || let value = (read s :: Integer) in value > 255
literal token = fromJust $ getLiteralStringExt lit token literal token = runIdentity $ getLiteralStringExt lit token
lit (T_DollarBraced {}) = return "0" lit (T_DollarBraced {}) = return "0"
lit (T_DollarArithmetic {}) = return "0" lit (T_DollarArithmetic {}) = return "0"
lit (T_DollarExpansion {}) = return "0" lit (T_DollarExpansion {}) = return "0"
@@ -310,7 +386,7 @@ checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments)
check (exec:arg:term:_) = do check (exec:arg:term:_) = do
execS <- getLiteralString exec execS <- getLiteralString exec
termS <- getLiteralString term termS <- getLiteralString term
cmdS <- getLiteralStringExt (const $ return " ") arg let cmdS = getLiteralStringDef " " arg
guard $ execS `elem` ["-exec", "-execdir"] && termS `elem` [";", "+"] guard $ execS `elem` ["-exec", "-execdir"] && termS `elem` [";", "+"]
guard $ cmdS `matches` commandRegex guard $ cmdS `matches` commandRegex
@@ -324,26 +400,18 @@ prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\n
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\"" prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol" prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'" prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
checkUnusedEchoEscapes = CommandCheck (Basename "echo") (f . arguments) checkUnusedEchoEscapes = CommandCheck (Basename "echo") f
where where
isDashE = mkRegex "^-.*e"
hasEscapes = mkRegex "\\\\[rnt]" hasEscapes = mkRegex "\\\\[rnt]"
f args | concat (concatMap oversimplify allButLast) `matches` isDashE = f cmd =
return () whenShell [Sh, Bash, Ksh] $
where allButLast = reverse . drop 1 . reverse $ args unless (cmd `hasFlag` "e") $
f args = mapM_ checkEscapes args mapM_ examine $ arguments cmd
checkEscapes (T_NormalWord _ args) = examine token = do
mapM_ checkEscapes args let str = onlyLiteralString token
checkEscapes (T_DoubleQuoted id args) =
mapM_ checkEscapes args
checkEscapes (T_Literal id str) = examine id str
checkEscapes (T_SingleQuoted id str) = examine id str
checkEscapes _ = return ()
examine id str =
when (str `matches` hasEscapes) $ when (str `matches` hasEscapes) $
info id 2028 "echo won't expand escape sequences. Consider printf." info (getId token) 2028 "echo may not expand escape sequences. Use printf."
prop_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;" prop_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
@@ -414,10 +482,10 @@ prop_checkMkdirDashPM20 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 .././bin"
prop_checkMkdirDashPM21 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../../bin" prop_checkMkdirDashPM21 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../../bin"
checkMkdirDashPM = CommandCheck (Basename "mkdir") check checkMkdirDashPM = CommandCheck (Basename "mkdir") check
where where
check t = potentially $ 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."
@@ -437,10 +505,10 @@ 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 = potentially $ do check param = sequence_ $ do
str <- getLiteralString param str <- getLiteralString param
let id = getId param let id = getId param
return $ sequence_ $ mapMaybe (\f -> f id str) [ return $ sequence_ $ mapMaybe (\f -> f id str) [
@@ -469,14 +537,14 @@ prop_checkInteractiveSu4 = verifyNot checkInteractiveSu "su root < script"
checkInteractiveSu = CommandCheck (Basename "su") f checkInteractiveSu = CommandCheck (Basename "su") f
where where
f cmd = when (length (arguments cmd) <= 1) $ do f cmd = when (length (arguments cmd) <= 1) $ do
path <- pathTo cmd path <- getPathM cmd
when (all undirected path) $ when (all undirected path) $
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
@@ -493,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 ()
@@ -515,43 +582,89 @@ prop_checkPrintfVar12= verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4" prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1" prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1"
prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2" prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
prop_checkPrintfVar16= verifyNot checkPrintfVar "printf $'string'"
prop_checkPrintfVar17= verify checkPrintfVar "printf '%-*s\\n' 1"
prop_checkPrintfVar18= verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
prop_checkPrintfVar19= verifyNot checkPrintfVar "printf '%(%s)T'"
prop_checkPrintfVar20= verifyNot checkPrintfVar "printf '%d %(%s)T' 42"
prop_checkPrintfVar21= verify checkPrintfVar "printf '%d %(%s)T'"
prop_checkPrintfVar22= verify checkPrintfVar "printf '%s\n%s' foo"
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where 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
f (format:params) = check format params f (format:params) = check format params
f _ = return () f _ = return ()
countFormats string =
case string of
'%':'%':rest -> countFormats rest
'%':'(':rest -> 1 + countFormats (dropWhile (/= ')') rest)
'%':'*':rest -> 2 + countFormats rest -- width is specified as an argument
'%':rest -> 1 + countFormats rest
_:rest -> countFormats rest
[] -> 0
check format more = do check format more = do
fromMaybe (return ()) $ do sequence_ $ do
string <- getLiteralString format string <- getLiteralString format
let vars = countFormats string let formats = getPrintfFormats string
let formatCount = length formats
let argCount = length more
return $ do return $ if
when (vars == 0 && more /= []) $ | argCount == 0 && formatCount == 0 ->
return () -- This is fine
| 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 ->
when (vars > 0 return () -- We don't know so trust the user
&& ((length more) `mod` vars /= 0 || null more) | argCount < formatCount && onlyTrailingTs formats argCount ->
&& all (not . mayBecomeMultipleArgs) more) $ return () -- Allow trailing %()Ts since they use the current time
| argCount > 0 && argCount `mod` formatCount == 0 ->
return () -- Great: a suitable number of arguments
| otherwise ->
warn (getId format) 2183 $ warn (getId format) 2183 $
"This format string has " ++ show vars ++ " variables, but is passed " ++ show (length more) ++ " 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
"Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"." "Don't use variables in the printf format string. Use printf '..%s..' \"$foo\"."
where
onlyTrailingTs format argCount =
all (== 'T') $ drop argCount format
prop_checkGetPrintfFormats1 = getPrintfFormats "%s" == "s"
prop_checkGetPrintfFormats2 = getPrintfFormats "%0*s" == "*s"
prop_checkGetPrintfFormats3 = getPrintfFormats "%(%s)T" == "T"
prop_checkGetPrintfFormats4 = getPrintfFormats "%d%%%(%s)T" == "dT"
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
where
-- Get the arguments in the string as a string of type characters,
-- e.g. "Hello %s" -> "s" and "%(%s)T %0*d\n" -> "T*d"
getFormats :: String -> String
getFormats string =
case string of
'%':'%':rest -> getFormats rest
'%':'(':rest ->
case dropWhile (/= ')') rest of
')':c:trailing -> c : getFormats trailing
_ -> ""
'%':rest -> regexBasedGetFormats rest
_:rest -> getFormats rest
[] -> ""
regexBasedGetFormats rest =
case matchRegex re rest of
Just [width, precision, typ, rest, _] ->
(if width == "*" then "*" else "") ++
(if precision == "*" then "*" else "") ++
typ ++ getFormats rest
Nothing -> take 1 rest ++ getFormats rest
where
-- constructed based on specifications in "man printf"
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)([diouxXfFeEgGaAcsbq])((\n|.)*)"
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \______ /
-- V V V V V
-- flags field width precision format character rest
-- field width and precision can be specified with a '*' instead of a digit,
-- in which case printf will accept one more argument for each '*' used
prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)" prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)"
@@ -574,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
@@ -597,9 +705,8 @@ prop_checkExportedExpansions3 = verifyNot checkExportedExpansions "export foo"
prop_checkExportedExpansions4 = verifyNot checkExportedExpansions "export ${foo?}" prop_checkExportedExpansions4 = verifyNot checkExportedExpansions "export ${foo?}"
checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments) checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
where where
check t = potentially $ 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."
@@ -613,28 +720,27 @@ prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1"
prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}" prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}"
checkReadExpansions = CommandCheck (Exactly "read") check checkReadExpansions = CommandCheck (Exactly "read") check
where where
options = getGnuOpts "sreu:n:N:i:p:a:" options = getGnuOpts flagsForRead
getVars cmd = fromMaybe [] $ do getVars cmd = fromMaybe [] $ do
opts <- options cmd opts <- options $ arguments cmd
return . map snd $ filter (\(x,_) -> x == "" || x == "a") opts return [y | (x,(_, y)) <- opts, null x || x == "a"]
check cmd = mapM_ warning $ getVars cmd check cmd = mapM_ warning $ getVars cmd
warning t = potentially $ 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'"
@@ -645,7 +751,7 @@ checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments)
re = mkRegex "\\$\\{?[0-9*@]" re = mkRegex "\\$\\{?[0-9*@]"
f = mapM_ checkArg f = mapM_ checkArg
checkArg arg = checkArg arg =
let string = fromJust $ getLiteralStringExt (const $ return "_") arg in let string = getLiteralStringDef "_" arg in
when ('=' `elem` string && string `matches` re) $ when ('=' `elem` string && string `matches` re) $
err (getId arg) 2142 err (getId arg) 2142
"Aliases can't use positional parameters. Use a function." "Aliases can't use positional parameters. Use a function."
@@ -658,7 +764,7 @@ checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
where where
f = mapM_ checkArg f = mapM_ checkArg
checkArg arg | '=' `elem` concat (oversimplify arg) = checkArg arg | '=' `elem` concat (oversimplify arg) =
forM_ (take 1 $ filter (not . isLiteral) $ getWordParts arg) $ forM_ (find (not . isLiteral) $ getWordParts arg) $
\x -> warn (getId x) 2139 "This expands when defined, not when used. Consider escaping." \x -> warn (getId x) 2139 "This expands when defined, not when used. Consider escaping."
checkArg _ = return () checkArg _ = return ()
@@ -678,20 +784,24 @@ prop_checkFindWithoutPath3 = verifyNot checkFindWithoutPath "find . -type f"
prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print" prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"
prop_checkFindWithoutPath5 = verifyNot checkFindWithoutPath "find -O3 ." prop_checkFindWithoutPath5 = verifyNot checkFindWithoutPath "find -O3 ."
prop_checkFindWithoutPath6 = verifyNot checkFindWithoutPath "find -D exec ." prop_checkFindWithoutPath6 = verifyNot checkFindWithoutPath "find -D exec ."
prop_checkFindWithoutPath7 = verifyNot checkFindWithoutPath "find --help"
prop_checkFindWithoutPath8 = verifyNot checkFindWithoutPath "find -Hx . -print"
checkFindWithoutPath = CommandCheck (Basename "find") f checkFindWithoutPath = CommandCheck (Basename "find") f
where where
f (T_SimpleCommand _ _ (cmd:args)) = f t@(T_SimpleCommand _ _ (cmd:args)) =
unless (hasPath args) $ unless (t `hasFlag` "help" || hasPath args) $
info (getId cmd) 2185 "Some finds don't have a default path. Specify '.' explicitly." info (getId cmd) 2185 "Some finds don't have a default path. Specify '.' explicitly."
-- This is a bit of a kludge. find supports flag arguments both before and after the path, -- This is a bit of a kludge. find supports flag arguments both before and
-- as well as multiple non-flag arguments that are not the path. We assume that all the -- after the path, as well as multiple non-flag arguments that are not the
-- pre-path flags are single characters, which is generally the case except for -O3. -- path. We assume that all the pre-path flags are single characters from a
-- list of GNU and macOS flags.
hasPath (first:rest) = hasPath (first:rest) =
let flag = fromJust $ getLiteralStringExt (const $ return "___") first in let flag = getLiteralStringDef "___" first in
not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest
hasPath [] = False hasPath [] = False
isLeadingFlag flag = length flag <= 2 || "-O" `isPrefixOf` flag isLeadingFlag flag = length flag <= 2 || all (`elem` leadingFlagChars) flag
leadingFlagChars="-EHLPXdfsxO0123456789"
prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10" prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10"
@@ -739,21 +849,21 @@ prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }"
checkLocalScope = CommandCheck (Exactly "local") $ \t -> checkLocalScope = CommandCheck (Exactly "local") $ \t ->
whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local
path <- getPathM t path <- getPathM t
unless (any isFunction path) $ unless (any isFunctionLike path) $
err (getId t) 2168 "'local' is only valid in functions." err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)" prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)" prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $ checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
\t -> warn (getId t) 2186 "tempfile is deprecated. Use mktemp instead." \t -> warn (getId $ getCommandTokenOrThis t) 2186 "tempfile is deprecated. Use mktemp instead."
prop_checkDeprecatedEgrep = verify checkDeprecatedEgrep "egrep '.+'" prop_checkDeprecatedEgrep = verify checkDeprecatedEgrep "egrep '.+'"
checkDeprecatedEgrep = CommandCheck (Basename "egrep") $ checkDeprecatedEgrep = CommandCheck (Basename "egrep") $
\t -> info (getId t) 2196 "egrep is non-standard and deprecated. Use grep -E instead." \t -> info (getId $ getCommandTokenOrThis t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."
prop_checkDeprecatedFgrep = verify checkDeprecatedFgrep "fgrep '*' files" prop_checkDeprecatedFgrep = verify checkDeprecatedFgrep "fgrep '*' files"
checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $ checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $
\t -> info (getId t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead." \t -> info (getId $ getCommandTokenOrThis t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
prop_checkWhileGetoptsCase1 = verify checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; esac; done" prop_checkWhileGetoptsCase1 = verify checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; esac; done"
prop_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; b) bar;; esac; done" prop_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; b) bar;; esac; done"
@@ -765,7 +875,7 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
f :: Token -> Analysis f :: Token -> Analysis
f t@(T_SimpleCommand _ _ (cmd:arg1:_)) = do f t@(T_SimpleCommand _ _ (cmd:arg1:_)) = do
path <- getPathM t path <- getPathM t
potentially $ do sequence_ $ do
options <- getLiteralString arg1 options <- getLiteralString arg1
(T_WhileExpression _ _ body) <- findFirst whileLoop path (T_WhileExpression _ _ body) <- findFirst whileLoop path
caseCmd <- mapMaybe findCase body !!! 0 caseCmd <- mapMaybe findCase body !!! 0
@@ -790,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) = potentially $ 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
@@ -806,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
@@ -841,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
@@ -851,7 +961,7 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
Nothing -> Nothing ->
checkWord' token checkWord' token
checkWord' token = fromMaybe (return ()) $ do checkWord' token = sequence_ $ do
filename <- getPotentialPath token filename <- getPotentialPath token
let path = fixPath filename let path = fixPath filename
return . when (path `elem` importantPaths) $ return . when (path `elem` importantPaths) $
@@ -864,7 +974,7 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
getPotentialPath = getLiteralStringExt f getPotentialPath = getLiteralStringExt f
where where
f (T_Glob _ str) = return str f (T_Glob _ str) = return str
f (T_DollarBraced _ word) = f (T_DollarBraced _ _ word) =
let var = onlyLiteralString word in let var = onlyLiteralString word in
-- This shouldn't handle non-colon cases. -- This shouldn't handle non-colon cases.
if any (`isInfixOf` var) [":?", ":-", ":="] if any (`isInfixOf` var) [":?", ":-", ":="]
@@ -873,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",
@@ -901,10 +1011,9 @@ missingDestination handler token = do
_ -> return () _ -> return ()
where where
args = getAllFlags token args = getAllFlags token
params = map fst $ filter (\(_,x) -> x == "") args params = [x | (x,"") <- args]
hasTarget = hasTarget =
any (\x -> x /= "" && x `isPrefixOf` "target-directory") $ any (\(_,x) -> x /= "" && x `isPrefixOf` "target-directory") args
map snd args
prop_checkMvArguments1 = verify checkMvArguments "mv 'foo bar'" prop_checkMvArguments1 = verify checkMvArguments "mv 'foo bar'"
prop_checkMvArguments2 = verifyNot checkMvArguments "mv foo bar" prop_checkMvArguments2 = verifyNot checkMvArguments "mv foo bar"
@@ -947,7 +1056,7 @@ checkFindRedirections = CommandCheck (Basename "find") f
prop_checkWhich = verify checkWhich "which '.+'" prop_checkWhich = verify checkWhich "which '.+'"
checkWhich = CommandCheck (Basename "which") $ checkWhich = CommandCheck (Basename "which") $
\t -> info (getId t) 2230 "which is non-standard. Use builtin 'command -v' instead." \t -> info (getId $ getCommandTokenOrThis t) 2230 "'which' is non-standard. Use builtin 'command -v' instead."
prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file" prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input" prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
@@ -964,7 +1073,7 @@ checkSudoRedirect = CommandCheck (Basename "sudo") f
Just (T_Redirecting _ redirs _) -> Just (T_Redirecting _ redirs _) ->
mapM_ warnAbout redirs mapM_ warnAbout redirs
warnAbout (T_FdRedirect _ s (T_IoFile id op file)) warnAbout (T_FdRedirect _ s (T_IoFile id op file))
| (s == "" || s == "&") && not (special file) = | (null s || s == "&") && not (special file) =
case op of case op of
T_Less _ -> T_Less _ ->
info (getId op) 2024 info (getId op) 2024
@@ -988,9 +1097,9 @@ prop_checkSudoArgs6 = verifyNot checkSudoArgs "sudo -n -u export ls"
prop_checkSudoArgs7 = verifyNot checkSudoArgs "sudo docker export foo" prop_checkSudoArgs7 = verifyNot checkSudoArgs "sudo docker export foo"
checkSudoArgs = CommandCheck (Basename "sudo") f checkSudoArgs = CommandCheck (Basename "sudo") f
where where
f t = potentially $ do f t = sequence_ $ do
opts <- parseOpts t opts <- parseOpts $ arguments t
let nonFlags = map snd $ filter (\(flag, _) -> flag == "") 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
@@ -999,5 +1108,40 @@ checkSudoArgs = CommandCheck (Basename "sudo") f
-- This mess is why ShellCheck prefers not to know. -- This mess is why ShellCheck prefers not to know.
parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:" parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:"
prop_checkSourceArgs1 = verify checkSourceArgs "#!/bin/sh\n. script arg"
prop_checkSourceArgs2 = verifyNot checkSourceArgs "#!/bin/sh\n. script"
prop_checkSourceArgs3 = verifyNot checkSourceArgs "#!/bin/bash\n. script arg"
checkSourceArgs = CommandCheck (Exactly ".") f
where
f t = whenShell [Sh, Dash] $
case arguments t of
(file:arg1:_) -> warn (getId arg1) 2240 $
"The dot command does not support arguments in sh/dash. Set them as variables."
_ -> return ()
prop_checkChmodDashr1 = verify checkChmodDashr "chmod -r 0755 dir"
prop_checkChmodDashr2 = verifyNot checkChmodDashr "chmod -R 0755 dir"
prop_checkChmodDashr3 = verifyNot checkChmodDashr "chmod a-r dir"
checkChmodDashr = CommandCheck (Basename "chmod") f
where
f t = mapM_ check $ arguments t
check t = sequence_ $ do
flag <- getLiteralString t
guard $ flag == "-r"
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

@@ -0,0 +1,21 @@
{-
This empty file is provided for ease of patching in site specific checks.
However, there are no guarantees regarding compatibility between versions.
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Checks.Custom (checker, ShellCheck.Checks.Custom.runTests) where
import ShellCheck.AnalyzerLib
import Test.QuickCheck
checker :: Parameters -> Checker
checker params = Checker {
perScript = const $ return (),
perToken = const $ return ()
}
prop_CustomTestsWork = True
return []
runTests = $quickCheckAll

View File

@@ -30,9 +30,11 @@ import ShellCheck.Regex
import Control.Monad import Control.Monad
import Control.Monad.RWS import Control.Monad.RWS
import Data.Char import Data.Char
import Data.Functor.Identity
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Set as Set
import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess) import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
@@ -72,7 +74,7 @@ prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar" prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
checkForDecimals = ForShell [Sh, Dash, Bash] f checkForDecimals = ForShell [Sh, Dash, Bash] f
where where
f t@(TA_Expansion id _) = potentially $ do f t@(TA_Expansion id _) = sequence_ $ do
str <- getLiteralString t str <- getLiteralString t
first <- str !!! 0 first <- str !!! 0
guard $ isDigit first && '.' `elem` str guard $ isDigit first && '.' `elem` str
@@ -127,7 +129,7 @@ prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT" prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT" prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null" prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/dash\necho $LINENO" prop_checkBashisms48= verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE" prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file" prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1" prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
@@ -136,6 +138,49 @@ prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar" prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"
prop_checkBashisms55= verify checkBashisms "#!/bin/sh\necho ${@%foo}" prop_checkBashisms55= verify checkBashisms "#!/bin/sh\necho ${@%foo}"
prop_checkBashisms56= verifyNot checkBashisms "#!/bin/sh\necho ${##}" prop_checkBashisms56= verifyNot checkBashisms "#!/bin/sh\necho ${##}"
prop_checkBashisms57= verifyNot checkBashisms "#!/bin/dash\nulimit -c 0"
prop_checkBashisms58= verify checkBashisms "#!/bin/sh\nulimit -c 0"
prop_checkBashisms59 = verify checkBashisms "#!/bin/sh\njobs -s"
prop_checkBashisms60 = verifyNot checkBashisms "#!/bin/sh\njobs -p"
prop_checkBashisms61 = verifyNot checkBashisms "#!/bin/sh\njobs -lp"
prop_checkBashisms62 = verify checkBashisms "#!/bin/sh\nexport -f foo"
prop_checkBashisms63 = verifyNot checkBashisms "#!/bin/sh\nexport -p"
prop_checkBashisms64 = verify checkBashisms "#!/bin/sh\nreadonly -a"
prop_checkBashisms65 = verifyNot checkBashisms "#!/bin/sh\nreadonly -p"
prop_checkBashisms66 = verifyNot checkBashisms "#!/bin/sh\ncd -P ."
prop_checkBashisms67 = verify checkBashisms "#!/bin/sh\ncd -P -e ."
prop_checkBashisms68 = verify checkBashisms "#!/bin/sh\numask -p"
prop_checkBashisms69 = verifyNot checkBashisms "#!/bin/sh\numask -S"
prop_checkBashisms70 = verify checkBashisms "#!/bin/sh\ntrap -l"
prop_checkBashisms71 = verify checkBashisms "#!/bin/sh\ntype -a ls"
prop_checkBashisms72 = verifyNot checkBashisms "#!/bin/sh\ntype ls"
prop_checkBashisms73 = verify checkBashisms "#!/bin/sh\nunset -n namevar"
prop_checkBashisms74 = verifyNot checkBashisms "#!/bin/sh\nunset -f namevar"
prop_checkBashisms75 = verifyNot checkBashisms "#!/bin/sh\necho \"-n foo\""
prop_checkBashisms76 = verifyNot checkBashisms "#!/bin/sh\necho \"-ne foo\""
prop_checkBashisms77 = verifyNot checkBashisms "#!/bin/sh\necho -Q foo"
prop_checkBashisms78 = verify checkBashisms "#!/bin/sh\necho -ne foo"
prop_checkBashisms79 = verify checkBashisms "#!/bin/sh\nhash -l"
prop_checkBashisms80 = verifyNot checkBashisms "#!/bin/sh\nhash -r"
prop_checkBashisms81 = verifyNot checkBashisms "#!/bin/dash\nhash -v"
prop_checkBashisms82 = verifyNot checkBashisms "#!/bin/sh\nset -v +o allexport -o errexit -C"
prop_checkBashisms83 = verifyNot checkBashisms "#!/bin/sh\nset --"
prop_checkBashisms84 = verify checkBashisms "#!/bin/sh\nset -o pipefail"
prop_checkBashisms85 = verify checkBashisms "#!/bin/sh\nset -B"
prop_checkBashisms86 = verifyNot checkBashisms "#!/bin/dash\nset -o emacs"
prop_checkBashisms87 = verify checkBashisms "#!/bin/sh\nset -o emacs"
prop_checkBashisms88 = verifyNot checkBashisms "#!/bin/sh\nset -- wget -o foo 'https://some.url'"
prop_checkBashisms89 = verifyNot checkBashisms "#!/bin/sh\nopts=$-\nset -\"$opts\""
prop_checkBashisms90 = verifyNot checkBashisms "#!/bin/sh\nset -o \"$opt\""
prop_checkBashisms91 = verify checkBashisms "#!/bin/sh\nwait -n"
prop_checkBashisms92 = verify checkBashisms "#!/bin/sh\necho $((16#FF))"
prop_checkBashisms93 = verify checkBashisms "#!/bin/sh\necho $(( 10#$(date +%m) ))"
prop_checkBashisms94 = verify checkBashisms "#!/bin/sh\n[ -v var ]"
prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\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
@@ -144,169 +189,246 @@ 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` [ "-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" _) =
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" && "-" `isPrefixOf` argString = | t `isCommand` "echo" && argString `matches` flagRegex =
unless ("--" `isPrefixOf` argString) $ -- echo "-----" if isDash
if isDash then
then when (argString /= "-n") $
when (argString /= "-n") $ warnMsg (getId arg) 3036 "echo flags besides -n"
warnMsg (getId arg) "echo flags besides -n" else
else warnMsg (getId arg) 3037 "echo flags are"
warnMsg (getId arg) "echo flags are" where
where argString = concat $ oversimplify arg argString = concat $ oversimplify arg
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))
| t `isCommand` "set" = unless isDash $
checkOptions $ getLiteralArgs args
where
-- Get the literal options from a list of arguments,
-- up until the first non-literal one
getLiteralArgs :: [Token] -> [(Id, String)]
getLiteralArgs = foldr go []
where
go first rest = case getLiteralString first of
Just str -> (getId first, str) : rest
Nothing -> []
-- Check a flag-option pair (such as -o errexit)
checkOptions (flag@(fid,flag') : opt@(oid,opt') : rest)
| flag' `matches` oFlagRegex = do
when (opt' `notElem` longOptions) $
warnMsg oid 3040 $ "set option " <> opt' <> " is"
checkFlags (flag:rest)
| otherwise = checkFlags (flag:opt:rest)
checkOptions (flag:rest) = checkFlags (flag:rest)
checkOptions _ = return ()
-- Check that each option in a sequence of flags
-- (such as -aveo) is valid
checkFlags (flag@(fid, flag'):rest)
| startsOption flag' = do
unless (flag' `matches` validFlagsRegex) $
forM_ (tail flag') $ \letter ->
when (letter `notElem` optionsSet) $
warnMsg fid 3041 $ "set flag " <> ('-':letter:" is")
checkOptions rest
| beginsWithDoubleDash flag' = do
warnMsg fid 3042 $ "set flag " <> flag' <> " is"
checkOptions rest
-- Either a word that doesn't start with a dash, or simply '--',
-- so stop checking.
| otherwise = return ()
checkFlags [] = return ()
options = "abCefhmnuvxo"
optionsSet = Set.fromList options
startsOption = (`matches` mkRegex "^(\\+|-[^-])")
oFlagRegex = mkRegex $ "^[-+][" <> options <> "]*o$"
validFlagsRegex = mkRegex $ "^[-+]([" <> options <> "]+o?|o)$"
beginsWithDoubleDash = (`matches` mkRegex "^--.+$")
longOptions = Set.fromList
[ "allexport", "errexit", "ignoreeof", "monitor", "noclobber"
, "noexec", "noglob", "nolog", "notify" , "nounset", "verbose"
, "vi", "xtrace" ]
bashism t@(T_SimpleCommand id _ (cmd:rest)) = bashism t@(T_SimpleCommand id _ (cmd:rest)) =
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"
potentially $ do sequence_ $ do
allowed <- Map.lookup name allowedFlags allowed' <- Map.lookup name allowedFlags
(word, flag) <- listToMaybe $ allowed <- allowed'
filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags (word, flag) <- find
return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is" (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
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 = potentially $ do check token = sequence_ $ do
str <- getLiteralString token str <- getLiteralString token
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)
when (name == "printf") $ potentially $ do when (name == "printf") $ sequence_ $ 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 [
("exec", []),
("export", ["-p"]),
("printf", []),
("read", if isDash then ["r", "p"] else ["r"]),
("ulimit", ["f"])
] ]
bashism t@(T_SourceCommand id src _) = allowedFlags = Map.fromList [
let name = fromMaybe "" $ getCommandName src ("cd", Just ["L", "P"]),
in do ("exec", Just []),
when (name == "source") $ warnMsg id "'source' in place of '.' is" ("export", Just ["p"]),
("hash", Just $ if isDash then ["r", "v"] else ["r"]),
("jobs", Just ["l", "p"]),
("printf", Just []),
("read", Just $ if isDash then ["r", "p"] else ["r"]),
("readonly", Just ["p"]),
("trap", Just []),
("type", Just []),
("ulimit", if isDash then Nothing else Just ["f"]),
("umask", Just ["S"]),
("unset", Just ["f", "v"]),
("wait", Just [])
]
bashism t@(T_SourceCommand id src _)
| getCommandName src == Just "source" = warnMsg id 3051 "'source' in place of '.' is"
bashism (TA_Expansion _ (T_Literal id str : _))
| str `matches` radix = warnMsg id 3052 "arithmetic base conversion is"
where
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 = [
"LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME", -- This list deliberately excludes $BASH_VERSION as it's often used
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS" -- for shell identification.
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
"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 = [ "LINENO" ] dashVars = [ "_" ]
isBashVariable var = isBashVariable var =
(var `elem` bashDynamicVars (var `elem` bashDynamicVars
|| var `elem` bashVars && not (isAssigned var)) || var `elem` bashVars && not (isAssigned var))
@@ -318,31 +440,44 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
_ -> False _ -> False
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')" prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
prop_checkEchoSed1b= verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")"
prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')" prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
prop_checkEchoSed2b= verify checkEchoSed "rm $(sed -e 's,foo,bar,' <<< $cow)"
checkEchoSed = ForShell [Bash, Ksh] f checkEchoSed = ForShell [Bash, Ksh] f
where where
f (T_Redirecting id lefts r) =
when (any redirectHereString lefts) $
checkSed id rcmd
where
redirectHereString :: Token -> Bool
redirectHereString t = case t of
(T_FdRedirect _ _ T_HereString{}) -> True
_ -> False
rcmd = oversimplify r
f (T_Pipeline id _ [a, b]) = f (T_Pipeline id _ [a, b]) =
when (acmd == ["echo", "${VAR}"]) $ when (acmd == ["echo", "${VAR}"]) $
case bcmd of checkSed id bcmd
["sed", v] -> checkIn v
["sed", "-e", v] -> checkIn v
_ -> return ()
where where
-- This should have used backreferences, but TDFA doesn't support them
sedRe = mkRegex "^s(.)([^\n]*)g?$"
isSimpleSed s = fromMaybe False $ do
[first,rest] <- matchRegex sedRe s
let delimiters = filter (== head first) rest
guard $ length delimiters == 2
return True
acmd = oversimplify a acmd = oversimplify a
bcmd = oversimplify b bcmd = oversimplify b
checkIn s =
when (isSimpleSed s) $
style id 2001 "See if you can use ${variable//search/replace} instead."
f _ = return () f _ = return ()
checkSed id ["sed", v] = checkIn id v
checkSed id ["sed", "-e", v] = checkIn id v
checkSed _ _ = return ()
-- This should have used backreferences, but TDFA doesn't support them
sedRe = mkRegex "^s(.)([^\n]*)g?$"
isSimpleSed s = isJust $ do
[h:_,rest] <- matchRegex sedRe s
let delimiters = filter (== h) rest
guard $ length delimiters == 2
checkIn id s =
when (isSimpleSed s) $
style id 2001 "See if you can use ${variable//search/replace} instead."
prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}" prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}"
prop_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}" prop_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}"
@@ -365,11 +500,11 @@ checkBraceExpansionVars = ForShell [Bash] f
T_DollarBraced {} -> return "$" T_DollarBraced {} -> return "$"
T_DollarExpansion {} -> return "$" T_DollarExpansion {} -> return "$"
T_DollarArithmetic {} -> return "$" T_DollarArithmetic {} -> return "$"
otherwise -> return "-" _ -> return "-"
toString t = fromJust $ getLiteralStringExt literalExt t toString t = runIdentity $ getLiteralStringExt literalExt t
isEvaled t = do isEvaled t = do
cmd <- getClosestCommandM t cmd <- getClosestCommandM t
return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval" return $ maybe False (`isUnqualifiedCommand` "eval") cmd
prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3" prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3"
@@ -384,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

@@ -36,13 +36,29 @@ internalVariables = [
-- Ksh -- Ksh
, ".sh.version" , ".sh.version"
-- shflags
, "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP",
"FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION",
"flags_error", "flags_return"
] ]
variablesWithoutSpaces = [ specialVariablesWithoutSpaces = [
"$", "-", "?", "!", "$", "-", "?", "!", "#"
]
variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO", "BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID", "OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES" "COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
-- shflags
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
]
specialVariables = specialVariablesWithoutSpaces ++ ["@", "*"]
unbracedVariables = specialVariables ++ [
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
] ]
arrayVariables = [ arrayVariables = [
@@ -98,6 +114,10 @@ binaryTestOps = [
"-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>=" "-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="
] ]
arithmeticBinaryTestOps = [
"-eq", "-ne", "-lt", "-le", "-gt", "-ge"
]
unaryTestOps = [ unaryTestOps = [
"!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p", "!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p",
"-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n", "-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n",
@@ -109,9 +129,12 @@ shellForExecutable name =
case name of case name of
"sh" -> return Sh "sh" -> return Sh
"bash" -> return Bash "bash" -> return Bash
"bats" -> return Bash
"dash" -> return Dash "dash" -> return Dash
"ash" -> return Dash -- There's also a warning for this. "ash" -> return Dash -- There's also a warning for this.
"ksh" -> return Ksh "ksh" -> return Ksh
"ksh88" -> return Ksh "ksh88" -> return Ksh
"ksh93" -> return Ksh "ksh93" -> return Ksh
otherwise -> Nothing _ -> Nothing
flagsForRead = "sreu:n:N:i:p:a:t:"

409
src/ShellCheck/Fixer.hs Normal file
View File

@@ -0,0 +1,409 @@
{-
Copyright 2018-2019 Vidar Holen, Ng Zhi An
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Fixer (applyFix, removeTabStops, mapPositions, Ranged(..), runTests) where
import ShellCheck.Interface
import Control.Monad.State
import Data.Array
import Data.List
import Data.Semigroup
import GHC.Exts (sortWith)
import Test.QuickCheck
-- The Ranged class is used for types that has a start and end position.
class Ranged a where
start :: a -> Position
end :: a -> Position
overlap :: a -> a -> Bool
overlap x y =
(yStart >= xStart && yStart < xEnd) || (yStart < xStart && yEnd > xStart)
where
yStart = start y
yEnd = end y
xStart = start x
xEnd = end x
-- Set a new start and end position on a Ranged
setRange :: (Position, Position) -> a -> a
-- Tests auto-verify that overlap commutes
assertOverlap x y = overlap x y && overlap y x
assertNoOverlap x y = not (overlap x y) && not (overlap y x)
prop_overlap_contiguous = assertNoOverlap
(tFromStart 10 12 "foo" 1)
(tFromStart 12 14 "bar" 2)
prop_overlap_adjacent_zerowidth = assertNoOverlap
(tFromStart 3 3 "foo" 1)
(tFromStart 3 3 "bar" 2)
prop_overlap_enclosed = assertOverlap
(tFromStart 3 5 "foo" 1)
(tFromStart 1 10 "bar" 2)
prop_overlap_partial = assertOverlap
(tFromStart 1 5 "foo" 1)
(tFromStart 3 7 "bar" 2)
instance Ranged PositionedComment where
start = pcStartPos
end = pcEndPos
setRange (s, e) pc = pc {
pcStartPos = s,
pcEndPos = e
}
instance Ranged Replacement where
start = repStartPos
end = repEndPos
setRange (s, e) r = r {
repStartPos = s,
repEndPos = e
}
-- The Monoid instance for Fix merges fixes that do not conflict.
-- TODO: Make an efficient 'mconcat'
instance Monoid Fix where
mempty = newFix
mappend = (<>)
instance Semigroup Fix where
f1 <> f2 =
-- FIXME: This might need to also discard adjacent zero-width ranges for
-- when two fixes change the same AST node, e.g. `foo` -> "$(foo)"
if or [ r2 `overlap` r1 | r1 <- fixReplacements f1, r2 <- fixReplacements f2 ]
then f1
else newFix {
fixReplacements = fixReplacements f1 ++ fixReplacements f2
}
-- Conveniently apply a transformation to positions in a Fix
mapPositions :: (Position -> Position) -> Fix -> Fix
mapPositions f = adjustFix
where
adjustReplacement rep =
rep {
repStartPos = f $ repStartPos rep,
repEndPos = f $ repEndPos rep
}
adjustFix fix =
fix {
fixReplacements = map adjustReplacement $ fixReplacements fix
}
-- Rewrite a Ranged from a tabstop of 8 to 1
removeTabStops :: Ranged a => a -> Array Int String -> a
removeTabStops range ls =
let startColumn = realignColumn lineNo colNo range
endColumn = realignColumn endLineNo endColNo range
startPosition = (start range) { posColumn = startColumn }
endPosition = (end range) { posColumn = endColumn } in
setRange (startPosition, endPosition) range
where
realignColumn lineNo colNo c =
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
then real (ls ! fromIntegral (lineNo c)) 0 0 (colNo c)
else colNo c
real _ r v target | target <= v = r
-- hit this case at the end of line, and if we don't hit the target
-- return real + (target - v)
real [] r v target = r + (target - v)
real ('\t':rest) r v target = real rest (r+1) (v + 8 - (v `mod` 8)) target
real (_:rest) r v target = real rest (r+1) (v+1) target
lineNo = posLine . start
endLineNo = posLine . end
colNo = posColumn . start
endColNo = posColumn . end
-- A replacement that spans multiple line is applied by:
-- 1. merging the affected lines into a single string using `unlines`
-- 2. apply the replacement as if it only spanned a single line
-- The tricky part is adjusting the end column of the replacement
-- (the end line doesn't matter because there is only one line)
--
-- aaS <--- start of replacement (row 1 column 3)
-- bbbb
-- cEc
-- \------- end of replacement (row 3 column 2)
--
-- a flattened string will look like:
--
-- "aaS\nbbbb\ncEc\n"
--
-- The column of E has to be adjusted by:
-- 1. lengths of lines to be replaced, except the end row itself
-- 2. end column of the replacement
-- 3. number of '\n' by `unlines`
multiToSingleLine :: [Fix] -> Array Int String -> ([Fix], String)
multiToSingleLine fixes lines =
(map (mapPositions adjust) fixes, unlines $ elems lines)
where
-- A prefix sum tree from line number to column shift.
-- FIXME: The tree will be totally unbalanced.
shiftTree :: PSTree Int
shiftTree =
foldl (\t (n,s) -> addPSValue (n+1) (length s + 1) t) newPSTree $
assocs lines
singleString = unlines $ elems lines
adjust pos =
pos {
posLine = 1,
posColumn = (posColumn pos) +
(fromIntegral $ getPrefixSum (fromIntegral $ posLine pos) shiftTree)
}
-- Apply a fix and return resulting lines.
-- The number of lines can increase or decrease with no obvious mapping back, so
-- the function does not return an array.
applyFix :: Fix -> Array Int String -> [String]
applyFix fix fileLines =
let
untabbed = fix {
fixReplacements =
map (\c -> removeTabStops c fileLines) $
fixReplacements fix
}
(adjustedFixes, singleLine) = multiToSingleLine [untabbed] fileLines
in
lines . runFixer $ applyFixes2 adjustedFixes singleLine
-- start and end comes from pos, which is 1 based
prop_doReplace1 = doReplace 0 0 "1234" "A" == "A1234" -- technically not valid
prop_doReplace2 = doReplace 1 1 "1234" "A" == "A1234"
prop_doReplace3 = doReplace 1 2 "1234" "A" == "A234"
prop_doReplace4 = doReplace 3 3 "1234" "A" == "12A34"
prop_doReplace5 = doReplace 4 4 "1234" "A" == "123A4"
prop_doReplace6 = doReplace 5 5 "1234" "A" == "1234A"
doReplace start end o r =
let si = fromIntegral (start-1)
ei = fromIntegral (end-1)
(x, xs) = splitAt si o
z = drop (ei - si) xs
in
x ++ r ++ z
-- Fail if the 'expected' string is not result when applying 'fixes' to 'original'.
testFixes :: String -> String -> [Fix] -> Bool
testFixes expected original fixes =
actual == expected
where
actual = runFixer (applyFixes2 fixes original)
-- A Fixer allows doing repeated modifications of a string where each
-- replacement automatically accounts for shifts from previous ones.
type Fixer a = State (PSTree Int) a
-- Apply a single replacement using its indices into the original string.
-- It does not handle multiple lines, all line indices must be 1.
applyReplacement2 :: Replacement -> String -> Fixer String
applyReplacement2 rep string = do
tree <- get
let transform pos = pos + getPrefixSum pos tree
let originalPos = (repStartPos rep, repEndPos rep)
(oldStart, oldEnd) = tmap (fromInteger . posColumn) originalPos
(newStart, newEnd) = tmap transform (oldStart, oldEnd)
let (l1, l2) = tmap posLine originalPos in
when (l1 /= 1 || l2 /= 1) $
error "ShellCheck internal error, please report: bad cross-line fix"
let replacer = repString rep
let shift = (length replacer) - (oldEnd - oldStart)
let insertionPoint =
case repInsertionPoint rep of
InsertBefore -> oldStart
InsertAfter -> oldEnd+1
put $ addPSValue insertionPoint shift tree
return $ doReplace newStart newEnd string replacer
where
tmap f (a,b) = (f a, f b)
-- Apply a list of Replacements in the correct order
applyReplacements2 :: [Replacement] -> String -> Fixer String
applyReplacements2 reps str =
foldM (flip applyReplacement2) str $
reverse $ sortWith repPrecedence reps
-- Apply all fixes with replacements in the correct order
applyFixes2 :: [Fix] -> String -> Fixer String
applyFixes2 fixes = applyReplacements2 (concatMap fixReplacements fixes)
-- Get the final value of a Fixer.
runFixer :: Fixer a -> a
runFixer f = evalState f newPSTree
-- A Prefix Sum Tree that lets you look up the sum of values at and below an index.
-- It's implemented essentially as a Fenwick tree without the bit-based balancing.
-- The last Num is the sum of the left branch plus current element.
data PSTree n = PSBranch n (PSTree n) (PSTree n) n | PSLeaf
deriving (Show)
newPSTree :: Num n => PSTree n
newPSTree = PSLeaf
-- Get the sum of values whose keys are <= 'target'
getPrefixSum :: (Ord n, Num n) => n -> PSTree n -> n
getPrefixSum = f 0
where
f sum _ PSLeaf = sum
f sum target (PSBranch pivot left right cumulative) =
case target `compare` pivot of
LT -> f sum target left
GT -> f (sum+cumulative) target right
EQ -> sum+cumulative
-- Add a value to the Prefix Sum tree at the given index.
-- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5
addPSValue :: (Ord n, Num n) => n -> n -> PSTree n -> PSTree n
addPSValue key value tree = if value == 0 then tree else f tree
where
f PSLeaf = PSBranch key PSLeaf PSLeaf value
f (PSBranch pivot left right sum) =
case key `compare` pivot of
LT -> PSBranch pivot (f left) right (sum + value)
GT -> PSBranch pivot left (f right) sum
EQ -> PSBranch pivot left right (sum + value)
prop_pstreeSumsCorrectly kvs targets =
let
-- Trivial O(n * m) implementation
dumbPrefixSums :: [(Int, Int)] -> [Int] -> [Int]
dumbPrefixSums kvs targets =
let prefixSum target = sum [v | (k,v) <- kvs, k <= target]
in map prefixSum targets
-- PSTree O(n * log m) implementation
smartPrefixSums :: [(Int, Int)] -> [Int] -> [Int]
smartPrefixSums kvs targets =
let tree = foldl (\tree (pos, shift) -> addPSValue pos shift tree) PSLeaf kvs
in map (\x -> getPrefixSum x tree) targets
in smartPrefixSums kvs targets == dumbPrefixSums kvs targets
-- Semi-convenient functions for constructing tests.
testFix :: [Replacement] -> Fix
testFix list = newFix {
fixReplacements = list
}
tFromStart :: Int -> Int -> String -> Int -> Replacement
tFromStart start end repl order =
newReplacement {
repStartPos = newPosition {
posLine = 1,
posColumn = fromIntegral start
},
repEndPos = newPosition {
posLine = 1,
posColumn = fromIntegral end
},
repString = repl,
repPrecedence = order,
repInsertionPoint = InsertAfter
}
tFromEnd start end repl order =
(tFromStart start end repl order) {
repInsertionPoint = InsertBefore
}
prop_simpleFix1 = testFixes "hello world" "hell world" [
testFix [
tFromEnd 5 5 "o" 1
]]
prop_anchorsLeft = testFixes "-->foobar<--" "--><--" [
testFix [
tFromStart 4 4 "foo" 1,
tFromStart 4 4 "bar" 2
]]
prop_anchorsRight = testFixes "-->foobar<--" "--><--" [
testFix [
tFromEnd 4 4 "bar" 1,
tFromEnd 4 4 "foo" 2
]]
prop_anchorsBoth1 = testFixes "-->foobar<--" "--><--" [
testFix [
tFromStart 4 4 "bar" 2,
tFromEnd 4 4 "foo" 1
]]
prop_anchorsBoth2 = testFixes "-->foobar<--" "--><--" [
testFix [
tFromEnd 4 4 "foo" 2,
tFromStart 4 4 "bar" 1
]]
prop_composeFixes1 = testFixes "cd \"$1\" || exit" "cd $1" [
testFix [
tFromStart 4 4 "\"" 10,
tFromEnd 6 6 "\"" 10
],
testFix [
tFromEnd 6 6 " || exit" 5
]]
prop_composeFixes2 = testFixes "$(\"$1\")" "`$1`" [
testFix [
tFromStart 1 2 "$(" 5,
tFromEnd 4 5 ")" 5
],
testFix [
tFromStart 2 2 "\"" 10,
tFromEnd 4 4 "\"" 10
]]
prop_composeFixes3 = testFixes "(x)[x]" "xx" [
testFix [
tFromStart 1 1 "(" 4,
tFromEnd 2 2 ")" 3,
tFromStart 2 2 "[" 2,
tFromEnd 3 3 "]" 1
]]
prop_composeFixes4 = testFixes "(x)[x]" "xx" [
testFix [
tFromStart 1 1 "(" 4,
tFromStart 2 2 "[" 3,
tFromEnd 2 2 ")" 2,
tFromEnd 3 3 "]" 1
]]
prop_composeFixes5 = testFixes "\"$(x)\"" "`x`" [
testFix [
tFromStart 1 2 "$(" 2,
tFromEnd 3 4 ")" 2,
tFromStart 1 1 "\"" 1,
tFromEnd 4 4 "\"" 1
]]
return []
runTests = $quickCheckAll

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net

View File

@@ -0,0 +1,258 @@
{-
Copyright 2019 Vidar 'koala_man' Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Formatter.Diff (format, ShellCheck.Formatter.Diff.runTests) where
import ShellCheck.Interface
import ShellCheck.Fixer
import ShellCheck.Formatter.Format
import Control.Monad
import Data.Algorithm.Diff
import Data.Array
import Data.IORef
import Data.List
import qualified Data.Monoid as Monoid
import Data.Maybe
import qualified Data.Map as M
import GHC.Exts (sortWith)
import System.IO
import System.FilePath
import Test.QuickCheck
import Debug.Trace
ltt x = trace (show x) x
format :: FormatterOptions -> IO Formatter
format options = do
foundIssues <- newIORef False
reportedIssues <- newIORef False
shouldColor <- shouldOutputColor (foColorOption options)
let color = if shouldColor then colorize else nocolor
return Formatter {
header = return (),
footer = checkFooter foundIssues reportedIssues color,
onFailure = reportFailure color,
onResult = reportResult foundIssues reportedIssues color
}
contextSize = 3
red = 31
green = 32
yellow = 33
cyan = 36
bold = 1
nocolor n = id
colorize n s = (ansi n) ++ s ++ (ansi 0)
ansi n = "\x1B[" ++ show n ++ "m"
printErr :: ColorFunc -> String -> IO ()
printErr color = hPutStrLn stderr . color bold . color red
reportFailure color file msg = printErr color $ file ++ ": " ++ msg
checkFooter foundIssues reportedIssues color = do
found <- readIORef foundIssues
output <- readIORef reportedIssues
when (found && not output) $
printErr color "Issues were detected, but none were auto-fixable. Use another format to see them."
type ColorFunc = (Int -> String -> String)
data LFStatus = LinefeedMissing | LinefeedOk
data DiffDoc a = DiffDoc String LFStatus [DiffRegion a]
data DiffRegion a = DiffRegion (Int, Int) (Int, Int) [Diff a]
reportResult :: (IORef Bool) -> (IORef Bool) -> ColorFunc -> CheckResult -> SystemInterface IO -> IO ()
reportResult foundIssues reportedIssues color result sys = do
let comments = crComments result
unless (null comments) $ writeIORef foundIssues True
let suggestedFixes = mapMaybe pcFix comments
let fixmap = buildFixMap suggestedFixes
mapM_ output $ M.toList fixmap
where
output (name, fix) = do
file <- (siReadFile sys) name
case file of
Right contents -> do
putStrLn $ formatDoc color $ makeDiff name contents fix
writeIORef reportedIssues True
Left msg -> reportFailure color name msg
hasTrailingLinefeed str =
case str of
[] -> True
_ -> last str == '\n'
coversLastLine regions =
case regions of
[] -> False
_ -> (fst $ last regions)
-- TODO: Factor this out into a unified diff library because we're doing a lot
-- of the heavy lifting anyways.
makeDiff :: String -> String -> Fix -> DiffDoc String
makeDiff name contents fix = do
let hunks = groupDiff $ computeDiff contents fix
let lf = if coversLastLine hunks && not (hasTrailingLinefeed contents)
then LinefeedMissing
else LinefeedOk
DiffDoc name lf $ findRegions hunks
computeDiff :: String -> Fix -> [Diff String]
computeDiff contents fix =
let old = lines contents
array = listArray (1, fromIntegral $ (length old)) old
new = applyFix fix array
in getDiff old new
-- Group changes into hunks
groupDiff :: [Diff a] -> [(Bool, [Diff a])]
groupDiff = filter (\(_, l) -> not (null l)) . hunt []
where
-- Churn through 'Both's until we find a difference
hunt current [] = [(False, reverse current)]
hunt current (x@Both {}:rest) = hunt (x:current) rest
hunt current list =
let (context, previous) = splitAt contextSize current
in (False, reverse previous) : gather context 0 list
-- Pick out differences until we find a run of Both's
gather current n [] =
let (extras, patch) = splitAt (max 0 $ n - contextSize) current
in [(True, reverse patch), (False, reverse extras)]
gather current n list@(Both {}:_) | n == contextSize*2 =
let (context, previous) = splitAt contextSize current
in (True, reverse previous) : hunt context list
gather current n (x@Both {}:rest) = gather (x:current) (n+1) rest
gather current n (x:rest) = gather (x:current) 0 rest
-- Get line numbers for hunks
findRegions :: [(Bool, [Diff String])] -> [DiffRegion String]
findRegions = find' 1 1
where
find' _ _ [] = []
find' left right ((output, run):rest) =
let (dl, dr) = countDelta run
remainder = find' (left+dl) (right+dr) rest
in
if output
then DiffRegion (left, dl) (right, dr) run : remainder
else remainder
-- Get left/right line counts for a hunk
countDelta :: [Diff a] -> (Int, Int)
countDelta = count' 0 0
where
count' left right [] = (left, right)
count' left right (x:rest) =
case x of
Both {} -> count' (left+1) (right+1) rest
First {} -> count' (left+1) right rest
Second {} -> count' left (right+1) rest
formatRegion :: ColorFunc -> LFStatus -> DiffRegion String -> String
formatRegion color lf (DiffRegion left right diffs) =
let header = color cyan ("@@ -" ++ (tup left) ++ " +" ++ (tup right) ++" @@")
in
unlines $ header : reverse (getStrings lf (reverse diffs))
where
noLF = "\\ No newline at end of file"
getStrings LinefeedOk list = map format list
getStrings LinefeedMissing list@((Both _ _):_) = noLF : map format list
getStrings LinefeedMissing list@((First _):_) = noLF : map format list
getStrings LinefeedMissing (last:rest) = format last : getStrings LinefeedMissing rest
tup (a,b) = (show a) ++ "," ++ (show b)
format (Both x _) = ' ':x
format (First x) = color red $ '-':x
format (Second x) = color green $ '+':x
splitLast [] = ([], [])
splitLast x =
let (last, rest) = splitAt 1 $ reverse x
in (reverse rest, last)
formatDoc color (DiffDoc name lf regions) =
let (most, last) = splitLast regions
in
(color bold $ "--- " ++ ("a" </> name)) ++ "\n" ++
(color bold $ "+++ " ++ ("b" </> name)) ++ "\n" ++
concatMap (formatRegion color LinefeedOk) most ++
concatMap (formatRegion color lf) last
-- Create a Map from filename to Fix
buildFixMap :: [Fix] -> M.Map String Fix
buildFixMap fixes = perFile
where
splitFixes = concatMap splitFixByFile fixes
perFile = groupByMap (posFile . repStartPos . head . fixReplacements) splitFixes
-- There are currently no multi-file fixes, but let's handle it anyways
splitFixByFile :: Fix -> [Fix]
splitFixByFile fix = map makeFix $ groupBy sameFile (fixReplacements fix)
where
sameFile rep1 rep2 = (posFile $ repStartPos rep1) == (posFile $ repStartPos rep2)
makeFix reps = newFix { fixReplacements = reps }
groupByMap :: (Ord k, Monoid v) => (v -> k) -> [v] -> M.Map k v
groupByMap f = M.fromListWith Monoid.mappend . map (\x -> (f x, x))
-- For building unit tests
b n = Both n n
l = First
r = Second
prop_identifiesProperContext = groupDiff [b 1, b 2, b 3, b 4, l 5, b 6, b 7, b 8, b 9] ==
[(False, [b 1]), -- Omitted
(True, [b 2, b 3, b 4, l 5, b 6, b 7, b 8]), -- A change with three lines of context
(False, [b 9])] -- Omitted
prop_includesContextFromStartIfNecessary = groupDiff [b 4, l 5, b 6, b 7, b 8, b 9] ==
[ -- Nothing omitted
(True, [b 4, l 5, b 6, b 7, b 8]), -- A change with three lines of context
(False, [b 9])] -- Omitted
prop_includesContextUntilEndIfNecessary = groupDiff [b 4, l 5] ==
[ -- Nothing omitted
(True, [b 4, l 5])
] -- Nothing Omitted
prop_splitsIntoMultipleHunks = groupDiff [l 1, b 1, b 2, b 3, b 4, b 5, b 6, b 7, r 8] ==
[ -- Nothing omitted
(True, [l 1, b 1, b 2, b 3]),
(False, [b 4]),
(True, [b 5, b 6, b 7, r 8])
] -- Nothing Omitted
prop_splitsIntoMultipleHunksUnlessTouching = groupDiff [l 1, b 1, b 2, b 3, b 4, b 5, b 6, r 7] ==
[
(True, [l 1, b 1, b 2, b 3, b 4, b 5, b 6, r 7])
]
prop_countDeltasWorks = countDelta [b 1, l 2, r 3, r 4, b 5] == (3,4)
prop_countDeltasWorks2 = countDelta [] == (0,0)
return []
runTests = $quickCheckAll

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -21,6 +21,13 @@ module ShellCheck.Formatter.Format where
import ShellCheck.Data import ShellCheck.Data
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Fixer
import Control.Monad
import Data.Array
import Data.List
import System.IO
import System.Info
-- A formatter that carries along an arbitrary piece of data -- A formatter that carries along an arbitrary piece of data
data Formatter = Formatter { data Formatter = Formatter {
@@ -30,17 +37,17 @@ data Formatter = Formatter {
footer :: IO () footer :: IO ()
} }
sourceFile (PositionedComment pos _ _) = posFile pos sourceFile = posFile . pcStartPos
lineNo (PositionedComment pos _ _) = posLine pos lineNo = posLine . pcStartPos
endLineNo (PositionedComment _ end _) = posLine end endLineNo = posLine . pcEndPos
colNo (PositionedComment pos _ _) = posColumn pos colNo = posColumn . pcStartPos
endColNo (PositionedComment _ end _) = posColumn end endColNo = posColumn . pcEndPos
codeNo (PositionedComment _ _ (Comment _ code _)) = code codeNo = cCode . pcComment
messageText (PositionedComment _ _ (Comment _ _ t)) = t messageText = cMessage . pcComment
severityText :: PositionedComment -> String severityText :: PositionedComment -> String
severityText (PositionedComment _ _ (Comment c _ _)) = severityText pc =
case c of case cSeverity (pcComment pc) of
ErrorC -> "error" ErrorC -> "error"
WarningC -> "warning" WarningC -> "warning"
InfoC -> "info" InfoC -> "info"
@@ -50,18 +57,23 @@ severityText (PositionedComment _ _ (Comment c _ _)) =
makeNonVirtual comments contents = makeNonVirtual comments contents =
map fix comments map fix comments
where where
ls = lines contents list = lines contents
fix c@(PositionedComment start end comment) = PositionedComment start { arr = listArray (1, length list) list
posColumn = realignColumn lineNo colNo c untabbedFix f = newFix {
} end { fixReplacements = map (\r -> removeTabStops r arr) (fixReplacements f)
posColumn = realignColumn endLineNo endColNo c }
} comment fix c = (removeTabStops c arr) {
realignColumn lineNo colNo c = pcFix = fmap untabbedFix (pcFix c)
if lineNo c > 0 && lineNo c <= fromIntegral (length ls) }
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
else colNo c
real _ r v target | target <= v = r shouldOutputColor :: ColorOption -> IO Bool
real [] r v _ = r -- should never happen shouldOutputColor colorOption = do
real ('\t':rest) r v target = term <- hIsTerminalDevice stdout
real rest (r+1) (v + 8 - (v `mod` 8)) target let windows = "mingw" `isPrefixOf` os
real (_:rest) r v target = real rest (r+1) (v+1) target let isUsableTty = term && not windows
let useColor = case colorOption of
ColorAlways -> True
ColorNever -> False
ColorAuto -> isUsableTty
return useColor

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net

View File

@@ -1,6 +1,6 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -30,6 +30,7 @@ import GHC.Exts
import System.IO import System.IO
import qualified Data.ByteString.Lazy.Char8 as BL import qualified Data.ByteString.Lazy.Char8 as BL
format :: IO Formatter
format = do format = do
ref <- newIORef [] ref <- newIORef []
return Formatter { return Formatter {
@@ -39,8 +40,29 @@ format = do
footer = finish ref footer = finish ref
} }
instance ToJSON (PositionedComment) where instance ToJSON Replacement where
toJSON comment@(PositionedComment start end (Comment level code string)) = toJSON replacement =
let start = repStartPos replacement
end = repEndPos replacement
str = repString replacement in
object [
"precedence" .= repPrecedence replacement,
"insertionPoint" .=
case repInsertionPoint replacement of
InsertBefore -> "beforeStart" :: String
InsertAfter -> "afterEnd",
"line" .= posLine start,
"column" .= posColumn start,
"endLine" .= posLine end,
"endColumn" .= posColumn end,
"replacement" .= str
]
instance ToJSON PositionedComment where
toJSON comment =
let start = pcStartPos comment
end = pcEndPos comment
c = pcComment comment in
object [ object [
"file" .= posFile start, "file" .= posFile start,
"line" .= posLine start, "line" .= posLine start,
@@ -48,11 +70,15 @@ instance ToJSON (PositionedComment) where
"column" .= posColumn start, "column" .= posColumn start,
"endColumn" .= posColumn end, "endColumn" .= posColumn end,
"level" .= severityText comment, "level" .= severityText comment,
"code" .= code, "code" .= cCode c,
"message" .= string "message" .= cMessage c,
"fix" .= pcFix comment
] ]
toEncoding comment@(PositionedComment start end (Comment level code string)) = toEncoding comment =
let start = pcStartPos comment
end = pcEndPos comment
c = pcComment comment in
pairs ( pairs (
"file" .= posFile start "file" .= posFile start
<> "line" .= posLine start <> "line" .= posLine start
@@ -60,15 +86,25 @@ instance ToJSON (PositionedComment) where
<> "column" .= posColumn start <> "column" .= posColumn start
<> "endColumn" .= posColumn end <> "endColumn" .= posColumn end
<> "level" .= severityText comment <> "level" .= severityText comment
<> "code" .= code <> "code" .= cCode c
<> "message" .= string <> "message" .= cMessage c
<> "fix" .= pcFix comment
) )
instance ToJSON Fix where
toJSON fix = object [
"replacements" .= fixReplacements fix
]
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
collectResult ref result _ =
modifyIORef ref (\x -> crComments result ++ x) collectResult ref cr sys = mapM_ f groups
where
comments = crComments cr
groups = groupWith sourceFile comments
f :: [PositionedComment] -> IO ()
f group = modifyIORef ref (\x -> comments ++ x)
finish ref = do finish ref = do
list <- readIORef ref list <- readIORef ref
BL.putStrLn $ encode list BL.putStrLn $ encode list

View File

@@ -0,0 +1,127 @@
{-# LANGUAGE OverloadedStrings #-}
{-
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.JSON1 (format) where
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Data.Aeson
import Data.IORef
import Data.Monoid
import GHC.Exts
import System.IO
import qualified Data.ByteString.Lazy.Char8 as BL
format :: IO Formatter
format = do
ref <- newIORef []
return Formatter {
header = return (),
onResult = collectResult ref,
onFailure = outputError,
footer = finish ref
}
data Json1Output = Json1Output {
comments :: [PositionedComment]
}
instance ToJSON Json1Output where
toJSON result = object [
"comments" .= comments result
]
toEncoding result = pairs (
"comments" .= comments result
)
instance ToJSON Replacement where
toJSON replacement =
let start = repStartPos replacement
end = repEndPos replacement
str = repString replacement in
object [
"precedence" .= repPrecedence replacement,
"insertionPoint" .=
case repInsertionPoint replacement of
InsertBefore -> "beforeStart" :: String
InsertAfter -> "afterEnd",
"line" .= posLine start,
"column" .= posColumn start,
"endLine" .= posLine end,
"endColumn" .= posColumn end,
"replacement" .= str
]
instance ToJSON PositionedComment where
toJSON comment =
let start = pcStartPos comment
end = pcEndPos comment
c = pcComment comment in
object [
"file" .= posFile start,
"line" .= posLine start,
"endLine" .= posLine end,
"column" .= posColumn start,
"endColumn" .= posColumn end,
"level" .= severityText comment,
"code" .= cCode c,
"message" .= cMessage c,
"fix" .= pcFix comment
]
toEncoding comment =
let start = pcStartPos comment
end = pcEndPos comment
c = pcComment comment in
pairs (
"file" .= posFile start
<> "line" .= posLine start
<> "endLine" .= posLine end
<> "column" .= posColumn start
<> "endColumn" .= posColumn end
<> "level" .= severityText comment
<> "code" .= cCode c
<> "message" .= cMessage c
<> "fix" .= pcFix comment
)
instance ToJSON Fix where
toJSON fix = object [
"replacements" .= fixReplacements fix
]
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
collectResult ref cr sys = mapM_ f groups
where
comments = crComments cr
groups = groupWith sourceFile comments
f :: [PositionedComment] -> IO ()
f group = do
let filename = sourceFile (head group)
result <- siReadFile sys filename
let contents = either (const "") id result
let comments' = makeNonVirtual comments contents
modifyIORef ref (\x -> comments' ++ x)
finish ref = do
list <- readIORef ref
BL.putStrLn $ encode $ Json1Output { comments = list }

View File

@@ -0,0 +1,36 @@
{-
Copyright 2019 Austin Voecks
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.Quiet (format) where
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Control.Monad
import Data.IORef
import System.Exit
format :: FormatterOptions -> IO Formatter
format options =
return Formatter {
header = return (),
footer = return (),
onFailure = \ _ _ -> exitFailure,
onResult = \ result _ -> unless (null $ crComments result) exitFailure
}

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -19,21 +19,37 @@
-} -}
module ShellCheck.Formatter.TTY (format) where module ShellCheck.Formatter.TTY (format) where
import ShellCheck.Fixer
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Formatter.Format import ShellCheck.Formatter.Format
import Control.Monad
import Data.Array
import Data.Foldable
import Data.Ord
import Data.IORef
import Data.List import Data.List
import Data.Maybe
import GHC.Exts import GHC.Exts
import System.Info
import System.IO import System.IO
import System.Info
wikiLink = "https://www.shellcheck.net/wiki/"
-- An arbitrary Ord thing to order warnings
type Ranking = (Char, Severity, Integer)
-- Ansi coloring function
type ColorFunc = (String -> String -> String)
format :: FormatterOptions -> IO Formatter format :: FormatterOptions -> IO Formatter
format options = return Formatter { format options = do
header = return (), topErrorRef <- newIORef []
footer = return (), return Formatter {
onFailure = outputError options, header = return (),
onResult = outputResult options footer = outputWiki topErrorRef,
} onFailure = outputError options,
onResult = outputResult options topErrorRef
}
colorForLevel level = colorForLevel level =
case level of case level of
@@ -41,17 +57,65 @@ colorForLevel level =
"warning" -> 33 -- yellow "warning" -> 33 -- yellow
"info" -> 32 -- green "info" -> 32 -- green
"style" -> 32 -- green "style" -> 32 -- green
"verbose" -> 32 -- green
"message" -> 1 -- bold "message" -> 1 -- bold
"source" -> 0 -- none "source" -> 0 -- none
_ -> 0 -- none _ -> 0 -- none
rankError :: PositionedComment -> Ranking
rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err)
where
ranking =
if cCode (pcComment err) `elem` uninteresting
then 'Z'
else 'A'
-- A list of the most generic, least directly helpful
-- error codes to downrank.
uninteresting = [
1009, -- Mentioned parser error was..
1019, -- Expected this to be an argument
1036, -- ( is invalid here
1047, -- Expected 'fi'
1062, -- Expected 'done'
1070, -- Parsing stopped here (generic)
1072, -- Missing/unexpected ..
1073, -- Couldn't parse this ..
1088, -- Parsing stopped here (paren)
1089 -- Parsing stopped here (keyword)
]
appendComments errRef comments max = do
previous <- readIORef errRef
let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments
writeIORef errRef . take max . nubBy equal . sort $ previous ++ current
where
fst3 (x,_,_) = x
equal x y = fst3 x == fst3 y
outputWiki :: IORef [(Ranking, Integer, String)] -> IO ()
outputWiki errRef = do
issues <- readIORef errRef
unless (null issues) $ do
putStrLn "For more information:"
mapM_ showErr issues
where
showErr (_, code, msg) =
putStrLn $ " " ++ wikiLink ++ "SC" ++ show code ++ " -- " ++ shorten msg
limit = 36
shorten msg =
if length msg < limit
then msg
else (take (limit-3) msg) ++ "..."
outputError options file error = do outputError options file error = do
color <- getColorFunc $ foColorOption options color <- getColorFunc $ foColorOption options
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
outputResult options result sys = do outputResult options ref result sys = do
color <- getColorFunc $ foColorOption options color <- getColorFunc $ foColorOption options
let comments = crComments result let comments = crComments result
appendComments ref comments (fromIntegral $ foWikiLinkCount options)
let fileGroups = groupWith sourceFile comments let fileGroups = groupWith sourceFile comments
mapM_ (outputForFile color sys) fileGroups mapM_ (outputForFile color sys) fileGroups
@@ -59,37 +123,71 @@ outputForFile color sys comments = do
let fileName = sourceFile (head comments) let fileName = sourceFile (head comments)
result <- (siReadFile sys) fileName result <- (siReadFile sys) fileName
let contents = either (const "") id result let contents = either (const "") id result
let fileLines = lines contents let fileLinesList = lines contents
let lineCount = fromIntegral $ length fileLines let lineCount = length fileLinesList
let fileLines = listArray (1, lineCount) fileLinesList
let groups = groupWith lineNo comments let groups = groupWith lineNo comments
mapM_ (\x -> do forM_ groups $ \commentsForLine -> do
let lineNum = lineNo (head x) let lineNum = fromIntegral $ lineNo (head commentsForLine)
let line = if lineNum < 1 || lineNum > lineCount let line = if lineNum < 1 || lineNum > lineCount
then "" then ""
else fileLines !! fromIntegral (lineNum - 1) else fileLines ! fromIntegral lineNum
putStrLn "" putStrLn ""
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)) x forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c
putStrLn "" putStrLn ""
) groups showFixedString color commentsForLine (fromIntegral lineNum) fileLines
-- Pick out only the lines necessary to show a fix in action
sliceFile :: Fix -> Array Int String -> (Fix, Array Int String)
sliceFile fix lines =
(mapPositions adjust fix, sliceLines lines)
where
(minLine, maxLine) =
foldl (\(mm, mx) pos -> ((min mm $ fromIntegral $ posLine pos), (max mx $ fromIntegral $ posLine pos)))
(maxBound, minBound) $
concatMap (\x -> [repStartPos x, repEndPos x]) $ fixReplacements fix
sliceLines :: Array Int String -> Array Int String
sliceLines = ixmap (1, maxLine - minLine + 1) (\x -> x + minLine - 1)
adjust pos =
pos {
posLine = posLine pos - (fromIntegral minLine) + 1
}
showFixedString :: ColorFunc -> [PositionedComment] -> Int -> Array Int String -> IO ()
showFixedString color comments lineNum fileLines =
let line = fileLines ! fromIntegral lineNum in
case mapMaybe pcFix comments of
[] -> return ()
fixes -> do
-- Folding automatically removes overlap
let mergedFix = fold fixes
-- We show the complete, associated fixes, whether or not it includes this
-- and/or other unrelated lines.
let (excerptFix, excerpt) = sliceFile mergedFix fileLines
-- in the spirit of error prone
putStrLn $ color "message" "Did you mean: "
putStrLn $ unlines $ applyFix excerptFix excerpt
cuteIndent :: PositionedComment -> String cuteIndent :: PositionedComment -> String
cuteIndent comment = cuteIndent comment =
replicate (fromIntegral $ colNo comment - 1) ' ' ++ replicate (fromIntegral $ colNo comment - 1) ' ' ++
"^-- " ++ code (codeNo comment) ++ ": " ++ messageText comment makeArrow ++ " " ++ code (codeNo comment) ++ ": " ++ messageText comment
where
arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^"
makeArrow =
let sameLine = lineNo comment == endLineNo comment
delta = endColNo comment - colNo comment
in
if sameLine && delta > 2 && delta < 32 then arrow delta else "^--"
code code = "SC" ++ show code code num = "SC" ++ show num
getColorFunc :: ColorOption -> IO ColorFunc
getColorFunc colorOption = do getColorFunc colorOption = do
term <- hIsTerminalDevice stdout useColor <- shouldOutputColor colorOption
let windows = "mingw" `isPrefixOf` os
let isUsableTty = term && not windows
let useColor = case colorOption of
ColorAlways -> True
ColorNever -> False
ColorAuto -> isUsableTty
return $ if useColor then colorComment else const id return $ if useColor then colorComment else const id
where where
colorComment level comment = colorComment level comment =

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -17,16 +17,72 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
module ShellCheck.Interface where {-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
module ShellCheck.Interface
(
SystemInterface(..)
, CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csOptionalChecks)
, CheckResult(crFilename, crComments)
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
, ParseResult(prComments, prTokenPositions, prRoot)
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asOptionalChecks)
, AnalysisResult(arComments)
, FormatterOptions(foColorOption, foWikiLinkCount)
, Shell(Ksh, Sh, Bash, Dash)
, ExecutionMode(Executed, Sourced)
, ErrorMessage
, Code
, Severity(ErrorC, WarningC, InfoC, StyleC)
, Position(posFile, posLine, posColumn)
, Comment(cSeverity, cCode, cMessage)
, PositionedComment(pcStartPos , pcEndPos , pcComment, pcFix)
, ColorOption(ColorAuto, ColorAlways, ColorNever)
, TokenComment(tcId, tcComment, tcFix)
, emptyCheckResult
, newParseResult
, newAnalysisSpec
, newAnalysisResult
, newFormatterOptions
, newPosition
, newTokenComment
, mockedSystemInterface
, mockRcFile
, newParseSpec
, emptyCheckSpec
, newPositionedComment
, newComment
, Fix(fixReplacements)
, newFix
, InsertionPoint(InsertBefore, InsertAfter)
, Replacement(repStartPos, repEndPos, repString, repPrecedence, repInsertionPoint)
, newReplacement
, CheckDescription(cdName, cdDescription, cdPositive, cdNegative)
, newCheckDescription
) where
import ShellCheck.AST import ShellCheck.AST
import Control.DeepSeq
import Control.Monad.Identity import Control.Monad.Identity
import Data.List
import Data.Monoid
import Data.Ord
import Data.Semigroup
import GHC.Generics (Generic)
import qualified Data.Map as Map import qualified Data.Map as Map
newtype SystemInterface m = SystemInterface { data SystemInterface m = SystemInterface {
-- Read a file by filename, or return an error -- | Read a file by filename, or return an error
siReadFile :: String -> m (Either ErrorMessage String) siReadFile :: String -> m (Either ErrorMessage String),
-- | Given:
-- the current script,
-- a list of source-path annotations in effect,
-- and a sourced file,
-- find the sourced file
siFindSource :: String -> [String] -> String -> m FilePath,
-- | Get the configuration file (name, contents) for a filename
siGetConfig :: String -> m (Maybe (FilePath, String))
} }
-- ShellCheck input and output -- ShellCheck input and output
@@ -34,8 +90,12 @@ data CheckSpec = CheckSpec {
csFilename :: String, csFilename :: String,
csScript :: String, csScript :: String,
csCheckSourced :: Bool, csCheckSourced :: Bool,
csIgnoreRC :: Bool,
csExcludedWarnings :: [Integer], csExcludedWarnings :: [Integer],
csShellTypeOverride :: Maybe Shell csIncludedWarnings :: Maybe [Integer],
csShellTypeOverride :: Maybe Shell,
csMinSeverity :: Severity,
csOptionalChecks :: [String]
} deriving (Show, Eq) } deriving (Show, Eq)
data CheckResult = CheckResult { data CheckResult = CheckResult {
@@ -43,46 +103,109 @@ data CheckResult = CheckResult {
crComments :: [PositionedComment] crComments :: [PositionedComment]
} deriving (Show, Eq) } deriving (Show, Eq)
emptyCheckResult :: CheckResult
emptyCheckResult = CheckResult {
crFilename = "",
crComments = []
}
emptyCheckSpec :: CheckSpec emptyCheckSpec :: CheckSpec
emptyCheckSpec = CheckSpec { emptyCheckSpec = CheckSpec {
csFilename = "", csFilename = "",
csScript = "", csScript = "",
csCheckSourced = False, csCheckSourced = False,
csIgnoreRC = False,
csExcludedWarnings = [], csExcludedWarnings = [],
csShellTypeOverride = Nothing csIncludedWarnings = Nothing,
csShellTypeOverride = Nothing,
csMinSeverity = StyleC,
csOptionalChecks = []
}
newParseSpec :: ParseSpec
newParseSpec = ParseSpec {
psFilename = "",
psScript = "",
psCheckSourced = False,
psIgnoreRC = False,
psShellTypeOverride = Nothing
} }
-- Parser input and output -- Parser input and output
data ParseSpec = ParseSpec { data ParseSpec = ParseSpec {
psFilename :: String, psFilename :: String,
psScript :: String, psScript :: String,
psCheckSourced :: Bool psCheckSourced :: Bool,
psIgnoreRC :: Bool,
psShellTypeOverride :: Maybe Shell
} deriving (Show, Eq) } deriving (Show, Eq)
data ParseResult = ParseResult { data ParseResult = ParseResult {
prComments :: [PositionedComment], prComments :: [PositionedComment],
prTokenPositions :: Map.Map Id Position, prTokenPositions :: Map.Map Id (Position, Position),
prRoot :: Maybe Token prRoot :: Maybe Token
} deriving (Show, Eq) } deriving (Show, Eq)
newParseResult :: ParseResult
newParseResult = ParseResult {
prComments = [],
prTokenPositions = Map.empty,
prRoot = Nothing
}
-- Analyzer input and output -- Analyzer input and output
data AnalysisSpec = AnalysisSpec { data AnalysisSpec = AnalysisSpec {
asScript :: Token, asScript :: Token,
asShellType :: Maybe Shell, asShellType :: Maybe Shell,
asFallbackShell :: Maybe Shell,
asExecutionMode :: ExecutionMode, asExecutionMode :: ExecutionMode,
asCheckSourced :: Bool asCheckSourced :: Bool,
asOptionalChecks :: [String],
asTokenPositions :: Map.Map Id (Position, Position)
}
newAnalysisSpec token = AnalysisSpec {
asScript = token,
asShellType = Nothing,
asFallbackShell = Nothing,
asExecutionMode = Executed,
asCheckSourced = False,
asOptionalChecks = [],
asTokenPositions = Map.empty
} }
newtype AnalysisResult = AnalysisResult { newtype AnalysisResult = AnalysisResult {
arComments :: [TokenComment] arComments :: [TokenComment]
} }
newAnalysisResult = AnalysisResult {
-- Formatter options arComments = []
newtype FormatterOptions = FormatterOptions {
foColorOption :: ColorOption
} }
-- Formatter options
data FormatterOptions = FormatterOptions {
foColorOption :: ColorOption,
foWikiLinkCount :: Integer
}
newFormatterOptions = FormatterOptions {
foColorOption = ColorAuto,
foWikiLinkCount = 3
}
data CheckDescription = CheckDescription {
cdName :: String,
cdDescription :: String,
cdPositive :: String,
cdNegative :: String
}
newCheckDescription = CheckDescription {
cdName = "",
cdDescription = "",
cdPositive = "",
cdNegative = ""
}
-- Supporting data types -- Supporting data types
data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq) data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
@@ -91,16 +214,90 @@ data ExecutionMode = Executed | Sourced deriving (Show, Eq)
type ErrorMessage = String type ErrorMessage = String
type Code = Integer type Code = Integer
data Severity = ErrorC | WarningC | InfoC | StyleC deriving (Show, Eq, Ord) data Severity = ErrorC | WarningC | InfoC | StyleC
deriving (Show, Eq, Ord, Generic, NFData)
data Position = Position { data Position = Position {
posFile :: String, -- Filename posFile :: String, -- Filename
posLine :: Integer, -- 1 based source line posLine :: Integer, -- 1 based source line
posColumn :: Integer -- 1 based source column, where tabs are 8 posColumn :: Integer -- 1 based source column, where tabs are 8
} deriving (Show, Eq) } deriving (Show, Eq, Generic, NFData, Ord)
data Comment = Comment Severity Code String deriving (Show, Eq) newPosition :: Position
data PositionedComment = PositionedComment Position Position Comment deriving (Show, Eq) newPosition = Position {
data TokenComment = TokenComment Id Comment deriving (Show, Eq) posFile = "",
posLine = 1,
posColumn = 1
}
data Comment = Comment {
cSeverity :: Severity,
cCode :: Code,
cMessage :: String
} deriving (Show, Eq, Generic, NFData)
newComment :: Comment
newComment = Comment {
cSeverity = StyleC,
cCode = 0,
cMessage = ""
}
-- only support single line for now
data Replacement = Replacement {
repStartPos :: Position,
repEndPos :: Position,
repString :: String,
-- Order in which the replacements should happen: highest precedence first.
repPrecedence :: Int,
-- Whether to insert immediately before or immediately after the specified region.
repInsertionPoint :: InsertionPoint
} deriving (Show, Eq, Generic, NFData)
data InsertionPoint = InsertBefore | InsertAfter
deriving (Show, Eq, Generic, NFData)
newReplacement = Replacement {
repStartPos = newPosition,
repEndPos = newPosition,
repString = "",
repPrecedence = 1,
repInsertionPoint = InsertAfter
}
data Fix = Fix {
fixReplacements :: [Replacement]
} deriving (Show, Eq, Generic, NFData)
newFix = Fix {
fixReplacements = []
}
data PositionedComment = PositionedComment {
pcStartPos :: Position,
pcEndPos :: Position,
pcComment :: Comment,
pcFix :: Maybe Fix
} deriving (Show, Eq, Generic, NFData)
newPositionedComment :: PositionedComment
newPositionedComment = PositionedComment {
pcStartPos = newPosition,
pcEndPos = newPosition,
pcComment = newComment,
pcFix = Nothing
}
data TokenComment = TokenComment {
tcId :: Id,
tcComment :: Comment,
tcFix :: Maybe Fix
} deriving (Show, Eq, Generic, NFData)
newTokenComment = TokenComment {
tcId = Id 0,
tcComment = newComment,
tcFix = Nothing
}
data ColorOption = data ColorOption =
ColorAuto ColorAuto
@@ -111,11 +308,18 @@ data ColorOption =
-- For testing -- For testing
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
mockedSystemInterface files = SystemInterface { mockedSystemInterface files = SystemInterface {
siReadFile = rf siReadFile = rf,
siFindSource = fs,
siGetConfig = const $ return Nothing
} }
where where
rf file = rf file = return $
case filter ((== file) . fst) files of case find ((== file) . fst) files of
[] -> return $ Left "File not included in mock." Nothing -> Left "File not included in mock."
[(_, contents)] -> return $ Right contents Just (_, contents) -> Right contents
fs _ _ file = return file
mockRcFile rcfile mock = mock {
siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2015 Vidar Holen Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -30,7 +30,7 @@ import Text.Regex.TDFA
-- Precompile the regex -- Precompile the regex
mkRegex :: String -> Regex mkRegex :: String -> Regex
mkRegex str = mkRegex str =
let make :: RegexMaker Regex CompOption ExecOption String => String -> Regex let make :: String -> Regex
make = makeRegex make = makeRegex
in in
make str make str

View File

@@ -2,7 +2,7 @@
# For more information, see: https://docs.haskellstack.org/en/stable/yaml_configuration/ # For more information, see: https://docs.haskellstack.org/en/stable/yaml_configuration/
# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2) # Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
resolver: lts-8.5 resolver: lts-13.26
# Local packages, usually specified by relative directory name # Local packages, usually specified by relative directory name
packages: packages:

View File

@@ -5,6 +5,7 @@
set -o pipefail set -o pipefail
sponge() { sponge() {
local data
data="$(cat)" data="$(cat)"
printf '%s\n' "$data" > "$1" printf '%s\n' "$data" > "$1"
} }
@@ -22,7 +23,7 @@ modify() {
} }
detestify() { detestify() {
echo "-- AUTOGENERATED from ShellCheck by striptests. Do not modify." printf '%s\n' '-- AUTOGENERATED from ShellCheck by striptests. Do not modify.'
awk ' awk '
BEGIN { BEGIN {
state = 0; state = 0;
@@ -52,7 +53,7 @@ detestify() {
if [[ ! -e ShellCheck.cabal ]] if [[ ! -e 'ShellCheck.cabal' ]]
then then
echo "Run me from the ShellCheck directory." >&2 echo "Run me from the ShellCheck directory." >&2
exit 1 exit 1
@@ -64,7 +65,7 @@ then
exit 2 exit 2
fi fi
modify ShellCheck.cabal sed -e ' modify 'ShellCheck.cabal' sed -e '
/QuickCheck/d /QuickCheck/d
/^test-suite/{ s/.*//; q; } /^test-suite/{ s/.*//; q; }
' '

48
test/buildtest Executable file
View File

@@ -0,0 +1,48 @@
#!/bin/bash
# This script configures, builds and runs tests.
# It's meant for automatic cross-distro testing.
die() { echo "$*" >&2; exit 1; }
[ -e "ShellCheck.cabal" ] ||
die "ShellCheck.cabal not in current dir"
command -v cabal ||
die "cabal is missing"
cabal update ||
die "can't update"
if [ -e /etc/arch-release ]
then
# Arch has an unconventional packaging setup
flags=(--disable-library-vanilla --enable-shared --enable-executable-dynamic --ghc-options=-dynamic)
else
flags=()
fi
cabal install --dependencies-only --enable-tests "${flags[@]}" ||
cabal install --dependencies-only "${flags[@]}" ||
die "can't install dependencies"
cabal configure --enable-tests "${flags[@]}" ||
die "configure failed"
cabal build ||
die "build failed"
cabal test ||
die "test failed"
sc="$(find . -name shellcheck -type f -perm -111)"
[ -x "$sc" ] || die "Can't find executable"
"$sc" - << 'EOF' || die "execution failed"
#!/bin/sh
echo "Hello World"
EOF
"$sc" - << 'EOF' && die "negative execution failed"
#!/bin/sh
echo $1
EOF
echo "Success"
exit 0

76
test/check_release Executable file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# shellcheck disable=SC2257
failed=0
fail() {
echo "$(tput setaf 1)$*$(tput sgr0)"
failed=1
}
if git diff | grep -q ""
then
fail "There are uncommitted changes"
fi
current=$(git tag --points-at)
if [[ -z "$current" ]]
then
fail "No git tag on the current commit"
echo "Create one with: git tag -a v0.0.0"
fi
if [[ "$current" != v* ]]
then
fail "Bad tag format: expected v0.0.0"
fi
if [[ "$(git cat-file -t "$current")" != "tag" ]]
then
fail "Current tag is not annotated (required for Snap)."
fi
if [[ "$(git tag --points-at master)" != "$current" ]]
then
fail "You are not on master"
fi
version=${current#v}
if ! grep "Version:" ShellCheck.cabal | grep -qFw "$version"
then
fail "The cabal file does not match tag version $version"
fi
if ! grep -qF "## $current" CHANGELOG.md
then
fail "CHANGELOG.md does not contain '## $current'"
fi
if [[ $(git log -1 --pretty=%B) != "Stable version "* ]]
then
fail "Expected git log message to be 'Stable version ...'"
fi
i=1 j=1
cat << EOF
Manual Checklist
$((i++)). Make sure none of the automated checks above failed
$((i++)). Make sure Travis build currently passes: https://travis-ci.org/koalaman/shellcheck
$((i++)). Make sure SnapCraft build currently works: https://build.snapcraft.io/user/koalaman
$((i++)). Run test/distrotest to ensure that most distros can build OOTB.
$((i++)). Format and read over the manual for bad formatting and outdated info.
$((i++)). Make sure the Hackage package builds, so that all files are
Release Steps
$((j++)). \`cabal sdist\` to generate a Hackage package
$((j++)). \`git push --follow-tags\` to push commit
$((j++)). Wait for Travis to build
$((j++)). Verify release:
a. Check that the new versions are uploaded: https://shellcheck.storage.googleapis.com/index.html
b. Check that the docker images have version tags: https://hub.docker.com/u/koalaman
$((j++)). If no disaster, upload to Hackage: http://hackage.haskell.org/upload
$((j++)). Push a new commit that updates CHANGELOG.md
EOF
exit "$failed"

76
test/distrotest Executable file
View File

@@ -0,0 +1,76 @@
#!/bin/bash
# This script runs 'buildtest' on each of several distros
# via Docker.
set -o pipefail
exec 3>&1 4>&2
die() { echo "$*" >&4; exit 1; }
[ -e "ShellCheck.cabal" ] || die "ShellCheck.cabal not in this dir"
[ "$1" = "--run" ] || {
cat << EOF
This script pulls multiple distros via Docker and compiles
ShellCheck and dependencies for each one. It takes hours,
and is still highly experimental.
Make sure you're plugged in and have screen/tmux in place,
then re-run with $0 --run to continue.
Also note that dist* will be deleted.
EOF
exit 0
}
echo "Deleting 'dist' and 'dist-newstyle'..."
rm -rf dist dist-newstyle
log=$(mktemp) || die "Can't create temp file"
date >> "$log" || die "Can't write to log"
echo "Logging to $log" >&3
exec >> "$log" 2>&1
final=0
while read -r distro setup
do
[[ "$distro" = "#"* || -z "$distro" ]] && continue
printf '%s ' "$distro" >&3
docker pull "$distro" || die "Can't pull $distro"
printf 'pulled. ' >&3
tmp=$(mktemp -d) || die "Can't make temp dir"
cp -r . "$tmp/" || die "Can't populate test dir"
printf 'Result: ' >&3
< /dev/null docker run -v "$tmp:/mnt" "$distro" sh -c "
$setup
cd /mnt || exit 1
test/buildtest
"
ret=$?
if [ "$ret" = 0 ]
then
echo "OK" >&3
else
echo "FAIL with $ret. See $log" >&3
final=1
fi
rm -rf "$tmp"
done << EOF
# Docker tag Setup command
debian:stable apt-get update && apt-get install -y cabal-install
debian:testing apt-get update && apt-get install -y cabal-install
ubuntu:latest apt-get update && apt-get install -y cabal-install
haskell:latest true
opensuse/leap:latest zypper install -y cabal-install ghc
fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils
archlinux/base:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
# Other versions we want to support
ubuntu:18.04 apt-get update && apt-get install -y cabal-install
# Misc Haskell including current and latest Stack build
ubuntu:18.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
EOF
exit "$final"

View File

@@ -2,22 +2,30 @@ module Main where
import Control.Monad import Control.Monad
import System.Exit import System.Exit
import qualified ShellCheck.Checker
import qualified ShellCheck.Analytics import qualified ShellCheck.Analytics
import qualified ShellCheck.AnalyzerLib import qualified ShellCheck.AnalyzerLib
import qualified ShellCheck.Parser import qualified ShellCheck.ASTLib
import qualified ShellCheck.Checker
import qualified ShellCheck.Checks.Commands import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.Custom
import qualified ShellCheck.Checks.ShellSupport import qualified ShellCheck.Checks.ShellSupport
import qualified ShellCheck.Fixer
import qualified ShellCheck.Formatter.Diff
import qualified ShellCheck.Parser
main = do main = do
putStrLn "Running ShellCheck tests..." putStrLn "Running ShellCheck tests..."
results <- sequence [ results <- sequence [
ShellCheck.Checker.runTests, ShellCheck.Analytics.runTests
ShellCheck.Checks.Commands.runTests, ,ShellCheck.AnalyzerLib.runTests
ShellCheck.Checks.ShellSupport.runTests, ,ShellCheck.ASTLib.runTests
ShellCheck.Analytics.runTests, ,ShellCheck.Checker.runTests
ShellCheck.AnalyzerLib.runTests, ,ShellCheck.Checks.Commands.runTests
ShellCheck.Parser.runTests ,ShellCheck.Checks.Custom.runTests
,ShellCheck.Checks.ShellSupport.runTests
,ShellCheck.Fixer.runTests
,ShellCheck.Formatter.Diff.runTests
,ShellCheck.Parser.runTests
] ]
if and results if and results
then exitSuccess then exitSuccess

27
test/stacktest Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
# This script builds ShellCheck through `stack` using
# various resolvers. It's run via distrotest.
resolvers=(
nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
)
die() { echo "$*" >&2; exit 1; }
[ -e "ShellCheck.cabal" ] ||
die "ShellCheck.cabal not in current dir"
[ -e "stack.yaml" ] ||
die "stack.yaml not in current dir"
command -v stack ||
die "stack is missing"
stack setup || die "Failed to setup with default resolver"
stack build --test || die "Failed to build/test with default resolver"
for resolver in "${resolvers[@]}"
do
stack --resolver="$resolver" setup || die "Failed to setup $resolver"
stack --resolver="$resolver" build --test || die "Failed build/test with $resolver!"
done
echo "Success"