834 Commits

Author SHA1 Message Date
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
Vidar Holen
a3be776f80 Stable version 0.5.0
This release is dedicated to Valve for keeping PC gaming awesome.
Also, for that time they proved the need for a tool like ShellCheck.
2018-05-31 20:01:42 -07:00
Vidar Holen
b5e5d249c4 Merge pull request #1237 from ngzhian/1234
Check if builtin cd is called
2018-05-27 18:34:18 -07:00
Vidar Holen
efffc6150b Warn about string operations on $@/$* (fixes #1236) 2018-05-27 12:53:01 -07:00
Ng Zhi An
cb4a0c0250 Check if builtin cd is called
Fixes #1234
2018-05-27 11:42:37 -07:00
Vidar Holen
135cf5932f Parse here docs as per spec (fixes #1050) 2018-05-26 21:01:18 -07:00
Vidar Holen
467dfe07b6 Add a unit test and separate ids for 884eff0c 2018-05-23 19:51:36 -07:00
Vidar Holen
d140388bea Merge pull request #1232 from ngzhian/source-x
Add T_SourceCommand to wrap source commands and sourced code
2018-05-23 19:41:55 -07:00
Ng Zhi An
884eff0c36 Add T_SourceCommand to wrap source commands and sourced code
Fixes #1181
2018-05-22 22:43:26 -07:00
Vidar Holen
1d8047cce1 Warn about unnecessary subshells in tests 2018-05-22 22:35:37 -07:00
Vidar Holen
77546fba2f Merge pull request #1223 from ngzhian/1124
Handle offset references of the form [foo]:bar (fixes #1124)
2018-05-20 17:26:08 -07:00
Vidar Holen
4a5ee06ce4 Merge pull request #1206 from ngzhian/aeson
Change to aeson (fixes #1085)
2018-05-20 17:19:20 -07:00
Ng Zhi An
4dceecb1ed Handle offset references of the form [foo]:bar (fixes #1124) 2018-05-13 22:48:02 -07:00
Vidar Holen
5b226e733b Merge pull request #1203 from ngzhian/1199
Whitelist rename for SC2016 (fixes #1199)
2018-05-13 16:56:09 -07:00
Vidar Holen
46c10c1571 Merge branch 'master' into 1199 2018-05-13 16:55:53 -07:00
Vidar Holen
1de8ba0210 Merge pull request #1208 from ngzhian/1186
Add unset to list of commands exempt from 2016
2018-05-13 16:47:29 -07:00
Vidar Holen
407f6a63b9 Merge branch 'master' into 1186 2018-05-13 16:47:18 -07:00
Vidar Holen
7ee7448a70 Merge pull request #1221 from ngzhian/1192
Assignments are okay in SC2094 (fixes #1192)
2018-05-13 16:44:11 -07:00
Vidar Holen
48ebd41e22 Merge pull request #1222 from ngzhian/196
Suppress SC2016 for git filter-branch (fixes #196)
2018-05-13 15:45:20 -07:00
Ng Zhi An
0c88fbc76d Suppress SC2016 for git filter-branch (fixes #196) 2018-05-13 15:18:55 -07:00
Ng Zhi An
b3362f1dc3 Assignments are okay in SC2094 (fixes #1192) 2018-05-13 15:17:32 -07:00
Ng Zhi An
2c6bc43614 Add Data.Monoid 2018-05-13 11:44:21 -07:00
Vidar Holen
235bf6605f Merge pull request #1205 from ngzhian/remove-unused
Remove unused code
2018-05-12 19:15:42 -07:00
Vidar Holen
7029a713c7 Merge pull request #1207 from ngzhian/1184
When given a %* format string, expect one more argument
2018-05-12 18:50:23 -07:00
Vidar Holen
cf608dc2f6 Parse FD move operations like 2>&1- correctly. Fixes #1180. 2018-05-12 18:30:35 -07:00
Vidar Holen
aa3b3fdc56 Make .ghci look in ./src 2018-05-12 17:47:21 -07:00
Vidar Holen
bca2ad4e18 Don't think declare -x -F var is used (fixes #1209). 2018-05-12 17:34:23 -07:00
Vidar Holen
719e1854e5 Clarify 'export' suggestion in SC2034 (unused vars). 2018-05-11 21:39:54 -07:00
Ng Zhi An
20ad7dc8de Add unset to list of commands exempt from 2016
Fixes #1186
2018-05-06 16:45:57 -07:00
Ng Zhi An
f84859ab90 When given a %* format string, expect one more argument
Fixes #1184
2018-05-06 16:39:51 -07:00
Ng Zhi An
08235a1cb2 Change to aeson (fixes #1085)
Adds bytestring as a dependency for putStrLn encoded values.
2018-05-06 16:07:53 -07:00
Ng Zhi An
728922d2b8 Remove unused code 2018-05-06 15:24:34 -07:00
Ng Zhi An
a953dd3454 Whitelist rename for SC2016 (fixes #1199) 2018-05-06 10:28:17 -07:00
Vidar Holen
ef6a5b97b9 Refactor sudo checks into CommandChecks 2018-04-30 22:59:23 -07:00
Vidar Holen
8873a1732b Merge pull request #1195 from sdknudsen/master
Warn about invalid arguments to sudo
2018-04-30 21:50:28 -07:00
Stefan Knudsen
5adfce72e1 Warn about invalid arguments to sudo 2018-04-29 01:16:31 -04:00
Vidar Holen
12b3fdf661 Update gallery of bad code 2018-04-28 12:58:44 -07:00
Vidar Holen
bb4ce86fab Account for array index in SC2154 ${var:?} (fixes #1166) 2018-04-28 12:09:54 -07:00
Vidar Holen
7ec2fa2d3e Remove 'nice' from list of non-reading commands (fixes #1169) 2018-04-28 11:39:42 -07:00
Vidar Holen
5481ccd7f7 Warn about elseif or elsif as command names (fixes #1177) 2018-04-27 22:23:37 -07:00
Vidar Holen
a1d8947297 Fix broken test stripping 2018-04-22 14:57:43 -07:00
Vidar Holen
683a30abde Merge pull request #1174 from rasa/patch-1
Added scoop install
2018-04-22 14:13:35 -07:00
Vidar Holen
573936f353 Merge pull request #1185 from 0mp/master
Add FreeBSD installation instructions
2018-04-22 14:10:59 -07:00
Vidar Holen
ce7658ed86 Merge pull request #1168 from vmchale/master
Bump to ghc 8.4.1
2018-04-22 14:10:25 -07:00
Mateusz Piotrowski
0136d9ccce Add FreeBSD installation instructions 2018-04-19 13:25:25 +02:00
Ross Smith II
a4c6cea5e6 Added scoop install
See https://github.com/lukesampson/scoop/blob/master/bucket/shellcheck.json
2018-04-07 16:22:29 -07:00
Vidar Holen
32af2783f0 Allow stripping unit tests 2018-04-02 21:14:23 -07:00
Vanessa McHale
08aab3c161 hlints 2018-04-02 11:48:21 -05:00
Vanessa McHale
cf39adff75 bump to latest ghc 2018-04-02 11:44:18 -05:00
Vidar Holen
da4072a118 Blacklist base 4.6.0.1 to disable GHC 7.6.3 (#1131) 2018-03-29 21:41:00 -07:00
Vidar Holen
08d2eef411 Whitelist docker for SC2016 about '$var'. Fixes #1161 2018-03-29 19:27:34 -07:00
Vidar Holen
de257a6cf3 Merge pull request #1164 from Lin-Buo-Ren/improve-snap-package
Improve snap packaging
2018-03-29 11:49:26 -07:00
林博仁(Buo-Ren Lin)
68c24925bc Add info on connecting to removable-media interface in snap description
Signed-off-by: 林博仁(Buo-Ren Lin) <Buo.Ren.Lin@gmail.com>
2018-03-29 23:47:59 +08:00
林博仁(Buo-Ren Lin)
366dc5d3f8 Add snap install instructions to README
Currently shellcheck is only provided by the edge channel, should remove the --channel argument after it is in candidate/stable.

Signed-off-by: 林博仁(Buo-Ren Lin) <Buo.Ren.Lin@gmail.com>
2018-03-29 20:24:31 +08:00
林博仁(Buo-Ren Lin)
1ed743e410 Add snapcraft generated files to the Git tracking ignore rules
This patches uses the following gitignore syntax so that only entries in the root folder is ignored, it is suggested to apply it to existing rules as well.

```
A leading slash matches the beginning of the pathname. For
example, "/*.c" matches "cat-file.c" but not
"mozilla-sha1/sha1.c".
```

Refer-to: gitignore(5) manpage
Signed-off-by: 林博仁(Buo-Ren Lin) <Buo.Ren.Lin@gmail.com>
2018-03-29 20:05:52 +08:00
林博仁(Buo-Ren Lin)
9a2aad16ad Add removable-media plug so that scripts in removable media can be checked
Otherwise it will be blocked by Apparmor with the following message:

```
$ shellcheck script
audit: type=1400 audit(TIMESTAMP): apparmor="DENIED" operation="open" profile="snap.shellcheck.shellcheck" name=2F6D656469612F4C696E2D42756F2D52656E2F57696E646F7773205553422F717569636B72756E pid=10175 comm="shellcheck" requested_mask="r" denied_mask="r" fsuid=FSUID ouid=OUID
script: script: openBinaryFile: permission denied (Permission denied)
```

NOTE:

* This plug is not Auto-connect plug, it has to be manually connected by user with `snap connect shellcheck:removable-media :removable-media`
* Currently files under /mnt is not checkable as snapd doesn't provide an interface for it for now.

Refer-to: Interfaces reference - Snaps are universal Linux packages <https://docs.snapcraft.io/reference/interfaces>
Signed-off-by: 林博仁(Buo-Ren Lin) <Buo.Ren.Lin@gmail.com>
2018-03-29 20:00:23 +08:00
Vidar Holen
177cb10daa Enable strict mode with home access 2018-03-28 18:26:16 -07:00
Vidar Holen
4aca1ff128 Warn when printf arg count is not a multiple of format count 2018-03-28 08:57:38 -07:00
Vidar Holen
ffed7caff4 quickrun script now works with new source layout 2018-03-28 08:57:38 -07:00
Vidar Holen
ef28200199 Merge pull request #1159 from geirha/patch-1
Consider type a valid command in sh
2018-03-27 11:38:07 -07:00
Geir Hauge
55216792c9 Consider type a valid command in sh
[type](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/type.html) *is* defined by POSIX, and even the bourne shell has the `type` command.
2018-03-25 12:11:57 +02:00
Vidar Holen
51e115cf47 Attempt to fix snap build 2018-03-24 17:43:32 -07:00
Vidar Holen
764b242f1b Suggest quoting expansions in for loop globs 2018-03-24 17:43:20 -07:00
Vidar Holen
c3b606c68a Encourage users to use stable rather than latest 2018-03-21 17:55:21 -07:00
Vidar Holen
9f53109dfa Fix tagging of 'stable' 2018-03-21 17:54:36 -07:00
Vidar Holen
795a881219 Fix docker image workdir and add test 2018-03-21 09:39:06 -07:00
Vidar Holen
6dd5350e3b Merge pull request #1143 from pratikmallya/simplify_dockerbuild
Simplify Dockerfile
2018-03-20 22:31:36 -07:00
Vidar Holen
a5b359591c Merge branch 'master' into simplify_dockerbuild 2018-03-20 22:31:21 -07:00
Vidar Holen
966194e387 Merge branch 'pratikmallya-simplify_dockerbuild' 2018-03-20 22:10:24 -07:00
Vidar Holen
71bcc80c2f Allow more Docker build caching 2018-03-20 22:09:13 -07:00
Vidar Holen
48616225b3 Merge pull request #1147 from tdmalone/patch-1
README - Travis script for updating shellcheck ver
2018-03-20 19:28:39 -07:00
Tim Malone
99276cb9f5 README - Travis script for updating shellcheck ver
Shellcheck in default Travis builds is currently a version behind
2018-03-19 11:15:28 +11:00
Pratik Mallya
f769d4e92c Add TravisCI Build Status file 2018-03-15 23:13:54 -05:00
Pratik Mallya
71df01c00f Simplify Dockerfile
Use multi stage Dockerfile to greatly simplify build.
2018-03-15 23:12:28 -05:00
Vidar Holen
5364701914 Merge branch 'simplify_dockerbuild' of https://github.com/pratikmallya/shellcheck into pratikmallya-simplify_dockerbuild 2018-03-15 09:53:50 -07:00
Vidar Holen
fb97aca5a6 Merge pull request #1140 from phadej/src
Move library into src/
2018-03-15 16:52:26 +00:00
Pratik Mallya
6b81a9924c Simplify Dockerfile
Use multi stage Dockerfile to greatly simplify build.
2018-03-14 21:43:34 -05:00
Oleg Grenrus
cd7c077ecc Move library into src/ 2018-03-08 19:57:40 +02:00
Oleg Grenrus
b33607b048 Add custom-setup stanza and containers lowerbound
custom-setup:

- http://cabal.readthedocs.io/en/latest/developing-packages.html#custom-setup-scripts
- https://www.well-typed.com/blog/2015/07/cabal-setup-deps/

Bounds:
- containers-0.5 is required if Data.Map.Strict
- parsec-3.0 for Text.Parsec
- json-0.3.6 for makeObj
2018-03-08 19:35:50 +02:00
Vidar Holen
969230f171 MacPorts version appears unmaintained. Remove from docs. 2018-03-08 08:48:03 -08:00
Vidar Holen
a98d69f4ff Update CHANGELOG with $var[ and here doc expansion fixes 2018-03-04 15:42:52 -08:00
Vidar Holen
f71c142a44 Don't ignore parse failures in here documents. Fixes #1135. 2018-03-04 15:24:04 -08:00
Vidar Holen
9dfcf54f10 Functionality for emitting parse errors but still continue 2018-03-04 14:42:47 -08:00
Vidar Holen
c8cd9dd09c Add a debugParseScript for development 2018-03-03 15:36:50 -08:00
Vidar Holen
8b8aeb4409 Rephrase SC2069 (cmd 2>&1 > file) and make it a warning. Fixes #633 2018-03-03 13:33:24 -08:00
Vidar Holen
ee354ffce8 POSIX warning for export -[^p]. Fixes #1130. 2018-02-27 20:38:02 -08:00
Vidar Holen
9fc3ddf849 Fix SC1087 to trigger on any $var[, not just $var[@] 2018-02-25 18:25:47 -08:00
Vidar Holen
ecb9d07f52 Update changelog with associative arrays in (()) fix 2018-02-25 18:15:55 -08:00
Vidar Holen
d16bf41c3d Better support arrays in arithmetic contexts. Fixes #1074 2018-02-25 18:08:38 -08:00
Vidar Holen
8d5e3a80ae Merge pull request #1123 from jonhiggs/check-for-which-usage
Check for calls to `which`.
2018-02-25 17:08:37 -08:00
Vidar Holen
34e0fa53c8 Merge pull request #1125 from jonhiggs/nextnumber-macos-support
MacOS support for nextnumber
2018-02-25 17:08:15 -08:00
Jon Higgs
7fb27310e1 Rely upon /usr/bin/env to find bash
This allows you to use the homebrew install Bash 4 on MacOS systems. It
should compatible with most if not all modern Linux distros.
2018-02-26 11:24:09 +11:00
Jon Higgs
00d3c09ddb Raise error unless interpreter supports globstar 2018-02-26 11:23:42 +11:00
Jon Higgs
e8fc09414a Check for calls to which.
Favour the builtin `command -v` instead.
2018-02-26 10:00:25 +11:00
Vidar Holen
b7a8b090d2 SC2229: Warn about 'read $var' 2018-02-25 13:47:58 -08:00
Vidar Holen
72044a79c6 Update changelog 2018-02-25 13:47:58 -08:00
Vidar Holen
6511dc0246 Add missing import 2018-02-25 13:29:06 -08:00
Vidar Holen
740441f2c4 Merge pull request #1121 from PeterDaveHello/update-travis-ci-doc
[Docs] Update Travis CI part in README.md
2018-02-25 11:37:00 -08:00
Peter Dave Hello
b311563421 Update Travis CI part in README.md 2018-02-20 04:52:12 +08:00
Vidar Holen
6d257bfa17 Warn about 'while!' and 'while:' 2018-02-17 21:58:29 -08:00
Vidar Holen
d8717c7046 s/parser error/syntax error/g 2018-02-17 17:18:05 -08:00
Vidar Holen
7aa3a7ffc3 Improve message for SC2163 (export $var). Helps #1117 2018-02-17 17:12:11 -08:00
Vidar Holen
017af8333f Merge pull request #1112 from Nightfirecat/sc2154-local-test
Add test for `local` keyword in SC2154
2018-02-10 08:04:07 -08:00
Jordan Atwood
f73d6f2332 Add test for local keyword in SC2154
Follow-up to koalaman/shellcheck#988
2018-02-07 17:50:55 -08:00
koalaman
a840f4e464 Merge pull request #1100 from mkhl/SC2029
SC2029: Skip when there are options to ssh
2018-01-24 19:38:38 -08:00
Vidar Holen
0f5e40c076 Mention shellcheck-static for Arch 2018-01-24 09:02:56 -08:00
Martin Kühl
ccaacb108a SC2029: Skip when there are options to ssh
Fixes #327

SC2029 generates false positives when given an ssh command that includes
options with arguments because it assumes the first non-option must be
the host:port argument and the last argument is a command to run.
As suggested the comments on #327, this change fixes those by skipping
the check when there are any options present.
2018-01-24 13:05:22 +01:00
Martin Kühl
56751413b4 SC2029: Add false positive test
This change adds a test case for a valid command that gets falsely
flagged with SC2029.
2018-01-24 13:04:26 +01:00
Vidar Holen
ba5f20deda Fix parsing of escaped chars in regex groups. Fixes #1077 2018-01-21 16:13:16 -08:00
Vidar Holen
c86885427c Warn about comments/blanks before shebang. Fixes #844 2018-01-21 13:57:44 -08:00
Vidar Holen
7b3c4025fb Warn about redirs in the middle of 'find' commands. Fixes #405 2018-01-21 11:12:22 -08:00
Vidar Holen
3b004275cf Add unit test for issue #1091 2018-01-20 11:42:31 -08:00
koalaman
72971fa52b Merge pull request #1097 from sdknudsen/fix/recognize-ids-with-underscores
Use readVariableName combinator
2018-01-20 11:35:22 -08:00
Stefan Knudsen
dbdab5705f Use readVariableName combinator 2018-01-19 16:19:06 -05:00
Vidar Holen
46a3019ed7 Fix annotations for here documents (fixes #1071) 2018-01-17 19:20:10 -08:00
Vidar Holen
81978d15bd Remove unused here doc boundary concept. 2018-01-17 18:48:17 -08:00
Vidar Holen
1d0db9267d Mention SC2224-6 about mv/cp/ln without destination 2018-01-17 18:32:07 -08:00
Vidar Holen
a6fb9d1ef8 Warn about C-style comments 2018-01-17 18:14:36 -08:00
Vidar Holen
dc1e7c1bd4 Make docker image shellcheck-alpine behave more like alpine. 2018-01-14 18:01:18 -08:00
Vidar Holen
5b14dba489 Parse 'else if' correctly, and not like elif. Fixes #1088. 2018-01-13 22:42:19 -08:00
koalaman
ee997fdec4 Merge pull request #1093 from albertodonato/snap-package
Add snapcraft.yaml to build snap package
2018-01-13 19:36:06 -08:00
koalaman
1badeff383 Merge pull request #1092 from etam/patch-1
Update openSUSE instruction
2018-01-13 19:33:18 -08:00
Vidar Holen
2d5ed23ca1 Warn about cp/mv/ln with a single argument. Fixes #1080. 2018-01-13 16:44:58 -08:00
Alberto Donato
ec581cee90 Add snapcraft.yaml to build snap package 2018-01-13 00:31:26 +01:00
Adam Mizerski
bb32289ee3 Update openSUSE instruction
Package is main repo of all versions. No need to add repository.
2018-01-11 18:51:39 +01:00
Vidar Holen
31d6b063d9 Improve indented here doc token message. 2018-01-10 21:12:22 -08:00
Vidar Holen
3c5c74ff04 Add quote warning specific to : ${var=val}. Fixes #1084 2018-01-06 10:53:53 -08:00
koalaman
9657e8dda3 Merge pull request #1078 from CyberShadow/pull-20171221-031403
Extend SC2216/SC2217 with 'true' and 'false'
2017-12-20 20:44:06 -08:00
Vladimir Panteleev
6ed60b403f Extend SC2216/SC2217 with 'true' and 'false' 2017-12-21 03:29:17 +00:00
koalaman
8fa8823981 Merge pull request #1072 from vapier/master
convert http:// URIs to https://
2017-12-14 09:35:57 -08:00
Mike Frysinger
161801a86e convert http:// URIs to https://
Also update the ShellCheck homepage to the new dedicated domain.
2017-12-14 01:06:43 -05:00
Vidar Holen
c36f6d89ba Stable version 0.4.7
This release is dedicated to net neutrality. Remember when the Internet
was a meritocracy? [Please drink a verification can to continue.]
2017-12-08 20:29:12 -08:00
Vidar Holen
e801da0621 Add a changelog 2017-12-07 17:49:43 -08:00
koalaman
51e6bf809f Merge pull request #1041 from LukeShu/fix-isClosingFileOp
Fix isClosingFileOp (fixes issue #862)
2017-11-08 10:15:21 -08:00
Vidar Holen
3413a076ff Cabal: Don't make executables depend on library 2017-11-05 20:51:39 -08:00
Vidar Holen
53f63b85bb Use Data.Map.Strict instead of Map.insertWith' 2017-11-05 20:11:04 -08:00
Luke Shumaker
df068bc8ed Fix isClosingFileOp (fixes issue #862)
The isClosingFileOp function expected closing file ops to use T_IoFile, but
they actually use T_IoDuplicate; so it effectively always returned False.
2017-11-05 18:53:01 -05:00
Vidar Holen
102683ab04 Try to warn when using directives after commands (#981) 2017-11-04 15:22:17 -07:00
Vidar Holen
acead72c93 Improve directive parsing 2017-10-29 17:39:13 -07:00
Vidar Holen
0c1e2bbd4d Warn when using directives in front of elif and case items (#1036) 2017-10-29 16:31:46 -07:00
koalaman
5d9cb81008 Merge pull request #1026 from PeterDaveHello/patch-1
Enable syntax highlight in README.md code block
2017-10-15 19:09:32 -07:00
koalaman
1491402dcb Merge pull request #1027 from PeterDaveHello/README.md-Table-of-Contents
Add Table of Contents in README.md
2017-10-15 19:08:16 -07:00
Vidar Holen
436a46ebab Improve automated docker builds and tagging 2017-10-15 15:43:06 -07:00
Vidar Holen
db1e24d140 Dockerfile renamed "shellcheck" to "bin". Unbreak it. 2017-10-10 10:02:11 -07:00
Peter Dave Hello
35daf7534b Add Table of Contents in README.md 2017-10-10 20:25:04 +08:00
Peter Dave Hello
76ad5dbb9f Enable syntax highlight in README.md code block 2017-10-10 20:20:51 +08:00
Vidar Holen
f73736e5c9 Add Alpine-based docker image 2017-10-07 15:19:35 -07:00
Vidar Holen
3785a08906 Don't suggest $@ in [[ $* = "" ]] (#976) 2017-10-01 10:27:21 -07:00
Vidar Holen
74c199b51a Warn when one case pattern overrides another. 2017-09-16 15:23:51 -07:00
Vidar Holen
371dcdda3a Warn about missing default case for getopts. 2017-09-16 10:26:28 -07:00
Vidar Holen
38044e3f75 Fix 2062 for grep -e -foo bar* and --regex -foo bar* 2017-09-09 17:03:29 -07:00
Vidar Holen
b0f6f935f3 Don't suggset quoting in grep -- -foo bar* (#517) 2017-09-09 16:57:06 -07:00
Vidar Holen
bd2facb245 Suggest (( expr )) over let expr (#813) 2017-09-09 16:07:38 -07:00
Vidar Holen
895ba31337 Add ^@![]/ to allowed function characters (#909) 2017-09-09 15:34:08 -07:00
koalaman
ccc037d458 Merge pull request #988 from Nightfirecat/953-local-A-fix
SC2154: Fix false positive on `local`
2017-09-09 09:37:04 -07:00
koalaman
a1b370efbc Merge pull request #983 from Dynamic-Gravity/master
Updated readme installation instructions
2017-09-09 09:35:59 -07:00
Jordan Atwood
7f36c369f3 SC2154: Fix false positive on local 2017-09-06 15:40:31 -07:00
Unknown
7b55e73e03 Updated readme installation instructions
Added installation instructions for Solus distro.
2017-08-29 13:38:12 -04:00
Vidar Holen
6c068e7d29 Merge branch 'master' of github.com:koalaman/shellcheck 2017-08-13 19:45:31 -07:00
Vidar Holen
8dd40efb44 Add support for -a: emit for sourced files. 2017-08-13 19:34:45 -07:00
koalaman
751aebf984 Merge pull request #968 from ssbarnea/patch-1
Documented binary cabal install for MacOS
2017-08-13 10:52:03 -07:00
Sorin Sbarnea
3bf6913a15 Documented binary cabal install for MacOS
Installing haskell binaries is 50x faster than compiling it from source.
2017-08-11 12:01:28 +01:00
Vidar Holen
73d06c4f47 Autogenerate list of formats for --help 2017-08-06 15:48:59 -07:00
koalaman
72ed234291 Merge pull request #964 from blueyed/help-document-output-formats
List available output formats in --help output
2017-08-06 15:35:26 -07:00
koalaman
b94c03e5a1 Merge pull request #957 from martin-schwenke/issue-950
Fix incorrect detection of bash-style substring expansion (issue #950)
2017-08-06 15:34:23 -07:00
Daniel Hahler
226bc4409c Use spaces with list of dialects in --help for consistency 2017-08-06 16:25:31 +02:00
Daniel Hahler
4a6acb6ff0 List available output formats in --help output 2017-08-06 16:24:43 +02:00
koalaman
1d76abc439 Add storage bucket listing to readme 2017-07-30 10:52:06 -07:00
Martin Schwenke
807d899f3b Fix incorrect detection of bash-style substring expansion
Substring expansion detection only considers ':' as a separator..  It
needs to avoid triggering for ":-", ":=", ":+" and ":?", since they
mean other things.

This is a regression introduced by commit
a90b6d14b3

Signed-off-by: Martin Schwenke <martin@meltin.net>
2017-07-20 15:59:05 +10:00
koalaman
d6803ffa24 Merge pull request #955 from tsoernes/patch-1
Add Stack as install method
2017-07-18 20:39:39 -07:00
tsoernes
4ec8d73a14 Add Stack as install method 2017-07-19 04:17:09 +02:00
Vidar Holen
81388cefd2 Warn when calling functions before defining them. 2017-07-10 22:53:26 -07:00
Vidar Holen
43bb6a20ad Improve message for SC1052-54 about 'then;' 2017-07-08 17:25:54 -07:00
Vidar Holen
8f99d2b008 Don't warn about missing path for find -O3 . (#942) 2017-07-08 15:46:02 -07:00
Vidar Holen
79ae89076a Swap SC1041 and SC1042 for better sort order. 2017-07-08 15:21:58 -07:00
Vidar Holen
aa33280cb0 Improve here doc diagnosis 2017-07-08 14:00:02 -07:00
Vidar Holen
bd13224907 Use standard Haskell 'void' instead of custom 2017-07-08 10:23:51 -07:00
Vidar Holen
b064cf3038 Fix parsing here docs like << '#foo' (#947) 2017-07-07 22:26:12 -07:00
koalaman
79d6066450 Mention docker release tags in readme 2017-07-05 10:30:30 -07:00
Vidar Holen
1463cf773a Suggest explicit escape "\\n" for "\n" 2017-07-04 11:06:52 -07:00
Vidar Holen
31bb02d6b7 Ignore leading \ for commands (#927) 2017-07-03 16:40:11 -07:00
Vidar Holen
5bd33dbf92 Warn when piping/redirecting to mv/cp/echo/etc (#921) 2017-07-03 16:02:58 -07:00
Vidar Holen
a3c6aff0fb Improve parsing of line breaks in for statements (#926) 2017-07-03 13:58:10 -07:00
Vidar Holen
8184ef1e8b Don't complain about missing space in {( (#937) 2017-07-03 12:22:19 -07:00
Vidar Holen
a839a6657b Warn when commands start with dashes (#938) 2017-07-03 12:06:59 -07:00
Vidar Holen
a10b924570 Mention correct operator when warning about spaces around += (#944) 2017-07-03 10:44:09 -07:00
Vidar Holen
8f31ae913b Skip command argument when checking trap signal specs (#946) 2017-07-03 10:36:51 -07:00
Vidar Holen
a06ad41bfa Add Linux binaries to readme 2017-06-24 23:00:59 -07:00
Vidar Holen
21f5bf01eb Make TravisCI auto-build Linux executables. 2017-06-24 22:29:35 -07:00
koalaman
2ded4df6fa Merge pull request #935 from blueyed/README-fixes
README: style fixes, add Neomake
2017-06-14 10:47:28 -07:00
Daniel Hahler
90da31f226 README: style fixes, add Neomake 2017-06-13 21:22:50 +02:00
koalaman
b1486ec1e9 Merge pull request #903 from mrshu/mrshu/pushd-popd-like-cd
SC2164: Make SC2164 apply to `pushd` and `popd`
2017-06-12 09:54:05 -07:00
mr.Shu
954aa99b11 Analytics.hs: Refactor cd, popd and pushd checks
* Refactor the check of unchecked `cd`, `pushd` and `popd` into one
  function.

Signed-off-by: mr.Shu <mr@shu.io>
2017-06-12 12:16:01 +02:00
mr.Shu
79872f92f8 Merge branch 'master' of https://github.com/koalaman/shellcheck into mrshu/pushd-popd-like-cd
Signed-off-by: mr.Shu <mr@shu.io>
2017-06-12 11:29:19 +02:00
koalaman
bf9b841b07 Link to Windows executables in the Readme 2017-06-10 10:10:09 -07:00
Vidar Holen
5fad708df5 Zip compiled Windows executables. 2017-06-10 09:26:08 -07:00
Vidar Holen
5cece759cc Autobuild Windows .exe files 2017-06-09 21:40:59 -07:00
Vidar Holen
50c8172de4 Allow escaping ( with quotes in [ .. ] (#925) 2017-06-03 11:45:25 -07:00
Vidar Holen
ce950edbfd Don't trigger SC2026 when followed by empty literals (#923) 2017-06-03 09:38:47 -07:00
Royce Remer
f8e75d3e89 add compilation documentation for test runners 2017-05-28 16:06:55 -07:00
Vidar Holen
6f4e06d83c Avoid rescanning tree for lastpipe on every node. 2017-05-28 16:04:42 -07:00
Vladimir Panteleev
505ff7832f Recognize bash's shopt -s lastpipe
Fixes #732.
2017-05-28 14:56:49 -07:00
Vidar Holen
ac3f0b3360 SC2114 about rm -rf /usr is no longer silenced by -- 2017-05-28 14:44:58 -07:00
Vidar Holen
070a465b64 Recognize missing and superfluous cases in getopts loops. 2017-05-28 13:38:04 -07:00
Vidar Holen
4243c6a0bf Treat + like :+ to squash SC2068 2017-05-24 19:20:28 -07:00
Vidar Holen
8bc89bc451 Mention DevGuide in the README 2017-05-21 17:15:51 -07:00
Vidar Holen
5099ebf9b9 Allow comments after shellcheck directives. 2017-05-21 13:56:22 -07:00
Vidar Holen
d943ef6f77 Update Docker instructions. 2017-05-20 21:13:53 -07:00
mr.Shu
5e4c288cf4 SC2174: Do not warn at mkdir -pm 0700 ../foo
* Do not warn when `mkdir -pm 0700` is used with combination of paths
  like `..` and `.`

* Fixes #854

Signed-off-by: mr.Shu <mr@shu.io>
2017-05-16 11:45:04 -07:00
mr.Shu
9e35aa7ce8 SC2164: Make SC2164 apply to pushd and popd
* Since `pushd` and `popd` have the same failure cases, make the check
  for SC2164 apply to them as well.

* This commit also refactors the code a bit as `hasSetE` is now used in
  multiple places.

* Fixes #863.

Signed-off-by: mr.Shu <mr@shu.io>
2017-05-14 14:00:10 +02:00
Vincent van der Weele
21d7068bc8 Add VSCode integration to editor list 2017-05-09 19:03:26 -07:00
Vidar Holen
324aa3cc88 Improve and deduplicate string comparison warnings. 2017-04-22 21:09:42 -07:00
Dan Kegel
9c4f651e6b Document shell directive added by 944313c6 2017-04-18 09:57:38 -07:00
Vidar Holen
3cf8b9ceab Parse ksh nested arrays and warn about var=(( 2017-04-17 21:01:16 -07:00
Vidar Holen
5c01b6c7f5 Parse empty [ ] conditionals 2017-04-16 18:11:00 -07:00
Vidar Holen
7604e5eb58 Warn when using a glob as a command name. 2017-04-15 19:53:09 -07:00
Vidar Holen
4fb1080809 Warn when redirecting to a literal integer. 2017-04-15 17:20:33 -07:00
Vidar Holen
4f9a80db15 Remove leftover debug trace 2017-04-15 13:44:01 -07:00
Vidar Holen
3a38c50b8e Fix shellcheck warnings :P 2017-04-15 13:24:41 -07:00
Vidar Holen
fd79e80e78 Fix SC2120 triggering on sourced files and ${!var*} 2017-04-15 11:26:47 -07:00
Vidar Holen
1fd9b474ba Don't warn about quoting variables in [ -v 'bar[$foo]' ] 2017-04-15 10:57:10 -07:00
Vidar Holen
faafc99704 Don't trigger SC2037 when quoting (PAGER="cat" foo) 2017-04-15 10:33:56 -07:00
Vidar Holen
bc882fd85a Recognize more invalid shebangs 2017-04-08 16:34:00 -07:00
Vidar Holen
41b6e3d5eb Don't warn about [ -v foo ] being unassigned. 2017-04-08 15:19:47 -07:00
Vidar Holen
da1691912b Replace _otherwise with _ in cases 2017-04-08 14:00:52 -07:00
Vaibhav Sagar
0feb95b337 Implement fixes suggested by HLint 2017-04-08 11:07:32 -07:00
Vidar Holen
f0e0d9ffdb Don't suggest \[\] in PS1 for non-bash 2017-04-02 14:50:15 -07:00
Vidar Holen
3c75674b50 Warn about unquoted expansions in arrays. 2017-04-02 14:28:12 -07:00
Vidar Holen
8e5e77ad76 Don't suggest removing $ for (( $! + ${!var} )) 2017-04-02 09:49:47 -07:00
Vidar Holen
66c7cf19e2 Fix missing backslash in SC1003 about '\'' 2017-04-01 22:01:05 -07:00
xenopeek
36573b5b26 Update README.md
Command to install on Arch Linux based distros
2017-03-27 21:59:31 -07:00
Robert de Bock
9e4a9c8c6c Update README.md
Fixed a typo: scipts -> scripts.
2017-03-27 21:58:51 -07:00
Robert de Bock
c2fcb742db Update README.md
Adding a usage example for Docker.
2017-03-27 21:58:51 -07:00
Vaibhav Sagar
e8b4a79b65 Update resolver 2017-03-26 13:18:20 -07:00
Vidar Holen
08d1d37094 Stable version 0.4.6
This release is dedicated to Stardew Valley, for fixing the Harvest Moon
unplayable issue where Ann gives up on her inventions after marriage ;_;
2017-03-26 11:48:27 -07:00
Vidar Holen
e28e90133d Add missing import 2017-03-18 16:42:17 -07:00
Vidar Holen
2688a81526 Don't suggest indirection for 'declare var$n=foo' 2017-03-18 14:54:52 -07:00
Vidar Holen
82c3084438 Add HandBrakeCLI to list of stdin swallowing apps 2017-03-18 13:10:54 -07:00
Russ Kubik
c6ff1933b7 Update README.md 2017-03-17 09:32:53 -07:00
Vidar Holen
22c86256ac Decode UTF-8 sequences over 0x10FFFF as latin1 2017-03-10 10:11:05 -08:00
Daniel M. Capella
a3a4873190 readme: Add ALE Vim plugin 2017-03-08 10:10:24 -08:00
Vidar Holen
750212af39 Add less common actions to find -o check. 2017-03-03 20:43:00 -08:00
Vidar Holen
2154583fd3 Don't parse unicode quotes as real quotes. 2017-02-25 15:14:52 -08:00
Vidar Holen
35c74e4747 Getting command name from busybox now gets applet name 2017-02-12 10:56:29 -08:00
Vidar Holen
46fb91b44d Manually decode input files as lenient UTF-8. 2017-01-22 15:24:21 -08:00
Vidar Holen
128d5d6013 Don't warn about grep pattern issues when using -F. 2017-01-21 16:20:55 -08:00
koalaman
41176c23a6 Merge pull request #822 from bittorf/master
SC2164: show two possible variants for circumenting the warning
2017-01-15 15:58:19 -08:00
koalaman
342726f480 Merge pull request #823 from dasilvacontin/patch-1
Add TravisCI Setup to README.md
2017-01-15 15:57:09 -08:00
Vidar Holen
1863f2f12d Warn about += bashism in sh and dash. 2017-01-14 12:40:09 -08:00
Vidar Holen
8809a36952 Warn when finding HTML entities like &amp; 2017-01-14 11:59:31 -08:00
Vidar Holen
7f307c5775 Count | as a regex metacharacter for 2076. 2017-01-12 19:02:46 -08:00
David da Silva
eb12086d05 Fix typos in Travis CI section of the README.md 2017-01-11 11:48:57 +01:00
David da Silva
6259a32601 Add TravisCI Setup to README.md 2017-01-11 10:04:48 +01:00
Bastian Bittorf
4e13c7cbc1 SC2164: show two possible variants for circumenting the warning
always calling 'exit' is not good in e.g. functions.
the basic idea is at least that the returncode of
cd *is* evaluated somehow and not ignored.

Reported-by: Garance Alistair Drosehn <drosehn@rpi.edu>

Signed-off-by: Bastian Bittorf <bittorf@bluebottle.com>
2017-01-10 12:30:47 +01:00
Vidar Holen
edb01fa855 Warn about ( -d foo ) and similar. 2017-01-09 23:49:22 -08:00
koalaman
85e6c35845 Merge pull request #814 from Scorpiokat/master
This commit fixes #797
2017-01-03 21:25:45 -08:00
Ekaterina Efimova
43f667a8f9 Update Analytics.hs 2017-01-03 21:48:35 +03:00
Ekaterina Efimova
daacc98a8f Update Analytics.hs 2017-01-03 21:40:32 +03:00
Scorpiokat
40907b1636 This commit fixes #803 2017-01-03 14:55:12 +00:00
Scorpiokat
96168fc707 This commit fixes #797 2017-01-02 18:01:24 +00:00
Vidar Holen
6aee12a572 Warnings for braces/globs/arrays in [/[[. 2016-12-31 13:18:36 -08:00
Vidar Holen
0048f34b11 Merge branch 'master' of github.com:koalaman/shellcheck 2016-12-30 17:11:32 -08:00
Vidar Holen
e679ff222a Warn when using deprecated egrep/fgrep. 2016-12-30 16:55:31 -08:00
koalaman
6f5648faca Update README.md 2016-12-30 10:31:34 -08:00
Vidar Holen
30e94ea7ab Warn about comparisons and cases that can never match. 2016-12-29 14:04:49 -08:00
Vidar Holen
d8f8a2fa14 Minor 2% parser speedup 2016-12-29 10:56:00 -08:00
Vidar Holen
df3cc70658 Improve code for warning about escaped single quotes. 2016-12-28 21:51:28 -08:00
Vidar Holen
5669702362 Warn about missing and invalid subscripts in array assignments. 2016-12-28 18:58:03 -08:00
Vidar Holen
bd9d05c759 Warn about missing space in [ foo= bar ] 2016-12-27 21:20:59 -08:00
Austin English
af87fe9315 add missing references to dash 2016-12-20 17:10:55 -08:00
koalaman
c2850e436f Merge pull request #804 from austin987/readme2
README.md: sort package managers, add emerge (Gentoo)
2016-12-20 10:16:42 -08:00
Austin English
d73fc8f91d README.md: sort package managers, add emerge (Gentoo) 2016-12-20 12:11:10 -06:00
Vidar Holen
7124c113e8 Don't warn about .sh.version being unused (for Ksh) 2016-12-17 16:03:06 -08:00
Vidar Holen
f594f01d35 Add warnings about redirections without commands. 2016-12-17 15:03:52 -08:00
Vidar Holen
6ccb7e9129 Fix 'for file; do ..' counting $file as a safe variable. 2016-12-17 12:55:14 -08:00
Vidar Holen
838f0ce4dc Count "%(%Y%m%d)T" as a single format in printf. 2016-12-17 12:48:09 -08:00
Vidar Holen
105b09792c Fix SC095 about < /dev/null when using ssh -n 2016-12-17 12:41:00 -08:00
Vidar Holen
f6618d4332 Fix BSD style flag parsing to stop at -- 2016-12-11 10:58:01 -08:00
Vidar Holen
0a381be37b Improve heredoc delimiter handling. 2016-12-11 10:09:13 -08:00
Vidar Holen
46e47dad45 Don't quote source in 2041 and 2043. 2016-12-10 22:41:25 -08:00
Vidar Holen
fee6c94d40 Alias ash to dash with warning. 2016-12-10 10:57:01 -08:00
Vidar Holen
cf1c46d852 Add deprecation warning for tempfile 2016-12-10 09:58:27 -08:00
Vidar Holen
a40efffec9 Explicitly import Data.Monoid 2016-12-10 08:49:07 -08:00
Vidar Holen
44b96fca66 Merge branch 'bug562' 2016-12-03 10:21:49 -08:00
Vidar Holen
b4fb439191 Save string read by T_ParamSubSpecialChar 2016-12-03 10:19:14 -08:00
MortimerMcMire315
0897ab7092 Add handling for special characters in parameter substitutions.
Fixes koalaman/shellcheck#562. Special characters inside braces are
parsed into T_ParamSubSpecialChar instead of T_Literal so that they are
not flagged in the function checkInexplicablyUnquoted when sandwiched
between double quotes.
2016-11-28 16:06:02 -05:00
koalaman
9703f89f79 Merge pull request #774 from oyvindio/tag-releases-in-dockerhub
Push git tags to dockerhub as docker tags
2016-11-28 11:39:48 -08:00
koalaman
aadf02e635 Merge pull request #781 from austin987/patch-1
Add info/link on ignoring issues to README
2016-11-28 11:33:05 -08:00
Øyvind Ingebrigtsen Øvergaard
090051bdc1 Use $TRAVIS_TAG in $TAG if defined 2016-11-27 13:42:49 +01:00
Austin English
b8c96b4361 Add info/link on ignoring issues to README 2016-11-21 19:53:26 -06:00
Vidar Holen
f26038125d Allow spaces/comments before filewide annotations. 2016-11-20 18:06:36 -08:00
Vidar Holen
08f7ff37c5 Some cleanup and refactoring. 2016-11-12 15:51:36 -08:00
Øyvind Ingebrigtsen Øvergaard
60fc33ebdf Push git tags to dockerhub as docker tags 2016-11-11 20:32:05 +01:00
Vidar Holen
3a006f7bcb Use camelcase for cabal commands to align with stack. 2016-11-10 13:13:42 -08:00
koalaman
89b6fd58fa Reformat issue template 2016-10-31 18:20:39 -07:00
koalaman
069ddeffcc Merge pull request #766 from ryanoasis/issue-template
Add an issue template
2016-10-31 17:46:47 -07:00
Ryan L McIntyre
59c4ed106c Create ISSUE_TEMPLATE.md
* Suggestion for an issue template to help with the amount of issues
2016-10-31 20:25:55 -04:00
Vidar Holen
5efb724a3e Stable version 0.4.5
This release is dedicated to Google Inc for four great years of
employment and being good sports about hobby projects like this.
2016-10-21 14:00:50 -07:00
Vidar Holen
619b6c42f3 Improve parsing of fd close/duplicate redirections. 2016-10-21 11:31:58 -07:00
Vidar Holen
88c56ecd53 Allow unrecognized directives with warnings. 2016-10-14 12:14:20 -07:00
Vidar Holen
6b62b5bf7e Don't warn about [ a '>' b ] needing escapes. 2016-10-01 14:54:28 -07:00
Vidar Holen
8672af29ef Split duplicate SC1009 into SC1014 for if [ grep foo bar ] 2016-10-01 13:34:14 -07:00
Vidar Holen
1a8e34bfea Don't suggest grep -c when used with -o 2016-10-01 13:26:53 -07:00
Vidar Holen
b0dae063bf Add info when using 'find' without path 2016-09-25 11:56:32 -07:00
Vidar Holen
4fc4899803 Consider ${foo:=bar} an assignment. 2016-09-24 19:01:13 -07:00
Vidar Holen
cd4896192c Don't consider ~foo constant. 2016-09-24 15:32:44 -07:00
Vidar Holen
868d53af95 Warn about passing globs to unset. 2016-09-24 14:49:52 -07:00
Vidar Holen
6a4b86cbea Fix warning for >& 2016-09-24 14:08:00 -07:00
Vidar Holen
fe2398edc9 Warn about >& in sh 2016-09-24 14:03:54 -07:00
Vidar Holen
3a7dc86de1 Don't warn about unused vars with readonly -f 2016-09-24 13:42:20 -07:00
koalaman
1c0ec9c6f6 Merge pull request #734 from NLKNguyen/add_CI_CD_solution_with_Docker
Add CI/CD solution with Docker
2016-09-11 23:42:06 -07:00
NLKNguyen
84110dbef4 Change DOCKER_REPO value and add test runner 2016-09-08 23:57:46 -07:00
NLKNguyen
1a7e98beaf Use cleaner escaping method 2016-09-08 21:58:51 -07:00
NLKNguyen
a5d5831a2e Fix syntax error with traditional if-clause instead of escaping bracket 2016-09-08 21:41:06 -07:00
NLKNguyen
47201822f9 Change syntax style for readability 2016-09-08 21:31:23 -07:00
NLKNguyen
32689ef5eb Test Dockerfiles and Travis CI on downstream repos 2016-09-08 21:05:54 -07:00
Vidar Holen
87481dce25 Warn about printf hello world and printf "%s %s" foo 2016-09-06 21:16:59 -07:00
Vidar Holen
a90b6d14b3 Count b as a reference in ${a:b} 2016-09-05 14:01:53 -07:00
Vidar Holen
5a46eeb09a Allow #inline comments without SC2046. 2016-09-05 12:38:35 -07:00
Vidar Holen
47a7065a7a Add style note for 'mycmd; if [ $? -eq 0 ]'. 2016-08-28 20:54:08 -07:00
koalaman
dbafbb3b3b Merge pull request #722 from kpankonen/docker
add Dockerfile that will build shellcheck
2016-08-07 11:43:37 -07:00
Vidar Holen
13a2070a32 Support multidimensional KSH arrays and warn in Bash. 2016-08-06 18:40:08 -07:00
Kevin Pankonen
fa4874c044 add Dockerfile that will build shellcheck 2016-08-05 16:42:21 -07:00
Vidar Holen
6a71ff6f46 Don't suggest removing $ in (( ${COLUMNS-80} )) 2016-07-30 10:42:33 -07:00
Vidar Holen
36263fb3f5 s/range/class/ when warning about tr '[abc]' 2016-07-05 08:51:40 -07:00
Vidar Holen
6dc419bbf5 Improve warning for 'else if'. 2016-07-02 15:40:29 -07:00
Vidar Holen
7af3470a91 Improve parser errors when reparsing array indices. 2016-07-01 22:06:50 -07:00
Vidar Holen
42f7479fb8 Don't warn about missing shebang when using directives. 2016-07-01 22:02:06 -07:00
Vidar Holen
50084c06c5 Don't warn when $(seq) is used unquoted. 2016-07-01 21:26:46 -07:00
Vidar Holen
e3bef9dc97 Warn about (( 1 -lt 2 )) 2016-07-01 20:33:07 -07:00
Vidar Holen
6c1abb2dee Performance: make readDollarExpr fail early if no $ 2016-06-30 10:01:03 -07:00
Vidar Holen
43c26061b9 Improve parsing for ambiguous $((foo) ) and ((foo) ). 2016-06-26 22:13:48 -07:00
Vidar Holen
07fd5724b8 Recognize declare -A statements when value is inlined. 2016-06-26 14:57:52 -07:00
Vidar Holen
eb2472ada8 Merge branch 'master' of github.com:koalaman/shellcheck 2016-06-26 14:40:43 -07:00
Vidar Holen
3e5ecaa262 Parse indices of associative arrays properly 2016-06-26 14:39:49 -07:00
koalaman
e1cec6c5d3 Merge pull request #694 from eatnumber1/end_column
Emit the end line in the JSON
2016-06-18 15:09:58 -07:00
Russell Harmon
eaa319ec57 Emit the end line in the JSON.
This handles the case where the end line is not on the same line as the
start line when using the new end column feature.
2016-06-18 15:06:28 -07:00
koalaman
717b5e91f5 Merge pull request #693 from eatnumber1/end_column
Make SC1035 emit an end column
2016-06-18 15:05:44 -07:00
Russell Harmon
7f5f5b7fb5 Make SC1035 emit a proper end column
Example JSON output:
```
$ shellcheck -s bash -f json /dev/stdin <<< "[[0 -eq 1 ]]"
[{"file":"/tmp/zshNCNwPz","line":1,"column":1,"endColumn":3,"level":"error","code":1035,"message":"You need a space after the [[ and before the ]]."}]
```
2016-06-18 14:59:47 -07:00
Russell Harmon
856d57f7d8 PositionedComment and ParseNote contains end cols.
This change makes PositionedComment and ParseNote contain end columns.
It additionally modifies the JSON formatter to show the end column in an
"endColumn" property. No modifications to the messages shown by any
other formatter have been made.

Currently, all checks set the end column to the start column. It should
now be possible however to start setting the end column in the parser.
Additional work is needed to set the end column during AST analysis.
2016-06-18 14:58:00 -07:00
koalaman
c45e9d4878 Merge pull request #677 from Maffblaster/patch-1
Add Gentoo to supported distribution list.
2016-05-26 12:11:02 -07:00
Matthew Marchese
89c6f6c800 Add Gentoo to supported distribution list.
It was missing from the list. I thought it was best to add it. :)
2016-05-26 09:56:36 -07:00
Vidar Holen
85e69f86eb In (( x = y )), logic to not reference x also grabbed y 2016-05-24 09:12:47 -07:00
Vidar Holen
47fd16b8e8 Stable version 0.4.4
This release is dedicated to AlphaGo.
The second golden age of AI is upon us!
2016-05-15 13:53:37 -07:00
Vidar Holen
1d04754b37 Don't warn about a && b || c in if/while/until. 2016-05-14 17:14:32 -07:00
Vidar Holen
13ff0a7432 Warn when arrays are appended/assigned scalars. 2016-05-14 16:24:18 -07:00
Vidar Holen
40136fe249 Fix parsing of [[ a =~ {$var} ]] 2016-05-08 12:19:25 -07:00
Vidar Holen
86999ded1f Improve 'let' parsing, trigger unused var for ((a=1)) 2016-04-30 13:45:39 -07:00
Vidar Holen
7551a241ad Add missing warnings for {$i..10} similar to {1..$i} 2016-04-23 16:00:56 -07:00
Vidar Holen
2f0ae44de4 Fix parsing of here documents 2016-04-16 19:14:02 -07:00
Vidar Holen
51d8caf2c9 Add missing import 2016-04-16 09:53:15 -07:00
Vidar Holen
f835c2d4c1 Fix handling of spaces in shebangs. 2016-04-16 09:42:07 -07:00
Vidar Holen
db0c8c2dc9 Separate out command specific checks.
The checks use a better interface and give
an overall speed boost of 10%.
2016-04-10 17:01:40 -07:00
Vidar Holen
9911470d67 Don't warn about LINENO in dash 2016-04-09 09:56:07 -07:00
Vidar Holen
a5821c3a4d s/sh/bash/ in SC2176 because of posix guarantees. 2016-03-19 16:47:55 -07:00
Vidar Holen
c91083354f Warn about timing pipelines and compound commands in sh/dash. 2016-03-19 16:14:55 -07:00
Vidar Holen
2957fb64c9 Allow parsing 'time ( foo )' 2016-03-19 16:13:54 -07:00
koalaman
459e30804f Merge pull request #622 from Arguggi/master
Add stack support
2016-03-19 14:50:48 -07:00
koalaman
49569e10e6 Merge pull request #628 from benmwebb/patch-1
Fix a handful of typos.
2016-03-11 09:02:02 -08:00
Ben Webb
ba0221a1da Fix a handful of typos. 2016-03-10 21:27:07 -08:00
Vidar Holen
944313c6ba Directives after the shebang now apply to the entire script.
Also adds support for the shell= directive.
2016-03-08 20:16:16 -08:00
Vidar Holen
6af1aeb259 Add warning for multi-digit FDs in posix/dash. 2016-03-08 17:37:12 -08:00
Arguggi
b7c9d23452 Add stack support 2016-03-02 15:38:02 +01:00
koalaman
e792d69293 Merge pull request #608 from cs-shadow/master
Fix broken links in README
2016-02-17 09:25:56 -08:00
Chandan Singh
4d8f2eb707 Fix broken links in README
Presently, the links pointed to by "GCC error compatibility" and
"CheckStyle compatible XML output" are broken due to improper
way of referring files. This change fixes those links.

Also, this commit removes some trailing whitespaces in README.
2016-02-16 18:09:15 +00:00
Vidar Holen
8a3bd25f7c Improve error for missing final ) in $((foo) 2016-02-07 14:16:40 -08:00
Vidar Holen
825c1b5d22 Support parsing $((( as $( ((, with warning. 2016-02-06 22:19:29 -08:00
Vidar Holen
92473b512a Add warning for trailing spaces after \ breaks. 2016-02-01 20:20:34 -08:00
Vidar Holen
7e75d12ce1 Improve expansion in single quote false positives. 2016-01-26 19:57:18 -08:00
Vidar Holen
7d278c3ca1 Ignore SC2055 if rhs of either != is a glob. 2016-01-26 19:12:47 -08:00
Vidar Holen
5f1175fb58 Add special case warning to quote in eval echo {1..$n} 2016-01-25 18:46:55 -08:00
koalaman
257b794322 Merge pull request #578 from jwilk/master
Fix typo
2016-01-19 17:08:32 -08:00
Jakub Wilk
89572d3a96 Fix typo 2016-01-20 01:00:22 +01:00
koalaman
15edcbd4d5 Merge pull request #577 from neil-greenwood/readme-sed-quote
Fix missing quote in README.md
2016-01-18 09:00:53 -08:00
Neil Greenwood
736febaa3c Update README.md
Fix missing quote in `sed` example.
2016-01-18 14:53:15 +00:00
Vidar Holen
a21df2d88f Stable version 0.4.3
This is purely a bugfix bump that works on GHC 7.6.3,
as currently found in Debian stable and Ubuntu LTS.
2016-01-13 16:54:54 -08:00
Vidar Holen
d473fb8867 Use system over callCommand to allow linking on ghc 7.6.3. 2016-01-13 14:10:21 -08:00
Vidar Holen
f754363733 Add >= process-1.2.0.0 as dependency due to custom build. 2016-01-11 10:56:40 -08:00
Vidar Holen
ef1f8f535e Stable version 0.4.2
This release is dedicated to Tom Lehrer, who lives
the dream of having quit just to loaf about indefinitely.
2016-01-09 15:30:07 -08:00
Vidar Holen
f9909504dd Make SC2174 only trigger for nested directories. 2016-01-09 14:24:31 -08:00
koalaman
fa4cefda9d Merge pull request #572 from eatnumber1/mkdir
Add a warning when you mkdir with both -p and -m.
2016-01-09 13:58:53 -08:00
Russell Harmon
f2f6c66902 Add a warning when you mkdir with both -p and -m.
When using -p, parent directories will not be created with the mode
specified with -m and will instead be created using the default behavior
controlled by umask.
2016-01-08 14:15:11 -08:00
Vidar Holen
1f4dd85548 Change cabal Build-Type to Custom to actually use Setup.hs. 2016-01-05 14:10:45 -08:00
Vidar Holen
528381796e Allow escaped characters in [..] globs 2015-12-13 10:19:48 -08:00
Vidar Holen
ad7ad28246 Merge branch 'master' of github.com:koalaman/shellcheck 2015-12-12 15:53:18 -08:00
Vidar Holen
33ab998b02 Don't warn about quoting for [ -v var ] 2015-12-12 15:47:35 -08:00
koalaman
9c28237d52 Merge pull request #557 from eatnumber1/ignvars
Add more variables which are assumed to be set.
2015-12-10 20:26:00 -08:00
Russell Harmon
e0e5ba3a90 Add more variables which are assumed to be set.
This commit adds LC_MONETARY, LOGNAME, LD_LIBRARY_PATH, LANGUAGE,
DISPLAY, HOSTNAME, KRB5CCNAME, XAUTHORITY to be ignored when used
without initialization.
2015-12-10 20:05:50 -08:00
Vidar Holen
b4390414ef Fix remaining FIXME for tty error color output. 2015-12-06 12:48:53 -08:00
Vidar Holen
8acd5b13cd Add scripts for running shellcheck/tests interpreted. 2015-12-05 17:47:43 -08:00
Vidar Holen
d00ca0c283 Fix/generalize message for SC2070. 2015-12-05 17:29:06 -08:00
Vidar Holen
8bc98d89a7 Let SC2094 ignore echo/printf/sponge. 2015-12-05 16:50:11 -08:00
Vidar Holen
c7964a7a78 Warn about missing space in 'function foo{'. 2015-12-05 16:09:44 -08:00
Vidar Holen
8ec87d6655 Mention -- as alternative to ./* in SC2035 2015-12-05 13:45:56 -08:00
Vidar Holen
c3df2bf761 Don't warn about deprecated `` when just used for comments. 2015-12-05 13:33:39 -08:00
Vidar Holen
d1df3713ca Document --color in the man page. 2015-12-05 13:16:31 -08:00
Vidar Holen
23496e93b0 Fix compiler warning about missing field 2015-12-05 13:08:02 -08:00
koalaman
437e69fbba Merge pull request #553 from haguenau/add-color-switch
Add --color switch
2015-12-05 12:33:04 -08:00
David Haguenauer
63ad3f99ad Shorten long help line 2015-12-04 11:40:52 -05:00
David Haguenauer
0044c3dd6e Make use of --color with no argument equivalent to --color=always 2015-12-04 10:57:54 -05:00
David Haguenauer
a3d4101d6c Add initial support for --color 2015-12-03 17:55:56 -05:00
David Haguenauer
bd359c5c0f Delete trailing whitespace 2015-12-03 17:55:56 -05:00
Vidar Holen
498de63337 Mention that cabal installs to ~/.cabal/bin 2015-11-28 13:42:31 -08:00
Vidar Holen
52ab7dee2d Updating README.md to take over for shellcheck.net/about.html 2015-11-27 17:32:48 -08:00
Vidar Holen
1a5296659b Properly handle escaped double quotes in quoted backtick expressions. 2015-11-01 12:30:33 -08:00
Vidar Holen
a66ee2967c Trap warnings for kill/stop, non-XSI ints, "sig"-prefix and casing. 2015-10-31 17:36:24 -07:00
Vidar Holen
d985380f48 Consider "echo $(<file)" a bashism, and don't warn about UUOE. 2015-10-31 14:48:08 -07:00
Vidar Holen
6739c4a729 Count declare -p as a variable reference. 2015-10-31 14:16:45 -07:00
Vidar Holen
7415c9dcb7 Warn about non-posix function names like x-y 2015-10-31 13:53:17 -07:00
Vidar Holen
d3fc1f355d Merge branch 'master' of github.com:koalaman/shellcheck 2015-10-31 13:36:52 -07:00
Vidar Holen
48fd793581 Update getFlag function to also return non-flags. 2015-10-27 22:07:29 -07:00
koalaman
e5842e2e2b Merge pull request #516 from Fusl/patch-01
Fix typo "zyper" -> "zypper"
2015-10-22 13:59:50 -07:00
Fusl
cf445c7d20 Fix typo "zyper" -> "zypper" 2015-10-21 11:29:01 +02:00
Vidar Holen
ffb9578a98 Support parsing quoted test operators, and also warn about unicode dashes. 2015-10-17 16:33:21 -07:00
Vidar Holen
630f20e888 Count ~/ as dynamic for resolving source paths. 2015-10-17 10:50:07 -07:00
Vidar Holen
8f5f91f041 Warn about ]] with no corresponding [[. 2015-10-17 10:31:14 -07:00
Vidar Holen
8d9d4533c3 Don't trigger constant checks for -ot/-ef/-nt 2015-10-17 09:28:29 -07:00
koalaman
a4b4954a23 Merge pull request #512 from mimi1vx/opensuse-docu
install instructions for openSUSE
2015-10-17 09:10:12 -07:00
Ondřej Súkup
38cea9201d install instructions for openSUSE 2015-10-17 17:17:32 +02:00
Vidar Holen
4ce916ec1d Include bash builtin arrays in SC2128. 2015-10-14 12:31:09 -07:00
Vidar Holen
b9cb040128 Account for dereferencing for numerical ops in [[ ]]. 2015-10-14 11:11:17 -07:00
Vidar Holen
2488be7298 Don't warn about undefined HOSTNAME if it's being assigned. 2015-10-14 09:21:21 -07:00
Vidar Holen
d01b59a827 Don't warn about empty assignments when at end of command. 2015-10-13 12:16:39 -07:00
Vidar Holen
f77821625c Add dash as a first class supported shell. 2015-10-13 11:37:50 -07:00
Vidar Holen
1eece5b2ee Add warning about local in sh or not in bash functions. 2015-10-10 20:48:52 -07:00
Vidar Holen
58d45e3fa4 Warn about non-posix %q and flags for printf. 2015-10-10 20:14:17 -07:00
Vidar Holen
5aaa1a7d9a Split SC2165 messages into separate codes (2167). 2015-10-10 19:26:14 -07:00
Vidar Holen
3b36c2c820 Document exit codes in man page. 2015-10-10 19:19:38 -07:00
Vidar Holen
55692926b9 Don't consider {} and {a} brace expansions. 2015-10-03 21:12:28 -07:00
Vidar Holen
4172722167 Don't warn about quoted rhs of =~ when not a regex. 2015-10-03 15:21:57 -07:00
Vidar Holen
485593da2c Don't warn about disrespected quotes in ${#var}. 2015-10-01 18:56:45 -07:00
Vidar Holen
1181c6b3af Warn about ${!var} in POSIX mode. 2015-10-01 18:34:09 -07:00
Vidar Holen
ee181cfc43 Don't warn about comparisons when redirecting stderr for test. 2015-10-01 18:19:47 -07:00
Vidar Holen
c72667407b Merge branch 'master' of github.com:koalaman/shellcheck 2015-09-28 18:53:51 -07:00
Vidar Holen
5467a0f1d9 Account for set -o errexit and #!/bin/bash -e for unchecked cd. 2015-09-28 18:52:03 -07:00
Vidar Holen
3fc77d94ec Warn about [ -n foo ] when foo is entirely constant. 2015-09-28 18:46:32 -07:00
koalaman
23e0420cb1 Merge pull request #469 from spartacus06/fedora-readme
Add Fedora install procedure to README.md
2015-09-21 08:29:44 -07:00
Seth Jennings
a898165ac7 Add Fedora install procedure to README.md 2015-09-21 09:12:19 -05:00
Vidar Holen
ba5e3db31a Stable version 0.4.1
This is purely a bugfix bump because I botched the
0.4.0 cabal file and subsequent Hackage release :|
2015-09-05 19:32:37 -07:00
Vidar Holen
56145217fe Fixed the cabal file, which didn't include formatters :| 2015-09-05 19:17:22 -07:00
Vidar Holen
94d265ce41 Stable version 0.4.0
This release is dedicated to the Debian project,
for basically inventing the modern Linux distro.
2015-09-05 16:28:44 -07:00
Vidar Holen
0f00de80fd Support {n}>&1 named file descriptors. 2015-09-05 09:48:34 -07:00
Vidar Holen
c808c9b6fe End here doc tokens on < and >. 2015-09-03 20:58:21 -07:00
Vidar Holen
bf9297e2a5 Parse ksh ${ ..; } expansions. 2015-09-03 20:26:02 -07:00
Vidar Holen
7f547cc0ec Warn when using unescaped parens in eval. 2015-09-02 21:11:39 -07:00
Vidar Holen
01c27dc96a Parse double arithmetic negation, i.e. (( !!a )); 2015-09-02 19:31:22 -07:00
koalaman
856a204ec3 Merge pull request #446 from mbeniamino/master
Added FlexibleContexts extension in Parser.hs to fix compilation.
2015-08-23 10:01:22 -07:00
Matteo Beniamino
f054e2e2cc Added FlexibleContexts extension in Parser.hs to fix compilation. 2015-08-23 10:55:18 +02:00
Vidar Holen
090e09e4ca Support nested trinary expressions. 2015-08-22 14:20:20 -07:00
Vidar Holen
10276c878d Don't consider ${!os*} reference of 'os' 2015-08-22 13:55:17 -07:00
Vidar Holen
ae4aea4530 Adds support for a SHELLCHECK_OPTS environment variable. 2015-08-22 13:15:10 -07:00
Vidar Holen
d0029ae1d4 Ignore === in assignments, and add a special warning 1097 for ==. 2015-08-22 12:20:03 -07:00
Vidar Holen
eea7bc326e Improved parsing error expected messages. 2015-08-22 11:06:49 -07:00
Vidar Holen
73cd2cdd6f Also warn about missing 'then' before 'else' 2015-08-19 20:27:14 -07:00
Vidar Holen
a01862bc12 Adds a #shellcheck source=file directive to override source statements. 2015-08-19 19:09:55 -07:00
Vidar Holen
ccb6bf1ed5 Don't warn about uuoc for cat -n and similar. 2015-08-16 18:26:38 -07:00
Vidar Holen
136b654867 Update man page with info for -x. 2015-08-16 18:20:08 -07:00
Vidar Holen
f31c8bd3a3 Preliminary support for sourced files. 2015-08-16 17:18:51 -07:00
Vidar Holen
0dd61b65d8 Moved the various AST convenience functions to a separate module. 2015-08-16 12:53:23 -07:00
Vidar Holen
07747b30fb Warn about undefined behavior of test -a and -o. 2015-08-15 22:42:06 -07:00
Vidar Holen
26d16eb8ad Avoid new 2129 warnings when disabling the first one. 2015-08-15 14:23:45 -07:00
Vidar Holen
54b2d14847 Stop 2164 and 2103 about 'cd foo || exit' from overlapping. 2015-08-15 14:10:40 -07:00
Vidar Holen
f653362b18 Warn about bashism [^bar]. 2015-08-15 13:50:09 -07:00
Vidar Holen
f85441add9 Removed unused pragma. 2015-08-15 13:25:34 -07:00
Vidar Holen
67cfcfd206 Separated formatters into multiple files. 2015-08-15 12:51:13 -07:00
Vidar Holen
72eeafe002 Some cleanup to make room for future improvements. 2015-08-15 09:34:19 -07:00
Vidar Holen
6d9e8472e6 Less confusing message for SC2140 2015-08-02 19:55:47 -07:00
Vidar Holen
47d68019e5 Fixed exit status for missing files. 2015-08-02 19:13:01 -07:00
Vidar Holen
cbda90eeb5 Warn about zero-width spaces. 2015-07-25 13:59:00 -07:00
koalaman
722b0606e8 Merge pull request #416 from chadbrewbaker/master
cleanup
2015-07-23 12:26:59 -07:00
Chad Brewbaker
95cfd87589 cleanup 2015-07-23 12:51:38 -05:00
Vidar Holen
0a1beb883f Don't warn about unquoted :+ expansions. 2015-07-19 12:28:31 -07:00
Vidar Holen
83adcba88e Warn when nested loops have conflicting variable names. 2015-07-19 11:30:57 -07:00
Vidar Holen
35fb5073f4 Fixed cd foo; bar; cd -; check not trigger on top level 2015-07-18 19:58:09 -07:00
Vidar Holen
de59c3586b Warn about 'cd' when not used as part of a conditional 2015-07-18 19:37:58 -07:00
Vidar Holen
8894333556 Warn about 'export $foo' 2015-07-18 13:23:36 -07:00
Vidar Holen
b1843c520f Warn when using read without -r 2015-07-18 12:30:08 -07:00
Vidar Holen
d406ba9950 Special case messages for [ true ] and false/0/1 2015-07-18 11:47:28 -07:00
Vidar Holen
d5dfb4a7c1 Add a .ghci file to allow running ghci without cabal repl 2015-07-18 11:09:11 -07:00
Vidar Holen
7929a9dbba Makes literal check work for args to -z and -n 2015-07-18 11:08:42 -07:00
Vidar Holen
7e84ad031f Warn about [ foo ] when "foo" is non-constant with constant parts. 2015-07-18 09:56:58 -07:00
Vidar Holen
7eef12102b Make 2077 point to the = in [ foo=bar ] 2015-07-18 09:22:52 -07:00
Vidar Holen
0522a5f0bd Allow escaped linefeeds to break lines in $(()) 2015-07-15 09:01:29 -07:00
Vidar Holen
6c21e4671b Added bug reporting info to README.md. 2015-06-30 20:12:38 -07:00
Vidar Holen
3d83b87c9a Added copyright notice to individual source files. 2015-06-30 19:21:12 -07:00
Vidar Holen
f86d68bcc0 Fixed incorrectly documented flag syntax for -V. 2015-06-30 19:10:41 -07:00
Vidar Holen
1e65d36874 Included copyright information in the man page. 2015-06-30 19:07:08 -07:00
Vidar Holen
1ff67a61b4 Stable version 0.3.8. Now GPLv3!
This release is dedicated to Richard Stallman.
Happy hacking, and may we all live in a free GNUtopia!
2015-06-20 10:17:48 -07:00
Vidar Holen
349dfdab35 Relicense from AGPL v3 to GPL v3. 2015-06-20 09:52:07 -07:00
Vidar Holen
1ab29ddb39 Made SC2082 indirection message more sh friendly. 2015-06-14 17:20:52 -07:00
Vidar Holen
09b7788412 Don't consider declare -Ffp assignments. 2015-06-14 16:51:18 -07:00
Vidar Holen
ef2135f3aa Don't warn about unpassed parameters if used in child functions. 2015-06-14 16:14:05 -07:00
Vidar Holen
d10c3b2709 Don't warn about 2>&1 > foo if piping or capturing. 2015-06-14 15:39:44 -07:00
Vidar Holen
ca37794b7c Allow empty scripts, $()s and <()s. Also improves related error messages. 2015-06-14 14:30:11 -07:00
Vidar Holen
8b8b48ef55 Make quoted tilde message suggest $HOME instead. 2015-06-14 10:24:40 -07:00
Vidar Holen
aea0310a07 Added install instructions for native Windows 2015-06-14 10:16:32 -07:00
Vidar Holen
7fff088ce9 Don't output ansi escape codes on Windows 2015-06-07 18:08:23 -07:00
Vidar Holen
65ab8c8ecb Account for brace expansion in SC2115 (catastrophic rm). 2015-06-07 13:02:49 -07:00
Vidar Holen
3a041954d1 Don't warn about a&&b||c if c == printf 2015-05-24 12:41:54 -07:00
Vidar Holen
828378cdff Warn about injecting filenames into sh -c strings with find. 2015-05-23 13:03:07 -07:00
Vidar Holen
509cda4dcf Count mapfile/readarray as variable assignment 2015-05-10 13:13:42 -07:00
Vidar Holen
6076f0b1da Parse variables and subexpressions in brace expansions 2015-05-10 12:49:50 -07:00
Vidar Holen
1d26c280d6 Warn when failing to quote here strings. 2015-05-09 12:12:11 -07:00
Vidar Holen
c785d43e34 Fixed unquoted $@ warning message. 2015-05-09 12:07:33 -07:00
Vidar Holen
4c3e731445 Don't warn about a/b*c for ksh or when b == c 2015-05-09 11:48:08 -07:00
Vidar Holen
3940462da3 Warn about glob matching in [[ ]] for != as well as ==. 2015-05-09 11:34:34 -07:00
Vidar Holen
bb7ef5834b Warn on unindexed references for implicit arrays. 2015-04-21 19:19:15 -07:00
Vidar Holen
2f7bd556e8 Stable version 0.3.7
This is purely a bugfix bump that compiles on GHC 7.10
and uses a regex lib that doesn't crash on OS X.
2015-04-16 08:13:49 -07:00
Vidar Holen
081751c1b5 Fixed case sensitivity in 'trap' bashism test 2015-04-05 10:33:54 -07:00
Vidar Holen
cc86aab3f1 Added multiple new checks from checkbashisms 2015-04-05 10:25:00 -07:00
Vidar Holen
9f1f00cdd1 Switch from regex-compat to regex-tdfa 2015-04-04 16:26:28 -07:00
Vidar Holen
93debd3556 Add expect to list of known non-shells 2015-04-02 19:22:47 -07:00
Vidar Holen
47b971c582 Declared FlexibleContexts for GHC 7.10 2015-03-31 21:59:03 -07:00
56 changed files with 14872 additions and 6030 deletions

82
.compile_binaries Executable file
View File

@@ -0,0 +1,82 @@
#!/bin/bash
_cleanup(){
rm -rf dist shellcheck || true
}
build_linux() {
# Linux Docker image
name="$DOCKER_BASE"
DOCKER_BUILDS="$DOCKER_BUILDS $name"
docker build -t "$name:current" .
docker run "$name:current" --version
printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
docker run -v "$PWD:/mnt" "$name:current" myscript
# Copy static executable from docker image
id=$(docker create "$name:current")
docker cp "$id:/bin/shellcheck" "shellcheck"
docker rm "$id"
ls -l shellcheck
./shellcheck myscript
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-x86_64";
done
# 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'
_cleanup
}
build_aarch64() {
# Linux aarch64 static executable
docker run -v "$PWD:/mnt" koalaman/aarch64-builder 'buildsc'
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-aarch64"
done
}
build_armv6hf() {
# Linux armv6hf static executable
docker run -v "$PWD:/mnt" koalaman/armv6hf-builder -c 'compile-shellcheck'
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-armv6hf";
done
_cleanup
}
build_windows() {
# Windows .exe
docker run --user="$UID" -v "$PWD:/appdata" koalaman/winghc cuib
for tag in $TAGS
do
cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe";
done
_cleanup
}
build_osx() {
# Darwin x86_64 static executable
brew install cabal-install pandoc gnu-tar
sudo ln -s /usr/local/bin/gsha512sum /usr/local/bin/sha512sum
sudo ln -s /usr/local/bin/gtar /usr/local/bin/tar
export PATH="/usr/local/bin:$PATH"
cabal update
cabal install --dependencies-only
cabal build shellcheck
for tag in $TAGS
do
cp "dist/build/shellcheck/shellcheck" "deploy/shellcheck-$tag.darwin-x86_64";
done
_cleanup
}

6
.dockerignore Normal file
View File

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

1
.ghci Normal file
View File

@@ -0,0 +1 @@
:set -idist/build/autogen -isrc

27
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,27 @@
#### For bugs
- Rule Id (if any, e.g. SC1000):
- My shellcheck version (`shellcheck --version` or "online"):
- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
- [ ] I tried on shellcheck.net and verified that this is still a problem on the latest commit
#### For new checks and feature suggestions
- [ ] 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
#### Here's a snippet or screenshot that shows the problem:
```sh
#!/your/interpreter
your script here
```
#### Here's what shellcheck currently says:
#### Here's what I wanted or expected to see:

9
.gitignore vendored
View File

@@ -1,4 +1,4 @@
# Created by http://www.gitignore.io
# Created by https://www.gitignore.io
### Haskell ###
dist
@@ -12,4 +12,11 @@ cabal-dev
.cabal-sandbox/
cabal.sandbox.config
cabal.config
.stack-work
### Snap ###
/snap/.snapcraft/
/stage/
/parts/
/prime/
*.snap

65
.prepare_deploy Executable file
View File

@@ -0,0 +1,65 @@
#!/bin/bash
# This script packages up Travis compiled binaries
set -ex
shopt -s nullglob
cd deploy
cp ../LICENSE LICENSE.txt
sed -e $'s/$/\r/' > README.txt << END
This is a precompiled ShellCheck binary.
https://www.shellcheck.net/
ShellCheck is a static analysis tool for shell scripts.
It's licensed under the GNU General Public License v3.0.
Information and source code is available on the website.
This binary was compiled on $(date -u).
====== Latest commits ======
$(git log -n 3)
END
for file in ./*.exe
do
zip "${file%.*}.zip" README.txt LICENSE.txt "$file"
done
for file in *.linux-x86_64
do
base="${file%.*}"
cp "$file" "shellcheck"
tar -cJf "$base.linux.x86_64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in *.linux-aarch64
do
base="${file%.*}"
cp "$file" "shellcheck"
tar -cJf "$base.linux.aarch64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in *.linux-armv6hf
do
base="${file%.*}"
cp "$file" "shellcheck"
tar -cJf "$base.linux.armv6hf.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in *.darwin-x86_64
do
base="${file%.*}"
cp "$file" "shellcheck"
tar -cJf "$base.darwin.x86_64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in ./*
do
sha512sum "$file" > "$file.sha512sum"
done

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

66
.travis.yml Normal file
View File

@@ -0,0 +1,66 @@
sudo: required
language: sh
services:
- docker
matrix:
include:
- os: linux
env: BUILD=linux
- os: linux
env: BUILD=windows
- os: linux
env: BUILD=armv6hf
- os: linux
env: BUILD=aarch64
- os: osx
env: BUILD=osx
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
echo "Tags are $TAGS"
script:
- mkdir -p deploy
- source ./.compile_binaries
- ./striptests
- set -x; build_"$BUILD"; set +x;
- ./.prepare_deploy
after_success: |
if [ "$BUILD" = "linux" ]; then
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;
fi
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

394
CHANGELOG.md Normal file
View File

@@ -0,0 +1,394 @@
## 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
### Added
- SC2233/SC2234/SC2235: Suggest removing or replacing (..) around tests
- SC2232: Warn about invalid arguments to sudo
- SC2231: Suggest quoting expansions in for loop globs
- SC2229: Warn about 'read $var'
- SC2227: Warn about redirections in the middle of 'find' commands
- SC2224/SC2225/SC2226: Warn when using mv/cp/ln without a destination
- SC2223: Quote warning specific to `: ${var=value}`
- SC1131: Warn when using `elseif` or `elsif`
- SC1128: Warn about blanks/comments before shebang
- SC1127: Warn about C-style comments
### Fixed
- Annotations intended for a command's here documents now work
- Escaped characters inside groups in =~ regexes now parse
- Associative arrays are now respected in arithmetic contexts
- SC1087 about `$var[@]` now correctly triggers on any index
- Bad expansions in here documents are no longer ignored
- FD move operations like {fd}>1- now parse correctly
### Changed
- Here docs are now terminated as per spec, rather than by presumed intent
- SC1073: 'else if' is now parsed correctly and not like 'elif'
- SC2163: 'export $name' can now be silenced with 'export ${name?}'
- SC2183: Now warns when printf arg count is not a multiple of format count
## v0.4.7 - 2017-12-08
### Added
- Statically linked binaries for Linux and Windows (see README.md)!
- `-a` flag to also include warnings in `source`d files
- SC2221/SC2222: Warn about overridden case branches
- SC2220: Warn about unhandled error cases in getopt loops
- SC2218: Warn when using functions before they're defined
- SC2216/SC2217: Warn when piping/redirecting to mv/cp and other non-readers
- SC2215: Warn about commands starting with leading dash
- SC2214: Warn about superfluous getopt flags
- SC2213: Warn about unhandled getopt flags
- SC2212: Suggest `false` over `[ ]`
- SC2211: Warn when using a glob as a command name
- SC2210: Warn when redirecting to an integer, e.g. `foo 1>2`
- SC2206/SC2207: Suggest alternatives when using word splitting in arrays
- SC1117: Warn about double quoted, undefined backslash sequences
- SC1113/SC1114/SC1115: Recognized more malformed shebangs
### Fixed
- `[ -v foo ]` no longer warns if `foo` is undefined
- SC2037 is now suppressed by quotes, e.g. `PAGER="cat" man foo`
- Ksh nested array declarations now parse correctly
- Parameter Expansion without colons are now recognized, e.g. `${foo+bar}`
- The `lastpipe` option is now respected with regard to subshell warnings
- `\(` is now respected for grouping in `[`
- Leading `\` is now ignored for commands, to allow alias suppression
- Comments are now allowed after directives to e.g. explain 'disable'
## v0.4.6 - 2017-03-26
### Added
- SC2204/SC2205: Warn about `( -z foo )` and `( foo -eq bar )`
- SC2200/SC2201: Warn about brace expansion in [/[[
- SC2198/SC2199: Warn about arrays in [/[[
- SC2196/SC2197: Warn about deprected egrep/fgrep
- SC2195: Warn about unmatchable case branches
- SC2194: Warn about constant 'case' statements
- SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables
- SC2188/SC2189: Warn about redirections without commands
- SC2186: Warn about deprecated `tempfile`
- SC1109: Warn when finding `&amp;`/`&gt;`/`&lt;` unquoted
- SC1108: Warn about missing spaces in `[ var= foo ]`
### Changed
- All files are now read as UTF-8 with lenient latin1 fallback, ignoring locale
- Unicode quotes are no longer considered syntactic quotes
- `ash` scripts will now be checked as `dash` with a warning
### Fixed
- `-c` no longer suggested when using `grep -o | wc`
- Comments and whitespace are now allowed before filewide directives
- Here doc delimters with esoteric quoting like `foo""` are now handled
- 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
- `grep -F` now suppresses regex related suggestions
- Command name checks now recognize busybox applet names
## v0.4.5 - 2016-10-21
### Added
- A Docker build (thanks, kpankonen!)
- SC2185: Suggest explicitly adding path for `find`
- SC2184: Warn about unsetting globs (e.g. `unset foo[1]`)
- SC2183: Warn about `printf` with more formatters than variables
- SC2182: Warn about ignored arguments with `printf`
- SC2181: Suggest using command directly instead of `if [ $? -eq 0 ]`
- SC1106: Warn when using `test` operators in `(( 1 -eq 2 ))`
### Changed
- Unrecognized directives now causes a warning rather than parse failure.
### Fixed
- Indices in associative arrays are now parsed correctly
- Missing shebang warning squashed when specifying with a directive
- Ksh multidimensional arrays are now supported
- Variables in substring ${a:x:y} expansions now count as referenced
- SC1102 now also handles ambiguous `$((`
- Using `$(seq ..)` will no longer suggest quoting
- SC2148 (missing shebang) is now suppressed when using shell directives
- `[ a '>' b ]` is now recognized as being correctly escaped
## v0.4.4 - 2016-05-15
### Added
- Haskell Stack support (thanks, Arguggi!)
- SC2179/SC2178: Warn when assigning/appending strings to arrays
- SC1102: Warn about ambiguous `$(((`
- SC1101: Warn when \\ linebreaks have trailing spaces
### Changed
- Directives directly after the shebang now apply to the entire file
### Fixed
- `{$i..10}` is now flagged similar to `{1..$i}`
## v0.4.3 - 2016-01-13
### Fixed
- Build now works on GHC 7.6.3 as found on Debian Stable/Ubuntu LTS
## v0.4.2 - 2016-01-09
### Added
- First class support for the `dash` shell
- The `--color` flag similar to ls/grep's (thanks, haguenau!)
- SC2174: Warn about unexpected behavior of `mkdir -pm` (thanks, eatnumber1!)
- SC2172: Warn about non-portable use of signal numbers in `trap`
- SC2171: Warn about `]]` without leading `[[`
- SC2168: Warn about `local` outside functions
### Fixed
- Warnings about unchecked `cd` will no longer trigger with `set -e`
- `[ a -nt/-ot/-ef b ]` no longer warns about being constant
- Quoted test operators like `[ foo "<" bar ]` now parse
- Escaped quotes in backticks now parse correctly
## v0.4.1 - 2015-09-05
### Fixed
- Added missing files to Cabal, fixing the build
## v0.4.0 - 2015-09-05
### Added
- Support for following `source`d files
- Support for setting default flags in `SHELLCHECK_OPTS`
- An `--external-sources` flag for following arbitrary `source`d files
- A `source` directive to override the filename to `source`
- SC2166: Suggest using `[ p ] && [ q ]` over `[ p -a q ]`
- SC2165: Warn when nested `for` loops use the same variable name
- SC2164: Warn when using `cd` without checking that it succeeds
- SC2163: Warn about `export $var`
- SC2162: Warn when using `read` without `-r`
- SC2157: Warn about `[ "$var " ]` and similar never-empty string matches
### Fixed
- `cat -vnE file` and similar will no longer flag as UUOC
- Nested trinary operators in `(( ))` now parse correctly
- Ksh `${ ..; }` command expansions now parse
## v0.3.8 - 2015-06-20
### Changed
- ShellCheck's license has changed from AGPLv3 to GPLv3.
### Added
- SC2156: Warn about injecting filenames in `find -exec sh -c "{}" \;`
### Fixed
- Variables and command substitutions in brace expansions are now parsed
- ANSI colors are now disabled on Windows
- Empty scripts now parse
## v0.3.7 - 2015-04-16
### Fixed
- Build now works on GHC 7.10
- Use `regex-tdfa` over `regex-compat` since the latter crashes on OS X.
## v0.3.6 - 2015-03-28
### Added
- SC2155: Warn about masked return values in `export foo=$(exit 1)`
- SC2154: Warn when a lowercase variable is referenced but not assigned
- SC2152/SC2151: Warn about bad `return` values like `1234` and `"foo"`
- SC2150: Warn about `find -exec "shell command" \;`
### Fixed
- `coproc` is now supported
- Trinary operator now recognized in `((..))`
### Removed
- Zsh support has been removed
## v0.3.5 - 2014-11-09
### Added
- SC2148: Warn when not including a shebang
- SC2147: Warn about literal ~ in PATH
- SC1086: Warn about `$` in for loop variables, e.g. `for $i in ..`
- SC1084: Warn when the shebang uses `!#` instead of `#!`
### Fixed
- Empty and comment-only backtick expansions now parse
- Variables used in PS1/PROMPT\_COMMAND/trap now count as referenced
- ShellCheck now skips unreadable files and directories
- `-f gcc` on empty files no longer crashes
- Variables in $".." are now considered quoted
- Warnings about expansions in single quotes now include backticks
## v0.3.4 - 2014-07-08
### Added
- SC2146: Warn about precedence when combining `find -o` with actions
- SC2145: Warn when concatenating arrays and strings
### Fixed
- Case statements now support `;&` and `;;&`
- Indices in array declarations now parse correctly
- `let` expressions now parsed as arithmetic expressions
- Escaping is now respected in here documents
### Changed
- Completely drop Makefile in favor of Cabal (thanks rodrigosetti!)
## v0.3.3 - 2014-05-29
### Added
- SC2144: Warn when using globs in `[/[[`
- SC2143: Suggesting using `grep -q` over `[ "$(.. | grep)" ]`
- SC2142: Warn when referencing positional parameters in aliases
- SC2141: Warn about suspicious IFS assignments like `IFS="\n"`
- SC2140: Warn about bad embedded quotes like `echo "var="value""`
- SC2130: Warn when using `-eq` on strings
- SC2139: Warn about define time expansions in alias definitions
- SC2129: Suggest command grouping over `a >> log; b >> log; c >> log`
- SC2128: Warn when expanding arrays without an index
- SC2126: Suggest `grep -c` over `grep|wc`
- SC2123: Warn about accidentally overriding `$PATH`, e.g. `PATH=/my/dir`
- SC1083: Warn about literal `{/}` outside of quotes
- SC1082: Warn about UTF-8 BOMs
### Fixed
- SC2051 no longer triggers for `{1,$n}`, only `{1..$n}`
- Improved detection of single quoted `sed` variables, e.g. `sed '$s///'`
- Stop warning about single quoted variables in `PS1` and similar
- Support for Zsh short form loops, `=(..)`
### Removed
- SC1000 about unescaped lonely `$`, e.g. `grep "^foo$"`
## v0.3.2 - 2014-03-22
### Added
- SC2121: Warn about trying to `set` variables, e.g. `set var = value`
- SC2120/SC2119: Warn when a function uses `$1..` if none are ever passed
- SC2117: Warn when using `su` in interactive mode, e.g. `su foo; whoami`
- SC2116: Detect useless use of echo, e.g. `for i in $(echo $var)`
- SC2115/SC2114: Detect some catastrophic `rm -r "$empty/"` mistakes
- SC1081: Warn when capitalizing keywords like `While`
- SC1077: Warn when using acute accents instead of backticks
### Fixed
- Shells are now properly recognized in shebangs containing flags
- Stop warning about math on decimals in ksh/zsh
- Stop warning about decimal comparisons with `=`, e.g. `[ $version = 1.2 ]`
- Parsing of `|&`
- `${a[x]}` not counting as a reference of `x`
- `(( x[0] ))` not counting as a reference of `x`
## v0.3.1 - 2014-02-03
### Added
- The `-s` flag to specify shell dialect
- SC2105/SC2104: Warn about `break/continue` outside loops
- SC1076: Detect invalid `[/[[` arithmetic like `[ 1 + 2 = 3 ]`
- SC1075: Suggest using `elif` over `else if`
### Fixed
- Don't warn when comma separating elements in brace expansions
- Improved detection of single quoted `sed` variables, e.g. `sed '$d'`
- Parsing of arithmetic for loops using `{..}` instead of `do..done`
- Don't treat the last pipeline stage as a subshell in ksh/zsh
## v0.3.0 - 2014-01-19
### Added
- A man page (thanks Dridi!)
- GCC compatible error reporting (`shellcheck -f gcc`)
- CheckStyle compatible XML error reporting (`shellcheck -f checkstyle`)
- Error codes for each warning, e.g. SC1234
- Allow disabling warnings with `# shellcheck disable=SC1234`
- Allow disabling warnings with `--exclude`
- SC2103: Suggest using subshells over `cd foo; bar; cd ..`
- SC2102: Warn about duplicates in char ranges, e.g. `[10-15]`
- SC2101: Warn about named classes not inside a char range, e.g. `[:digit:]`
- SC2100/SC2099: Warn about bad math expressions like `i=i+5`
- SC2098/SC2097: Warn about `foo=bar echo $foo`
- SC2095: Warn when using `ssh`/`ffmpeg` in `while read` loops
- Better warnings for missing here doc tokens
### Fixed
- Don't warn when single quoting variables with `ssh/perl/eval`
- `${!var}` is now counted as a variable reference
### Removed
- Suggestions about using parameter expansion over basename
- The `jsoncheck` binary. Use `shellcheck -f json` instead.
## v0.2.0 - 2013-10-27
### Added
- Suggest `./*` instead of `*` when passing globs to commands
- Suggest `pgrep` over `ps | grep`
- Warn about unicode quotes
- Warn about assigned but unused variables
- Inform about client side expansion when using `ssh`
### Fixed
- CLI tool now uses exit codes and stderr canonically
- Parsing of extglobs containing empty patterns
- Parsing of bash style `eval foo=(bar)`
- Parsing of expansions in here documents
- Parsing of function names containing :+-
- Don't warn about `find|xargs` when using `-print0`
## v0.1.0 - 2013-07-23
### Added
- First release

36
Dockerfile Normal file
View File

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

157
LICENSE
View File

@@ -1,21 +1,33 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Employer mandated disclaimer:
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
I am providing code in the repository to you under an open source license.
Because this is my personal repository, the license you receive to my code is
from me and other individual contributors, and not my employer (Facebook).
- Vidar "koala_man" Holen
----
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
@@ -24,34 +36,44 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
@@ -60,7 +82,7 @@ modification follow.
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
@@ -537,45 +559,35 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
@@ -633,29 +645,40 @@ the "copyright" line and a pointer to where the full notice is found.
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
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.
This program 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 Affero General Public License for more details.
GNU General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/philosophy/why-not-lgpl.html>.

503
README.md
View File

@@ -1,88 +1,515 @@
[![Build Status](https://travis-ci.org/koalaman/shellcheck.svg?branch=master)](https://travis-ci.org/koalaman/shellcheck)
# ShellCheck - A shell script static analysis tool
http://www.shellcheck.net
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts:
Copyright 2012-2015, Vidar 'koala_man' Holen
Licensed under the GNU Affero General Public License, v3
![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 causes a shell to give cryptic error messages.
* To point out and clarify typical beginner's syntax issues that cause a shell
to give cryptic error messages.
- To point out and clarify typical intermediate level semantic problems,
that causes a shell to behave strangely and counter-intuitively.
* To point out and clarify typical intermediate level semantic problems that
cause a shell to behave strangely and counter-intuitively.
- To point out subtle caveats, corner cases and pitfalls, that may cause an
advanced user's otherwise working script to fail under future circumstances.
* To point out subtle caveats, corner cases and pitfalls that may cause an
advanced user's otherwise working script to fail under future circumstances.
ShellCheck is written in Haskell, and requires 2 GB of memory to compile.
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
* [How to use](#how-to-use)
* [On the web](#on-the-web)
* [From your terminal](#from-your-terminal)
* [In your editor](#in-your-editor)
* [In your build or test suites](#in-your-build-or-test-suites)
* [Installing](#installing)
* [Compiling from source](#compiling-from-source)
* [Installing Cabal](#installing-cabal)
* [Compiling ShellCheck](#compiling-shellcheck)
* [Running tests](#running-tests)
* [Gallery of bad code](#gallery-of-bad-code)
* [Quoting](#quoting)
* [Conditionals](#conditionals)
* [Frequently misused commands](#frequently-misused-commands)
* [Common beginner's mistakes](#common-beginners-mistakes)
* [Style](#style)
* [Data and typing errors](#data-and-typing-errors)
* [Robustness](#robustness)
* [Portability](#portability)
* [Miscellaneous](#miscellaneous)
* [Testimonials](#testimonials)
* [Ignoring issues](#ignoring-issues)
* [Reporting bugs](#reporting-bugs)
* [Contributing](#contributing)
* [Copyright](#copyright)
* [Other Resources](#other-resources)
## How to use
There are a number of ways to use ShellCheck!
### On the web
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!
### From your terminal
Run `shellcheck yourscript` in your terminal for instant output, as seen above.
### In your editor
You can see ShellCheck suggestions directly in a variety of editors.
* Vim, through [ALE](https://github.com/w0rp/ale), [Neomake](https://github.com/neomake/neomake), or [Syntastic](https://github.com/scrooloose/syntastic):
![Screenshot of Vim showing inlined shellcheck feedback](doc/vim-syntastic.png).
* 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).
* Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck).
* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
* VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck).
* Most other editors, through [GCC error compatibility](shellcheck.1.md#user-content-formats).
### In your build 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.
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/)
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-the-shellcheck-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
On systems with Cabal:
The easiest way to install ShellCheck locally is through your package manager.
On systems with Cabal (installs to `~/.cabal/bin`):
cabal update
cabal install shellcheck
cabal install ShellCheck
On systems with Stack (installs to `~/.local/bin`):
stack update
stack install ShellCheck
On Debian based distros:
apt-get install shellcheck
On Arch Linux based distros:
pacman -S shellcheck
or get the dependency free [shellcheck-static](https://aur.archlinux.org/packages/shellcheck-static/) from the AUR.
On Gentoo based distros:
emerge --ask shellcheck
On EPEL based distros:
yum -y install epel-release
yum install ShellCheck
On Fedora based distros:
dnf install ShellCheck
On FreeBSD:
pkg install hs-ShellCheck
On OS X with homebrew:
brew install shellcheck
ShellCheck is also available as an online service:
On OpenBSD:
http://www.shellcheck.net
pkg_add shellcheck
## Building with Cabal
On openSUSE
This sections describes how to build ShellCheck from a source directory.
zypper in ShellCheck
First, make sure cabal is installed. On Debian based distros:
Or use OneClickInstall - <https://software.opensuse.org/package/ShellCheck>
apt-get install cabal-install
On Solus:
On Fedora:
eopkg install shellcheck
yum install cabal-install
On Windows (via [chocolatey](https://chocolatey.org/packages/shellcheck)):
On Mac OS X with homebrew (http://brew.sh/):
```cmd
C:\> choco install shellcheck
```
brew install cabal-install
Or Windows (via [scoop](http://scoop.sh)):
On Mac OS X with MacPorts (http://www.macports.org/):
```cmd
C:\> scoop install shellcheck
```
port install hs-cabal-install
From Snap Store:
Let cabal update itself, in case your distro version is outdated:
snap install --channel=edge shellcheck
From Docker Hub:
```sh
docker run --rm -v "$PWD:/mnt" koalaman/shellcheck:stable 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.
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, armv6hf](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
* [Linux, aarch64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz) aka ARM64 (statically linked)
* [MacOS, x86_64](https://shellcheck.storage.googleapis.com/shellcheck-stable.darwin.x86_64.tar.xz)
* [Windows, x86](https://storage.googleapis.com/shellcheck/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.
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.
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.
### Installing a pre-compiled binary
The pre-compiled binaries come in `tar.xz` files. To decompress them, make sure
`xz` is installed.
On Debian/Ubuntu/Mint, you can `apt install xz-utils`.
On Redhat/Fedora/CentOS, `yum -y install xz`.
A simple installer may do something like:
```bash
scversion="stable" # or "v0.4.7", or "latest"
wget -qO- "https://storage.googleapis.com/shellcheck/shellcheck-${scversion?}.linux.x86_64.tar.xz" | tar -xJv
cp "shellcheck-${scversion}/shellcheck" /usr/bin/
shellcheck --version
```
## Compiling from source
This section describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile.
### Installing Cabal
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.
$ brew 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/>
Verify that `cabal` is installed and update its dependency list with
$ cabal update
$ cabal install cabal-install
With cabal installed, cd to the ShellCheck source directory and:
### Compiling ShellCheck
`git clone` this repository, and `cd` to the ShellCheck source directory to build/install:
$ cabal install
This will install ShellCheck to your ~/.cabal/bin directory.
Or if you intend to run the tests:
Add the directory to your PATH (for bash, add this to your ~/.bashrc file):
$ cabal install --enable-tests
export PATH=$HOME/.cabal/bin:$PATH
This will compile ShellCheck and install it to your `~/.cabal/bin` directory.
Verify that your PATH is set up correctly:
Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
$ which shellcheck
~/.cabal/bin/shellcheck
```sh
export PATH="$HOME/.cabal/bin:$PATH"
```
## Running tests
Log out and in again, and verify that your PATH is set up correctly:
```sh
$ which shellcheck
~/.cabal/bin/shellcheck
```
On native Windows, the `PATH` should already be set up, but the system
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
codepage to UTF-8 (65001) with `chcp`:
```cmd
chcp 65001
```
In Powershell ISE, you may need to additionally update the output encoding:
```powershell
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
```
### Running tests
To run the unit test suite:
cabal configure --enable-tests
cabal build
cabal test
$ cabal test
## Gallery of bad code
So what kind of things does ShellCheck look for? Here is an incomplete list of detected issues.
### Quoting
ShellCheck can recognize several types of incorrect quoting:
```sh
echo $1 # Unquoted variables
find . -name *.ogg # Unquoted find/grep patterns
rm "~/my file.txt" # Quoted tilde expansion
v='--verbose="true"'; cmd $v # Literal quotes in variables
for f in "*.ogg" # Incorrectly quoted 'for' loops
touch $@ # Unquoted $@
echo 'Don't forget to restart!' # Singlequote closed by apostrophe
echo 'Don\'t try this at home' # Attempting to escape ' in ''
echo 'Path is $PATH' # Variables in single quotes
trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap
```
### Conditionals
ShellCheck can recognize many types of incorrect test statements.
```sh
[[ n != 0 ]] # Constant test expressions
[[ -e *.mpg ]] # Existence checks of globs
[[ $foo==0 ]] # Always true due to missing spaces
[[ -n "$foo " ]] # Always true due to literals
[[ $foo =~ "fo+" ]] # Quoted regex in =~
[ foo =~ re ] # Unsupported [ ] operators
[ $1 -eq "shellcheck" ] # Numerical comparison of strings
[ $n && $m ] # && in [ .. ]
[ grep -q foo file ] # Command without $(..)
[[ "$$file" == *.jpg ]] # Comparisons that can't succeed
(( 1 -lt 2 )) # Using test operators in ((..))
```
### Frequently misused commands
ShellCheck can recognize instances where commands are used incorrectly:
```sh
grep '*foo*' file # Globs in regex contexts
find . -exec foo {} && bar {} \; # Prematurely terminated find -exec
sudo echo 'Var=42' > /etc/profile # Redirecting sudo
time --format=%s sleep 10 # Passing time(1) flags to time builtin
while read h; do ssh "$h" uptime # Commands eating while loop input
alias archive='mv $1 /backup' # Defining aliases with arguments
tr -cd '[a-zA-Z0-9]' # [] around ranges in tr
exec foo; echo "Done!" # Misused 'exec'
find -name \*.bak -o -name \*~ -delete # Implicit precedence in find
# find . -exec foo > bar \; # Redirections in find
f() { whoami; }; sudo f # External use of internal functions
```
### Common beginner's mistakes
ShellCheck recognizes many common beginner's syntax errors:
```sh
var = 42 # Spaces around = in assignments
$foo=42 # $ in assignments
for $var in *; do ... # $ in for loop variables
var$n="Hello" # Wrong indirect assignment
echo ${var$n} # Wrong indirect reference
var=(1, 2, 3) # Comma separated arrays
array=( [index] = value ) # Incorrect index initialization
echo $var[14] # Missing {} in array references
echo "Argument 10 is $10" # Positional parameter misreference
if $(myfunction); then ..; fi # Wrapping commands in $()
else if othercondition; then .. # Using 'else if'
f; f() { echo "hello world; } # Using function before definition
[ false ] # 'false' being true
if ( -f file ) # Using (..) instead of test
```
### Style
ShellCheck can make suggestions to improve style:
```sh
[[ -z $(find /tmp | grep mpg) ]] # Use grep -q instead
a >> log; b >> log; c >> log # Use a redirection block instead
echo "The time is `date`" # Use $() instead
cd dir; process *; cd ..; # Use subshells instead
echo $[1+2] # Use standard $((..)) instead of old $[]
echo $(($RANDOM % 6)) # Don't use $ on variables in $((..))
echo "$(date)" # Useless use of echo
cat file | grep foo # Useless use of cat
```
### Data and typing errors
ShellCheck can recognize issues related to data and typing:
```sh
args="$@" # Assigning arrays to strings
files=(foo bar); echo "$files" # Referencing arrays as strings
declare -A arr=(foo bar) # Associative arrays without index
printf "%s\n" "Arguments: $@." # Concatenating strings and arrays
[[ $# > 2 ]] # Comparing numbers as strings
var=World; echo "Hello " var # Unused lowercase variables
echo "Hello $name" # Unassigned lowercase variables
cmd | read bar; echo $bar # Assignments in subshells
cat foo | cp bar # Piping to commands that don't read
printf '%s: %s\n' foo # Mismatches in printf argument count
```
### Robustness
ShellCheck can make suggestions for improving the robustness of a script:
```sh
rm -rf "$STEAMROOT/"* # Catastrophic rm
touch ./-l; ls * # Globs that could become options
find . -exec sh -c 'a && b {}' \; # Find -exec shell injection
printf "Hello $name" # Variables in printf format
for f in $(ls *.txt); do # Iterating over ls output
export MYVAR=$(cmd) # Masked exit codes
case $version in 2.*) :;; 2.6.*) # Shadowed case branches
```
### Portability
ShellCheck will warn when using features not supported by the shebang. For example, if you set the shebang to `#!/bin/sh`, ShellCheck will warn about portability issues similar to `checkbashisms`:
```sh
echo {1..$n} # Works in ksh, but not bash/dash/sh
echo {1..10} # Works in ksh and bash, but not dash/sh
echo -n 42 # Works in ksh, bash and dash, undefined in sh
trap 'exit 42' sigint # Unportable signal spec
cmd &> file # Unportable redirection operator
read foo < /dev/tcp/host/22 # Unportable intercepted files
foo-bar() { ..; } # Undefined/unsupported function name
[ $UID = 0 ] # Variable undefined in dash/sh
local var=value # local is undefined in sh
time sleep 1 | sleep 5 # Undefined uses of 'time'
```
### Miscellaneous
ShellCheck recognizes a menagerie of other issues:
```sh
PS1='\e[0;32m\$\e[0m ' # PS1 colors not in \[..\]
PATH="$PATH:~/bin" # Literal tilde in $PATH
rm “file” # Unicode quotes
echo "Hello world" # Carriage return / DOS line endings
echo hello \ # Trailing spaces after \
var=42 echo $var # Expansion of inlined environment
#!/bin/bash -x -e # Common shebang errors
echo $((n/180*100)) # Unnecessary loss of precision
ls *[:digit:].txt # Bad character class globs
sed 's/foo/bar/' file > file # Redirecting to input
while getopts "a" f; do case $f in "b") # Unhandled getopts flags
```
## Testimonials
> At first you're like "shellcheck is awesome" but then you're like "wtf are we still using bash"
Alexander Tarasikov,
[via Twitter](https://twitter.com/astarasikov/status/568825996532707330)
## Ignoring issues
Issues can be ignored via environmental variable, command line, individually or globally within a file:
<https://github.com/koalaman/shellcheck/wiki/Ignore>
## Reporting bugs
Please use the GitHub issue tracker for any bugs or feature suggestions:
<https://github.com/koalaman/shellcheck/issues>
## Contributing
Please submit patches to code or documentation as GitHub pull requests! Check
out the [DevGuide](https://github.com/koalaman/shellcheck/wiki/DevGuide) on the
ShellCheck Wiki.
Contributions must be licensed under the GNU GPLv3.
The contributor retains the copyright.
## Copyright
ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE).
Copyright 2012-2019, [Vidar 'koala_man' Holen](https://github.com/koalaman/) and contributors.
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

@@ -8,21 +8,13 @@ import Distribution.Simple (
simpleUserHooks )
import Distribution.Simple.Setup ( SDistFlags )
-- | This requires the process package from,
--
-- https://hackage.haskell.org/package/process
--
import System.Process ( callCommand )
import System.Process ( system )
-- | This will use almost the default implementation, except we switch
-- out the default pre-sdist hook with our own, 'myPreSDist'.
--
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:
@@ -35,9 +27,10 @@ main = defaultMainWithHooks myHooks
--
myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo
myPreSDist _ _ = do
putStrLn "Building the man page..."
putStrLn "Building the man page (shellcheck.1) with pandoc..."
putStrLn pandoc_cmd
callCommand 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"
pandoc_cmd = "pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1"

View File

@@ -1,13 +1,13 @@
Name: ShellCheck
Version: 0.3.6
Version: 0.7.0
Synopsis: Shell script analysis tool
License: AGPL-3
License: GPL-3
License-file: LICENSE
Category: Static Analysis
Author: Vidar Holen
Maintainer: vidar@vidarholen.net
Homepage: http://www.shellcheck.net/
Build-Type: Simple
Homepage: https://www.shellcheck.net/
Build-Type: Custom
Cabal-Version: >= 1.8
Bug-reports: https://github.com/koalaman/shellcheck/issues
Description:
@@ -28,59 +28,107 @@ Extra-Source-Files:
shellcheck.1.md
-- built with a cabal sdist hook
shellcheck.1
-- convenience script for stripping tests
striptests
-- tests
test/shellcheck.hs
custom-setup
setup-depends:
base >= 4 && <5,
process >= 1.0 && <1.7,
Cabal >= 1.10 && <2.5
source-repository head
type: git
location: git://github.com/koalaman/shellcheck.git
library
hs-source-dirs: src
if impl(ghc < 8.0)
build-depends:
semigroups
build-depends:
base >= 4 && < 5,
containers,
directory,
json,
mtl,
aeson,
array,
-- GHC 7.6.3 (base 4.6.0.1) is buggy (#1131, #1119) in optimized mode.
-- Just disable that version entirely to fail fast.
base > 4.6.0.1 && < 5,
bytestring,
containers >= 0.5,
deepseq >= 1.4.0.0,
Diff >= 0.2.0,
directory >= 1.2.3.0,
mtl >= 2.2.1,
filepath,
parsec,
regex-compat,
QuickCheck >= 2.7.4
regex-tdfa,
QuickCheck >= 2.7.4,
-- When cabal supports it, move this to setup-depends:
process
exposed-modules:
ShellCheck.Analytics
ShellCheck.AST
ShellCheck.ASTLib
ShellCheck.Analytics
ShellCheck.Analyzer
ShellCheck.AnalyzerLib
ShellCheck.Checker
ShellCheck.Checks.Commands
ShellCheck.Checks.Custom
ShellCheck.Checks.ShellSupport
ShellCheck.Data
ShellCheck.Options
ShellCheck.Fixer
ShellCheck.Formatter.Format
ShellCheck.Formatter.CheckStyle
ShellCheck.Formatter.Diff
ShellCheck.Formatter.GCC
ShellCheck.Formatter.JSON
ShellCheck.Formatter.JSON1
ShellCheck.Formatter.TTY
ShellCheck.Formatter.Quiet
ShellCheck.Interface
ShellCheck.Parser
ShellCheck.Simple
ShellCheck.Regex
other-modules:
Paths_ShellCheck
executable shellcheck
if impl(ghc < 8.0)
build-depends:
semigroups
build-depends:
ShellCheck,
aeson,
array,
base >= 4 && < 5,
bytestring,
containers,
directory,
json,
mtl,
parsec,
regex-compat,
transformers,
QuickCheck >= 2.7.4
deepseq >= 1.4.0.0,
Diff >= 0.2.0,
directory >= 1.2.3.0,
mtl >= 2.2.1,
filepath,
parsec >= 3.0,
QuickCheck >= 2.7.4,
regex-tdfa,
ShellCheck
main-is: shellcheck.hs
test-suite test-shellcheck
type: exitcode-stdio-1.0
build-depends:
ShellCheck,
aeson,
array,
base >= 4 && < 5,
bytestring,
containers,
directory,
json,
mtl,
deepseq >= 1.4.0.0,
Diff >= 0.2.0,
directory >= 1.2.3.0,
mtl >= 2.2.1,
filepath,
parsec,
regex-compat,
transformers,
QuickCheck >= 2.7.4
QuickCheck >= 2.7.4,
regex-tdfa,
ShellCheck
main-is: test/shellcheck.hs

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +0,0 @@
module ShellCheck.Options where
data Shell = Ksh | Sh | Bash
deriving (Show, Eq)
data AnalysisOptions = AnalysisOptions {
optionShellType :: Maybe Shell,
optionExcludes :: [Integer]
}
defaultAnalysisOptions = AnalysisOptions {
optionShellType = Nothing,
optionExcludes = []
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,80 +0,0 @@
{-
This file is part of ShellCheck.
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Simple (shellCheck, ShellCheckComment, scLine, scColumn, scSeverity, scCode, scMessage, runTests) where
import Data.List
import Data.Maybe
import ShellCheck.Analytics hiding (runTests)
import ShellCheck.Options
import ShellCheck.Parser hiding (runTests)
import Test.QuickCheck.All (quickCheckAll)
import Text.Parsec.Pos
shellCheck :: AnalysisOptions -> String -> [ShellCheckComment]
shellCheck options script =
let (ParseResult result notes) = parseShell options "-" script in
let allNotes = notes ++ concat (maybeToList $ do
(tree, posMap) <- result
let list = runAnalytics options tree
return $ map (noteToParseNote posMap) $ filterByAnnotation tree list
)
in
map formatNote $ nub $ sortNotes allNotes
data ShellCheckComment = ShellCheckComment { scLine :: Int, scColumn :: Int, scSeverity :: String, scCode :: Int, scMessage :: String }
instance Show ShellCheckComment where
show c = concat ["(", show $ scLine c, ",", show $ scColumn c, ") ", scSeverity c, ": ", show (scCode c), " ", scMessage c]
severityToString s =
case s of
ErrorC -> "error"
WarningC -> "warning"
InfoC -> "info"
StyleC -> "style"
formatNote (ParseNote pos severity code text) =
ShellCheckComment (sourceLine pos) (sourceColumn pos) (severityToString severity) (fromIntegral code) text
testCheck = shellCheck defaultAnalysisOptions { optionExcludes = [2148] } -- Ignore #! warnings
prop_findsParseIssue =
let comments = testCheck "echo \"$12\"" in
length comments == 1 && scCode (head comments) == 1037
prop_commentDisablesParseIssue1 =
null $ testCheck "#shellcheck disable=SC1037\necho \"$12\""
prop_commentDisablesParseIssue2 =
null $ testCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\""
prop_findsAnalysisIssue =
let comments = testCheck "echo $1" in
length comments == 1 && scCode (head comments) == 2086
prop_commentDisablesAnalysisIssue1 =
null $ testCheck "#shellcheck disable=SC2086\necho $1"
prop_commentDisablesAnalysisIssue2 =
null $ testCheck "#shellcheck disable=SC2086\n#lol\necho $1"
prop_optionDisablesIssue1 =
null $ shellCheck (defaultAnalysisOptions { optionExcludes = [2086, 2148] }) "echo $1"
prop_optionDisablesIssue2 =
null $ shellCheck (defaultAnalysisOptions { optionExcludes = [2148, 1037] }) "echo \"$10\""
return []
runTests = $quickCheckAll

BIN
doc/emacs-flycheck.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
doc/terminal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
doc/vim-syntastic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

13
nextnumber Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# TODO: Find a less trashy way to get the next available error code
if ! shopt -s globstar
then
echo "Error: This script depends on Bash 4." >&2
exit 1
fi
for i in 1 2
do
last=$(grep -hv "^prop" ./**/*.hs | grep -Ewo "$i[0-9]{3}" | sort -n | tail -n 1)
echo "Next ${i}xxx: $((last+1))"
done

5
quickrun Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# quickrun runs ShellCheck in an interpreted mode.
# This allows testing changes without recompiling.
runghc -isrc -idist/build/autogen shellcheck.hs "$@"

15
quicktest Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# quicktest runs the ShellCheck unit tests in an interpreted mode.
# This allows running tests without compiling, which can be faster.
# 'cabal test' remains the source of truth.
(
var=$(echo 'main' | ghci test/shellcheck.hs 2>&1 | tee /dev/stderr)
if [[ $var == *ExitSuccess* ]]
then
exit 0
else
grep -C 3 -e "Fail" -e "Tracing" <<< "$var"
exit 1
fi
) 2>&1

View File

@@ -16,7 +16,7 @@ errors and pitfalls where the shell just gives a cryptic error message or
strange behavior, but it also reports on a few more advanced issues where
corner cases can cause delayed failures.
ShellCheck gives shell specific advice. Consider the line:
ShellCheck gives shell specific advice. Consider this line:
(( area = 3.14*r*r ))
@@ -29,9 +29,27 @@ will warn that decimals are not supported.
+ For scripts starting with `#!/bin/ksh` (or using `-s ksh`), ShellCheck will
not warn at all, as `ksh` supports decimals in arithmetic contexts.
# OPTIONS
**-a**,\ **--check-sourced**
: Emit warnings in sourced files. Normally, `shellcheck` will only warn
about issues in the specified files. With this option, any issues in
sourced files will also be reported.
**-C**[*WHEN*],\ **--color**[=*WHEN*]
: For TTY output, enable colors *always*, *never* or *auto*. The default
is *auto*. **--color** without an argument is equivalent to
**--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*...]
: Explicitly exclude the specified codes from the report. Subsequent **-e**
@@ -44,15 +62,60 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
standard output. Subsequent **-f** options are ignored, see **FORMATS**
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*
: Specify Bourne shell dialect. Valid values are *sh*, *bash* and *ksh*.
The default is to use the file's shebang, or *bash* if the target shell
can't be determined.
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
The default is to deduce the shell from the file's `shell` directive,
shebang, or `.bash/.bats/.dash/.ksh` extension, in that order. *sh* refers to
POSIX `sh` (not the system's), and will warn of portability issues.
**-V**\ *version*,\ **--version**
**-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**
: 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**
: Follow 'source' statements even when the file is not specified as input.
By default, `shellcheck` will only follow files specified on the command
line (plus `/dev/null`). This option allows following any file the script
may `source`.
**FILES...**
: One or more script files to check, or "-" for standard input.
: Print version and exit.
# FORMATS
@@ -89,27 +152,59 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
...
</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
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
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
command-or-structure
@@ -119,7 +214,12 @@ For example, to suppress SC2035 about using `./*.jpg`:
# shellcheck disable=SC2035
echo "Files: " *.jpg
Here a shell brace group is used to suppress on multiple lines:
To tell ShellCheck where to look for an otherwise dynamically determined file:
# shellcheck source=./lib.sh
source "$(find_install_dir)/lib.sh"
Here a shell brace group is used to suppress a warning on multiple lines:
# shellcheck disable=SC2016
{
@@ -134,15 +234,108 @@ Valid keys are:
The command can be a simple command like `echo foo`, or a compound command
like a function definition, subshell block or loop.
**enable**
: Enable an optional check by name, as listed with **--list-optional**.
Only file-wide `enable` directives are considered.
# AUTHOR
ShellCheck is written and maintained by Vidar Holen.
**source**
: 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
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**
: Overrides the shell detected from the shebang. This is useful for
files meant to be included (and thus lacking a shebang), or possibly
as a more targeted alternative to 'disable=2039'.
# 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 using `which` since it gives full paths and is common enough
disable=SC2230
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
The environment variable `SHELLCHECK_OPTS` can be set with default flags:
export SHELLCHECK_OPTS='--shell=bash --exclude=SC2016'
Its value will be split on spaces and prepended to the command line on each
invocation.
# RETURN VALUES
ShellCheck uses the follow exit codes:
+ 0: All files successfully scanned with no issues.
+ 1: All files successfully scanned with some issues.
+ 2: Some files could not be processed (e.g. file not found).
+ 3: ShellCheck was invoked with bad syntax (e.g. unknown flag).
+ 4: ShellCheck was invoked with bad options (e.g. unknown formatter).
# LOCALE
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
sequences. `LC_CTYPE` is respected for output, and defaults to UTF-8 for
locales where encoding is unspecified (such as the `C` locale).
Windows users seeing `commitBuffer: invalid argument (invalid character)`
should set their terminal to use UTF-8 with `chcp 65001`.
# AUTHORS
ShellCheck is developed and maintained by Vidar Holen, with assistance from a
long list of wonderful contributors.
# REPORTING BUGS
Bugs and issues can be reported on GitHub:
https://github.com/koalaman/shellcheck/issues
# COPYRIGHT
Copyright 2012-2019, Vidar Holen and contributors.
Licensed under the GNU General Public License version 3 or later,
see https://gnu.org/licenses/gpl.html
# SEE ALSO
sh(1) bash(1)

View File

@@ -1,263 +1,162 @@
{-
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/shellcheck
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
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 Affero General Public License for more details.
GNU General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
import Control.Exception
import Control.Monad
import Control.Monad.Trans
import Control.Monad.Trans.Error
import Control.Monad.Trans.List
import Data.Char
import Data.Maybe
import Data.Monoid
import GHC.Exts
import GHC.IO.Device
import Prelude hiding (catch)
import ShellCheck.Data
import ShellCheck.Options
import ShellCheck.Simple
import ShellCheck.Analytics
import System.Console.GetOpt
import System.Directory
import System.Environment
import System.Exit
import System.IO
import Text.JSON
import qualified Data.Map as Map
import qualified ShellCheck.Analyzer
import ShellCheck.Checker
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Regex
import qualified ShellCheck.Formatter.CheckStyle
import ShellCheck.Formatter.Format
import qualified ShellCheck.Formatter.Diff
import qualified ShellCheck.Formatter.GCC
import qualified ShellCheck.Formatter.JSON
import qualified ShellCheck.Formatter.JSON1
import qualified ShellCheck.Formatter.TTY
import qualified ShellCheck.Formatter.Quiet
import Control.Exception
import Control.Monad
import Control.Monad.Except
import Data.Bits
import Data.Char
import Data.Either
import Data.Functor
import Data.IORef
import Data.List
import qualified Data.Map as Map
import Data.Maybe
import Data.Monoid
import Data.Semigroup (Semigroup (..))
import Prelude hiding (catch)
import System.Console.GetOpt
import System.Directory
import System.Environment
import System.Exit
import System.FilePath
import System.IO
data Flag = Flag String String
data Status = NoProblems | SomeProblems | BadInput | SupportFailure | SyntaxFailure | RuntimeException deriving (Ord, Eq)
data Status =
NoProblems
| SomeProblems
| SupportFailure
| SyntaxFailure
| RuntimeException
deriving (Ord, Eq, Show)
data JsonComment = JsonComment FilePath ShellCheckComment
instance Error Status where
noMsg = RuntimeException
instance Semigroup Status where
(<>) = max
instance Monoid Status where
mempty = NoProblems
mappend = max
mappend = (Data.Semigroup.<>)
header = "Usage: shellcheck [OPTIONS...] FILES..."
data Options = Options {
checkSpec :: CheckSpec,
externalSources :: Bool,
sourcePaths :: [FilePath],
formatterOptions :: FormatterOptions,
minSeverity :: Severity
}
defaultOptions = Options {
checkSpec = emptyCheckSpec,
externalSources = False,
sourcePaths = [],
formatterOptions = newFormatterOptions {
foColorOption = ColorAuto
},
minSeverity = StyleC
}
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
options = [
Option "a" ["check-sourced"]
(NoArg $ Flag "sourced" "false") "Include warnings from sourced files",
Option "C" ["color"]
(OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN")
"Use color (auto, always, never)",
Option "i" ["include"]
(ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings",
Option "e" ["exclude"]
(ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings",
(ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings",
Option "f" ["format"]
(ReqArg (Flag "format") "FORMAT") "output format",
(ReqArg (Flag "format") "FORMAT") $
"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"]
(ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (bash,sh,ksh)",
(ReqArg (Flag "shell") "SHELLNAME")
"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"]
(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"]
(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 = hPutStrLn stderr
printErr = lift . hPutStrLn stderr
instance JSON (JsonComment) where
showJSON (JsonComment filename c) = makeObj [
("file", showJSON $ filename),
("line", showJSON $ scLine c),
("column", showJSON $ scColumn c),
("level", showJSON $ scSeverity c),
("code", showJSON $ scCode c),
("message", showJSON $ scMessage c)
]
readJSON = undefined
parseArguments :: [String] -> ErrorT Status IO ([Flag], [FilePath])
parseArguments :: [String] -> ExceptT Status IO ([Flag], [FilePath])
parseArguments argv =
case getOpt Permute options argv of
(opts, files, []) -> return (opts, files)
(_, _, errors) -> do
liftIO . printErr $ concat errors ++ "\n" ++ usageInfo header options
printErr $ concat errors ++ "\n" ++ getUsageInfo
throwError SyntaxFailure
formats :: Map.Map String (AnalysisOptions -> [FilePath] -> IO Status)
formats = Map.fromList [
("json", forJson),
("gcc", forGcc),
("checkstyle", forCheckstyle),
("tty", forTty)
formats :: FormatterOptions -> Map.Map String (IO Formatter)
formats options = Map.fromList [
("checkstyle", ShellCheck.Formatter.CheckStyle.format),
("diff", ShellCheck.Formatter.Diff.format options),
("gcc", ShellCheck.Formatter.GCC.format),
("json", ShellCheck.Formatter.JSON.format),
("json1", ShellCheck.Formatter.JSON1.format),
("tty", ShellCheck.Formatter.TTY.format options),
("quiet", ShellCheck.Formatter.Quiet.format options)
]
toStatus = liftM (either id (const NoProblems)) . runErrorT
catchExceptions :: IO Status -> IO Status
catchExceptions action = action -- action `catch` handler
formatList = intercalate ", " names
where
handler err = do
printErr $ show (err :: SomeException)
return RuntimeException
names = Map.keys $ formats (formatterOptions defaultOptions)
checkComments comments = if null comments then NoProblems else SomeProblems
forTty :: AnalysisOptions -> [FilePath] -> IO Status
forTty options files = do
output <- mapM doFile files
return $ mconcat output
where
clear = ansi 0
ansi n = "\x1B[" ++ show n ++ "m"
colorForLevel "error" = 31 -- red
colorForLevel "warning" = 33 -- yellow
colorForLevel "info" = 32 -- green
colorForLevel "style" = 32 -- green
colorForLevel "message" = 1 -- bold
colorForLevel "source" = 0 -- none
colorForLevel _ = 0 -- none
colorComment level comment =
ansi (colorForLevel level) ++ comment ++ clear
doFile path = catchExceptions $ do
contents <- readContents path
doInput path contents
doInput filename contents = do
let fileLines = lines contents
let lineCount = length fileLines
let comments = getComments options contents
let groups = groupWith scLine comments
colorFunc <- getColorFunc
mapM_ (\x -> do
let lineNum = scLine (head x)
let line = if lineNum < 1 || lineNum > lineCount
then ""
else fileLines !! (lineNum - 1)
putStrLn ""
putStrLn $ colorFunc "message"
("In " ++ filename ++" line " ++ show lineNum ++ ":")
putStrLn (colorFunc "source" line)
mapM_ (\c -> putStrLn (colorFunc (scSeverity c) $ cuteIndent c)) x
putStrLn ""
) groups
return . checkComments $ comments
cuteIndent comment =
replicate (scColumn comment - 1) ' ' ++
"^-- " ++ code (scCode comment) ++ ": " ++ scMessage comment
code code = "SC" ++ show code
getColorFunc = do
term <- hIsTerminalDevice stdout
return $ if term then colorComment else const id
forJson :: AnalysisOptions -> [FilePath] -> IO Status
forJson options files = catchExceptions $ do
comments <- runListT $ do
file <- ListT $ return files
comment <- ListT $ commentsFor options file
return $ JsonComment file comment
putStrLn $ encodeStrict comments
return $ checkComments comments
-- Mimic GCC "file:line:col: (error|warning|note): message" format
forGcc :: AnalysisOptions -> [FilePath] -> IO Status
forGcc options files = do
files <- mapM process files
return $ mconcat files
where
process file = catchExceptions $ do
contents <- readContents file
let comments = makeNonVirtual (getComments options contents) contents
mapM_ (putStrLn . format file) comments
return $ checkComments comments
format filename c = concat [
filename, ":",
show $ scLine c, ":",
show $ scColumn c, ": ",
case scSeverity c of
"error" -> "error"
"warning" -> "warning"
_ -> "note",
": ",
concat . lines $ scMessage c,
" [SC", show $ scCode c, "]"
]
-- Checkstyle compatible output. A bit of a hack to avoid XML dependencies
forCheckstyle :: AnalysisOptions -> [FilePath] -> IO Status
forCheckstyle options files = do
putStrLn "<?xml version='1.0' encoding='UTF-8'?>"
putStrLn "<checkstyle version='4.3'>"
statuses <- mapM process files
putStrLn "</checkstyle>"
return $ mconcat statuses
where
process file = catchExceptions $ do
comments <- commentsFor options file
putStrLn (formatFile file comments)
return $ checkComments comments
severity "error" = "error"
severity "warning" = "warning"
severity _ = "info"
attr s v = concat [ s, "='", escape v, "' " ]
escape = concatMap escape'
escape' c = if isOk c then [c] else "&#" ++ show (ord c) ++ ";"
isOk x = any ($x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")]
formatFile name comments = concat [
"<file ", attr "name" name, ">\n",
concatMap format comments,
"</file>"
]
format c = concat [
"<error ",
attr "line" $ show . scLine $ c,
attr "column" $ show . scColumn $ c,
attr "severity" $ severity . scSeverity $ c,
attr "message" $ scMessage c,
attr "source" $ "ShellCheck.SC" ++ show (scCode c),
"/>\n"
]
commentsFor options file = liftM (getComments options) $ readContents file
getComments = shellCheck
readContents :: FilePath -> IO String
readContents file =
if file == "-"
then getContents
else readFile file
-- Realign comments from a tabstop of 8 to 1
makeNonVirtual comments contents =
map fix comments
where
ls = lines contents
fix c = c {
scColumn =
if scLine c > 0 && scLine c <= length ls
then real (ls !! (scLine c - 1)) 0 0 (scColumn c)
else scColumn c
}
real _ r v target | target <= v = r
real [] r v _ = r -- should never happen
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
getOption [] _ = Nothing
getOption [] _ = Nothing
getOption (Flag var val:_) name | name == var = return val
getOption (_:rest) flag = getOption rest flag
getOption (_:rest) flag = getOption rest flag
getOptions options name =
map (\(Flag _ val) -> val) . filter (\(Flag var _) -> var == name) $ options
@@ -271,19 +170,19 @@ split char str =
else split' rest (a: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
excludeCodes codes =
filter (not . hasCode)
getEnvArgs = do
opts <- getEnv "SHELLCHECK_OPTS" `catch` cantWaitForLookupEnv
return . filter (not . null) $ opts `splitOn` mkRegex " +"
where
hasCode c = scCode c `elem` codes
cantWaitForLookupEnv :: IOException -> IO String
cantWaitForLookupEnv = const $ return ""
main = do
args <- getArgs
params <- getArgs
envOpts <- getEnvArgs
let args = envOpts ++ params
status <- toStatus $ do
(flags, files) <- parseArguments args
process flags files
@@ -291,64 +190,391 @@ main = do
statusToCode status =
case status of
NoProblems -> ExitSuccess
SomeProblems -> ExitFailure 1
BadInput -> ExitFailure 5
SyntaxFailure -> ExitFailure 3
SupportFailure -> ExitFailure 4
NoProblems -> ExitSuccess
SomeProblems -> ExitFailure 1
SyntaxFailure -> ExitFailure 3
SupportFailure -> ExitFailure 4
RuntimeException -> ExitFailure 2
process :: [Flag] -> [FilePath] -> ErrorT Status IO ()
process :: [Flag] -> [FilePath] -> ExceptT Status IO Status
process flags files = do
options <- foldM (flip parseOption) defaultAnalysisOptions flags
options <- foldM (flip parseOption) defaultOptions flags
verifyFiles files
let format = fromMaybe "tty" $ getOption flags "format"
case Map.lookup format formats of
Nothing -> do
liftIO $ do
let formatters = formats $ formatterOptions options
formatter <-
case Map.lookup format formatters of
Nothing -> do
printErr $ "Unknown format " ++ format
printErr "Supported formats:"
mapM_ (printErr . write) $ Map.keys formats
mapM_ (printErr . write) $ Map.keys formatters
throwError SupportFailure
where write s = " " ++ s
Just f -> ExceptT $ fmap Right f
sys <- lift $ ioInterface options files
lift $ runFormatter sys formatter options files
runFormatter :: SystemInterface IO -> Formatter -> Options -> [FilePath]
-> IO Status
runFormatter sys format options files = do
header format
result <- foldM f NoProblems files
footer format
return result
where
f :: Status -> FilePath -> IO Status
f status file = do
newStatus <- process file `catch` handler file
return $ status `mappend` newStatus
handler :: FilePath -> IOException -> IO Status
handler file e = reportFailure file (show e)
reportFailure file str = do
onFailure format file str
return RuntimeException
process :: FilePath -> IO Status
process filename = do
input <- siReadFile sys filename
either (reportFailure filename) check input
where
check contents = do
let checkspec = (checkSpec options) {
csFilename = filename,
csScript = contents
}
result <- checkScript sys checkspec
onResult format result sys
return $
if null (crComments result)
then NoProblems
else SomeProblems
parseEnum name value list =
case filter ((== value) . fst) list of
[(name, value)] -> return value
[] -> do
printErr $ "Unknown value for --" ++ name ++ ". " ++
"Valid options are: " ++ (intercalate ", " $ map fst list)
throwError SupportFailure
where write s = " " ++ s
Just f -> ErrorT $ liftM Left $ f options files
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 =
case flag of
Flag "shell" str ->
fromMaybe (die $ "Unknown shell: " ++ str) $ do
shell <- shellForExecutable str
return $ return options { optionShellType = Just shell }
fromMaybe (die $ "Unknown shell: " ++ str) $ do
shell <- shellForExecutable str
return $ return options {
checkSpec = (checkSpec options) {
csShellTypeOverride = Just shell
}
}
Flag "exclude" str -> do
new <- mapM parseNum $ split ',' str
let old = optionExcludes options
return options { optionExcludes = new ++ old }
new <- mapM parseNum $ filter (not . null) $ split ',' str
let old = csExcludedWarnings . checkSpec $ options
return options {
checkSpec = (checkSpec options) {
csExcludedWarnings = new ++ old
}
}
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
liftIO printVersion
throwError NoProblems
_ -> return options
Flag "list-optional" _ -> do
liftIO printOptional
throwError NoProblems
Flag "help" _ -> do
liftIO $ putStrLn getUsageInfo
throwError NoProblems
Flag "externals" _ ->
return options {
externalSources = True
}
Flag "color" color -> do
option <- parseColorOption color
return options {
formatterOptions = (formatterOptions options) {
foColorOption = option
}
}
Flag "source-path" str -> do
let paths = splitSearchPath str
return options {
sourcePaths = (sourcePaths options) ++ paths
}
Flag "sourced" _ ->
return options {
checkSpec = (checkSpec options) {
csCheckSourced = True
}
}
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
die s = do
liftIO $ printErr s
printErr s
throwError SupportFailure
parseNum ('S':'C':str) = parseNum str
parseNum num = do
unless (all isDigit num) $ do
liftIO . printErr $ "Bad exclusion: " ++ num
printErr $ "Invalid number: " ++ num
throwError SyntaxFailure
return (Prelude.read num :: Integer)
ioInterface options files = do
inputs <- mapM normalize files
cache <- newIORef emptyCache
configCache <- newIORef ("", Nothing)
return SystemInterface {
siReadFile = get cache inputs,
siFindSource = findSourceFile inputs (sourcePaths options),
siGetConfig = getConfig configCache
}
where
emptyCache :: Map.Map FilePath String
emptyCache = Map.empty
get cache inputs file = do
map <- readIORef cache
case Map.lookup file map of
Just x -> return $ Right x
Nothing -> fetch cache inputs file
fetch cache inputs file = do
ok <- allowable inputs file
if ok
then (do
(contents, shouldCache) <- inputFile file
when shouldCache $
modifyIORef cache $ Map.insert file contents
return $ Right contents
) `catch` handler
else return $ Left (file ++ " was not specified as input (see shellcheck -x).")
where
handler :: IOException -> IO (Either ErrorMessage String)
handler ex = return . Left $ show ex
allowable inputs x =
if externalSources options
then return True
else do
path <- normalize x
return $ path `elem` inputs
normalize x =
canonicalizePath x `catch` fallback x
where
fallback :: FilePath -> IOException -> IO FilePath
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
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 <- filterM ((allowable inputs) `andM` doesFileExist) $
(adjustPath filename):(map (</> filename) $ map adjustPath $ sourcePathFlag ++ sourcePathAnnotation)
case sources of
[] -> return deflt
(first:_) -> return first
scriptdir = dropFileName currentScript
adjustPath str =
case (splitDirectories str) of
("SCRIPTDIR":rest) -> joinPath (scriptdir:rest)
_ -> str
inputFile file = do
(handle, shouldCache) <-
if file == "-"
then return (stdin, True)
else do
h <- openBinaryFile file ReadMode
reopenable <- hIsSeekable h
return (h, not reopenable)
hSetBinaryMode handle True
contents <- decodeString <$> hGetContents handle -- closes handle
seq (length contents) $
return (contents, shouldCache)
-- Decode a char8 string into a utf8 string, with fallback on
-- ISO-8859-1. This avoids depending on additional libraries.
decodeString = decode
where
decode [] = []
decode (c:rest) | isAscii c = c : decode rest
decode (c:rest) =
let num = (fromIntegral $ ord c) :: Int
next = case num of
_ | num >= 0xF8 -> Nothing
| num >= 0xF0 -> construct (num .&. 0x07) 3 rest
| num >= 0xE0 -> construct (num .&. 0x0F) 2 rest
| num >= 0xC0 -> construct (num .&. 0x1F) 1 rest
| True -> Nothing
in
case next of
Just (n, remainder) -> chr n : decode remainder
Nothing -> c : decode rest
construct x 0 rest = do
guard $ x <= 0x10FFFF
return (x, rest)
construct x n (c:rest) =
let num = (fromIntegral $ ord c) :: Int in
if num >= 0x80 && num <= 0xBF
then construct ((x `shiftL` 6) .|. (num .&. 0x3f)) (n-1) rest
else Nothing
construct _ _ _ = Nothing
verifyFiles files =
when (null files) $ do
liftIO $ printErr "No files specified.\n"
liftIO $ printErr $ usageInfo header options
printErr "No files specified.\n"
printErr $ usageInfo usageHeader options
throwError SyntaxFailure
printVersion = do
putStrLn "ShellCheck - shell script analysis tool"
putStrLn $ "version: " ++ shellcheckVersion
putStrLn "license: GNU Affero General Public License, version 3"
putStrLn "website: http://www.shellcheck.net"
putStrLn "license: GNU General Public License, version 3"
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 ""

54
snap/snapcraft.yaml Normal file
View File

@@ -0,0 +1,54 @@
name: shellcheck
summary: A shell script static analysis tool
description: |
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh
shell scripts.
The goals of ShellCheck are
- To point out and clarify typical beginner's syntax issues that cause a
shell to give cryptic error messages.
- To point out and clarify typical intermediate level semantic problems that
cause a shell to behave strangely and counter-intuitively.
- To point out subtle caveats, corner cases and pitfalls that may cause an
advanced user's otherwise working script to fail under future
circumstances.
By default ShellCheck can only check non-hidden files under /home, to make
ShellCheck be able to check files under /media and /run/media you must
connect it to the `removable-media` interface manually:
# snap connect shellcheck:removable-media
version: git
base: core18
grade: stable
confinement: strict
apps:
shellcheck:
command: usr/bin/shellcheck
plugs: [home, removable-media]
parts:
shellcheck:
plugin: dump
source: .
build-packages:
- cabal-install
- 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 update || cat /var/log/squid/*
cabal install -j
install -d $SNAPCRAFT_PART_INSTALL/usr/bin
install .cabal-sandbox/bin/shellcheck $SNAPCRAFT_PART_INSTALL/usr/bin

View File

@@ -1,59 +1,70 @@
{-
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/shellcheck
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
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 Affero General Public License for more details.
GNU General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
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 DeriveGeneric, DeriveAnyClass #-}
module ShellCheck.AST where
import Control.Monad
import GHC.Generics (Generic)
import Control.Monad.Identity
import qualified Text.Regex as Re
import Control.DeepSeq
import Text.Parsec
import qualified ShellCheck.Regex as Re
import Prelude hiding (id)
data 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 Dashed = Dashed | Undashed deriving (Show, Eq)
data AssignmentMode = Assign | Append deriving (Show, Eq)
data FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
newtype FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
newtype FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
newtype Root = Root Token
data Token =
TA_Binary Id String Token Token
| TA_Assignment Id String Token Token
| TA_Variable Id String [Token]
| TA_Expansion Id [Token]
| TA_Index Id Token
| TA_Sequence Id [Token]
| TA_Trinary Id Token Token Token
| TA_Unary Id String Token
| TC_And Id ConditionType String Token Token
| TC_Binary Id ConditionType String Token Token
| TC_Group Id ConditionType Token
| TC_Noary Id ConditionType Token
| TC_Nullary Id ConditionType Token
| TC_Or Id ConditionType String Token Token
| TC_Unary Id ConditionType String Token
| TC_Empty Id ConditionType
| T_AND_IF Id
| T_AndIf Id (Token) (Token)
| T_AndIf Id Token Token
| T_Arithmetic Id Token
| T_Array Id [Token]
| T_IndexedElement Id Token Token
| T_Assignment Id AssignmentMode String (Maybe Token) Token
| T_IndexedElement Id [Token] Token
-- Store the index as string, and parse as arithmetic or string later
| T_UnparsedIndex Id SourcePos String
| T_Assignment Id AssignmentMode String [Token] Token
| T_Backgrounded Id Token
| T_Backticked Id [Token]
| T_Bang Id
| T_Banged Id Token
| T_BraceExpansion Id String
| T_BraceExpansion Id [Token]
| T_BraceGroup Id [Token]
| T_CLOBBER Id
| T_Case Id
@@ -65,11 +76,12 @@ data Token =
| T_DSEMI Id
| T_Do Id
| T_DollarArithmetic Id Token
| T_DollarBraced Id Token
| T_DollarBraced Id Bool Token
| T_DollarBracket Id Token
| T_DollarDoubleQuoted Id [Token]
| T_DollarExpansion Id [Token]
| T_DollarSingleQuoted Id String
| T_DollarBraceCommandExpansion Id [Token]
| T_Done Id
| T_DoubleQuoted Id [Token]
| T_EOF Id
@@ -92,6 +104,7 @@ data Token =
| T_IfExpression Id [([Token],[Token])] [Token]
| T_In Id
| T_IoFile Id Token Token
| T_IoDuplicate Id Token String
| T_LESSAND Id
| T_LESSGREAT Id
| T_Lbrace Id
@@ -101,13 +114,14 @@ data Token =
| T_NEWLINE Id
| T_NormalWord Id [Token]
| T_OR_IF Id
| T_OrIf Id (Token) (Token)
| T_OrIf Id Token Token
| T_ParamSubSpecialChar Id String -- e.g. '%' in ${foo%bar} or '/' in ${foo/bar/baz}
| T_Pipeline Id [Token] [Token] -- [Pipe separators] [Commands]
| T_ProcSub Id String [Token]
| T_Rbrace Id
| T_Redirecting Id [Token] Token
| T_Rparen Id
| T_Script Id String [Token]
| T_Script Id Token [Token] -- Shebang T_Literal, followed by script.
| T_Select Id
| T_SelectIn Id String [Token] [Token]
| T_Semi Id
@@ -123,18 +137,29 @@ data Token =
| T_Pipe Id String
| T_CoProc Id (Maybe String) Token
| T_CoProcBody Id Token
| T_Include Id Token
| T_SourceCommand Id Token Token
| T_BatsTest Id Token Token
deriving (Show)
data Annotation = DisableComment Integer deriving (Show, Eq)
data Annotation =
DisableComment Integer
| EnableComment String
| SourceOverride String
| ShellOverride String
| SourcePath String
deriving (Show, Eq)
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
-- I apologize for nothing!
lolHax s = Re.subRegex (Re.mkRegex "(Id [0-9]+)") (show s) "(Id 0)"
-- This is an abomination.
tokenEquals :: Token -> Token -> Bool
tokenEquals a b = kludge a == kludge b
where kludge s = Re.subRegex (Re.mkRegex "\\(Id [0-9]+\\)") (show s) "(Id 0)"
instance Eq Token where
(==) a b = lolHax a == lolHax b
(==) = tokenEquals
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> Token) -> Token -> m Token
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> m Token) -> Token -> m Token
analyze f g i =
round
where
@@ -142,21 +167,16 @@ analyze f g i =
f t
newT <- delve t
g t
return . i $ newT
i newT
roundAll = mapM round
roundMaybe Nothing = return Nothing
roundMaybe (Just v) = do
s <- round v
return (Just s)
dl l v = do
x <- roundAll l
return $ v x
dll l m v = do
x <- roundAll l
y <- roundAll m
return $ v x m
return $ v x y
d1 t v = do
x <- round t
return $ v x
@@ -169,18 +189,24 @@ analyze f g i =
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 index value) = do
a <- roundMaybe index
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 t1 t2) = d2 t1 t2 $ T_IndexedElement 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
@@ -227,7 +253,7 @@ analyze f g i =
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_DollarBraced id braced op) = d1 op $ T_DollarBraced id braced
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
@@ -235,9 +261,10 @@ analyze f g i =
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_Noary id typ token) = d1 token $ TC_Noary id typ
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
@@ -246,12 +273,16 @@ analyze f g i =
c <- round t3
return $ TA_Trinary id a b c
delve (TA_Expansion id t) = dl t $ TA_Expansion id
delve (TA_Index id t) = d1 t $ TA_Index 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_BatsTest id name t) = d2 name t $ T_BatsTest id
delve t = return t
getId :: Token -> Id
getId t = case t of
T_AND_IF id -> id
T_OR_IF id -> id
@@ -292,10 +323,13 @@ getId t = case t of
T_NormalWord id _ -> id
T_DoubleQuoted id _ -> id
T_DollarExpansion id _ -> id
T_DollarBraced 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
@@ -328,13 +362,13 @@ getId t = case t of
TC_Group id _ _ -> id
TC_Binary id _ _ _ _ -> id
TC_Unary id _ _ _ -> id
TC_Noary 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
TA_Index id _ -> id
T_ProcSub id _ _ -> id
T_Glob id _ -> id
T_ForArithmetic id _ _ _ _ -> id
@@ -345,17 +379,19 @@ getId t = case t of
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
T_BatsTest id _ _ -> id
blank :: Monad m => Token -> m ()
blank = const $ return ()
doAnalysis f = analyze f blank id
doStackAnalysis startToken endToken = analyze startToken endToken id
doTransform i = runIdentity . analyze blank blank i
doAnalysis :: Monad m => (Token -> m ()) -> Token -> m Token
doAnalysis f = analyze f blank return
doStackAnalysis :: Monad m => (Token -> m ()) -> (Token -> m ()) -> Token -> m Token
doStackAnalysis startToken endToken = analyze startToken endToken return
doTransform :: (Token -> Token) -> Token -> Token
doTransform i = runIdentity . analyze blank blank (return . i)
isLoop t = case t of
T_WhileExpression {} -> True
T_UntilExpression {} -> True
T_ForIn {} -> True
T_ForArithmetic {} -> True
T_SelectIn {} -> True
_ -> False

513
src/ShellCheck/ASTLib.hs Normal file
View File

@@ -0,0 +1,513 @@
{-
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.ASTLib where
import ShellCheck.AST
import Control.Monad.Writer
import Control.Monad
import Data.Char
import Data.Functor
import Data.List
import Data.Maybe
-- Is this a type of loop?
isLoop t = case t of
T_WhileExpression {} -> True
T_UntilExpression {} -> True
T_ForIn {} -> True
T_ForArithmetic {} -> True
T_SelectIn {} -> True
_ -> False
-- Will this split into multiple words when used as an argument?
willSplit x =
case x of
T_DollarBraced {} -> True
T_DollarExpansion {} -> True
T_Backticked {} -> True
T_BraceExpansion {} -> True
T_Glob {} -> True
T_Extglob {} -> True
T_NormalWord _ l -> any willSplit l
_ -> False
isGlob T_Extglob {} = True
isGlob T_Glob {} = True
isGlob (T_NormalWord _ l) = any isGlob l
isGlob _ = False
-- Is this shell word a constant?
isConstant token =
case token of
-- This ignores some cases like ~"foo":
T_NormalWord _ (T_Literal _ ('~':_) : _) -> False
T_NormalWord _ l -> all isConstant l
T_DoubleQuoted _ l -> all isConstant l
T_SingleQuoted _ _ -> True
T_Literal _ _ -> True
_ -> False
-- Is this an empty literal?
isEmpty token =
case token of
T_NormalWord _ l -> all isEmpty l
T_DoubleQuoted _ l -> all isEmpty l
T_SingleQuoted _ "" -> True
T_Literal _ "" -> True
_ -> False
-- Quick&lazy oversimplification of commands, throwing away details
-- and returning a list like ["find", ".", "-name", "${VAR}*" ].
oversimplify token =
case token of
(T_NormalWord _ l) -> [concat (concatMap oversimplify l)]
(T_DoubleQuoted _ l) -> [concat (concatMap oversimplify l)]
(T_SingleQuoted _ s) -> [s]
(T_DollarBraced _ _ _) -> ["${VAR}"]
(T_DollarArithmetic _ _) -> ["${VAR}"]
(T_DollarExpansion _ _) -> ["${VAR}"]
(T_Backticked _ _) -> ["${VAR}"]
(T_Glob _ s) -> [s]
(T_Pipeline _ _ [x]) -> oversimplify x
(T_Literal _ x) -> [x]
(T_ParamSubSpecialChar _ x) -> [x]
(T_SimpleCommand _ vars words) -> concatMap oversimplify words
(T_Redirecting _ _ foo) -> oversimplify foo
(T_DollarSingleQuoted _ s) -> [s]
(T_Annotation _ _ s) -> oversimplify s
-- Workaround for let "foo = bar" parsing
(TA_Sequence _ [TA_Expansion _ v]) -> concatMap oversimplify v
_ -> []
-- Turn a SimpleCommand foo -avz --bar=baz into args "a", "v", "z", "bar",
-- each in a tuple of (token, stringFlag). Non-flag arguments are added with
-- stringFlag == "".
getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
let tokenAndText = map (\x -> (x, concat $ oversimplify x)) args
(flagArgs, rest) = break (stopCondition . snd) tokenAndText
in
concatMap flag flagArgs ++ map (\(t, _) -> (t, "")) rest
where
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
flag (x, '-':args) = map (\v -> (x, [v])) args
flag (x, _) = [ (x, "") ]
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
-- Get all flags in a GNU way, up until --
getAllFlags :: Token -> [(Token, String)]
getAllFlags = getFlagsUntil (== "--")
-- Get all flags in a BSD way, up until first non-flag argument or --
getLeadingFlags = getFlagsUntil (\x -> x == "--" || (not $ "-" `isPrefixOf` x))
-- Check if a command has a flag.
hasFlag cmd str = str `elem` (map snd $ getAllFlags cmd)
-- Is this token a word that starts with a dash?
isFlag token =
case getWordParts token of
T_Literal _ ('-':_) : _ -> True
_ -> False
-- Is this token a flag where the - is unquoted?
isUnquotedFlag token = fromMaybe False $ do
str <- getLeadingUnquotedString token
return $ "-" `isPrefixOf` str
-- Given a T_DollarBraced, return a simplified version of the string contents.
bracedString (T_DollarBraced _ _ l) = concat $ oversimplify l
bracedString _ = error "Internal shellcheck error, please report! (bracedString on non-variable)"
-- Is this an expansion of multiple items of an array?
isArrayExpansion t@(T_DollarBraced _ _ _) =
let string = bracedString t in
"@" `isPrefixOf` string ||
not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string
isArrayExpansion _ = False
-- Is it possible that this arg becomes multiple args?
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
where
f t@(T_DollarBraced _ _ _) =
let string = bracedString t in
"!" `isPrefixOf` string
f (T_DoubleQuoted _ parts) = any f parts
f (T_NormalWord _ parts) = any f parts
f _ = False
-- Is it certain that this word will becomes multiple words?
willBecomeMultipleArgs t = willConcatInAssignment t || f t
where
f T_Extglob {} = True
f T_Glob {} = True
f T_BraceExpansion {} = True
f (T_DoubleQuoted _ parts) = any f parts
f (T_NormalWord _ parts) = any f parts
f _ = False
-- This does token cause implicit concatenation in assignments?
willConcatInAssignment token =
case token of
t@T_DollarBraced {} -> isArrayExpansion t
(T_DoubleQuoted _ parts) -> any willConcatInAssignment parts
(T_NormalWord _ parts) -> any willConcatInAssignment parts
_ -> False
-- Maybe get the literal string corresponding to this token
getLiteralString :: Token -> Maybe String
getLiteralString = getLiteralStringExt (const Nothing)
-- Definitely get a literal string, skipping over all non-literals
onlyLiteralString :: Token -> String
onlyLiteralString = fromJust . getLiteralStringExt (const $ return "")
-- Maybe get a literal string, but only if it's an unquoted argument.
getUnquotedLiteral (T_NormalWord _ list) =
concat <$> mapM str list
where
str (T_Literal _ s) = return s
str _ = Nothing
getUnquotedLiteral _ = Nothing
-- 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.
getTrailingUnquotedLiteral :: Token -> Maybe Token
getTrailingUnquotedLiteral t =
case t of
(T_NormalWord _ list@(_:_)) ->
from (last list)
_ -> Nothing
where
from t =
case t of
T_Literal {} -> return t
_ -> Nothing
-- Get the leading, unquoted, literal string of a token (if any).
getLeadingUnquotedString :: Token -> Maybe String
getLeadingUnquotedString t =
case t of
T_NormalWord _ ((T_Literal _ s) : _) -> return s
_ -> Nothing
-- Maybe get the literal string of this token and any globs in it.
getGlobOrLiteralString = getLiteralStringExt f
where
f (T_Glob _ str) = return str
f _ = Nothing
-- Maybe get the literal value of a token, using a custom function
-- to map unrecognized Tokens into strings.
getLiteralStringExt :: (Token -> Maybe String) -> Token -> Maybe String
getLiteralStringExt more = g
where
allInList = fmap concat . mapM g
g (T_DoubleQuoted _ l) = allInList l
g (T_DollarDoubleQuoted _ l) = allInList l
g (T_NormalWord _ l) = allInList l
g (TA_Expansion _ l) = allInList l
g (T_SingleQuoted _ s) = return s
g (T_Literal _ s) = return s
g (T_ParamSubSpecialChar _ s) = return s
g (T_DollarSingleQuoted _ s) = return $ decodeEscapes s
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?
isLiteral t = isJust $ getLiteralString t
-- 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_DoubleQuoted _ l) = l
-- TA_Expansion is basically T_NormalWord for arithmetic expressions
getWordParts (TA_Expansion _ l) = concatMap getWordParts l
getWordParts other = [other]
-- Return a list of NormalWords that would result from brace expansion
braceExpand (T_NormalWord id list) = take 1000 $ do
items <- mapM part list
return $ T_NormalWord id items
where
part (T_BraceExpansion id items) = do
item <- items
braceExpand item
part x = return x
-- Maybe get a SimpleCommand from immediate wrappers like T_Redirections
getCommand t =
case t of
T_Redirecting _ _ w -> getCommand w
T_SimpleCommand _ _ (w:_) -> return t
T_Annotation _ _ t -> getCommand t
_ -> Nothing
-- Maybe get the command name string of a token representing a command
getCommandName :: Token -> Maybe String
getCommandName = fst . getCommandNameAndToken
-- 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
getCommandNameAndToken :: Token -> (Maybe String, Token)
getCommandNameAndToken t = fromMaybe (Nothing, t) $ do
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
s <- getLiteralString w
if "busybox" `isSuffixOf` s || "builtin" == s
then
case rest of
(applet:_) -> return (getLiteralString applet, applet)
_ -> return (Just s, w)
else
return (Just s, w)
-- If a command substitution is a single command, get its name.
-- $(date +%s) = Just "date"
getCommandNameFromExpansion :: Token -> Maybe String
getCommandNameFromExpansion t =
case t of
T_DollarExpansion _ [c] -> extract c
T_Backticked _ [c] -> extract c
T_DollarBraceCommandExpansion _ [c] -> extract c
_ -> Nothing
where
extract (T_Pipeline _ _ [cmd]) = getCommandName cmd
extract _ = Nothing
-- Get the basename of a token representing a command
getCommandBasename = fmap basename . getCommandName
where
basename = reverse . takeWhile (/= '/') . reverse
isAssignment t =
case t of
T_Redirecting _ _ w -> isAssignment w
T_SimpleCommand _ (w:_) [] -> True
T_Assignment {} -> True
T_Annotation _ _ w -> isAssignment w
_ -> False
isOnlyRedirection t =
case t of
T_Pipeline _ _ [x] -> isOnlyRedirection x
T_Annotation _ _ w -> isOnlyRedirection w
T_Redirecting _ (_:_) c -> isOnlyRedirection c
T_SimpleCommand _ [] [] -> 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
-- Get the lists of commands from tokens that contain them, such as
-- the body of while loops or branches of if statements.
getCommandSequences :: Token -> [[Token]]
getCommandSequences t =
case t of
T_Script _ _ cmds -> [cmds]
T_BraceGroup _ cmds -> [cmds]
T_Subshell _ cmds -> [cmds]
T_WhileExpression _ _ cmds -> [cmds]
T_UntilExpression _ _ cmds -> [cmds]
T_ForIn _ _ _ cmds -> [cmds]
T_ForArithmetic _ _ _ _ cmds -> [cmds]
T_IfExpression _ thens elses -> map snd thens ++ [elses]
T_Annotation _ _ t -> getCommandSequences t
_ -> []
-- Get a list of names of associative arrays
getAssociativeArrays t =
nub . execWriter $ doAnalysis f t
where
f :: Token -> Writer [String] ()
f t@T_SimpleCommand {} = fromMaybe (return ()) $ do
name <- getCommandName t
let assocNames = ["declare","local","typeset"]
guard $ elem name assocNames
let flags = getAllFlags t
guard $ elem "A" $ map snd flags
let args = map fst . filter ((==) "" . snd) $ flags
let names = mapMaybe (getLiteralStringExt nameAssignments) args
return $ tell names
f _ = return ()
nameAssignments t =
case t of
T_Assignment _ _ name _ _ -> return name
_ -> Nothing
-- A Pseudoglob is a wildcard pattern used for checking if a match can succeed.
-- For example, [[ $(cmd).jpg == [a-z] ]] will give the patterns *.jpg and ?, which
-- can be proven never to match.
data PseudoGlob = PGAny | PGMany | PGChar Char
deriving (Eq, Show)
-- Turn a word into a PG pattern, replacing all unknown/runtime values with
-- PGMany.
wordToPseudoGlob :: Token -> Maybe [PseudoGlob]
wordToPseudoGlob word =
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
-- exact semantics.
wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob]
wordToExactPseudoGlob word =
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_Glob _ "?" -> return [PGAny]
T_Glob _ "*" -> return [PGMany]
_ -> fail "Unknown token type"
-- Reorder a PseudoGlob for more efficient matching, e.g.
-- f?*?**g -> f??*g
simplifyPseudoGlob :: [PseudoGlob] -> [PseudoGlob]
simplifyPseudoGlob = f
where
f [] = []
f (x@(PGChar _) : rest ) = x : f rest
f list =
let (anys, rest) = span (\x -> x == PGMany || x == PGAny) list in
order anys ++ f rest
order s = let (any, many) = partition (== PGAny) s in
any ++ take 1 many
-- Check whether the two patterns can ever overlap.
pseudoGlobsCanOverlap :: [PseudoGlob] -> [PseudoGlob] -> Bool
pseudoGlobsCanOverlap = matchable
where
matchable x@(xf:xs) y@(yf:ys) =
case (xf, yf) of
(PGMany, _) -> matchable x ys || matchable xs y
(_, PGMany) -> matchable x ys || matchable xs y
(PGAny, _) -> matchable xs ys
(_, PGAny) -> matchable xs ys
(_, _) -> xf == yf && matchable xs ys
matchable [] [] = True
matchable (PGMany : rest) [] = matchable rest []
matchable (_:_) [] = False
matchable [] r = matchable r []
-- Check whether the first pattern always overlaps the second.
pseudoGlobIsSuperSetof :: [PseudoGlob] -> [PseudoGlob] -> Bool
pseudoGlobIsSuperSetof = matchable
where
matchable x@(xf:xs) y@(yf:ys) =
case (xf, yf) of
(PGMany, PGMany) -> matchable x ys
(PGMany, _) -> matchable x ys || matchable xs y
(_, PGMany) -> False
(PGAny, _) -> matchable xs ys
(_, PGAny) -> False
(_, _) -> xf == yf && matchable xs ys
matchable [] [] = True
matchable (PGMany : rest) [] = matchable rest []
matchable _ _ = False
wordsCanBeEqual x y = fromMaybe True $
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
-- Is this an expansion that can be quoted,
-- e.g. $(foo) `foo` $foo (but not {foo,})?
isQuoteableExpansion t = case t of
T_DollarBraced {} -> True
_ -> isCommandSubstitution t
isCommandSubstitution t = case t of
T_DollarExpansion {} -> True
T_DollarBraceCommandExpansion {} -> True
T_Backticked {} -> True
_ -> 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 ts) = code == ts
hasNum _ = False

3445
src/ShellCheck/Analytics.hs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
{-
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.Analyzer (analyzeScript, ShellCheck.Analyzer.optionalChecks) where
import ShellCheck.Analytics
import ShellCheck.AnalyzerLib
import ShellCheck.Interface
import Data.List
import Data.Monoid
import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.Custom
import qualified ShellCheck.Checks.ShellSupport
-- TODO: Clean up the cruft this is layered on
analyzeScript :: AnalysisSpec -> AnalysisResult
analyzeScript spec = newAnalysisResult {
arComments =
filterByAnnotation spec params . nub $
runAnalytics spec
++ runChecker params (checkers params)
}
where
params = makeParameters spec
checkers params = mconcat $ map ($ params) [
ShellCheck.Checks.Commands.checker,
ShellCheck.Checks.Custom.checker,
ShellCheck.Checks.ShellSupport.checker
]
optionalChecks = mconcat $ [
ShellCheck.Analytics.optionalChecks
]

View File

@@ -0,0 +1,974 @@
{-
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/>.
-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TemplateHaskell #-}
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 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 AnalyzerM a = RWS Parameters [TokenComment] Cache a
nullCheck = const $ return ()
data Checker = Checker {
perScript :: Root -> Analysis,
perToken :: Token -> Analysis
}
runChecker :: Parameters -> Checker -> [TokenComment]
runChecker params checker = notes
where
root = rootNode params
check = perScript checker `composeAnalyzers` (\(Root x) -> void $ doAnalysis (perToken checker) x)
notes = snd $ evalRWS (check $ Root root) params Cache
instance Semigroup Checker where
(<>) x y = Checker {
perScript = perScript x `composeAnalyzers` perScript y,
perToken = perToken x `composeAnalyzers` perToken y
}
instance Monoid Checker where
mempty = Checker {
perScript = nullCheck,
perToken = nullCheck
}
mappend = (Data.Semigroup.<>)
composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis
composeAnalyzers f g x = f x >> g x
data Parameters = Parameters {
-- Whether this script has the 'lastpipe' option set/default.
hasLastpipe :: Bool,
-- Whether this script has 'set -e' anywhere.
hasSetE :: Bool,
-- A linear (bad) analysis of data flow
variableFlow :: [StackData],
-- 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
data Cache = Cache {}
data Scope = SubshellScope String | NoneScope deriving (Show, Eq)
data StackData =
StackScope Scope
| StackScopeEnd
-- (Base expression, specific position, var name, assigned values)
| Assignment (Token, Token, String, DataType)
| Reference (Token, Token, String)
deriving (Show)
data DataType = DataString DataSource | DataArray DataSource
deriving (Show)
data DataSource =
SourceFrom [Token]
| SourceExternal
| SourceDeclaration
| SourceInteger
| SourceChecked
deriving (Show)
data VariableState = Dead Token String | Alive deriving (Show)
defaultSpec pr = spec {
asShellType = Nothing,
asCheckSourced = False,
asExecutionMode = Executed,
asTokenPositions = prTokenPositions pr
} where spec = newAnalysisSpec (fromJust $ prRoot pr)
pScript s =
let
pSpec = newParseSpec {
psFilename = "script",
psScript = s
}
in runIdentity $ parseScript (mockedSystemInterface []) pSpec
-- For testing. If parsed, returns whether there are any comments
producesComments :: Checker -> String -> Maybe Bool
producesComments c s = do
let pr = pScript s
prRoot pr
let spec = defaultSpec pr
let params = makeParameters spec
return . not . null $ runChecker params c
makeComment :: Severity -> Id -> Code -> String -> TokenComment
makeComment severity id code note =
newTokenComment {
tcId = id,
tcComment = newComment {
cSeverity = severity,
cCode = code,
cMessage = note
}
}
addComment note = note `deepseq` tell [note]
warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m ()
warn id code str = addComment $ makeComment WarningC id code str
err id code str = addComment $ makeComment ErrorC id code str
info id code str = addComment $ makeComment InfoC id code str
style id code str = addComment $ makeComment StyleC id code str
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 withFix `deepseq` withFix
makeParameters spec =
let params = Parameters {
rootNode = root,
shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec,
hasSetE = containsSetE root,
hasLastpipe =
case shellType params of
Bash -> containsLastpipe root
Dash -> False
Sh -> False
Ksh -> True,
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
parentMap = getParentTree root,
variableFlow = getVariableFlow params root,
tokenPositions = asTokenPositions spec
} in params
where root = asScript spec
-- Does this script mention 'set -e' anywhere?
-- Used as a hack to disable certain warnings.
containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
where
isSetE t =
case t of
T_Script _ (T_Literal _ str) _ -> str `matches` re
T_SimpleCommand {} ->
t `isUnqualifiedCommand` "set" &&
("errexit" `elem` oversimplify t ||
"e" `elem` map snd (getAllFlags t))
_ -> False
re = mkRegex "[[:space:]]-[^-]*e"
-- Does this script mention 'shopt -s lastpipe' anywhere?
-- Also used as a hack.
containsLastpipe root =
isNothing $ doAnalysis (guard . not . isShoptLastPipe) root
where
isShoptLastPipe t =
case t of
T_SimpleCommand {} ->
t `isUnqualifiedCommand` "shopt" &&
("lastpipe" `elem` oversimplify t)
_ -> False
prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh
prop_determineShell1 = determineShellTest "#!/usr/bin/env ksh" == Ksh
prop_determineShell2 = determineShellTest "" == Bash
prop_determineShell3 = determineShellTest "#!/bin/sh -e" == Sh
prop_determineShell4 = determineShellTest "#!/bin/ksh\n#shellcheck shell=sh\nfoo" == Sh
prop_determineShell5 = determineShellTest "#shellcheck shell=sh\nfoo" == Sh
prop_determineShell6 = determineShellTest "#! /bin/sh" == Sh
prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh
determineShellTest = determineShellTest' Nothing
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
determineShell fallbackShell t = fromMaybe Bash $ do
shellString <- foldl mplus Nothing $ getCandidates t
shellForExecutable shellString `mplus` fallbackShell
where
forAnnotation t =
case t of
(ShellOverride s) -> return s
_ -> fail ""
getCandidates :: Token -> [Maybe String]
getCandidates t@T_Script {} = [Just $ fromShebang t]
getCandidates (T_Annotation _ annotations s) =
map forAnnotation annotations ++
[Just $ fromShebang s]
fromShebang (T_Script _ (T_Literal _ s) _) = 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.
-- This is used to populate parentMap in Parameters
getParentTree :: Token -> Map.Map Id Token
getParentTree t =
snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
where
pre t = modify (first ((:) t))
post t = do
(x, map) <- get
case x of
_: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
getTokenMap :: Token -> Map.Map Id Token
getTokenMap t =
execState (doAnalysis f t) Map.empty
where
f t = modify (Map.insert (getId t) t)
-- Is this token in a quoting free context? (i.e. would variable expansion split)
-- True: Assignments, [[ .. ]], here docs, already in double quotes
-- False: Regular words
isStrictlyQuoteFree = isQuoteFreeNode True
-- Like above, but also allow some cases where splitting may be desired.
-- True: Like above + for loops
-- False: Like above
isQuoteFree = isQuoteFreeNode False
isQuoteFreeNode strict tree t =
(isQuoteFreeElement t == Just True) ||
head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False])
where
-- Is this node self-quoting in itself?
isQuoteFreeElement t =
case t of
T_Assignment {} -> return True
T_FdRedirect {} -> return True
_ -> Nothing
-- Are any subnodes inherently self-quoting?
isQuoteFreeContext t =
case t of
TC_Nullary _ DoubleBracket _ -> return True
TC_Unary _ DoubleBracket _ _ -> return True
TC_Binary _ DoubleBracket _ _ _ -> return True
TA_Sequence {} -> return True
T_Arithmetic {} -> return True
T_Assignment {} -> return True
T_Redirecting {} -> return False
T_DoubleQuoted _ _ -> return True
T_DollarDoubleQuoted _ _ -> return True
T_CaseExpression {} -> return True
T_HereDoc {} -> return True
T_DollarBraced {} -> return True
-- When non-strict, pragmatically assume it's desirable to split here
T_ForIn {} -> return (not strict)
T_SelectIn {} -> return (not strict)
_ -> Nothing
-- Check if a token is a parameter to a certain command by name:
-- Example: isParamTo (parentMap params) "sed" t
isParamTo :: Map.Map Id Token -> String -> Token -> Bool
isParamTo tree cmd =
go
where
go x = case Map.lookup (getId x) tree of
Nothing -> False
Just parent -> check parent
check t =
case t of
T_SingleQuoted _ _ -> go t
T_DoubleQuoted _ _ -> go t
T_NormalWord _ _ -> go t
T_SimpleCommand {} -> isCommand t cmd
T_Redirecting {} -> isCommand t cmd
_ -> False
-- Get the parent command (T_Redirecting) of a Token, if any.
getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token
getClosestCommand tree t =
findFirst findCommand $ getPath tree t
where
findCommand t =
case t of
T_Redirecting {} -> return True
T_Script {} -> return False
_ -> Nothing
-- Like above, if koala_man knew Haskell when starting this project.
getClosestCommandM t = do
tree <- asks parentMap
return $ getClosestCommand tree t
-- 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)
where
go currentId (T_NormalWord id [word]:rest)
| currentId == getId word = go id rest
go currentId (T_DoubleQuoted id [word]:rest)
| currentId == getId word = go id rest
go currentId (T_SimpleCommand _ _ (word:_):_)
| currentId == getId word = True
go _ _ = False
-- A list of the element and all its parents up to the root node.
getPath tree t = t :
case Map.lookup (getId t) tree of
Nothing -> []
Just parent -> getPath tree parent
-- Version of the above taking the map from the current context
-- Todo: give this the name "getPath"
getPathM t = do
map <- asks parentMap
return $ getPath map t
isParentOf tree parent child =
elem (getId parent) . map getId $ getPath tree child
parents params = getPath (parentMap params)
-- Find the first match in a list where the predicate is Just True.
-- Stops if it's Just False and ignores Nothing.
findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a
findFirst p l =
case l of
[] -> Nothing
(x:xs) ->
case p x of
Just True -> return x
Just False -> Nothing
Nothing -> findFirst p xs
-- Check whether a word is entirely output from a single command
tokenIsJustCommandOutput t = case t of
T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds
T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ cmds]] -> check cmds
T_NormalWord id [T_Backticked _ cmds] -> check cmds
T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ cmds]] -> check cmds
_ -> False
where
check [x] = not $ isOnlyRedirection x
check _ = False
-- TODO: Replace this with a proper Control Flow Graph
getVariableFlow params t =
let (_, stack) = runState (doStackAnalysis startScope endScope t) []
in reverse stack
where
startScope t =
let scopeType = leadType params t
in do
when (scopeType /= NoneScope) $ modify (StackScope scopeType:)
when (assignFirst t) $ setWritten t
endScope t =
let scopeType = leadType params t
in do
setRead t
unless (assignFirst t) $ setWritten t
when (scopeType /= NoneScope) $ modify (StackScopeEnd:)
assignFirst T_ForIn {} = True
assignFirst T_SelectIn {} = True
assignFirst (T_BatsTest {}) = True
assignFirst _ = False
setRead t =
let read = getReferencedVariables (parentMap params) t
in mapM_ (\v -> modify (Reference v:)) read
setWritten t =
let written = getModifiedVariables t
in mapM_ (\v -> modify (Assignment v:)) written
leadType params t =
case t of
T_DollarExpansion _ _ -> SubshellScope "$(..) expansion"
T_Backticked _ _ -> SubshellScope "`..` expansion"
T_Backgrounded _ _ -> SubshellScope "backgrounding &"
T_Subshell _ _ -> SubshellScope "(..) group"
T_BatsTest {} -> SubshellScope "@bats test"
T_CoProcBody _ _ -> SubshellScope "coproc"
T_Redirecting {} ->
if fromMaybe False causesSubshell
then SubshellScope "pipeline"
else NoneScope
_ -> NoneScope
where
parentPipeline = do
parent <- Map.lookup (getId t) (parentMap params)
case parent of
T_Pipeline {} -> return parent
_ -> Nothing
causesSubshell = do
(T_Pipeline _ _ list) <- parentPipeline
if length list <= 1
then return False
else if not $ hasLastpipe params
then return True
else return . not $ (getId . head $ reverse list) == getId t
getModifiedVariables t =
case t of
T_SimpleCommand _ vars [] ->
concatMap (\x -> case x of
T_Assignment id _ name _ w ->
[(x, x, name, dataTypeFrom DataString w)]
_ -> []
) vars
c@T_SimpleCommand {} ->
getModifiedVariableCommand c
TA_Unary _ "++|" v@(TA_Variable _ name _) ->
[(t, v, name, DataString $ SourceFrom [v])]
TA_Unary _ "|++" v@(TA_Variable _ name _) ->
[(t, v, name, DataString $ SourceFrom [v])]
TA_Assignment _ op (TA_Variable _ name _) rhs -> maybeToList $ do
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
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".
-- This is to prevent [ -v foo ] being unassigned or unused.
TC_Unary id _ "-v" token -> maybeToList $ do
str <- fmap (takeWhile (/= '[')) $ -- Quoted index
flip getLiteralStringExt token $ \x ->
case x of
T_Glob _ s -> return s -- Unquoted index
_ -> Nothing
guard . not . null $ str
return (t, token, str, DataString SourceChecked)
T_DollarBraced _ _ l -> maybeToList $ do
let string = bracedString t
let modifier = getBracedModifier string
guard $ ":=" `isPrefixOf` modifier
return (t, t, getBracedReference string, DataString $ SourceFrom [l])
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
t@(T_CoProc _ name _) ->
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
--Points to 'for' rather than variable
T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)]
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
_ -> []
isClosingFileOp op =
case op of
T_IoDuplicate _ (T_GREATAND _) "-" -> True
T_IoDuplicate _ (T_LESSAND _) "-" -> True
_ -> False
-- Consider 'export/declare -x' a reference, since it makes the var available
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
case x of
"export" -> if "f" `elem` flags
then []
else concatMap getReference rest
"declare" -> if
any (`elem` flags) ["x", "p"] &&
(not $ any (`elem` flags) ["f", "F"])
then concatMap getReference rest
else []
"trap" ->
case rest of
head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head
_ -> []
_ -> []
where
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
getReference _ = []
flags = map snd $ getAllFlags base
getReferencedVariableCommand _ = []
-- 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 _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
case x of
"read" ->
let params = map getLiteral rest
readArrayVars = getReadArrayVariables rest
in
catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params
"getopts" ->
case rest of
opts:var:_ -> maybeToList $ getLiteral var
_ -> []
"let" -> concatMap letParamToLiteral rest
"export" ->
if "f" `elem` flags then [] else concatMap getModifierParamString rest
"declare" -> if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
"typeset" -> declaredVars
"local" -> concatMap getModifierParamString rest
"readonly" ->
if any (`elem` flags) ["f", "p"]
then []
else concatMap getModifierParamString rest
"set" -> maybeToList $ do
params <- getSetParams rest
return (base, base, "@", DataString $ SourceFrom params)
"printf" -> maybeToList $ getPrintfVariable rest
"mapfile" -> 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
flags = map snd $ getAllFlags base
stripEquals s = let rest = dropWhile (/= '=') s in
if rest == "" then "" else tail rest
stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) =
T_NormalWord id1 (T_Literal id2 (stripEquals s):rs)
stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) =
T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
stripEqualsFrom t = t
declaredVars = concatMap (getModifierParam defaultType) rest
where
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
getLiteralOfDataType t d = do
s <- getLiteralString t
when ("-" `isPrefixOf` s) $ fail "argument"
return (base, t, s, d)
getLiteral t = getLiteralOfDataType t (DataString SourceExternal)
getLiteralArray t = getLiteralOfDataType t (DataArray SourceExternal)
getModifierParamString = getModifierParam DataString
getModifierParam def t@(T_Assignment _ _ name _ value) =
[(base, t, name, dataTypeFrom def value)]
getModifierParam def t@T_NormalWord {} = maybeToList $ do
name <- getLiteralString t
guard $ isVariableName name
return (base, t, name, def SourceDeclaration)
getModifierParam _ _ = []
letParamToLiteral token =
if var == ""
then []
else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])]
where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token
getSetParams (t:_:rest) | getLiteralString t == Just "-o" = getSetParams rest
getSetParams (t:rest) =
let s = getLiteralString t in
case s of
Just "--" -> return rest
Just ('-':_) -> getSetParams rest
_ -> return (t:fromMaybe [] (getSetParams rest))
getSetParams [] = Nothing
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
where
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 [] = fail "not found"
-- 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
-- just get the last one, if it's a variable name.
getMapfileArray base arguments = do
lastArg <- listToMaybe (reverse arguments)
name <- getLiteralString lastArg
guard $ isVariableName name
return (base, lastArg, name, DataArray SourceExternal)
-- get all the array variables used in read, e.g. read -a arr
getReadArrayVariables args =
map (getLiteralArray . snd)
(filter (isArrayFlag . fst) (zip args (tail args)))
isArrayFlag x = fromMaybe False $ do
str <- getLiteralString x
return $ case str of
'-':'-':_ -> False
'-':str -> 'a' `elem` str
_ -> False
-- 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 _ = []
getIndexReferences s = fromMaybe [] $ do
match <- matchRegex re s
index <- match !!! 0
return $ matchAllStrings variableNameRegex index
where
re = mkRegex "(\\[.*\\])"
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
getOffsetReferences mods = fromMaybe [] $ do
-- if mods start with [, then drop until ]
match <- matchRegex re mods
offsets <- match !!! 1
return $ matchAllStrings variableNameRegex offsets
where
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
getReferencedVariables parents t =
case t of
T_DollarBraced id _ l -> let str = bracedString t in
(t, t, getBracedReference str) :
map (\x -> (l, l, x)) (
getIndexReferences str
++ getOffsetReferences (getBracedModifier str))
TA_Variable id name _ ->
if isArithmeticAssignment t
then []
else [(t, t, name)]
T_Assignment id mode str _ word ->
[(t, t, str) | mode == Append] ++ specialReferences str t word
TC_Unary id _ "-v" token -> getIfReference t token
TC_Unary id _ "-R" token -> getIfReference t token
TC_Binary id DoubleBracket op lhs rhs ->
if isDereferencing op
then concatMap (getIfReference t) [lhs, rhs]
else []
T_BatsTest {} -> [ -- pretend @test references vars to avoid warnings
(t, t, "lines"),
(t, t, "status"),
(t, t, "output")
]
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
x -> getReferencedVariableCommand x
where
-- Try to reduce false positives for unused vars only referenced from evaluated vars
specialReferences name base word =
if name `elem` [
"PS1", "PS2", "PS3", "PS4",
"PROMPT_COMMAND"
]
then
map (\x -> (base, base, x)) $
getVariablesFromLiteralToken word
else []
literalizer t = case t of
T_Glob _ s -> return s -- Also when parsed as globs
_ -> Nothing
getIfReference context token = maybeToList $ do
str <- getLiteralStringExt literalizer token
guard . not $ null str
when (isDigit $ head str) $ fail "is a number"
return (context, token, getBracedReference str)
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
isArithmeticAssignment t = case getPath parents t of
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
_ -> False
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
--- Command specific checks
-- Compare a command to a string: t `isCommand` "sed" (also matches /usr/bin/sed)
isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `isSuffixOf` cmd)
-- Compare a command to a literal. Like above, but checks full path.
isUnqualifiedCommand token str = isCommandMatch token (== str)
isCommandMatch token matcher = fromMaybe False $
fmap matcher (getCommandName token)
-- Does this regex look like it was intended as a glob?
-- True: *foo*
-- False: .*foo.*
isConfusedGlobRegex :: String -> Bool
isConfusedGlobRegex ('*':_) = True
isConfusedGlobRegex [x,'*'] | x `notElem` "\\." = True
isConfusedGlobRegex _ = False
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
isVariableChar x = isVariableStartChar x || isDigit x
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
prop_isVariableName1 = isVariableName "_fo123"
prop_isVariableName2 = not $ isVariableName "4"
prop_isVariableName3 = not $ isVariableName "test: "
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
isVariableName _ = False
getVariablesFromLiteralToken token =
getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token)
-- Try to get referenced variables from a literal string like "$foo"
-- Ignores tons of cases like arithmetic evaluation and array indices.
prop_getVariablesFromLiteral1 =
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
getVariablesFromLiteral string =
map (!! 0) $ matchAllSubgroups variableRegex string
where
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
-- Get the variable name from an expansion like ${var:-foo}
prop_getBracedReference1 = getBracedReference "foo" == "foo"
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
prop_getBracedReference3 = getBracedReference "#" == "#"
prop_getBracedReference4 = getBracedReference "##" == "#"
prop_getBracedReference5 = getBracedReference "#!" == "!"
prop_getBracedReference6 = getBracedReference "!#" == "#"
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
prop_getBracedReference11= getBracedReference "!os*" == ""
prop_getBracedReference12= getBracedReference "!os?bar**" == ""
prop_getBracedReference13= getBracedReference "foo[bar]" == "foo"
getBracedReference s = fromMaybe s $
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
where
noPrefix = dropPrefix s
dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest
dropPrefix "" = ""
takeName s = do
let name = takeWhile isVariableChar s
guard . not $ null name
return name
getSpecial (c:_) =
if c `elem` "*@#?-$!" then return [c] else fail "not special"
getSpecial _ = fail "empty"
nameExpansion ('!':rest) = do -- e.g. ${!foo*bar*}
let suffix = dropWhile isVariableChar rest
guard $ suffix /= rest -- e.g. ${!@}
first <- suffix !!! 0
guard $ first `elem` "*?"
return ""
nameExpansion _ = Nothing
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
getBracedModifier s = fromMaybe "" . listToMaybe $ do
let var = getBracedReference s
a <- dropModifier s
dropPrefix var a
where
dropPrefix [] t = return t
dropPrefix (a:b) (c:d) | a == c = dropPrefix b d
dropPrefix _ _ = []
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
dropModifier x = [x]
-- Useful generic functions.
-- 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.
headOrDefault _ (a:_) = a
headOrDefault def _ = def
--- Get element n of a list, or Nothing. Like `!!` but safe.
(!!!) list i =
case drop i list of
[] -> Nothing
(r:_) -> Just r
-- Run a command if the shell is in the given list
whenShell l c = do
shell <- asks shellType
when (shell `elem` l ) c
filterByAnnotation asSpec params =
filter (not . shouldIgnore)
where
token = asScript asSpec
shouldIgnore note =
any (shouldIgnoreFor (getCode note)) $
getPath parents (T_Bang $ tcId note)
shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec
shouldIgnoreFor code t = isAnnotationIgnoringCode code t
parents = parentMap params
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?
isCountingReference (T_DollarBraced id _ token) =
case concat $ oversimplify token of
'#':_ -> True
_ -> False
isCountingReference _ = False
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
isQuotedAlternativeReference t =
case t of
T_DollarBraced _ _ _ ->
getBracedModifier (bracedString t) `matches` re
_ -> False
where
re = mkRegex "(^|\\]):?\\+"
-- getGnuOpts "erd:u:" will parse a SimpleCommand like
-- read -re -d : -u 3 bar
-- into
-- 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 []
process [(token, flag)] = do
takesArg <- Map.lookup flag flagMap
guard $ not takesArg
return [(flag, token)]
process ((token1, flag1):rest2@((token2, flag2):rest)) = do
takesArg <- Map.lookup flag1 flagMap
if takesArg
then do
guard $ flag2 == ""
more <- process rest
return $ (flag1, token2) : more
else do
more <- process rest2
return $ (flag1, token1) : more
supportsArrays shell = shell == Bash || shell == Ksh
-- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh)
isBashLike :: Parameters -> Bool
isBashLike params =
case shellType params of
Bash -> True
Ksh -> True
Dash -> False
Sh -> False
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

394
src/ShellCheck/Checker.hs Normal file
View File

@@ -0,0 +1,394 @@
{-
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/>.
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Analyzer
import Data.Either
import Data.Functor
import Data.List
import Data.Maybe
import Data.Ord
import Control.Monad.Identity
import qualified Data.Map as Map
import qualified System.IO
import Prelude hiding (readFile)
import Control.Monad
import Test.QuickCheck.All
tokenToPosition startMap t = fromMaybe fail $ do
span <- Map.lookup (tcId t) startMap
return $ newPositionedComment {
pcStartPos = fst span,
pcEndPos = snd span,
pcComment = tcComment t,
pcFix = tcFix t
}
where
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
shellFromFilename filename = foldl mplus Nothing 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 =
map (\(ext,sh) -> if ext `isSuffixOf` filename then Just sh else Nothing) shellExtensions
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
checkScript sys spec = do
results <- checkScript (csScript spec)
return emptyCheckResult {
crFilename = csFilename spec,
crComments = results
}
where
checkScript contents = do
result <- parseScript sys newParseSpec {
psFilename = csFilename spec,
psScript = contents,
psCheckSourced = csCheckSourced spec,
psIgnoreRC = csIgnoreRC spec,
psShellTypeOverride = csShellTypeOverride spec
}
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 =
fromMaybe [] $
(arComments . analyzeScript . analysisSpec)
<$> prRoot result
let translator = tokenToPosition tokenPositions
return . nub . sortMessages . filter shouldInclude $
(parseMessages ++ map translator analysisMessages)
shouldInclude pc =
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)
order pc =
let pos = pcStartPos pc
comment = pcComment pc in
(posFile pos,
posLine pos,
posColumn pos,
cSeverity comment,
cCode comment,
cMessage comment)
getPosition = pcStartPos
getErrors sys spec =
sort . map getCode . crComments $
runIdentity (checkScript sys spec)
where
getCode = cCode . pcComment
check = checkWithIncludes []
checkWithSpec includes =
getErrors (mockedSystemInterface includes)
checkWithIncludes includes src =
checkWithSpec includes emptyCheckSpec {
csScript = src,
csExcludedWarnings = [2148]
}
checkRecursive includes src =
checkWithSpec includes emptyCheckSpec {
csScript = src,
csExcludedWarnings = [2148],
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_commentDisablesParseIssue1 =
null $ check "#shellcheck disable=SC1037\necho \"$12\""
prop_commentDisablesParseIssue2 =
null $ check "#shellcheck disable=SC1037\n#lol\necho \"$12\""
prop_findsAnalysisIssue =
check "echo $1" == [2086]
prop_commentDisablesAnalysisIssue1 =
null $ check "#shellcheck disable=SC2086\necho $1"
prop_commentDisablesAnalysisIssue2 =
null $ check "#shellcheck disable=SC2086\n#lol\necho $1"
prop_optionDisablesIssue1 =
null $ getErrors
(mockedSystemInterface [])
emptyCheckSpec {
csScript = "echo $1",
csExcludedWarnings = [2148, 2086]
}
prop_optionDisablesIssue2 =
null $ getErrors
(mockedSystemInterface [])
emptyCheckSpec {
csScript = "echo \"$10\"",
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 =
[] == check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
prop_canParseDevNull =
[] == check "source /dev/null"
prop_failsWhenNotSourcing =
[1091, 2154] == check "source lol; echo \"$bar\""
prop_worksWhenSourcing =
null $ checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\""
prop_worksWhenSourcingWithDashDash =
null $ checkWithIncludes [("lib", "bar=1")] "source -- lib; echo \"$bar\""
prop_worksWhenDotting =
null $ checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\""
-- FIXME: This should really be giving [1093], "recursively sourced"
prop_noInfiniteSourcing =
[] == checkWithIncludes [("lib", "source lib")] "source lib"
prop_canSourceBadSyntax =
[1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
prop_cantSourceDynamic =
[1090] == checkWithIncludes [("lib", "")] ". \"$1\""
prop_cantSourceDynamic2 =
[1090] == checkWithIncludes [("lib", "")] "source ~/foo"
prop_canSourceDynamicWhenRedirected =
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
prop_recursiveAnalysis =
[2086] == checkRecursive [("lib", "echo $1")] "source lib"
prop_recursiveParsing =
[1037] == checkRecursive [("lib", "echo \"$10\"")] "source lib"
prop_nonRecursiveAnalysis =
[] == checkWithIncludes [("lib", "echo $1")] "source lib"
prop_nonRecursiveParsing =
[] == checkWithIncludes [("lib", "echo \"$10\"")] "source lib"
prop_sourceDirectiveDoesntFollowFile =
null $ checkWithIncludes
[("foo", "source bar"), ("bar", "baz=3")]
"#shellcheck source=foo\n. \"$1\"; echo \"$baz\""
prop_filewideAnnotationBase = [2086] == check "#!/bin/sh\necho $1"
prop_filewideAnnotation1 = null $
check "#!/bin/sh\n# shellcheck disable=2086\necho $1"
prop_filewideAnnotation2 = null $
check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation3 = null $
check "#!/bin/sh\n#unrelated\n# shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation4 = null $
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
prop_filewideAnnotation5 = null $
check "#!/bin/sh\n\n\n\n#shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation6 = null $
check "#shellcheck shell=sh\n#unrelated\n#shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation7 = null $
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
prop_filewideAnnotationBase2 = [2086, 2181] == check "true\n[ $? == 0 ] && echo $1"
prop_filewideAnnotation8 = null $
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'
2039 `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_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 = 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 []
runTests = $quickCheckAll

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,545 @@
{-
Copyright 2012-2016 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.AnalyzerLib
import ShellCheck.Interface
import ShellCheck.Regex
import Control.Monad
import Control.Monad.RWS
import Data.Char
import Data.List
import Data.Maybe
import qualified Data.Map as Map
import qualified Data.Set as Set
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
data ForShell = ForShell [Shell] (Token -> Analysis)
getChecker params list = Checker {
perScript = nullCheck,
perToken = foldl composeAnalyzers nullCheck $ mapMaybe include list
}
where
shell = shellType params
include (ForShell list a) = do
guard $ shell `elem` list
return a
checker params = getChecker params checks
checks = [
checkForDecimals
,checkBashisms
,checkEchoSed
,checkBraceExpansionVars
,checkMultiDimensionalArrays
,checkPS1Assignments
]
testChecker (ForShell _ t) =
Checker {
perScript = nullCheck,
perToken = t
}
verify c s = producesComments (testChecker c) s == Just True
verifyNot c s = producesComments (testChecker c) s == Just False
prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
checkForDecimals = ForShell [Sh, Dash, Bash] f
where
f t@(TA_Expansion id _) = potentially $ do
str <- getLiteralString t
first <- str !!! 0
guard $ isDigit first && '.' `elem` str
return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
f _ = return ()
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"
prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
prop_checkBashisms5 = verify checkBashisms "source file"
prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}"
prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}"
prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}"
prop_checkBashisms13= verify checkBashisms "exec -c env"
prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \""
prop_checkBashisms15= verify checkBashisms "let n++"
prop_checkBashisms16= verify checkBashisms "echo $RANDOM"
prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))"
prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"
prop_checkBashisms19= verify checkBashisms "foo > file*.txt"
prop_checkBashisms20= verify checkBashisms "read -ra foo"
prop_checkBashisms21= verify checkBashisms "[ -a foo ]"
prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]"
prop_checkBashisms23= verify checkBashisms "trap mything ERR INT"
prop_checkBashisms24= verifyNot checkBashisms "trap mything INT TERM"
prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123"
prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM"
prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*"
prop_checkBashisms28= verify checkBashisms "exec {n}>&2"
prop_checkBashisms29= verify checkBashisms "echo ${!var}"
prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\""
prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\""
prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo"
prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo"
prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo"
prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM"
prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }"
prop_checkBashisms40= verify checkBashisms "echo $(<file)"
prop_checkBashisms41= verify checkBashisms "echo `<file`"
prop_checkBashisms42= verify checkBashisms "trap foo int"
prop_checkBashisms43= verify checkBashisms "trap foo sigint"
prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"
prop_checkBashisms55= verify checkBashisms "#!/bin/sh\necho ${@%foo}"
prop_checkBashisms56= verifyNot checkBashisms "#!/bin/sh\necho ${##}"
prop_checkBashisms57= verifyNot checkBashisms "#!/bin/dash\nulimit -c 0"
prop_checkBashisms58= verify checkBashisms "#!/bin/sh\nulimit -c 0"
prop_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 $_"
checkBashisms = ForShell [Sh, Dash] $ \t -> do
params <- ask
kludge params t
where
-- This code was copy-pasted from Analytics where params was a variable
kludge params = bashism
where
isDash = shellType params == Dash
warnMsg id s =
if isDash
then warn id 2169 $ "In dash, " ++ s ++ " not supported."
else warn id 2039 $ "In POSIX sh, " ++ s ++ " undefined."
bashism (T_ProcSub id _ _) = warnMsg id "process substitution is"
bashism (T_Extglob id _ _) = warnMsg id "extglob is"
bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is"
bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\" is"
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id "arithmetic for loops are"
bashism (T_Arithmetic id _) = warnMsg id "standalone ((..)) is"
bashism (T_DollarBracket id _) = warnMsg id "$[..] in place of $((..)) is"
bashism (T_SelectIn id _ _ _) = warnMsg id "select loops are"
bashism (T_BraceExpansion id _) = warnMsg id "brace expansion is"
bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is"
bashism (T_HereString id _) = warnMsg id "here-strings are"
bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
unless isDash $ warnMsg id $ "lexicographical " ++ op ++ " is"
bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "-nt", "-ef" ] =
unless isDash $ warnMsg id $ op ++ " is"
bashism (TC_Binary id SingleBracket "==" _ _) =
warnMsg id "== in place of = is"
bashism (TC_Binary id SingleBracket "=~" _ _) =
warnMsg id "=~ regex matching is"
bashism (TC_Unary id SingleBracket "-v" _) =
warnMsg id "unary -v (in place of [ -n \"${var+x}\" ]) is"
bashism (TC_Unary id _ "-a" _) =
warnMsg id "unary -a in place of -e is"
bashism (TA_Unary id op _)
| op `elem` [ "|++", "|--", "++|", "--|"] =
warnMsg id $ filter (/= '|') op ++ " is"
bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are"
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is"
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id ">& is"
bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are"
bashism (T_FdRedirect id num _)
| all isDigit num && length num > 1 = warnMsg id "FDs outside 0-9 are"
bashism (T_Assignment id Append _ _ _) =
warnMsg id "+= is"
bashism (T_IoFile id _ word) | isNetworked =
warnMsg id "/dev/{tcp,udp} is"
where
file = onlyLiteralString word
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
bashism (T_Glob id str) | "[^" `isInfixOf` str =
warnMsg id "^ in place of ! in glob bracket expressions is"
bashism t@(TA_Variable id str _) | isBashVariable str =
warnMsg id $ str ++ " is"
bashism t@(T_DollarBraced id _ token) = do
mapM_ check expansion
when (isBashVariable var) $
warnMsg id $ var ++ " is"
where
str = bracedString t
var = getBracedReference str
check (regex, feature) =
when (isJust $ matchRegex regex str) $ warnMsg id feature
bashism t@(T_Pipe id "|&") =
warnMsg id "|& in place of 2>&1 | is"
bashism (T_Array id _) =
warnMsg id "arrays are"
bashism (T_IoFile id _ t) | isGlob t =
warnMsg id "redirecting to/from globs is"
bashism (T_CoProc id _ _) =
warnMsg id "coproc is"
bashism (T_Function id _ _ str _) | not (isVariableName str) =
warnMsg id "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
warnMsg id "$(<file) to read files is"
bashism (T_Backticked id [x]) | isOnlyRedirection x =
warnMsg id "`<file` to read files is"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "echo" && argString `matches` flagRegex =
if isDash
then
when (argString /= "-n") $
warnMsg (getId arg) "echo flags besides -n"
else
warnMsg (getId arg) "echo flags are"
where
argString = concat $ oversimplify arg
flagRegex = mkRegex "^-[eEsn]+$"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
warnMsg (getId arg) "exec flags are"
bashism t@(T_SimpleCommand id _ _)
| t `isCommand` "let" = warnMsg id "'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 (first:rest) = fromMaybe [] $ do
str <- getLiteralString first
return $ (getId first, str) : getLiteralArgs rest
getLiteralArgs [] = []
-- 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 $ "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 $ "set flag " <> ('-':letter:" is")
checkOptions rest
| beginsWithDoubleDash flag' = do
warnMsg fid $ "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)) =
let name = fromMaybe "" $ getCommandName t
flags = getLeadingFlags t
in do
when (name `elem` unsupportedCommands) $
warnMsg id $ "'" ++ name ++ "' is"
potentially $ do
allowed' <- Map.lookup name allowedFlags
allowed <- allowed'
(word, flag) <- listToMaybe $
filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"
when (name == "source") $ warnMsg id "'source' in place of '.' is"
when (name == "trap") $
let
check token = potentially $ do
str <- getLiteralString token
let upper = map toUpper str
return $ do
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
warnMsg (getId token) $ "trapping " ++ str ++ " is"
when ("SIG" `isPrefixOf` upper) $
warnMsg (getId token)
"prefixing signal names with 'SIG' is"
when (not isDash && upper /= str) $
warnMsg (getId token)
"using lower/mixed case for signal names is"
in
mapM_ check (drop 1 rest)
when (name == "printf") $ potentially $ do
format <- rest !!! 0 -- flags are covered by allowedFlags
let literal = onlyLiteralString format
guard $ "%q" `isInfixOf` literal
return $ warnMsg (getId format) "printf %q is"
where
unsupportedCommands = [
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
"typeset"
] ++ if not isDash then ["local"] else []
allowedFlags = Map.fromList [
("cd", Just ["L", "P"]),
("exec", Just []),
("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 _) =
let name = fromMaybe "" $ getCommandName src
in when (name == "source") $ warnMsg id "'source' in place of '.' is"
bashism (TA_Expansion _ (T_Literal id str : _)) | str `matches` radix =
when (str `matches` radix) $ warnMsg id "arithmetic base conversion is"
where
radix = mkRegex "^[0-9]+#"
bashism _ = return ()
varChars="_0-9a-zA-Z"
expansion = let re = mkRegex in [
(re $ "^![" ++ varChars ++ "]", "indirect expansion is"),
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", "string indexing is"),
(re $ "^([*@][%#]|#[@*])", "string operations on $@/$* are"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")
]
bashVars = [
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS",
"_"
]
bashDynamicVars = [ "RANDOM", "SECONDS" ]
dashVars = [ "_" ]
isBashVariable var =
(var `elem` bashDynamicVars
|| var `elem` bashVars && not (isAssigned var))
&& not (isDash && var `elem` dashVars)
isAssigned var = any f (variableFlow params)
where
f x = case x of
Assignment (_, _, name, _) -> name == var
_ -> False
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
prop_checkEchoSed1b= verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")"
prop_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
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]) =
when (acmd == ["echo", "${VAR}"]) $
checkSed id bcmd
where
acmd = oversimplify a
bcmd = oversimplify b
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 = fromMaybe False $ do
[first,rest] <- matchRegex sedRe s
let delimiters = filter (== head first) rest
guard $ length delimiters == 2
return True
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_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}"
prop_checkBraceExpansionVars3 = verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg"
prop_checkBraceExpansionVars4 = verify checkBraceExpansionVars "echo {$i..100}"
checkBraceExpansionVars = ForShell [Bash] f
where
f t@(T_BraceExpansion id list) = mapM_ check list
where
check element =
when (any (`isInfixOf` toString element) ["$..", "..$"]) $ do
c <- isEvaled element
if c
then style id 2175 "Quote this invalid brace expansion since it should be passed literally to eval."
else warn id 2051 "Bash doesn't support variables in brace range expansions."
f _ = return ()
literalExt t =
case t of
T_DollarBraced {} -> return "$"
T_DollarExpansion {} -> return "$"
T_DollarArithmetic {} -> return "$"
otherwise -> return "-"
toString t = fromJust $ getLiteralStringExt literalExt t
isEvaled t = do
cmd <- getClosestCommandM t
return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval"
prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3"
prop_checkMultiDimensionalArrays2 = verifyNot checkMultiDimensionalArrays "foo[a]=3"
prop_checkMultiDimensionalArrays3 = verify checkMultiDimensionalArrays "foo=( [a][b]=c )"
prop_checkMultiDimensionalArrays4 = verifyNot checkMultiDimensionalArrays "foo=( [a]=c )"
prop_checkMultiDimensionalArrays5 = verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}"
prop_checkMultiDimensionalArrays6 = verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}"
checkMultiDimensionalArrays = ForShell [Bash] f
where
f token =
case token of
T_Assignment _ _ name (first:second:_) _ -> about second
T_IndexedElement _ (first:second:_) _ -> about second
T_DollarBraced {} ->
when (isMultiDim token) $ about token
_ -> return ()
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
isMultiDim t = getBracedModifier (bracedString t) `matches` re
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '"
prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '"
prop_checkPS14a= verify checkPS1Assignments "export PS1=$'\\e[3m; '"
prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"
prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'"
prop_checkPS18 = verifyNot checkPS1Assignments "PS1='\\[\\e\\]'"
checkPS1Assignments = ForShell [Bash] f
where
f token = case token of
(T_Assignment _ _ "PS1" _ word) -> warnFor word
_ -> return ()
warnFor word =
let contents = concat $ oversimplify word in
when (containsUnescaped contents) $
info (getId word) 2025 "Make sure all escape sequences are enclosed in \\[..\\] to prevent line wrapping issues"
containsUnescaped s =
let unenclosed = subRegex enclosedRegex s "" in
isJust $ matchRegex escapeRegex unenclosed
enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -1,5 +1,6 @@
module ShellCheck.Data where
import ShellCheck.Interface
import Data.Version (showVersion)
import Paths_ShellCheck (version)
@@ -24,20 +25,46 @@ internalVariables = [
"FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",
"HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",
"IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
"LC_MESSAGES", "LC_NUMERIC", "LINES", "MAIL", "MAILCHECK", "MAILPATH",
"OPTERR", "PATH", "POSIXLY_CORRECT", "PROMPT_COMMAND",
"PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT",
"TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC",
"LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LINES", "MAIL",
"MAILCHECK", "MAILPATH", "OPTERR", "PATH", "POSIXLY_CORRECT",
"PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL",
"TIMEFORMAT", "TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC",
-- Other
"USER", "TZ", "TERM"
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
"HOSTNAME", "KRB5CCNAME", "XAUTHORITY"
-- Ksh
, ".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",
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
"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 = [
"BASH_ALIASES", "BASH_ARGC", "BASH_ARGV", "BASH_CMDS", "BASH_LINENO",
"BASH_REMATCH", "BASH_SOURCE", "BASH_VERSINFO", "COMP_WORDS", "COPROC",
"DIRSTACK", "FUNCNAME", "GROUPS", "MAPFILE", "PIPESTATUS", "COMPREPLY"
]
commonCommands = [
@@ -66,6 +93,14 @@ commonCommands = [
"zcat"
]
nonReadingCommands = [
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
"cp", "du", "echo", "export", "false", "fg", "fuser", "getconf",
"getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
"set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname"
]
sampleWords = [
"alpha", "bravo", "charlie", "delta", "echo", "foxtrot",
"golf", "hotel", "india", "juliett", "kilo", "lima", "mike",
@@ -73,3 +108,27 @@ sampleWords = [
"tango", "uniform", "victor", "whiskey", "xray", "yankee",
"zulu"
]
binaryTestOps = [
"-nt", "-ot", "-ef", "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le",
"-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="
]
unaryTestOps = [
"!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p",
"-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n",
"-o", "-v", "-R"
]
shellForExecutable :: String -> Maybe Shell
shellForExecutable name =
case name of
"sh" -> return Sh
"bash" -> return Bash
"bats" -> return Bash
"dash" -> return Dash
"ash" -> return Dash -- There's also a warning for this.
"ksh" -> return Ksh
"ksh88" -> return Ksh
"ksh93" -> return Ksh
otherwise -> Nothing

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
(y, z) = splitAt (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 () of
_ | target < pivot -> f sum target left
_ | target > pivot -> f (sum+cumulative) target right
_ -> 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 () of
_ | key < pivot -> PSBranch pivot (f left) right (sum + value)
_ | key > pivot -> PSBranch pivot left (f right) sum
_ -> 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 . map snd . filter (\(k,v) -> k <= target) $ kvs
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

@@ -0,0 +1,95 @@
{-
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.CheckStyle (format) where
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Data.Char
import Data.List
import GHC.Exts
import System.IO
format :: IO Formatter
format = return Formatter {
header = do
putStrLn "<?xml version='1.0' encoding='UTF-8'?>"
putStrLn "<checkstyle version='4.3'>",
onFailure = outputError,
onResult = outputResults,
footer = putStrLn "</checkstyle>"
}
outputResults cr sys =
if null comments
then outputFile (crFilename cr) "" []
else mapM_ outputGroup fileGroups
where
comments = crComments cr
fileGroups = groupWith sourceFile comments
outputGroup group = do
let filename = sourceFile (head group)
result <- (siReadFile sys) filename
let contents = either (const "") id result
outputFile filename contents group
outputFile filename contents warnings = do
let comments = makeNonVirtual warnings contents
putStrLn . formatFile filename $ comments
formatFile name comments = concat [
"<file ", attr "name" name, ">\n",
concatMap formatComment comments,
"</file>"
]
formatComment c = concat [
"<error ",
attr "line" $ show . lineNo $ c,
attr "column" $ show . colNo $ c,
attr "severity" . severity $ severityText c,
attr "message" $ messageText c,
attr "source" $ "ShellCheck.SC" ++ show (codeNo c),
"/>\n"
]
outputError file error = putStrLn $ concat [
"<file ", attr "name" file, ">\n",
"<error ",
attr "line" "1",
attr "column" "1",
attr "severity" "error",
attr "message" error,
attr "source" "ShellCheck",
"/>\n",
"</file>"
]
attr s v = concat [ s, "='", escape v, "' " ]
escape = concatMap escape'
escape' c = if isOk c then [c] else "&#" ++ show (ord c) ++ ";"
isOk x = any ($x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")]
severity "error" = "error"
severity "warning" = "warning"
severity _ = "info"

View File

@@ -0,0 +1,255 @@
{-
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
didOutput <- newIORef False
shouldColor <- shouldOutputColor (foColorOption options)
let color = if shouldColor then colorize else nocolor
return Formatter {
header = return (),
footer = checkFooter didOutput color,
onFailure = reportFailure color,
onResult = reportResult didOutput 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 didOutput color = do
output <- readIORef didOutput
unless 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) -> ColorFunc -> CheckResult -> SystemInterface IO -> IO ()
reportResult didOutput color result sys = do
let comments = crComments result
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 didOutput 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

@@ -0,0 +1,79 @@
{-
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.Format where
import ShellCheck.Data
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
data Formatter = Formatter {
header :: IO (),
onResult :: CheckResult -> SystemInterface IO -> IO (),
onFailure :: FilePath -> ErrorMessage -> IO (),
footer :: IO ()
}
sourceFile = posFile . pcStartPos
lineNo = posLine . pcStartPos
endLineNo = posLine . pcEndPos
colNo = posColumn . pcStartPos
endColNo = posColumn . pcEndPos
codeNo = cCode . pcComment
messageText = cMessage . pcComment
severityText :: PositionedComment -> String
severityText pc =
case cSeverity (pcComment pc) of
ErrorC -> "error"
WarningC -> "warning"
InfoC -> "info"
StyleC -> "style"
-- Realign comments from a tabstop of 8 to 1
makeNonVirtual comments contents =
map fix comments
where
list = lines contents
arr = listArray (1, length list) list
untabbedFix f = newFix {
fixReplacements = map (\r -> removeTabStops r arr) (fixReplacements f)
}
fix c = (removeTabStops c arr) {
pcFix = fmap untabbedFix (pcFix c)
}
shouldOutputColor :: ColorOption -> IO Bool
shouldOutputColor colorOption = do
term <- hIsTerminalDevice stdout
let windows = "mingw" `isPrefixOf` os
let isUsableTty = term && not windows
let useColor = case colorOption of
ColorAlways -> True
ColorNever -> False
ColorAuto -> isUsableTty
return useColor

View File

@@ -0,0 +1,65 @@
{-
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.GCC (format) where
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Data.List
import GHC.Exts
import System.IO
format :: IO Formatter
format = return Formatter {
header = return (),
footer = return (),
onFailure = outputError,
onResult = outputAll
}
outputError file error = hPutStrLn stderr $ file ++ ": " ++ error
outputAll 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
outputResult filename contents group
outputResult filename contents warnings = do
let comments = makeNonVirtual warnings contents
mapM_ (putStrLn . formatComment filename) comments
formatComment filename c = concat [
filename, ":",
show $ lineNo c, ":",
show $ colNo c, ": ",
case severityText c of
"error" -> "error"
"warning" -> "warning"
_ -> "note",
": ",
concat . lines $ messageText c,
" [SC", show $ codeNo c, "]"
]

View File

@@ -0,0 +1,110 @@
{-# 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.JSON (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
}
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 = modifyIORef ref (\x -> comments ++ x)
finish ref = do
list <- readIORef ref
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

@@ -0,0 +1,197 @@
{-
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.TTY (format) where
import ShellCheck.Fixer
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Control.Monad
import Data.Array
import Data.Foldable
import Data.Ord
import Data.IORef
import Data.List
import Data.Maybe
import GHC.Exts
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 options = do
topErrorRef <- newIORef []
return Formatter {
header = return (),
footer = outputWiki topErrorRef,
onFailure = outputError options,
onResult = outputResult options topErrorRef
}
colorForLevel level =
case level of
"error" -> 31 -- red
"warning" -> 33 -- yellow
"info" -> 32 -- green
"style" -> 32 -- green
"verbose" -> 32 -- green
"message" -> 1 -- bold
"source" -> 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
color <- getColorFunc $ foColorOption options
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
outputResult options ref result sys = do
color <- getColorFunc $ foColorOption options
let comments = crComments result
appendComments ref comments (fromIntegral $ foWikiLinkCount options)
let fileGroups = groupWith sourceFile comments
mapM_ (outputForFile color sys) fileGroups
outputForFile color sys comments = do
let fileName = sourceFile (head comments)
result <- (siReadFile sys) fileName
let contents = either (const "") id result
let fileLinesList = lines contents
let lineCount = length fileLinesList
let fileLines = listArray (1, lineCount) fileLinesList
let groups = groupWith lineNo comments
mapM_ (\commentsForLine -> do
let lineNum = fromIntegral $ lineNo (head commentsForLine)
let line = if lineNum < 1 || lineNum > lineCount
then ""
else fileLines ! fromIntegral lineNum
putStrLn ""
putStrLn $ color "message" $
"In " ++ fileName ++" line " ++ show lineNum ++ ":"
putStrLn (color "source" line)
mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) commentsForLine
putStrLn ""
showFixedString color commentsForLine (fromIntegral lineNum) fileLines
) groups
-- 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 comment =
replicate (fromIntegral $ colNo comment - 1) ' ' ++
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 num = "SC" ++ show num
getColorFunc :: ColorOption -> IO ColorFunc
getColorFunc colorOption = do
useColor <- shouldOutputColor colorOption
return $ if useColor then colorComment else const id
where
colorComment level comment =
ansi (colorForLevel level) ++ comment ++ clear
clear = ansi 0
ansi n = "\x1B[" ++ show n ++ "m"

328
src/ShellCheck/Interface.hs Normal file
View File

@@ -0,0 +1,328 @@
{-
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/>.
-}
{-# 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 Control.DeepSeq
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
data SystemInterface m = SystemInterface {
-- Read a file by filename, or return an error
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
data CheckSpec = CheckSpec {
csFilename :: String,
csScript :: String,
csCheckSourced :: Bool,
csIgnoreRC :: Bool,
csExcludedWarnings :: [Integer],
csIncludedWarnings :: Maybe [Integer],
csShellTypeOverride :: Maybe Shell,
csMinSeverity :: Severity,
csOptionalChecks :: [String]
} deriving (Show, Eq)
data CheckResult = CheckResult {
crFilename :: String,
crComments :: [PositionedComment]
} deriving (Show, Eq)
emptyCheckResult :: CheckResult
emptyCheckResult = CheckResult {
crFilename = "",
crComments = []
}
emptyCheckSpec :: CheckSpec
emptyCheckSpec = CheckSpec {
csFilename = "",
csScript = "",
csCheckSourced = False,
csIgnoreRC = False,
csExcludedWarnings = [],
csIncludedWarnings = Nothing,
csShellTypeOverride = Nothing,
csMinSeverity = StyleC,
csOptionalChecks = []
}
newParseSpec :: ParseSpec
newParseSpec = ParseSpec {
psFilename = "",
psScript = "",
psCheckSourced = False,
psIgnoreRC = False,
psShellTypeOverride = Nothing
}
-- Parser input and output
data ParseSpec = ParseSpec {
psFilename :: String,
psScript :: String,
psCheckSourced :: Bool,
psIgnoreRC :: Bool,
psShellTypeOverride :: Maybe Shell
} deriving (Show, Eq)
data ParseResult = ParseResult {
prComments :: [PositionedComment],
prTokenPositions :: Map.Map Id (Position, Position),
prRoot :: Maybe Token
} deriving (Show, Eq)
newParseResult :: ParseResult
newParseResult = ParseResult {
prComments = [],
prTokenPositions = Map.empty,
prRoot = Nothing
}
-- Analyzer input and output
data AnalysisSpec = AnalysisSpec {
asScript :: Token,
asShellType :: Maybe Shell,
asFallbackShell :: Maybe Shell,
asExecutionMode :: ExecutionMode,
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 {
arComments :: [TokenComment]
}
newAnalysisResult = AnalysisResult {
arComments = []
}
-- 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
data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
data ExecutionMode = Executed | Sourced deriving (Show, Eq)
type ErrorMessage = String
type Code = Integer
data Severity = ErrorC | WarningC | InfoC | StyleC
deriving (Show, Eq, Ord, Generic, NFData)
data Position = Position {
posFile :: String, -- Filename
posLine :: Integer, -- 1 based source line
posColumn :: Integer -- 1 based source column, where tabs are 8
} deriving (Show, Eq, Generic, NFData, Ord)
newPosition :: Position
newPosition = Position {
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)
instance Ord Replacement where
compare r1 r2 = (repStartPos r1) `compare` (repStartPos r2)
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 =
ColorAuto
| ColorAlways
| ColorNever
deriving (Ord, Eq, Show)
-- For testing
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
mockedSystemInterface files = SystemInterface {
siReadFile = rf,
siFindSource = fs,
siGetConfig = const $ return Nothing
}
where
rf file =
case filter ((== file) . fst) files of
[] -> return $ Left "File not included in mock."
[(_, contents)] -> return $ Right contents
fs _ _ file = return file
mockRcFile rcfile mock = mock {
siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
}

3428
src/ShellCheck/Parser.hs Normal file

File diff suppressed because it is too large Load Diff

80
src/ShellCheck/Regex.hs Normal file
View File

@@ -0,0 +1,80 @@
{-
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/>.
-}
{-# LANGUAGE FlexibleContexts #-}
-- Basically Text.Regex based on regex-tdfa instead of the buggy regex-posix.
module ShellCheck.Regex where
import Data.List
import Data.Maybe
import Control.Monad
import Text.Regex.TDFA
-- Precompile the regex
mkRegex :: String -> Regex
mkRegex str =
let make :: String -> Regex
make = makeRegex
in
make str
-- Does the regex match?
matches :: String -> Regex -> Bool
matches = flip match
-- Get all subgroups of the first match
matchRegex :: Regex -> String -> Maybe [String]
matchRegex re str = do
(_, _, _, groups) <- matchM re str :: Maybe (String,String,String,[String])
return groups
-- Get all full matches
matchAllStrings :: Regex -> String -> [String]
matchAllStrings re = unfoldr f
where
f :: String -> Maybe (String, String)
f str = do
(_, match, rest, _) <- matchM re str :: Maybe (String, String, String, [String])
return (match, rest)
-- Get all subgroups from all matches
matchAllSubgroups :: Regex -> String -> [[String]]
matchAllSubgroups re = unfoldr f
where
f :: String -> Maybe ([String], String)
f str = do
(_, _, rest, groups) <- matchM re str :: Maybe (String, String, String, [String])
return (groups, rest)
-- Replace regex in input with string
subRegex :: Regex -> String -> String -> String
subRegex re input replacement = f input
where
f str = fromMaybe str $ do
(before, match, after) <- matchM re str :: Maybe (String, String, String)
when (null match) $ error ("Internal error: substituted empty in " ++ str)
return $ before ++ replacement ++ f after
-- Split a string based on a regex.
splitOn :: String -> Regex -> [String]
splitOn input re =
case matchM re input :: Maybe (String, String, String) of
Just (before, match, after) -> before : after `splitOn` re
Nothing -> [input]

35
stack.yaml Normal file
View File

@@ -0,0 +1,35 @@
# This file was automatically generated by stack init
# 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)
resolver: lts-13.26
# Local packages, usually specified by relative directory name
packages:
- '.'
# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3)
extra-deps: []
# Override default flag values for local packages and extra-deps
flags: {}
# Extra package databases containing global packages
extra-package-dbs: []
# Control whether we use the GHC we find on the path
# system-ghc: true
# Require a specific version of stack, using version ranges
# require-stack-version: -any # Default
# require-stack-version: >= 1.0.0
# Override the architecture used by stack, especially useful on Windows
# arch: i386
# arch: x86_64
# Extra directories used by stack for building
# extra-include-dirs: [/path/to/dir]
# extra-lib-dirs: [/path/to/dir]
# Allow a newer minor version of GHC than the snapshot specifies
# compiler-check: newer-minor

78
striptests Executable file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# This file strips all unit tests from ShellCheck, removing
# the dependency on QuickCheck and Template Haskell and
# reduces the binary size considerably.
set -o pipefail
sponge() {
local data
data="$(cat)"
printf '%s\n' "$data" > "$1"
}
modify() {
if ! "${@:2}" < "$1" | sponge "$1"
then
{
printf 'Failed to modify %s: ' "$1"
printf '%q ' "${@:2}"
printf '\n'
} >&2
exit 1
fi
}
detestify() {
printf '%s\n' '-- AUTOGENERATED from ShellCheck by striptests. Do not modify.'
awk '
BEGIN {
state = 0;
}
/LANGUAGE TemplateHaskell/ { next; }
/^import.*Test\./ { next; }
/^module/ {
sub(/,[^,)]*runTests/, "");
}
# Delete tests
/^prop_/ { state = 1; next; }
# ..and any blank lines following them.
state == 1 && /^ / { next; }
# Template Haskell marker
/^return / {
exit;
}
{ state = 0; print; }
'
}
if [[ ! -e 'ShellCheck.cabal' ]]
then
echo "Run me from the ShellCheck directory." >&2
exit 1
fi
if [[ -d '.git' ]] && ! git diff --exit-code > /dev/null 2>&1
then
echo "You have local changes! These may be overwritten." >&2
exit 2
fi
modify 'ShellCheck.cabal' sed -e '
/QuickCheck/d
/^test-suite/{ s/.*//; q; }
'
find . -name '.git' -prune -o -type f -name '*.hs' -print |
while IFS= read -r file
do
modify "$file" detestify
done

35
test/buildtest Executable file
View File

@@ -0,0 +1,35 @@
#!/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"
cabal install --dependencies-only --enable-tests ||
die "can't install dependencies"
cabal configure --enable-tests ||
die "configure failed"
cabal build ||
die "build failed"
cabal test ||
die "test failed"
dist/build/shellcheck/shellcheck - << 'EOF' || die "execution failed"
#!/bin/sh
echo "Hello World"
EOF
dist/build/shellcheck/shellcheck - << 'EOF' && die "negative execution failed"
#!/bin/sh
echo $1
EOF
echo "Success"
exit 0

75
test/check_release Executable file
View File

@@ -0,0 +1,75 @@
#!/usr/bin/env bash
failed=0
fail() {
echo "$(tput setaf 1)$*$(tput sgr0)"
failed=1
}
if git diff | grep -q ""
then
fail "There are uncommited 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"

80
test/distrotest Executable file
View File

@@ -0,0 +1,80 @@
#!/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'..."
rm -rf dist
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
opensuse/leap:latest zypper install -y cabal-install ghc
# Other Ubuntu versions we want to support
ubuntu:19.04 apt-get update && apt-get install -y cabal-install
ubuntu:18.10 apt-get update && apt-get install -y cabal-install
# Misc Haskell including current and latest Stack build
ubuntu:18.10 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
haskell:latest true
# Known to currently fail
centos:latest yum install -y epel-release && yum install -y cabal-install
fedora:latest dnf install -y cabal-install
archlinux/base:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
EOF
exit "$final"

View File

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

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"