363 Commits

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

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

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

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

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

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

We can address this with two parts:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    if a; then
        ...

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

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

Fixes #1406
2018-12-25 09:33:58 +08:00
Vidar Holen
9acc8fcb53 Fix semigroup incompatibility 2018-12-23 11:08:48 -08:00
Vidar Holen
897f019353 Move Ranged definition to Fixer to avoid overpromising 2018-12-22 10:04:00 -08:00
Ng Zhi An
0636e7023c Fix applying multiple fixes per line
Fixes #1421
2018-12-21 14:34:03 +08:00
Vidar Holen
08ca1ee6e9 Remove unnecessary Regex constraint 2018-12-17 20:15:39 -08:00
Vidar Holen
eb3e6fe8e1 Add ShellCheck.Fixer to the cabal file 2018-12-17 20:14:49 -08:00
Vidar Holen
ecd61bfc68 Merge pull request #1376 from ngzhian/autofix
Add method to apply a multi-line replacement
2018-12-17 17:24:59 -08:00
Ng Zhi An
a8d88dfe98 Fix calculation of changed lines 2018-12-17 00:20:50 -08:00
Ng Zhi An
7d2c519d64 Remove spurious new line in fix message 2018-12-17 00:20:50 -08:00
Ng Zhi An
3403f8d75b Fix bug in overlap check 2018-12-17 00:20:50 -08:00
Ng Zhi An
408a3b99d8 Remove overlaps before applying replacements 2018-12-17 00:20:50 -08:00
Ng Zhi An
bc111141f8 Move fix application logic to separate module 2018-12-17 00:20:50 -08:00
Ng Zhi An
3471ad45b1 Smarter sorting and application of fix to handle multiple replacements 2018-12-17 00:20:50 -08:00
Ng Zhi An
d5ba41035b Add method to apply a multi-line replacement 2018-12-16 21:53:48 -08:00
49 changed files with 5657 additions and 3129 deletions

73
.compile_binaries Executable file
View File

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

6
.dockerignore Normal file
View File

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

57
.github_deploy Executable file
View File

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

3
.gitignore vendored
View File

@@ -13,9 +13,6 @@ cabal-dev
cabal.sandbox.config
cabal.config
.stack-work
dist-newstyle/
.ghc.environment.*
cabal.project.local
### Snap ###
/snap/.snapcraft/

113
.multi_arch_docker Executable file
View File

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

View File

@@ -35,6 +35,14 @@ do
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%.*}"
@@ -43,8 +51,15 @@ do
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

View File

@@ -1,80 +1,64 @@
sudo: required
language: sh
language: shell
os: linux
services:
- docker
before_install:
- DOCKER_BASE="$DOCKER_USERNAME/shellcheck"
- DOCKER_BUILDS=""
- TAGS=""
- test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
- test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
- echo "Tags are $TAGS"
jobs:
include:
- stage: Build
# This must weirdly not have a dash, otherwise an empty job is created
env: BUILD=linux
- env: BUILD=windows
- env: BUILD=armv6hf
- env: BUILD=aarch64
- env: BUILD=osx
os: osx
- stage: Deploy docker image
# Deploy only for pushes to master branch, not other branches, not PRs.
if: type = push
script:
- source ./.multi_arch_docker
- set -ex; multi_arch_docker::main; set +x
# This is in global context and runs for every stage that doesn't override it.
before_install: |
DOCKER_BASE="$DOCKER_USERNAME/shellcheck"
DOCKER_BUILDS=""
export TAGS=""
test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
echo "Tags are $TAGS"
# This is in global context and runs for every stage that doesn't override it.
script:
- mkdir deploy
# Remove all tests to reduce binary size
- mkdir -p deploy
- source ./.compile_binaries
- ./striptests
# Linux Docker image
- name="$DOCKER_BASE"
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
- docker build -t "$name:current" .
- docker run "$name:current" --version
- printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
- docker run -v "$PWD:/mnt" "$name:current" myscript
# Copy static executable from docker image
- id=$(docker create "$name:current")
- docker cp "$id:/bin/shellcheck" "shellcheck"
- docker rm "$id"
- ls -l shellcheck
- ./shellcheck myscript
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux-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'
# 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
- rm -f shellcheck || true
# 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
- rm -rf dist shellcheck || true
# Misc packaging
- set -ex; build_"$BUILD"; set +x;
- ./.prepare_deploy
- ./.github_deploy
after_success:
- docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
- for repo in $DOCKER_BUILDS;
do
for tag in $TAGS;
do
echo "Deploying $repo:current as $repo:$tag...";
docker tag "$repo:current" "$repo:$tag" || exit 1;
docker push "$repo:$tag" || exit 1;
done;
done;
after_failure:
- id
- pwd
- df -h
- find . -name '*.log' -type f -exec grep "" /dev/null {} +
- find . -ls
# This is in global context and runs for every stage that doesn't override it.
after_failure: |
id
pwd
df -h
find . -name '*.log' -type f -exec grep "" /dev/null {} +
find . -ls
# This is in global context and runs for every stage that doesn't override it.
deploy:
provider: gcs
skip_cleanup: true
access_key_id: GOOG7MDN7WEH6IIGBDCA
secret_access_key:
secure: Bcx2cT0/E2ikj7sdamVq52xlLZF9dz9ojGPtoKfPyQhkkZa+McVI4xgUSuyyoSxyKj77sofx2y8m6PJYYumT4g5hREV1tfeUkl0J2DQFMbGDYEt7kxVkXCxojNvhHwTzLFv0ezstrxWWxQm81BfQQ4U9lggRXtndAP4czZnOeHPINPSiue1QNwRAEw05r5UoIUJXy/5xyUrjIxn381pAs+gJqP2COeN9kTKYH53nS/AAws29RprfZFnPlo7xxWmcjRcdS5KPdGXI/c6tQp5zl2iTh510VC1PN2w1Wvnn/oNWhiNdqPyVDsojIX5+sS3nejzJA+KFMxXSBlyXIY3wPpS/MdscU79X6Q5f9ivsFfsm7gNBmxHUPNn0HAvU4ROT/CCE9j6jSbs5PC7QBo3CK4++jxAwE/pd9HUc2rs3k0ofx3rgveJ7txpy5yPKfwIIBi98kVKlC4w7dLvNTOfjW1Imt2yH87XTfsE0UIG9st1WII6s4l/WgBx2GuwKdt6+3QUYiAlCFckkxWi+fAvpHZUEL43Qxub5fN+ZV7Zib1n7opchH4QKGBb6/y0WaDCmtCfu0lppoe/TH6saOTjDFj67NJSElK6ZDxGZ3uw4R+ret2gm6WRKT2Oeub8J33VzSa7VkmFpMPrAAfPa9N1Z4ewBLoTmvxSg2A0dDrCdJio=
bucket: shellcheck
local-dir: deploy
bucket: shellcheck-private
local_dir: deploy
on:
repo: koalaman/shellcheck
condition: $TRAVIS_BUILD_STAGE_NAME = Build
all_branches: true

View File

@@ -1,6 +1,57 @@
## Since previous release
## v0.7.1 - 2020-04-04
### Fixed
- `-f diff` no longer claims that it found more issues when it didn't
- Known empty variables now correctly trigger SC2086
- ShellCheck should now be compatible with Cabal 3
- SC2154 and all command-specific checks now trigger for builtins
called with `builtin`
### Added
- SC1136: Warn about unexpected characters after ]/]]
- SC2254: Suggest quoting expansions in case statements
- SC2255: Suggest using `$((..))` in `[ 2*3 -eq 6 ]`
- SC2256: Warn about translated strings that are known variables
- SC2257: Warn about arithmetic mutation in redirections
- SC2258: Warn about trailing commas in for loop elements
### Changed
- SC2230: 'command -v' suggestion is now off by default (-i deprecate-which)
- SC1081: Keywords are now correctly parsed case sensitively, with a warning
## v0.7.0 - 2019-07-28
### Added
- Precompiled binaries for macOS and Linux aarch64
- Preliminary support for fix suggestions
- New `-f diff` unified diff format for auto-fixes
- Files containing Bats tests can now be checked
- Directory wide directives can now be placed in a `.shellcheckrc`
- Optional checks: Use `--list-optional` to show a list of tests,
Enable with `-o` flags or `enable=name` directives
- Source paths: Use `-P dir1:dir2` or a `source-path=dir1` directive
to specify search paths for sourced files.
- json1 format like --format=json but treats tabs as single characters
- Recognize FLAGS variables created by the shflags library.
- Site-specific changes can now be made in Custom.hs for ease of patching
- SC2154: Also warn about unassigned uppercase variables (optional)
- SC2252: Warn about `[ $a != x ] || [ $a != y ]`, similar to SC2055
- SC2251: Inform about ineffectual ! in front of commands
- SC2250: Warn about variable references without braces (optional)
- SC2249: Warn about `case` with missing default case (optional)
- SC2248: Warn about unquoted variables without special chars (optional)
- SC2247: Warn about $"(cmd)" and $"{var}"
- SC2246: Warn if a shebang's interpreter ends with /
- SC2245: Warn that Ksh ignores all but the first glob result in `[`
- SC2243/SC2244: Suggest using explicit -n for `[ $foo ]` (optional)
- SC1135: Suggest not ending double quotes just to make $ literal
### Changed
- If a directive or shebang is not specified, a `.bash/.bats/.dash/.ksh`
extension will be used to infer the shell type when present.
- Disabling SC2120 on a function now disables SC2119 on call sites
### Fixed
- SC2183 no longer warns about missing printf args for `%()T`
## v0.6.0 - 2018-12-02
### Added

View File

@@ -3,64 +3,24 @@ FROM ubuntu:18.04 AS build
USER root
WORKDIR /opt/shellCheck
# Install OS deps, including GHC from HVR-PPA
# https://launchpad.net/~hvr/+archive/ubuntu/ghc
RUN apt-get -yq update \
&& apt-get -yq install software-properties-common \
&& apt-add-repository -y "ppa:hvr/ghc" \
&& apt-get -yq update \
&& apt-get -yq install cabal-install-2.4 ghc-8.4.3 pandoc \
&& rm -rf /var/lib/apt/lists/*
ENV PATH="/opt/ghc/bin:${PATH}"
# Use gold linker and check tools versions
RUN ln -s $(which ld.gold) /usr/local/bin/ld && \
cabal --version \
&& ghc --version \
&& ld --version
# 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)
#
# We also patch regex-tdfa and aeson removing hard-coded -O2 flag.
# This makes compilation faster and binary smaller.
# Performance loss is unnoticeable for ShellCheck
#
# Remember to update versions, once in a while.
COPY ShellCheck.cabal ./
RUN cabal update && \
cabal get regex-tdfa-1.2.3.1 && sed -i 's/-O2//' regex-tdfa-1.2.3.1/regex-tdfa.cabal && \
cabal get aeson-1.4.0.0 && sed -i 's/-O2//' aeson-1.4.0.0/aeson.cabal && \
echo 'packages: . regex-tdfa-1.2.3.1 aeson-1.4.0.0 > cabal.project' && \
cabal new-build --dependencies-only \
--disable-executable-dynamic --enable-split-sections --disable-tests
RUN cabal update && cabal install --dependencies-only --ghc-options="-optlo-Os -split-sections"
# Copy source and build it
COPY LICENSE Setup.hs shellcheck.hs shellcheck.1.md ./
COPY LICENSE shellcheck.hs ./
COPY src src
COPY test test
# This SED is the only "nastyness" we have to do
# Hopefully soon we could add per-component ld-options to cabal.project
RUN sed -i 's/-- STATIC/ld-options: -static -pthread -Wl,--gc-sections/' ShellCheck.cabal && \
cat ShellCheck.cabal && \
cabal new-build \
--disable-executable-dynamic --enable-split-sections --disable-tests && \
cp $(find dist-newstyle -type f -name shellcheck) . && \
strip --strip-all shellcheck && \
file shellcheck && \
ls -l shellcheck
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>"

26
Dockerfile.multi-arch Normal file
View File

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

187
README.md
View File

@@ -8,45 +8,45 @@ ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell
The goals of ShellCheck are
- To point out and clarify typical beginner's syntax issues that cause a shell
* To point out and clarify typical beginner's syntax issues that cause a shell
to give cryptic error messages.
- To point out and clarify typical intermediate level semantic problems that
* To point out and clarify typical intermediate level semantic problems that
cause a shell to behave strangely and counter-intuitively.
- To point out subtle caveats, corner cases and pitfalls that may cause an
* To point out subtle caveats, corner cases and pitfalls that may cause an
advanced user's otherwise working script to fail under future circumstances.
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)
- [Travis CI](#travis-ci)
- [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)
* [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
@@ -54,7 +54,7 @@ There are a number of ways to use ShellCheck!
### On the web
Paste a shell script on https://www.shellcheck.net for instant feedback.
Paste a shell script on <https://www.shellcheck.net> for instant feedback.
[ShellCheck.net](https://www.shellcheck.net) is always synchronized to the latest git commit, and is the easiest way to give ShellCheck a go. Tell your friends!
@@ -85,8 +85,46 @@ You can see ShellCheck suggestions directly in a variety of editors.
### 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.
ShellCheck makes canonical use of exit codes, and can output simple JSON, CheckStyle compatible XML, GCC compatible warnings as well as human readable text (with or without ANSI colors). See the [Integration](https://github.com/koalaman/shellcheck/wiki/Integration) wiki page for more documentation.
For example, in a Makefile:
```Makefile
check-scripts:
# Fail if any of these files have warnings
shellcheck myscripts/*.sh
```
or in a Travis CI `.travis.yml` file:
```yaml
script:
# Fail if any of these files have warnings
- shellcheck myscripts/*.sh
```
Services and platforms that have ShellCheck pre-installed and ready to use:
* [Travis CI](https://travis-ci.org/)
* [Codacy](https://www.codacy.com/)
* [Code Climate](https://codeclimate.com/)
* [Code Factor](https://www.codefactor.io/)
* [Github](https://github.com/features/actions)(only Linux)
Services and platforms with third party plugins:
* [SonarQube](https://www.sonarqube.org/) through [sonar-shellcheck-plugin](https://github.com/emerald-squad/sonar-shellcheck-plugin)
Most other services, including [GitLab](https://about.gitlab.com/), let you install
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
or by downloading and unpacking a [binary release](#installing-a-pre-compiled-binary).
It's a good idea to manually install a specific ShellCheck version regardless. This avoids
any surprise build breaks when a new version with new warnings is published.
For customized filtering or reporting, ShellCheck can output simple JSON, CheckStyle compatible XML,
GCC compatible warnings as well as human readable text (with or without ANSI colors). See the
[Integration](https://github.com/koalaman/shellcheck/wiki/Integration) wiki page for more documentation.
## Installing
@@ -141,15 +179,23 @@ On openSUSE
zypper in ShellCheck
Or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
Or use OneClickInstall - <https://software.opensuse.org/package/ShellCheck>
On Solus:
eopkg install shellcheck
On Windows (via [scoop](http://scoop.sh)):
On Windows (via [chocolatey](https://chocolatey.org/packages/shellcheck)):
scoop install shellcheck
```cmd
C:\> choco install shellcheck
```
Or Windows (via [scoop](http://scoop.sh)):
```cmd
C:\> scoop install shellcheck
```
From Snap Store:
@@ -158,42 +204,55 @@ From Snap Store:
From Docker Hub:
```sh
docker pull koalaman/shellcheck:stable # Or :v0.4.7 for that version, or :latest for daily builds
docker run -v "$PWD:/mnt" koalaman/shellcheck myscript
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.
Using the [nix package manager](https://nixos.org/nix):
```sh
nix-env -iA nixpkgs.shellcheck
```
Alternatively, you can download pre-compiled binaries for the latest release here:
* [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)
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-stable.zip)
* [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
* [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
* [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)
* [MacOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz)
* [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip)
or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds.
or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
(including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds).
Distro packages already come with a `man` page. If you are building from source, it can be installed with:
pandoc -s -t man shellcheck.1.md -o shellcheck.1
sudo mv shellcheck.1 /usr/share/man/man1
```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
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
If you still want to do so in order to upgrade at your leisure or ensure the latest release, follow the steps to install the shellcheck binary, bellow.
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 the shellcheck binary
### Installing a pre-compiled binary
*Pre-requisite*: the program 'xz' needs to be installed on the system.
To install it on debian/ubuntu/linux mint, run `apt install xz-utils`.
To install it on Redhat/Fedora/CentOS, run `yum -y install xz`.
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
export scversion="stable" # or "v0.4.7", or "latest"
wget "https://storage.googleapis.com/shellcheck/shellcheck-${scversion}.linux.x86_64.tar.xz"
tar --xz -xvf shellcheck-"${scversion}".linux.x86_64.tar.xz
cp shellcheck-"${scversion}"/shellcheck /usr/bin/
scversion="stable" # or "v0.4.7", or "latest"
wget -qO- "https://github.com/koalaman/shellcheck/releases/download/${scversion?}/shellcheck-${scversion?}.linux.x86_64.tar.xz" | tar -xJv
cp "shellcheck-${scversion}/shellcheck" /usr/bin/
shellcheck --version
```
@@ -207,11 +266,9 @@ ShellCheck is built and packaged using Cabal. Install the package `cabal-install
On MacOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source.
brew install cask
brew cask install haskell-platform
cabal install cabal-install
$ 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/
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
@@ -247,12 +304,15 @@ may use a legacy codepage. In `cmd.exe`, `powershell.exe` and Powershell ISE,
make sure to use a TrueType font, not a Raster font, and set the active
codepage to UTF-8 (65001) with `chcp`:
> chcp 65001
Active code page: 65001
```cmd
chcp 65001
```
In Powershell ISE, you may need to additionally update the output encoding:
> [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
```powershell
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
```
### Running tests
@@ -430,13 +490,13 @@ Alexander Tarasikov,
Issues can be ignored via environmental variable, command line, individually or globally within a file:
https://github.com/koalaman/shellcheck/wiki/Ignore
<https://github.com/koalaman/shellcheck/wiki/Ignore>
## Reporting bugs
Please use the GitHub issue tracker for any bugs or feature suggestions:
https://github.com/koalaman/shellcheck/issues
<https://github.com/koalaman/shellcheck/issues>
## Contributing
@@ -451,11 +511,12 @@ The contributor retains the copyright.
ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE).
Copyright 2012-2018, Vidar 'koala_man' Holen and contributors.
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

@@ -1,81 +0,0 @@
{-# LANGUAGE CPP #-}
{-# OPTIONS_GHC -Wall #-}
module Main (main) where
import Distribution.PackageDescription (
HookedBuildInfo,
emptyHookedBuildInfo )
import Distribution.Simple (
Args,
UserHooks ( preSDist ),
defaultMainWithHooks,
simpleUserHooks )
import Distribution.Simple.Setup ( SDistFlags )
import System.Process ( system )
import System.Directory ( doesFileExist, getModificationTime )
#ifndef MIN_VERSION_cabal_doctest
#define MIN_VERSION_cabal_doctest(x,y,z) 0
#endif
#if MIN_VERSION_cabal_doctest(1,0,0)
import Distribution.Extra.Doctest ( addDoctestsUserHook )
main :: IO ()
main = defaultMainWithHooks $ addDoctestsUserHook "doctests" myHooks
where
myHooks = simpleUserHooks { preSDist = myPreSDist }
#else
#ifdef MIN_VERSION_Cabal
-- If the macro is defined, we have new cabal-install,
-- but for some reason we don't have cabal-doctest in package-db
--
-- Probably we are running cabal sdist, when otherwise using new-build
-- workflow
#warning You are configuring this package without cabal-doctest installed. \
The doctests test-suite will not work as a result. \
To fix this, install cabal-doctest before configuring.
#endif
main :: IO ()
main = defaultMainWithHooks myHooks
where
myHooks = simpleUserHooks { preSDist = myPreSDist }
#endif
-- | This hook will be executed before e.g. @cabal sdist@. It runs
-- pandoc to create the man page from shellcheck.1.md. If the pandoc
-- command is not found, this will fail with an error message:
--
-- /bin/sh: pandoc: command not found
--
-- Since the man page is listed in the Extra-Source-Files section of
-- our cabal file, a failure here should result in a failure to
-- create the distribution tarball (that's a good thing).
--
myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo
myPreSDist _ _ = do
exists <- doesFileExist "shellcheck.1"
if exists
then do
source <- getModificationTime "shellcheck.1.md"
target <- getModificationTime "shellcheck.1"
if target < source
then makeManPage
else putStrLn "shellcheck.1 is more recent than shellcheck.1.md"
else makeManPage
return emptyHookedBuildInfo
where
makeManPage = do
putStrLn "Building the man page (shellcheck.1) with pandoc..."
putStrLn pandoc_cmd
result <- system pandoc_cmd
putStrLn $ "pandoc exited with " ++ show result
pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"

View File

@@ -1,5 +1,5 @@
Name: ShellCheck
Version: 0.6.0
Version: 0.7.1
Synopsis: Shell script analysis tool
License: GPL-3
License-file: LICENSE
@@ -7,7 +7,7 @@ Category: Static Analysis
Author: Vidar Holen
Maintainer: vidar@vidarholen.net
Homepage: https://www.shellcheck.net/
Build-Type: Custom
Build-Type: Simple
Cabal-Version: >= 1.8
Bug-reports: https://github.com/koalaman/shellcheck/issues
Description:
@@ -26,16 +26,12 @@ Extra-Source-Files:
-- documentation
README.md
shellcheck.1.md
-- built with a cabal sdist hook
shellcheck.1
custom-setup
setup-depends:
base >= 4 && <5,
directory >= 1.2 && <1.4,
process >= 1.0 && <1.7,
cabal-doctest >= 1.0.6 && <1.1,
Cabal >= 1.10 && <2.5
-- A script to build the man page using pandoc
manpage
-- convenience script for stripping tests
striptests
-- tests
test/shellcheck.hs
source-repository head
type: git
@@ -47,17 +43,19 @@ library
build-depends:
semigroups
build-depends:
-- GHC 7.6.3 (base 4.6.0.1) is buggy (#1131, #1119) in optimized mode.
-- Just disable that version entirely to fail fast.
aeson,
base > 4.6.0.1 && < 5,
array,
base >= 4.8.0.0 && < 5,
bytestring,
containers >= 0.5,
deepseq >= 1.4.0.0,
directory,
Diff >= 0.2.0,
directory >= 1.2.3.0,
mtl >= 2.2.1,
filepath,
parsec,
regex-tdfa,
QuickCheck >= 2.7.4,
-- When cabal supports it, move this to setup-depends:
process
exposed-modules:
@@ -68,13 +66,18 @@ library
ShellCheck.AnalyzerLib
ShellCheck.Checker
ShellCheck.Checks.Commands
ShellCheck.Checks.Custom
ShellCheck.Checks.ShellSupport
ShellCheck.Data
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.Regex
@@ -87,31 +90,37 @@ executable shellcheck
semigroups
build-depends:
aeson,
array,
base >= 4 && < 5,
bytestring,
deepseq >= 1.4.0.0,
ShellCheck,
containers,
directory,
deepseq >= 1.4.0.0,
Diff >= 0.2.0,
directory >= 1.2.3.0,
mtl >= 2.2.1,
filepath,
parsec >= 3.0,
regex-tdfa
QuickCheck >= 2.7.4,
regex-tdfa,
ShellCheck
main-is: shellcheck.hs
-- Marker to add flags for static linking
-- STATIC
test-suite test-shellcheck
type: exitcode-stdio-1.0
build-depends:
aeson,
array,
base >= 4 && < 5,
bytestring,
containers,
deepseq >= 1.4.0.0,
Diff >= 0.2.0,
directory >= 1.2.3.0,
mtl >= 2.2.1,
filepath,
parsec,
QuickCheck >= 2.7.4,
regex-tdfa,
ShellCheck
main-is: test/shellcheck.hs
test-suite doctests
type: exitcode-stdio-1.0
main-is: doctests.hs
build-depends:
base,
doctest >= 0.16.0 && <0.17,
QuickCheck >=2.11 && <2.13,
ShellCheck,
template-haskell
x-doctest-options: --fast
ghc-options: -Wall -threaded
hs-source-dirs: test

4
manpage Executable file
View File

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

View File

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

View File

@@ -3,10 +3,3 @@
# This allows testing changes without recompiling.
runghc -isrc -idist/build/autogen shellcheck.hs "$@"
# Note: with new-build you can
#
# % cabal new-run --disable-optimization -- shellcheck "$@"
#
# This does build the executable, but as the optimisation is disabled,
# the build is quite fast.

View File

@@ -1,21 +1,15 @@
#!/bin/bash
# shellcheck disable=SC2091
# quicktest runs the ShellCheck unit tests.
# Once `doctests` test executable is build, we can just run it
# This allows running tests without compiling library, which is faster.
#!/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.
$(find dist -type f -name doctests)
# Note: if you have build the project with new-build
#
# % cabal new-build -w ghc-8.4.3 --enable-tests
#
# and have cabal-plan installed (e.g. with cabal new-install cabal-plan),
# then you can quicktest with
#
# % $(cabal-plan list-bin doctests)
#
# Once the test executable exists, we can simply run it to perform doctests
# which use GHCi under the hood.
(
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

@@ -29,14 +29,13 @@ 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 files will also be reported.
sourced files will also be reported.
**-C**[*WHEN*],\ **--color**[=*WHEN*]
@@ -44,6 +43,13 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
is *auto*. **--color** without an argument is equivalent to
**--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**
@@ -56,16 +62,39 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
standard output. Subsequent **-f** options are ignored, see **FORMATS**
below for more information.
**-S**\ *SEVERITY*,\ **--severity=***severity*
**--list-optional**
: Specify minimum severity of errors to consider. Valid values are *error*,
*warning*, *info* and *style*. The default is *style*.
: 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*, *dash* and *ksh*.
The default is to use the file's shebang, or *bash* if the target shell
can't be determined.
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.
**-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**
@@ -78,11 +107,15 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
**-x**,\ **--external-sources**
: Follow 'source' statements even when the file is not specified as input.
: Follow `source` statements even when the file is not specified as input.
By default, `shellcheck` will only follow files specified on the command
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.
# FORMATS
@@ -119,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
@@ -169,17 +234,64 @@ 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.
**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 [ ! -z foo ] instead of suggesting -n
disable=SC2236
If no `.shellcheckrc` is found in any of the parent directories, ShellCheck
will look in `~/.shellcheckrc` followed by the XDG config directory
(usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on
Windows. Only the first file found will be used.
Note for Snap users: the Snap sandbox disallows access to hidden files.
Use `shellcheckrc` without the dot instead.
Note for Docker users: ShellCheck will only be able to look for files that
are mounted in the container, so `~/.shellcheckrc` will not be read.
# ENVIRONMENT VARIABLES
The environment variable `SHELLCHECK_OPTS` can be set with default flags:
export SHELLCHECK_OPTS='--shell=bash --exclude=SC2016'
@@ -189,7 +301,7 @@ invocation.
# RETURN VALUES
ShellCheck uses the follow exit codes:
ShellCheck uses the following exit codes:
+ 0: All files successfully scanned with no issues.
+ 1: All files successfully scanned with some issues.
@@ -198,6 +310,7 @@ ShellCheck uses the follow exit codes:
+ 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
@@ -206,20 +319,23 @@ 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`.
# AUTHOR
ShellCheck is written and maintained by Vidar Holen.
# 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-2015, Vidar Holen.
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,5 +1,5 @@
{-
Copyright 2012-2015 Vidar Holen
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -17,6 +17,7 @@
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 qualified ShellCheck.Analyzer
import ShellCheck.Checker
import ShellCheck.Data
import ShellCheck.Interface
@@ -24,9 +25,12 @@ 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
@@ -46,6 +50,7 @@ import System.Console.GetOpt
import System.Directory
import System.Environment
import System.Exit
import System.FilePath
import System.IO
data Flag = Flag String String
@@ -67,6 +72,7 @@ instance Monoid Status where
data Options = Options {
checkSpec :: CheckSpec,
externalSources :: Bool,
sourcePaths :: [FilePath],
formatterOptions :: FormatterOptions,
minSeverity :: Severity
}
@@ -74,6 +80,7 @@ data Options = Options {
defaultOptions = Options {
checkSpec = emptyCheckSpec,
externalSources = False,
sourcePaths = [],
formatterOptions = newFormatterOptions {
foColorOption = ColorAuto
},
@@ -87,11 +94,23 @@ options = [
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",
Option "f" ["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 (sh, bash, dash, ksh)",
@@ -102,10 +121,13 @@ options = [
(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.",
"The number of wiki links to show, when applicable",
Option "x" ["external-sources"]
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES"
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES",
Option "" ["help"]
(NoArg $ Flag "help" "true") "Show this usage summary and exit"
]
getUsageInfo = usageInfo usageHeader options
printErr = lift . hPutStrLn stderr
@@ -114,15 +136,18 @@ parseArguments argv =
case getOpt Permute options argv of
(opts, files, []) -> return (opts, files)
(_, _, errors) -> do
printErr $ concat errors ++ "\n" ++ usageInfo usageHeader options
printErr $ concat errors ++ "\n" ++ getUsageInfo
throwError SyntaxFailure
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),
("tty", ShellCheck.Formatter.TTY.format options)
("json1", ShellCheck.Formatter.JSON1.format),
("tty", ShellCheck.Formatter.TTY.format options),
("quiet", ShellCheck.Formatter.Quiet.format options)
]
formatList = intercalate ", " names
@@ -267,10 +292,30 @@ parseOption flag options =
}
}
Flag "include" str -> do
new <- mapM parseNum $ filter (not . null) $ split ',' str
let old = csIncludedWarnings . checkSpec $ options
return options {
checkSpec = (checkSpec options) {
csIncludedWarnings =
if null new
then old
else Just new `mappend` old
}
}
Flag "version" _ -> do
liftIO printVersion
throwError NoProblems
Flag "list-optional" _ -> do
liftIO printOptional
throwError NoProblems
Flag "help" _ -> do
liftIO $ putStrLn getUsageInfo
throwError NoProblems
Flag "externals" _ ->
return options {
externalSources = True
@@ -284,6 +329,12 @@ parseOption flag options =
}
}
Flag "source-path" str -> do
let paths = splitSearchPath str
return options {
sourcePaths = (sourcePaths options) ++ paths
}
Flag "sourced" _ ->
return options {
checkSpec = (checkSpec options) {
@@ -307,7 +358,26 @@ parseOption flag options =
}
}
_ -> return options
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
printErr s
@@ -322,12 +392,16 @@ parseOption flag options =
ioInterface options files = do
inputs <- mapM normalize files
cache <- newIORef emptyCache
configCache <- newIORef ("", Nothing)
return SystemInterface {
siReadFile = get cache inputs
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
@@ -344,7 +418,6 @@ ioInterface options files = do
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
@@ -362,6 +435,88 @@ ioInterface options files = do
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
findM p = foldr go (pure Nothing)
where
go x acc = do
b <- p x
if b then pure (Just x) else acc
findSourceFile inputs sourcePathFlag currentScript sourcePathAnnotation original =
if isAbsolute original
then
let (_, relative) = splitDrive original
in find relative original
else
find original original
where
find filename deflt = do
sources <- findM ((allowable inputs) `andM` doesFileExist) $
(adjustPath filename):(map (</> filename) $ map adjustPath $ sourcePathFlag ++ sourcePathAnnotation)
case sources of
Nothing -> return deflt
Just first -> return first
scriptdir = dropFileName currentScript
adjustPath str =
case (splitDirectories str) of
("SCRIPTDIR":rest) -> joinPath (scriptdir:rest)
_ -> str
inputFile file = do
(handle, shouldCache) <-
if file == "-"
@@ -418,3 +573,14 @@ printVersion = do
putStrLn $ "version: " ++ shellcheckVersion
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 ""

View File

@@ -23,7 +23,8 @@ description: |
# snap connect shellcheck:removable-media
version: git
grade: devel
base: core18
grade: stable
confinement: strict
apps:
@@ -34,11 +35,11 @@ apps:
parts:
shellcheck:
plugin: dump
source: ./
source: .
build-packages:
- cabal-install
- squid3
build: |
- squid
override-build: |
# See comments in .snapsquid.conf
[ "$http_proxy" ] && {
squid3 -f .snapsquid.conf
@@ -48,6 +49,6 @@ parts:
cabal sandbox init
cabal update || cat /var/log/squid/*
cabal install -j
install: |
install -d $SNAPCRAFT_PART_INSTALL/usr/bin
install .cabal-sandbox/bin/shellcheck $SNAPCRAFT_PART_INSTALL/usr/bin

View File

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

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2015 Vidar Holen
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -25,6 +25,7 @@ import Control.Monad.Writer
import Control.Monad
import Data.Char
import Data.Functor
import Data.Functor.Identity
import Data.List
import Data.Maybe
@@ -46,6 +47,7 @@ willSplit x =
T_BraceExpansion {} -> True
T_Glob {} -> True
T_Extglob {} -> True
T_DoubleQuoted _ l -> any willBecomeMultipleArgs l
T_NormalWord _ l -> any willSplit l
_ -> False
@@ -81,7 +83,7 @@ oversimplify token =
(T_NormalWord _ l) -> [concat (concatMap oversimplify l)]
(T_DoubleQuoted _ l) -> [concat (concatMap oversimplify l)]
(T_SingleQuoted _ s) -> [s]
(T_DollarBraced _ _) -> ["${VAR}"]
(T_DollarBraced _ _ _) -> ["${VAR}"]
(T_DollarArithmetic _ _) -> ["${VAR}"]
(T_DollarExpansion _ _) -> ["${VAR}"]
(T_Backticked _ _) -> ["${VAR}"]
@@ -133,11 +135,11 @@ isUnquotedFlag token = fromMaybe False $ do
return $ "-" `isPrefixOf` str
-- Given a T_DollarBraced, return a simplified version of the string contents.
bracedString (T_DollarBraced _ l) = concat $ oversimplify l
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 _ _) =
isArrayExpansion t@(T_DollarBraced _ _ _) =
let string = bracedString t in
"@" `isPrefixOf` string ||
not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string
@@ -146,7 +148,7 @@ isArrayExpansion _ = False
-- Is it possible that this arg becomes multiple args?
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
where
f t@(T_DollarBraced _ _) =
f t@(T_DollarBraced _ _ _) =
let string = bracedString t in
"!" `isPrefixOf` string
f (T_DoubleQuoted _ parts) = any f parts
@@ -175,9 +177,13 @@ willConcatInAssignment token =
getLiteralString :: Token -> Maybe String
getLiteralString = getLiteralStringExt (const Nothing)
-- Definitely get a literal string, with a given default for all non-literals
getLiteralStringDef :: String -> Token -> String
getLiteralStringDef x = runIdentity . getLiteralStringExt (const $ return x)
-- Definitely get a literal string, skipping over all non-literals
onlyLiteralString :: Token -> String
onlyLiteralString = fromJust . getLiteralStringExt (const $ return "")
onlyLiteralString = getLiteralStringDef ""
-- Maybe get a literal string, but only if it's an unquoted argument.
getUnquotedLiteral (T_NormalWord _ list) =
@@ -216,7 +222,7 @@ getGlobOrLiteralString = getLiteralStringExt f
-- 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 :: Monad m => (Token -> m String) -> Token -> m String
getLiteralStringExt more = g
where
allInList = fmap concat . mapM g
@@ -297,6 +303,11 @@ getCommand t =
getCommandName :: Token -> Maybe String
getCommandName = fst . getCommandNameAndToken
-- Maybe get the name+arguments of a command.
getCommandArgv t = do
(T_SimpleCommand _ _ args@(_:_)) <- getCommand t
return args
-- Get the command name token from a command, i.e.
-- the token representing 'ls' in 'ls -la 2> foo'.
-- If it can't be determined, return the original token.
@@ -351,22 +362,34 @@ isOnlyRedirection t =
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.
-- the conditions and bodies 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_WhileExpression _ cond cmds -> [cond, cmds]
T_UntilExpression _ cond cmds -> [cond, cmds]
T_ForIn _ _ _ cmds -> [cmds]
T_ForArithmetic _ _ _ _ cmds -> [cmds]
T_IfExpression _ thens elses -> map snd thens ++ [elses]
T_IfExpression _ thens elses -> (concatMap (\(a,b) -> [a,b]) thens) ++ [elses]
T_Annotation _ _ t -> getCommandSequences t
T_DollarExpansion _ cmds -> [cmds]
T_DollarBraceCommandExpansion _ cmds -> [cmds]
T_Backticked _ cmds -> [cmds]
_ -> []
-- Get a list of names of associative arrays
@@ -374,7 +397,7 @@ getAssociativeArrays t =
nub . execWriter $ doAnalysis f t
where
f :: Token -> Writer [String] ()
f t@T_SimpleCommand {} = fromMaybe (return ()) $ do
f t@T_SimpleCommand {} = sequence_ $ do
name <- getCommandName t
let assocNames = ["declare","local","typeset"]
guard $ elem name assocNames
@@ -485,8 +508,21 @@ wordsCanBeEqual x y = fromMaybe True $
-- 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
T_DollarBraced {} -> 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

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2015 Vidar Holen
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -17,7 +17,7 @@
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) where
module ShellCheck.Analyzer (analyzeScript, ShellCheck.Analyzer.optionalChecks) where
import ShellCheck.Analytics
import ShellCheck.AnalyzerLib
@@ -25,6 +25,7 @@ import ShellCheck.Interface
import Data.List
import Data.Monoid
import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.Custom
import qualified ShellCheck.Checks.ShellSupport
@@ -34,12 +35,18 @@ analyzeScript spec = newAnalysisResult {
arComments =
filterByAnnotation spec params . nub $
runAnalytics spec
++ runChecker params (checkers params)
++ runChecker params (checkers spec params)
}
where
params = makeParameters spec
checkers params = mconcat $ map ($ params) [
ShellCheck.Checks.Commands.checker,
checkers spec params = mconcat $ map ($ params) [
ShellCheck.Checks.Commands.checker spec,
ShellCheck.Checks.Custom.checker,
ShellCheck.Checks.ShellSupport.checker
]
optionalChecks = mconcat $ [
ShellCheck.Analytics.optionalChecks,
ShellCheck.Checks.Commands.optionalChecks
]

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2015 Vidar Holen
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -18,29 +18,30 @@
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 qualified Data.Map as Map
import Data.Maybe
import Data.Semigroup
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Regex
prop :: Bool -> IO ()
prop False = putStrLn "FAIL"
prop True = return ()
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
@@ -76,14 +77,22 @@ composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis
composeAnalyzers f g x = f x >> g x
data Parameters = Parameters {
hasLastpipe :: Bool, -- Whether this script has the 'lastpipe' option set/default.
hasSetE :: Bool, -- Whether this script has 'set -e' anywhere.
variableFlow :: [StackData], -- A linear (bad) analysis of data flow
parentMap :: Map.Map Id Token, -- A map from Id to parent Token
shellType :: Shell, -- The shell type, such as Bash or Ksh
shellTypeSpecified :: Bool, -- True if shell type was forced via flags
rootNode :: Token, -- The root node of the AST
tokenPositions :: Map.Map Id (Position, Position) -- map from token id to start and end position
-- 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
@@ -154,11 +163,14 @@ 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 id code str fix = addComment $
let comment = makeComment WarningC id code str in
comment {
tcFix = Just fix
}
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 =
@@ -171,7 +183,7 @@ makeCommentWithFix severity id code str fix =
makeParameters spec =
let params = Parameters {
rootNode = root,
shellType = fromMaybe (determineShell root) $ asShellType spec,
shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec,
hasSetE = containsSetE root,
hasLastpipe =
case shellType params of
@@ -180,7 +192,7 @@ makeParameters spec =
Sh -> False
Ksh -> True,
shellTypeSpecified = isJust $ asShellType spec,
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
parentMap = getParentTree root,
variableFlow = getVariableFlow params root,
tokenPositions = asTokenPositions spec
@@ -194,7 +206,7 @@ containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
where
isSetE t =
case t of
T_Script _ str _ -> str `matches` re
T_Script _ (T_Literal _ str) _ -> str `matches` re
T_SimpleCommand {} ->
t `isUnqualifiedCommand` "set" &&
("errexit" `elem` oversimplify t ||
@@ -215,37 +227,34 @@ containsLastpipe root =
_ -> False
-- |
-- >>> prop $ determineShellTest "#!/bin/sh" == Sh
-- >>> prop $ determineShellTest "#!/usr/bin/env ksh" == Ksh
-- >>> prop $ determineShellTest "" == Bash
-- >>> prop $ determineShellTest "#!/bin/sh -e" == Sh
-- >>> prop $ determineShellTest "#!/bin/ksh\n#shellcheck shell=sh\nfoo" == Sh
-- >>> prop $ determineShellTest "#shellcheck shell=sh\nfoo" == Sh
-- >>> prop $ determineShellTest "#! /bin/sh" == Sh
-- >>> prop $ determineShellTest "#! /bin/ash" == Dash
determineShellTest = determineShell . fromJust . prRoot . pScript
determineShell t = fromMaybe Bash $ do
shellString <- foldl mplus Nothing $ getCandidates t
shellForExecutable shellString
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 $
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 _ s t) = executableFromShebang s
shellString = getCandidate t
getCandidate :: Token -> String
getCandidate t@T_Script {} = fromShebang t
getCandidate (T_Annotation _ annotations s) =
headOrDefault (fromShebang s) [s | ShellOverride s <- annotations]
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 | "/env " `isInfixOf` s = headOrDefault "" (drop 1 $ words s)
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
@@ -285,7 +294,7 @@ isQuoteFree = isQuoteFreeNode False
isQuoteFreeNode strict tree t =
(isQuoteFreeElement t == Just True) ||
head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False])
headOrDefault False (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t))
where
-- Is this node self-quoting in itself?
isQuoteFreeElement t =
@@ -376,10 +385,6 @@ isParentOf tree parent child =
parents params = getPath (parentMap params)
pathTo t = do
parents <- reader parentMap
return $ getPath parents t
-- Find the first match in a list where the predicate is Just True.
-- Stops if it's Just False and ignores Nothing.
findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a
@@ -423,6 +428,7 @@ getVariableFlow params t =
assignFirst T_ForIn {} = True
assignFirst T_SelectIn {} = True
assignFirst (T_BatsTest {}) = True
assignFirst _ = False
setRead t =
@@ -440,9 +446,10 @@ leadType params t =
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
if causesSubshell == Just True
then SubshellScope "pipeline"
else NoneScope
_ -> NoneScope
@@ -480,6 +487,12 @@ getModifiedVariables t =
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
@@ -492,10 +505,10 @@ getModifiedVariables t =
guard . not . null $ str
return (t, token, str, DataString SourceChecked)
T_DollarBraced _ l -> maybeToList $ do
T_DollarBraced _ _ l -> maybeToList $ do
let string = bracedString t
let modifier = getBracedModifier string
guard $ ":=" `isPrefixOf` modifier
guard $ any (`isPrefixOf` modifier) ["=", ":="]
return (t, t, getBracedReference string, DataString $ SourceFrom [l])
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo
@@ -528,14 +541,11 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
(not $ any (`elem` flags) ["f", "F"])
then concatMap getReference rest
else []
"readonly" ->
if any (`elem` flags) ["f", "p"]
then []
else concatMap getReference rest
"trap" ->
case rest of
head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head
head:_ -> map (\x -> (base, head, x)) $ getVariablesFromLiteralToken head
_ -> []
"alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token]
_ -> []
where
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
@@ -553,9 +563,11 @@ getReferencedVariableCommand _ = []
-- 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)) =
getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T_Literal _ x:_):rest)) =
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
case x of
"builtin" ->
getModifiedVariableCommand $ T_SimpleCommand id cmdPrefix rest
"read" ->
let params = map getLiteral rest
readArrayVars = getReadArrayVariables rest
@@ -588,11 +600,15 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
"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
stripEquals s = drop 1 $ dropWhile (/= '=') s
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]]) =
@@ -623,7 +639,7 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
getModifierParam _ _ = []
letParamToLiteral token =
if var == ""
if null var
then []
else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])]
where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token
@@ -639,7 +655,11 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
where
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, var, DataString $ SourceFrom list)
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, varName, varType $ SourceFrom list)
where
(varName, varType) = case elemIndex '[' var of
Just i -> (take i var, DataArray)
Nothing -> (var, DataString)
f (_:rest) = f rest
f [] = fail "not found"
@@ -653,9 +673,22 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
return (base, lastArg, name, DataArray SourceExternal)
-- get all the array variables used in read, e.g. read -a arr
getReadArrayVariables args = do
getReadArrayVariables args =
map (getLiteralArray . snd)
(filter (\(x,_) -> getLiteralString x == Just "-a") (zip (args) (tail args)))
(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 _ = []
@@ -666,11 +699,10 @@ getIndexReferences s = fromMaybe [] $ do
where
re = mkRegex "(\\[.*\\])"
-- |
-- >>> prop $ getOffsetReferences ":bar" == ["bar"]
-- >>> prop $ getOffsetReferences ":bar:baz" == ["bar", "baz"]
-- >>> prop $ getOffsetReferences "[foo]:bar" == ["bar"]
-- >>> prop $ getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
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
@@ -681,7 +713,7 @@ getOffsetReferences mods = fromMaybe [] $ do
getReferencedVariables parents t =
case t of
T_DollarBraced id l -> let str = bracedString t in
T_DollarBraced id _ l -> let str = bracedString t in
(t, t, getBracedReference str) :
map (\x -> (l, l, x)) (
getIndexReferences str
@@ -700,6 +732,12 @@ getReferencedVariables parents t =
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
@@ -720,9 +758,8 @@ getReferencedVariables parents t =
_ -> Nothing
getIfReference context token = maybeToList $ do
str <- getLiteralStringExt literalizer token
guard . not $ null str
when (isDigit $ head str) $ fail "is a number"
str@(h:_) <- getLiteralStringExt literalizer token
when (isDigit h) $ fail "is a number"
return (context, token, getBracedReference str)
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
@@ -742,61 +779,53 @@ isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `
-- 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)
isCommandMatch token matcher = maybe False
matcher (getCommandName token)
-- |
-- Does this regex look like it was intended as a glob?
--
-- >>> isConfusedGlobRegex "*foo*"
-- True
--
-- >>> isConfusedGlobRegex ".*foo.*"
-- False
--
-- True: *foo*
-- False: .*foo.*
isConfusedGlobRegex :: String -> Bool
isConfusedGlobRegex ('*':_) = True
isConfusedGlobRegex [x,'*'] | x /= '\\' = 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 $ isVariableName "_fo123"
-- >>> prop $ not $ isVariableName "4"
-- >>> prop $ not $ isVariableName "test: "
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)
getVariablesFromLiteral (getLiteralStringDef " " token)
-- Try to get referenced variables from a literal string like "$foo"
-- Ignores tons of cases like arithmetic evaluation and array indices.
-- >>> prop $ getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
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 $ getBracedReference "foo" == "foo"
-- >>> prop $ getBracedReference "#foo" == "foo"
-- >>> prop $ getBracedReference "#" == "#"
-- >>> prop $ getBracedReference "##" == "#"
-- >>> prop $ getBracedReference "#!" == "!"
-- >>> prop $ getBracedReference "!#" == "#"
-- >>> prop $ getBracedReference "!foo#?" == "foo"
-- >>> prop $ getBracedReference "foo-bar" == "foo"
-- >>> prop $ getBracedReference "foo:-bar" == "foo"
-- >>> prop $ getBracedReference "foo: -1" == "foo"
-- >>> prop $ getBracedReference "!os*" == ""
-- >>> prop $ getBracedReference "!os?bar**" == ""
-- >>> prop $ getBracedReference "foo[bar]" == "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
@@ -819,11 +848,10 @@ getBracedReference s = fromMaybe s $
return ""
nameExpansion _ = Nothing
-- |
-- >>> prop $ getBracedModifier "foo:bar:baz" == ":bar:baz"
-- >>> prop $ getBracedModifier "!var:-foo" == ":-foo"
-- >>> prop $ getBracedModifier "foo[bar]" == "[bar]"
getBracedModifier s = fromMaybe "" . listToMaybe $ do
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
getBracedModifier s = headOrDefault "" $ do
let var = getBracedReference s
a <- dropModifier s
dropPrefix var a
@@ -837,18 +865,6 @@ getBracedModifier s = fromMaybe "" . listToMaybe $ do
-- 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
@@ -872,18 +888,17 @@ filterByAnnotation asSpec params =
shouldIgnore note =
any (shouldIgnoreFor (getCode note)) $
getPath parents (T_Bang $ tcId note)
shouldIgnoreFor num (T_Annotation _ anns _) =
any hasNum anns
where
hasNum (DisableComment ts) = num == ts
hasNum _ = False
shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec
shouldIgnoreFor _ _ = False
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) =
isCountingReference (T_DollarBraced id _ token) =
case concat $ oversimplify token of
'#':_ -> True
_ -> False
@@ -892,7 +907,7 @@ isCountingReference _ = False
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
isQuotedAlternativeReference t =
case t of
T_DollarBraced _ _ ->
T_DollarBraced _ _ _ ->
getBracedModifier (bracedString t) `matches` re
_ -> False
where
@@ -904,12 +919,11 @@ isQuotedAlternativeReference t =
-- 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
getGnuOpts str t = getOpts str $ getAllFlags t
getBsdOpts str t = getOpts str $ getLeadingFlags t
getOpts :: String -> [(Token, String)] -> Maybe [(String, Token)]
getOpts string flags = process flags
where
flags = flagTokenizer cmd
flagList (c:':':rest) = ([c], True) : flagList rest
flagList (c:rest) = ([c], False) : flagList rest
flagList [] = []
@@ -924,9 +938,23 @@ getOpts flagTokenizer string cmd = process flags
takesArg <- Map.lookup flag1 flagMap
if takesArg
then do
guard $ flag2 == ""
guard $ null flag2
more <- process rest
return $ (flag1, token2) : more
else do
more <- process rest2
return $ (flag1, token1) : more
supportsArrays shell = shell == Bash || shell == Ksh
-- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh)
isBashLike :: Parameters -> Bool
isBashLike params =
case shellType params of
Bash -> True
Ksh -> True
Dash -> False
Sh -> False
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2015 Vidar Holen
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -17,7 +17,8 @@
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.Checker (checkScript) where
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
import ShellCheck.Interface
import ShellCheck.Parser
@@ -34,6 +35,8 @@ 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 {
@@ -45,6 +48,17 @@ tokenToPosition startMap t = fromMaybe fail $ do
where
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
shellFromFilename filename = listToMaybe candidates
where
shellExtensions = [(".ksh", Ksh)
,(".bash", Bash)
,(".bats", Bash)
,(".dash", Dash)]
-- The `.sh` is too generic to determine the shell:
-- We fallback to Bash in this case and emit SC2148 if there is no shebang
candidates =
[sh | (ext,sh) <- shellExtensions, ext `isSuffixOf` filename]
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
checkScript sys spec = do
results <- checkScript (csScript spec)
@@ -58,6 +72,7 @@ checkScript sys spec = do
psFilename = csFilename spec,
psScript = contents,
psCheckSourced = csCheckSourced spec,
psIgnoreRC = csIgnoreRC spec,
psShellTypeOverride = csShellTypeOverride spec
}
let parseMessages = prComments result
@@ -66,26 +81,30 @@ checkScript sys spec = do
as {
asScript = root,
asShellType = csShellTypeOverride spec,
asFallbackShell = shellFromFilename $ csFilename spec,
asCheckSourced = csCheckSourced spec,
asExecutionMode = Executed,
asTokenPositions = tokenPositions
asTokenPositions = tokenPositions,
asOptionalChecks = csOptionalChecks spec
} where as = newAnalysisSpec root
let analysisMessages =
fromMaybe [] $
maybe []
(arComments . analyzeScript . analysisSpec)
<$> prRoot result
$ prRoot result
let translator = tokenToPosition tokenPositions
return . nub . sortMessages . filter shouldInclude $
(parseMessages ++ map translator analysisMessages)
shouldInclude pc =
let code = cCode (pcComment 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)
in
code `notElem` csExcludedWarnings spec &&
severity <= csMinSeverity spec
sortMessages = sortBy (comparing order)
sortMessages = sortOn order
order pc =
let pos = pcStartPos pc
comment = pcComment pc in
@@ -122,132 +141,261 @@ checkRecursive includes src =
csCheckSourced = True
}
-- | Dummy binding for doctest to run
--
-- >>> check "echo \"$12\""
-- [1037]
--
-- >>> check "#shellcheck disable=SC1037\necho \"$12\""
-- []
--
-- >>> check "#shellcheck disable=SC1037\n#lol\necho \"$12\""
-- []
--
-- >>> check "echo $1"
-- [2086]
--
-- >>> check "#shellcheck disable=SC2086\necho $1"
-- []
--
-- >>> check "#shellcheck disable=SC2086\n#lol\necho $1"
-- []
--
-- >>> :{
-- getErrors
-- (mockedSystemInterface [])
-- emptyCheckSpec {
-- csScript = "echo $1",
-- csExcludedWarnings = [2148, 2086]
-- }
-- :}
-- []
--
-- >>> :{
-- getErrors
-- (mockedSystemInterface [])
-- emptyCheckSpec {
-- csScript = "echo \"$10\"",
-- csExcludedWarnings = [2148, 1037]
-- }
-- :}
-- []
--
-- >>> check "#!/usr/bin/python\ntrue $1\n"
-- [1071]
--
-- >>> :{
-- getErrors
-- (mockedSystemInterface [])
-- emptyCheckSpec {
-- csScript = "#!/usr/bin/python\ntrue\n",
-- csShellTypeOverride = Just Sh
-- }
-- :}
-- []
--
-- >>> check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
-- []
--
-- >>> check "source /dev/null"
-- []
--
-- >>> check "source lol; echo \"$bar\""
-- [1091,2154]
--
-- >>> checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\""
-- []
--
-- >>> checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\""
-- []
--
-- >>> checkWithIncludes [("lib", "source lib")] "source lib"
-- []
--
-- >>> checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
-- [1094,2086]
--
-- >>> checkWithIncludes [("lib", "")] ". \"$1\""
-- [1090]
--
-- >>> checkWithIncludes [("lib", "")] "source ~/foo"
-- [1090]
--
-- >>> checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
-- []
--
-- >>> checkRecursive [("lib", "echo $1")] "source lib"
-- [2086]
--
-- >>> checkRecursive [("lib", "echo \"$10\"")] "source lib"
-- [1037]
--
-- >>> checkWithIncludes [("foo", "source bar"), ("bar", "baz=3")] "#shellcheck source=foo\n. \"$1\"; echo \"$baz\""
-- []
--
-- >>> check "#!/bin/sh\necho $1"
-- [2086]
--
-- >>> check "#!/bin/sh\n# shellcheck disable=2086\necho $1"
-- []
--
-- >>> check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
-- []
--
-- >>> check "#!/bin/sh\n#unrelated\n# shellcheck disable=2086\ntrue\necho $1"
-- []
--
-- >>> check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
-- []
--
-- >>> check "#!/bin/sh\n\n\n\n#shellcheck disable=2086\ntrue\necho $1"
-- []
--
-- >>> check "#shellcheck shell=sh\n#unrelated\n#shellcheck disable=2086\ntrue\necho $1"
-- []
--
-- >>> check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
-- []
--
-- check "true\n[ $? == 0 ] && echo $1"
-- [2086, 2181]
--
-- check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
-- []
--
-- >>> 2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
-- True
--
-- >>> check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n"
-- []
doctests :: ()
doctests = ()
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 =
null $ check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
prop_canParseDevNull =
null $ 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 =
null $ 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 =
null $ checkWithIncludes [("lib", "echo $1")] "source lib"
prop_nonRecursiveParsing =
null $ 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_canDisableShebangWarning = null $ result
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.sh",
csScript = "#shellcheck disable=SC2148\nfoo"
}
prop_shExtensionDoesntMatter = result == [2148]
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.sh",
csScript = "echo 'hello world'"
}
prop_sourcedFileUsesOriginalShellExtension = result == [2079]
where
result = checkWithSpec [("file.ksh", "(( 3.14 ))")] emptyCheckSpec {
csFilename = "file.bash",
csScript = "source file.ksh",
csCheckSourced = True
}
prop_canEnableOptionalsWithSpec = result == [2244]
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.sh",
csScript = "#!/bin/sh\n[ \"$1\" ]",
csOptionalChecks = ["avoid-nullary-conditions"]
}
prop_optionIncludes1 =
-- expect 2086, but not included, so nothing reported
null $ checkOptionIncludes (Just [2080]) "#!/bin/sh\n var='a b'\n echo $var"
prop_optionIncludes2 =
-- expect 2086, included, so it is reported
[2086] == checkOptionIncludes (Just [2086]) "#!/bin/sh\n var='a b'\n echo $var"
prop_optionIncludes3 =
-- expect 2086, no inclusions provided, so it is reported
[2086] == checkOptionIncludes Nothing "#!/bin/sh\n var='a b'\n echo $var"
prop_optionIncludes4 =
-- expect 2086 & 2154, only 2154 included, so only that's reported
[2154] == checkOptionIncludes (Just [2154]) "#!/bin/sh\n var='a b'\n echo $var\n echo $bar"
prop_readsRcFile = null result
where
result = checkWithRc "disable=2086" emptyCheckSpec {
csScript = "#!/bin/sh\necho $1",
csIgnoreRC = False
}
prop_canUseNoRC = result == [2086]
where
result = checkWithRc "disable=2086" emptyCheckSpec {
csScript = "#!/bin/sh\necho $1",
csIgnoreRC = True
}
prop_NoRCWontLookAtFile = result == [2086]
where
result = checkWithRc (error "Fail") emptyCheckSpec {
csScript = "#!/bin/sh\necho $1",
csIgnoreRC = True
}
prop_brokenRcGetsWarning = result == [1134, 2086]
where
result = checkWithRc "rofl" emptyCheckSpec {
csScript = "#!/bin/sh\necho $1",
csIgnoreRC = False
}
prop_canEnableOptionalsWithRc = result == [2244]
where
result = checkWithRc "enable=avoid-nullary-conditions" emptyCheckSpec {
csScript = "#!/bin/sh\n[ \"$1\" ]"
}
prop_sourcePathRedirectsName = result == [2086]
where
f "dir/myscript" _ "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource lib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_sourcePathAddsAnnotation = result == [2086]
where
f "dir/myscript" ["mypath"] "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source-path=mypath\nsource lib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_sourcePathRedirectsDirective = result == [2086]
where
f "dir/myscript" _ "lib" = return "foo/lib"
f _ _ _ = return "/dev/null"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source=lib\nsource kittens",
csFilename = "dir/myscript",
csCheckSourced = True
}
return []
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

@@ -17,8 +17,9 @@
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) where
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
import ShellCheck.AST
import ShellCheck.ASTLib
@@ -29,9 +30,13 @@ import ShellCheck.Regex
import Control.Monad
import Control.Monad.RWS
import Data.Char
import Data.Functor.Identity
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)
@@ -64,13 +69,12 @@ testChecker (ForShell _ t) =
verify c s = producesComments (testChecker c) s == Just True
verifyNot c s = producesComments (testChecker c) s == Just False
-- |
-- >>> prop $ verify checkForDecimals "((3.14*c))"
-- >>> prop $ verify checkForDecimals "foo[1.2]=bar"
-- >>> prop $ verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
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
f t@(TA_Expansion id _) = sequence_ $ do
str <- getLiteralString t
first <- str !!! 0
guard $ isDigit first && '.' `elem` str
@@ -78,63 +82,102 @@ checkForDecimals = ForShell [Sh, Dash, Bash] f
f _ = return ()
-- |
-- >>> prop $ verify checkBashisms "while read a; do :; done < <(a)"
-- >>> prop $ verify checkBashisms "[ foo -nt bar ]"
-- >>> prop $ verify checkBashisms "echo $((i++))"
-- >>> prop $ verify checkBashisms "rm !(*.hs)"
-- >>> prop $ verify checkBashisms "source file"
-- >>> prop $ verify checkBashisms "[ \"$a\" == 42 ]"
-- >>> prop $ verify checkBashisms "echo ${var[1]}"
-- >>> prop $ verify checkBashisms "echo ${!var[@]}"
-- >>> prop $ verify checkBashisms "echo ${!var*}"
-- >>> prop $ verify checkBashisms "echo ${var:4:12}"
-- >>> prop $ verifyNot checkBashisms "echo ${var:-4}"
-- >>> prop $ verify checkBashisms "echo ${var//foo/bar}"
-- >>> prop $ verify checkBashisms "exec -c env"
-- >>> prop $ verify checkBashisms "echo -n \"Foo: \""
-- >>> prop $ verify checkBashisms "let n++"
-- >>> prop $ verify checkBashisms "echo $RANDOM"
-- >>> prop $ verify checkBashisms "echo $((RANDOM%6+1))"
-- >>> prop $ verify checkBashisms "foo &> /dev/null"
-- >>> prop $ verify checkBashisms "foo > file*.txt"
-- >>> prop $ verify checkBashisms "read -ra foo"
-- >>> prop $ verify checkBashisms "[ -a foo ]"
-- >>> prop $ verifyNot checkBashisms "[ foo -a bar ]"
-- >>> prop $ verify checkBashisms "trap mything ERR INT"
-- >>> prop $ verifyNot checkBashisms "trap mything INT TERM"
-- >>> prop $ verify checkBashisms "cat < /dev/tcp/host/123"
-- >>> prop $ verify checkBashisms "trap mything ERR SIGTERM"
-- >>> prop $ verify checkBashisms "echo *[^0-9]*"
-- >>> prop $ verify checkBashisms "exec {n}>&2"
-- >>> prop $ verify checkBashisms "echo ${!var}"
-- >>> prop $ verify checkBashisms "printf -v '%s' \"$1\""
-- >>> prop $ verify checkBashisms "printf '%q' \"$1\""
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
-- >>> prop $ verify checkBashisms "#!/bin/sh\necho -n foo"
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\necho -n foo"
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\nlocal foo"
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
-- >>> prop $ verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
-- >>> prop $ verify checkBashisms "RANDOM=9; echo $RANDOM"
-- >>> prop $ verify checkBashisms "foo-bar() { true; }"
-- >>> prop $ verify checkBashisms "echo $(<file)"
-- >>> prop $ verify checkBashisms "echo `<file`"
-- >>> prop $ verify checkBashisms "trap foo int"
-- >>> prop $ verify checkBashisms "trap foo sigint"
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
-- >>> prop $ verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
-- >>> prop $ verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
-- >>> prop $ verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
-- >>> prop $ verify checkBashisms "#!/bin/sh\ncmd >& file"
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
-- >>> prop $ verify checkBashisms "#!/bin/sh\nfoo+=bar"
-- >>> prop $ verify checkBashisms "#!/bin/sh\necho ${@%foo}"
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\necho ${##}"
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
@@ -163,12 +206,14 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
unless isDash $ warnMsg id $ "lexicographical " ++ op ++ " is"
bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "-nt", "-ef" ] =
| op `elem` [ "-ot", "-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 _)
@@ -193,7 +238,7 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
bashism t@(TA_Variable id str _) | isBashVariable str =
warnMsg id $ str ++ " is"
bashism t@(T_DollarBraced id token) = do
bashism t@(T_DollarBraced id _ token) = do
mapM_ check expansion
when (isBashVariable var) $
warnMsg id $ var ++ " is"
@@ -221,20 +266,71 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
warnMsg id "`<file` to read files is"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "echo" && "-" `isPrefixOf` argString =
unless ("--" `isPrefixOf` argString) $ -- echo "-----"
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
| 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
@@ -242,16 +338,17 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
in do
when (name `elem` unsupportedCommands) $
warnMsg id $ "'" ++ name ++ "' is"
potentially $ do
allowed <- Map.lookup name allowedFlags
(word, flag) <- listToMaybe $
filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
sequence_ $ do
allowed' <- Map.lookup name allowedFlags
allowed <- allowed'
(word, flag) <- find
(\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
check token = sequence_ $ do
str <- getLiteralString token
let upper = map toUpper str
return $ do
@@ -266,7 +363,7 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
in
mapM_ check (drop 1 rest)
when (name == "printf") $ potentially $ do
when (name == "printf") $ sequence_ $ do
format <- rest !!! 0 -- flags are covered by allowedFlags
let literal = onlyLiteralString format
guard $ "%q" `isInfixOf` literal
@@ -278,16 +375,28 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
"typeset"
] ++ if not isDash then ["local"] else []
allowedFlags = Map.fromList [
("exec", []),
("export", ["-p"]),
("printf", []),
("read", if isDash then ["r", "p"] else ["r"]),
("ulimit", ["f"])
("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 do
when (name == "source") $ warnMsg id "'source' in place of '.' is"
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"
@@ -302,10 +411,11 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
]
bashVars = [
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS",
"_"
]
bashDynamicVars = [ "RANDOM", "SECONDS" ]
dashVars = [ ]
dashVars = [ "_" ]
isBashVariable var =
(var `elem` bashDynamicVars
|| var `elem` bashVars && not (isAssigned var))
@@ -316,39 +426,50 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
Assignment (_, _, name, _) -> name == var
_ -> False
-- |
-- >>> prop $ verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
-- >>> prop $ verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
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}"]) $
case bcmd of
["sed", v] -> checkIn v
["sed", "-e", v] -> checkIn v
_ -> return ()
checkSed id bcmd
where
-- This should have used backreferences, but TDFA doesn't support them
sedRe = mkRegex "^s(.)([^\n]*)g?$"
isSimpleSed s = fromMaybe False $ do
[first,rest] <- matchRegex sedRe s
let delimiters = filter (== head first) rest
guard $ length delimiters == 2
return True
acmd = oversimplify a
bcmd = oversimplify b
checkIn s =
when (isSimpleSed s) $
style id 2001 "See if you can use ${variable//search/replace} instead."
f _ = return ()
checkSed id ["sed", v] = checkIn id v
checkSed id ["sed", "-e", v] = checkIn id v
checkSed _ _ = return ()
-- |
-- >>> prop $ verify checkBraceExpansionVars "echo {1..$n}"
-- >>> prop $ verifyNot checkBraceExpansionVars "echo {1,3,$n}"
-- >>> prop $ verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg"
-- >>> prop $ verify checkBraceExpansionVars "echo {$i..100}"
-- This should have used backreferences, but TDFA doesn't support them
sedRe = mkRegex "^s(.)([^\n]*)g?$"
isSimpleSed s = isJust $ do
[h:_,rest] <- matchRegex sedRe s
let delimiters = filter (== h) rest
guard $ length delimiters == 2
checkIn id s =
when (isSimpleSed s) $
style id 2001 "See if you can use ${variable//search/replace} instead."
prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}"
prop_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
@@ -366,20 +487,19 @@ checkBraceExpansionVars = ForShell [Bash] f
T_DollarBraced {} -> return "$"
T_DollarExpansion {} -> return "$"
T_DollarArithmetic {} -> return "$"
otherwise -> return "-"
toString t = fromJust $ getLiteralStringExt literalExt t
_ -> return "-"
toString t = runIdentity $ getLiteralStringExt literalExt t
isEvaled t = do
cmd <- getClosestCommandM t
return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval"
return $ maybe False (`isUnqualifiedCommand` "eval") cmd
-- |
-- >>> prop $ verify checkMultiDimensionalArrays "foo[a][b]=3"
-- >>> prop $ verifyNot checkMultiDimensionalArrays "foo[a]=3"
-- >>> prop $ verify checkMultiDimensionalArrays "foo=( [a][b]=c )"
-- >>> prop $ verifyNot checkMultiDimensionalArrays "foo=( [a]=c )"
-- >>> prop $ verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}"
-- >>> prop $ verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}"
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 =
@@ -394,17 +514,16 @@ checkMultiDimensionalArrays = ForShell [Bash] f
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
isMultiDim t = getBracedModifier (bracedString t) `matches` re
-- |
-- >>> prop $ verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
-- >>> prop $ verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
-- >>> prop $ verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
-- >>> prop $ verify checkPS1Assignments "PS1=$'\\x1b[c '"
-- >>> prop $ verify checkPS1Assignments "PS1=$'\\e[3m; '"
-- >>> prop $ verify checkPS1Assignments "export PS1=$'\\e[3m; '"
-- >>> prop $ verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"
-- >>> prop $ verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
-- >>> prop $ verifyNot checkPS1Assignments "PS1='e033x1B'"
-- >>> prop $ verifyNot checkPS1Assignments "PS1='\\[\\e\\]'"
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
@@ -420,3 +539,7 @@ checkPS1Assignments = ForShell [Bash] f
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

@@ -36,13 +36,29 @@ internalVariables = [
-- 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 = [
@@ -98,6 +114,10 @@ binaryTestOps = [
"-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="
]
arithmeticBinaryTestOps = [
"-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",
@@ -109,9 +129,12 @@ 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
_ -> Nothing
flagsForRead = "sreu:n:N:i:p:a:t:"

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

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2015 Vidar Holen
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -21,6 +21,13 @@ 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 {
@@ -50,21 +57,23 @@ severityText pc =
makeNonVirtual comments contents =
map fix comments
where
ls = lines contents
fix c = c {
pcStartPos = (pcStartPos c) {
posColumn = realignColumn lineNo colNo c
}
, pcEndPos = (pcEndPos c) {
posColumn = realignColumn endLineNo endColNo c
}
list = lines contents
arr = listArray (1, length list) list
untabbedFix f = newFix {
fixReplacements = map (\r -> removeTabStops r arr) (fixReplacements f)
}
realignColumn lineNo colNo c =
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
else colNo c
real _ r v target | target <= v = r
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
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

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

View File

@@ -1,6 +1,6 @@
{-# LANGUAGE OverloadedStrings #-}
{-
Copyright 2012-2015 Vidar Holen
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -30,6 +30,7 @@ import GHC.Exts
import System.IO
import qualified Data.ByteString.Lazy.Char8 as BL
format :: IO Formatter
format = do
ref <- newIORef []
return Formatter {
@@ -45,11 +46,16 @@ instance ToJSON Replacement where
end = repEndPos replacement
str = repString replacement in
object [
"precedence" .= repPrecedence replacement,
"insertionPoint" .=
case repInsertionPoint replacement of
InsertBefore -> "beforeStart" :: String
InsertAfter -> "afterEnd",
"line" .= posLine start,
"endLine" .= posLine end,
"column" .= posColumn start,
"endLine" .= posLine end,
"endColumn" .= posColumn end,
"replaceWith" .= str
"replacement" .= str
]
instance ToJSON PositionedComment where
@@ -91,8 +97,13 @@ instance ToJSON Fix where
]
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
collectResult ref result _ =
modifyIORef ref (\x -> crComments result ++ x)
collectResult ref cr sys = mapM_ f groups
where
comments = crComments cr
groups = groupWith sourceFile comments
f :: [PositionedComment] -> IO ()
f group = modifyIORef ref (\x -> comments ++ x)
finish ref = do
list <- readIORef ref

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2015 Vidar Holen
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -19,10 +19,14 @@
-}
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
@@ -34,6 +38,8 @@ 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
@@ -51,6 +57,7 @@ colorForLevel level =
"warning" -> 33 -- yellow
"info" -> 32 -- green
"style" -> 32 -- green
"verbose" -> 32 -- green
"message" -> 1 -- bold
"source" -> 0 -- none
_ -> 0 -- none
@@ -116,72 +123,54 @@ outputForFile color sys comments = do
let fileName = sourceFile (head comments)
result <- (siReadFile sys) fileName
let contents = either (const "") id result
let fileLines = lines contents
let lineCount = fromIntegral $ length fileLines
let fileLinesList = lines contents
let lineCount = length fileLinesList
let fileLines = listArray (1, lineCount) fileLinesList
let groups = groupWith lineNo comments
mapM_ (\commentsForLine -> do
let lineNum = lineNo (head commentsForLine)
let lineNum = fromIntegral $ lineNo (head commentsForLine)
let line = if lineNum < 1 || lineNum > lineCount
then ""
else fileLines !! fromIntegral (lineNum - 1)
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 comments lineNum line
showFixedString color commentsForLine (fromIntegral lineNum) fileLines
) groups
hasApplicableFix lineNum comment = fromMaybe False $ do
replacements <- fixReplacements <$> pcFix comment
guard $ all (\c -> onSameLine (repStartPos c) && onSameLine (repEndPos c)) replacements
return True
-- 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
onSameLine pos = posLine pos == lineNum
(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
}
-- FIXME: Work correctly with multiple replacements
showFixedString color comments lineNum line =
case filter (hasApplicableFix lineNum) comments of
(first:_) -> do
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 $ fixedString first line
putStrLn ""
_ -> return ()
-- need to do something smart about sorting by end index
fixedString :: PositionedComment -> String -> String
fixedString comment line =
case (pcFix comment) of
Nothing -> ""
Just rs ->
applyReplacement (fixReplacements rs) line 0
where
applyReplacement [] s _ = s
applyReplacement (rep:xs) s offset =
let replacementString = repString rep
start = (posColumn . repStartPos) rep
end = (posColumn . repEndPos) rep
z = doReplace start end s replacementString
len_r = (fromIntegral . length) replacementString in
applyReplacement xs z (offset + (end - start) + len_r)
-- FIXME: Work correctly with tabs
-- start and end comes from pos, which is 1 based
-- doReplace 0 0 "1234" "A" -> "A1234" -- technically not valid
-- doReplace 1 1 "1234" "A" -> "A1234"
-- doReplace 1 2 "1234" "A" -> "A234"
-- doReplace 3 3 "1234" "A" -> "12A34"
-- doReplace 4 4 "1234" "A" -> "123A4"
-- 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
putStrLn $ unlines $ applyFix excerptFix excerpt
cuteIndent :: PositionedComment -> String
cuteIndent comment =
@@ -197,14 +186,9 @@ cuteIndent comment =
code num = "SC" ++ show num
getColorFunc :: ColorOption -> IO ColorFunc
getColorFunc 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
useColor <- shouldOutputColor colorOption
return $ if useColor then colorComment else const id
where
colorComment level comment =

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2015 Vidar Holen
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -21,11 +21,11 @@
module ShellCheck.Interface
(
SystemInterface(..)
, CheckSpec(csFilename, csScript, csCheckSourced, csExcludedWarnings, csShellTypeOverride, csMinSeverity)
, CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csOptionalChecks)
, CheckResult(crFilename, crComments)
, ParseSpec(psFilename, psScript, psCheckSourced, psShellTypeOverride)
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
, ParseResult(prComments, prTokenPositions, prRoot)
, AnalysisSpec(asScript, asShellType, asExecutionMode, asCheckSourced, asTokenPositions)
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asOptionalChecks)
, AnalysisResult(arComments)
, FormatterOptions(foColorOption, foWikiLinkCount)
, Shell(Ksh, Sh, Bash, Dash)
@@ -46,28 +46,43 @@ module ShellCheck.Interface
, newPosition
, newTokenComment
, mockedSystemInterface
, mockRcFile
, newParseSpec
, emptyCheckSpec
, newPositionedComment
, newComment
, Fix(fixReplacements)
, newFix
, Replacement(repStartPos, repEndPos, repString)
, 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
newtype SystemInterface m = SystemInterface {
data SystemInterface m = SystemInterface {
-- Read a file by filename, or return an error
siReadFile :: String -> m (Either ErrorMessage String)
siReadFile :: String -> m (Either ErrorMessage String),
-- Given:
-- the current script,
-- a list of source-path annotations in effect,
-- and a sourced file,
-- find the sourced file
siFindSource :: String -> [String] -> String -> m FilePath,
-- Get the configuration file (name, contents) for a filename
siGetConfig :: String -> m (Maybe (FilePath, String))
}
-- ShellCheck input and output
@@ -75,9 +90,12 @@ data CheckSpec = CheckSpec {
csFilename :: String,
csScript :: String,
csCheckSourced :: Bool,
csIgnoreRC :: Bool,
csExcludedWarnings :: [Integer],
csIncludedWarnings :: Maybe [Integer],
csShellTypeOverride :: Maybe Shell,
csMinSeverity :: Severity
csMinSeverity :: Severity,
csOptionalChecks :: [String]
} deriving (Show, Eq)
data CheckResult = CheckResult {
@@ -96,9 +114,12 @@ emptyCheckSpec = CheckSpec {
csFilename = "",
csScript = "",
csCheckSourced = False,
csIgnoreRC = False,
csExcludedWarnings = [],
csIncludedWarnings = Nothing,
csShellTypeOverride = Nothing,
csMinSeverity = StyleC
csMinSeverity = StyleC,
csOptionalChecks = []
}
newParseSpec :: ParseSpec
@@ -106,6 +127,7 @@ newParseSpec = ParseSpec {
psFilename = "",
psScript = "",
psCheckSourced = False,
psIgnoreRC = False,
psShellTypeOverride = Nothing
}
@@ -114,6 +136,7 @@ data ParseSpec = ParseSpec {
psFilename :: String,
psScript :: String,
psCheckSourced :: Bool,
psIgnoreRC :: Bool,
psShellTypeOverride :: Maybe Shell
} deriving (Show, Eq)
@@ -134,16 +157,20 @@ newParseResult = ParseResult {
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
}
@@ -166,6 +193,19 @@ newFormatterOptions = FormatterOptions {
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)
@@ -180,7 +220,7 @@ 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)
} deriving (Show, Eq, Generic, NFData, Ord)
newPosition :: Position
newPosition = Position {
@@ -206,13 +246,22 @@ newComment = Comment {
data Replacement = Replacement {
repStartPos :: Position,
repEndPos :: Position,
repString :: String
repString :: String,
-- Order in which the replacements should happen: highest precedence first.
repPrecedence :: Int,
-- Whether to insert immediately before or immediately after the specified region.
repInsertionPoint :: InsertionPoint
} deriving (Show, Eq, Generic, NFData)
data InsertionPoint = InsertBefore | InsertAfter
deriving (Show, Eq, Generic, NFData)
newReplacement = Replacement {
repStartPos = newPosition,
repEndPos = newPosition,
repString = ""
repString = "",
repPrecedence = 1,
repInsertionPoint = InsertAfter
}
data Fix = Fix {
@@ -259,11 +308,18 @@ data ColorOption =
-- For testing
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
mockedSystemInterface files = SystemInterface {
siReadFile = rf
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
rf file = return $
case find ((== file) . fst) files of
Nothing -> Left "File not included in mock."
Just (_, contents) -> Right contents
fs _ _ file = return file
mockRcFile rcfile mock = mock {
siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,3 +1,35 @@
resolver: lts-12.9
# 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

View File

@@ -1,2 +1,78 @@
#!/usr/bin/env bash
# This file was deprecated by the doctest build.
# 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

View File

@@ -11,21 +11,34 @@ command -v cabal ||
cabal update ||
die "can't update"
cabal install --dependencies-only --enable-tests ||
die "can't install dependencies"
cabal configure --enable-tests ||
if [ -e /etc/arch-release ]
then
# Arch has an unconventional packaging setup
flags=(--disable-library-vanilla --enable-shared --enable-executable-dynamic --ghc-options=-dynamic)
else
flags=()
fi
cabal install --dependencies-only --enable-tests "${flags[@]}" ||
cabal install --dependencies-only "${flags[@]}" ||
die "can't install dependencies"
cabal configure --enable-tests "${flags[@]}" ||
die "configure failed"
cabal build ||
die "build failed"
cabal test ||
die "test failed"
dist/build/shellcheck/shellcheck - << 'EOF' || die "execution failed"
sc="$(find . -name shellcheck -type f -perm -111)"
[ -x "$sc" ] || die "Can't find executable"
"$sc" - << 'EOF' || die "execution failed"
#!/bin/sh
echo "Hello World"
EOF
dist/build/shellcheck/shellcheck - << 'EOF' && die "negative execution failed"
"$sc" - << 'EOF' && die "negative execution failed"
#!/bin/sh
echo $1
EOF

76
test/check_release Executable file
View File

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

View File

@@ -17,13 +17,13 @@ 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.
Also note that dist* will be deleted.
EOF
exit 0
}
echo "Deleting 'dist'..."
rm -rf dist
echo "Deleting 'dist' and 'dist-newstyle'..."
rm -rf dist dist-newstyle
log=$(mktemp) || die "Can't create temp file"
date >> "$log" || die "Can't write to log"
@@ -61,20 +61,16 @@ done << EOF
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:latest zypper install -y cabal-install ghc
haskell:latest true
opensuse/leap:latest zypper install -y cabal-install ghc
fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils
archlinux/base:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
# Older Ubuntu versions we want to support
# Other versions we want to support
ubuntu:18.04 apt-get update && apt-get install -y cabal-install
ubuntu:17.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
base/archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
ubuntu:18.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
EOF
exit "$final"

View File

@@ -1,12 +0,0 @@
module Main where
import Build_doctests (flags, pkgs, module_sources)
import Data.Foldable (traverse_)
import Test.DocTest (doctest)
main :: IO ()
main = do
traverse_ putStrLn args
doctest args
where
args = flags ++ pkgs ++ module_sources

30
test/shellcheck.hs Normal file
View File

@@ -0,0 +1,30 @@
module Main where
import Control.Monad
import System.Exit
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.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