ShellCheck 0.4.1 → 0.4.2
raw patch · 12 files changed
+813/−206 lines, 12 filesdep ~QuickCheckdep ~basedep ~containersbuild-type:CustomPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependency ranges changed: QuickCheck, base, containers, directory, json, mtl, parsec, regex-tdfa
API changes (from Hackage documentation)
- ShellCheck.AST: instance Eq Annotation
- ShellCheck.AST: instance Eq AssignmentMode
- ShellCheck.AST: instance Eq CaseType
- ShellCheck.AST: instance Eq ConditionType
- ShellCheck.AST: instance Eq Dashed
- ShellCheck.AST: instance Eq FunctionKeyword
- ShellCheck.AST: instance Eq FunctionParentheses
- ShellCheck.AST: instance Eq Id
- ShellCheck.AST: instance Eq Quoted
- ShellCheck.AST: instance Eq Token
- ShellCheck.AST: instance Ord Id
- ShellCheck.AST: instance Show Annotation
- ShellCheck.AST: instance Show AssignmentMode
- ShellCheck.AST: instance Show CaseType
- ShellCheck.AST: instance Show ConditionType
- ShellCheck.AST: instance Show Dashed
- ShellCheck.AST: instance Show FunctionKeyword
- ShellCheck.AST: instance Show FunctionParentheses
- ShellCheck.AST: instance Show Id
- ShellCheck.AST: instance Show Quoted
- ShellCheck.AST: instance Show Token
- ShellCheck.Analytics: instance Eq Scope
- ShellCheck.Analytics: instance Show DataSource
- ShellCheck.Analytics: instance Show DataType
- ShellCheck.Analytics: instance Show Scope
- ShellCheck.Analytics: instance Show StackData
- ShellCheck.Analytics: instance Show VariableState
- ShellCheck.Formatter.Format: footer :: Formatter -> IO ()
- ShellCheck.Formatter.Format: header :: Formatter -> IO ()
- ShellCheck.Formatter.Format: onFailure :: Formatter -> FilePath -> ErrorMessage -> IO ()
- ShellCheck.Formatter.Format: onResult :: Formatter -> CheckResult -> String -> IO ()
- ShellCheck.Formatter.JSON: instance JSON PositionedComment
- ShellCheck.Interface: arComments :: AnalysisResult -> [TokenComment]
- ShellCheck.Interface: asExecutionMode :: AnalysisSpec -> ExecutionMode
- ShellCheck.Interface: asScript :: AnalysisSpec -> Token
- ShellCheck.Interface: asShellType :: AnalysisSpec -> Maybe Shell
- ShellCheck.Interface: crComments :: CheckResult -> [PositionedComment]
- ShellCheck.Interface: crFilename :: CheckResult -> String
- ShellCheck.Interface: csExcludedWarnings :: CheckSpec -> [Integer]
- ShellCheck.Interface: csFilename :: CheckSpec -> String
- ShellCheck.Interface: csScript :: CheckSpec -> String
- ShellCheck.Interface: csShellTypeOverride :: CheckSpec -> Maybe Shell
- ShellCheck.Interface: instance Eq CheckResult
- ShellCheck.Interface: instance Eq CheckSpec
- ShellCheck.Interface: instance Eq Comment
- ShellCheck.Interface: instance Eq ExecutionMode
- ShellCheck.Interface: instance Eq ParseResult
- ShellCheck.Interface: instance Eq ParseSpec
- ShellCheck.Interface: instance Eq Position
- ShellCheck.Interface: instance Eq PositionedComment
- ShellCheck.Interface: instance Eq Severity
- ShellCheck.Interface: instance Eq Shell
- ShellCheck.Interface: instance Eq TokenComment
- ShellCheck.Interface: instance Ord Severity
- ShellCheck.Interface: instance Show CheckResult
- ShellCheck.Interface: instance Show CheckSpec
- ShellCheck.Interface: instance Show Comment
- ShellCheck.Interface: instance Show ExecutionMode
- ShellCheck.Interface: instance Show ParseResult
- ShellCheck.Interface: instance Show ParseSpec
- ShellCheck.Interface: instance Show Position
- ShellCheck.Interface: instance Show PositionedComment
- ShellCheck.Interface: instance Show Severity
- ShellCheck.Interface: instance Show Shell
- ShellCheck.Interface: instance Show TokenComment
- ShellCheck.Interface: posColumn :: Position -> Integer
- ShellCheck.Interface: posFile :: Position -> String
- ShellCheck.Interface: posLine :: Position -> Integer
- ShellCheck.Interface: prComments :: ParseResult -> [PositionedComment]
- ShellCheck.Interface: prRoot :: ParseResult -> Maybe Token
- ShellCheck.Interface: prTokenPositions :: ParseResult -> Map Id Position
- ShellCheck.Interface: psFilename :: ParseSpec -> String
- ShellCheck.Interface: psScript :: ParseSpec -> String
- ShellCheck.Interface: siReadFile :: SystemInterface m -> String -> m (Either ErrorMessage String)
- ShellCheck.Parser: instance Eq Note
- ShellCheck.Parser: instance Eq ParseNote
- ShellCheck.Parser: instance Show Context
- ShellCheck.Parser: instance Show Note
- ShellCheck.Parser: instance Show ParseNote
+ ShellCheck.AST: instance GHC.Classes.Eq ShellCheck.AST.Annotation
+ ShellCheck.AST: instance GHC.Classes.Eq ShellCheck.AST.AssignmentMode
+ ShellCheck.AST: instance GHC.Classes.Eq ShellCheck.AST.CaseType
+ ShellCheck.AST: instance GHC.Classes.Eq ShellCheck.AST.ConditionType
+ ShellCheck.AST: instance GHC.Classes.Eq ShellCheck.AST.Dashed
+ ShellCheck.AST: instance GHC.Classes.Eq ShellCheck.AST.FunctionKeyword
+ ShellCheck.AST: instance GHC.Classes.Eq ShellCheck.AST.FunctionParentheses
+ ShellCheck.AST: instance GHC.Classes.Eq ShellCheck.AST.Id
+ ShellCheck.AST: instance GHC.Classes.Eq ShellCheck.AST.Quoted
+ ShellCheck.AST: instance GHC.Classes.Eq ShellCheck.AST.Token
+ ShellCheck.AST: instance GHC.Classes.Ord ShellCheck.AST.Id
+ ShellCheck.AST: instance GHC.Show.Show ShellCheck.AST.Annotation
+ ShellCheck.AST: instance GHC.Show.Show ShellCheck.AST.AssignmentMode
+ ShellCheck.AST: instance GHC.Show.Show ShellCheck.AST.CaseType
+ ShellCheck.AST: instance GHC.Show.Show ShellCheck.AST.ConditionType
+ ShellCheck.AST: instance GHC.Show.Show ShellCheck.AST.Dashed
+ ShellCheck.AST: instance GHC.Show.Show ShellCheck.AST.FunctionKeyword
+ ShellCheck.AST: instance GHC.Show.Show ShellCheck.AST.FunctionParentheses
+ ShellCheck.AST: instance GHC.Show.Show ShellCheck.AST.Id
+ ShellCheck.AST: instance GHC.Show.Show ShellCheck.AST.Quoted
+ ShellCheck.AST: instance GHC.Show.Show ShellCheck.AST.Token
+ ShellCheck.ASTLib: isFunction :: Token -> Bool
+ ShellCheck.ASTLib: isOnlyRedirection :: Token -> Bool
+ ShellCheck.Analytics: instance GHC.Classes.Eq ShellCheck.Analytics.Scope
+ ShellCheck.Analytics: instance GHC.Show.Show ShellCheck.Analytics.DataSource
+ ShellCheck.Analytics: instance GHC.Show.Show ShellCheck.Analytics.DataType
+ ShellCheck.Analytics: instance GHC.Show.Show ShellCheck.Analytics.Scope
+ ShellCheck.Analytics: instance GHC.Show.Show ShellCheck.Analytics.StackData
+ ShellCheck.Analytics: instance GHC.Show.Show ShellCheck.Analytics.VariableState
+ ShellCheck.Data: arrayVariables :: [[Char]]
+ ShellCheck.Formatter.Format: [footer] :: Formatter -> IO ()
+ ShellCheck.Formatter.Format: [header] :: Formatter -> IO ()
+ ShellCheck.Formatter.Format: [onFailure] :: Formatter -> FilePath -> ErrorMessage -> IO ()
+ ShellCheck.Formatter.Format: [onResult] :: Formatter -> CheckResult -> String -> IO ()
+ ShellCheck.Formatter.JSON: instance Text.JSON.JSON ShellCheck.Interface.PositionedComment
+ ShellCheck.Interface: ColorAlways :: ColorOption
+ ShellCheck.Interface: ColorAuto :: ColorOption
+ ShellCheck.Interface: ColorNever :: ColorOption
+ ShellCheck.Interface: Dash :: Shell
+ ShellCheck.Interface: FormatterOptions :: ColorOption -> FormatterOptions
+ ShellCheck.Interface: [arComments] :: AnalysisResult -> [TokenComment]
+ ShellCheck.Interface: [asExecutionMode] :: AnalysisSpec -> ExecutionMode
+ ShellCheck.Interface: [asScript] :: AnalysisSpec -> Token
+ ShellCheck.Interface: [asShellType] :: AnalysisSpec -> Maybe Shell
+ ShellCheck.Interface: [crComments] :: CheckResult -> [PositionedComment]
+ ShellCheck.Interface: [crFilename] :: CheckResult -> String
+ ShellCheck.Interface: [csExcludedWarnings] :: CheckSpec -> [Integer]
+ ShellCheck.Interface: [csFilename] :: CheckSpec -> String
+ ShellCheck.Interface: [csScript] :: CheckSpec -> String
+ ShellCheck.Interface: [csShellTypeOverride] :: CheckSpec -> Maybe Shell
+ ShellCheck.Interface: [foColorOption] :: FormatterOptions -> ColorOption
+ ShellCheck.Interface: [posColumn] :: Position -> Integer
+ ShellCheck.Interface: [posFile] :: Position -> String
+ ShellCheck.Interface: [posLine] :: Position -> Integer
+ ShellCheck.Interface: [prComments] :: ParseResult -> [PositionedComment]
+ ShellCheck.Interface: [prRoot] :: ParseResult -> Maybe Token
+ ShellCheck.Interface: [prTokenPositions] :: ParseResult -> Map Id Position
+ ShellCheck.Interface: [psFilename] :: ParseSpec -> String
+ ShellCheck.Interface: [psScript] :: ParseSpec -> String
+ ShellCheck.Interface: [siReadFile] :: SystemInterface m -> String -> m (Either ErrorMessage String)
+ ShellCheck.Interface: data ColorOption
+ ShellCheck.Interface: data FormatterOptions
+ ShellCheck.Interface: instance GHC.Classes.Eq ShellCheck.Interface.CheckResult
+ ShellCheck.Interface: instance GHC.Classes.Eq ShellCheck.Interface.CheckSpec
+ ShellCheck.Interface: instance GHC.Classes.Eq ShellCheck.Interface.ColorOption
+ ShellCheck.Interface: instance GHC.Classes.Eq ShellCheck.Interface.Comment
+ ShellCheck.Interface: instance GHC.Classes.Eq ShellCheck.Interface.ExecutionMode
+ ShellCheck.Interface: instance GHC.Classes.Eq ShellCheck.Interface.ParseResult
+ ShellCheck.Interface: instance GHC.Classes.Eq ShellCheck.Interface.ParseSpec
+ ShellCheck.Interface: instance GHC.Classes.Eq ShellCheck.Interface.Position
+ ShellCheck.Interface: instance GHC.Classes.Eq ShellCheck.Interface.PositionedComment
+ ShellCheck.Interface: instance GHC.Classes.Eq ShellCheck.Interface.Severity
+ ShellCheck.Interface: instance GHC.Classes.Eq ShellCheck.Interface.Shell
+ ShellCheck.Interface: instance GHC.Classes.Eq ShellCheck.Interface.TokenComment
+ ShellCheck.Interface: instance GHC.Classes.Ord ShellCheck.Interface.ColorOption
+ ShellCheck.Interface: instance GHC.Classes.Ord ShellCheck.Interface.Severity
+ ShellCheck.Interface: instance GHC.Show.Show ShellCheck.Interface.CheckResult
+ ShellCheck.Interface: instance GHC.Show.Show ShellCheck.Interface.CheckSpec
+ ShellCheck.Interface: instance GHC.Show.Show ShellCheck.Interface.ColorOption
+ ShellCheck.Interface: instance GHC.Show.Show ShellCheck.Interface.Comment
+ ShellCheck.Interface: instance GHC.Show.Show ShellCheck.Interface.ExecutionMode
+ ShellCheck.Interface: instance GHC.Show.Show ShellCheck.Interface.ParseResult
+ ShellCheck.Interface: instance GHC.Show.Show ShellCheck.Interface.ParseSpec
+ ShellCheck.Interface: instance GHC.Show.Show ShellCheck.Interface.Position
+ ShellCheck.Interface: instance GHC.Show.Show ShellCheck.Interface.PositionedComment
+ ShellCheck.Interface: instance GHC.Show.Show ShellCheck.Interface.Severity
+ ShellCheck.Interface: instance GHC.Show.Show ShellCheck.Interface.Shell
+ ShellCheck.Interface: instance GHC.Show.Show ShellCheck.Interface.TokenComment
+ ShellCheck.Parser: instance GHC.Classes.Eq ShellCheck.Parser.Note
+ ShellCheck.Parser: instance GHC.Classes.Eq ShellCheck.Parser.ParseNote
+ ShellCheck.Parser: instance GHC.Show.Show ShellCheck.Parser.Context
+ ShellCheck.Parser: instance GHC.Show.Show ShellCheck.Parser.Note
+ ShellCheck.Parser: instance GHC.Show.Show ShellCheck.Parser.ParseNote
- ShellCheck.Formatter.TTY: format :: IO Formatter
+ ShellCheck.Formatter.TTY: format :: FormatterOptions -> IO Formatter
Files
- README.md +237/−33
- ShellCheck.cabal +2/−2
- ShellCheck/ASTLib.hs +17/−4
- ShellCheck/Analytics.hs +323/−79
- ShellCheck/Checker.hs +3/−0
- ShellCheck/Data.hs +21/−15
- ShellCheck/Formatter/TTY.hs +17/−12
- ShellCheck/Interface.hs +14/−1
- ShellCheck/Parser.hs +80/−24
- shellcheck.1 +49/−22
- shellcheck.1.md +19/−3
- shellcheck.hs +31/−11
README.md view
@@ -1,27 +1,68 @@ # ShellCheck - A shell script static analysis tool -http://www.shellcheck.net+ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts: -Copyright 2012-2015, Vidar 'koala_man' Holen-Licensed under the GNU General Public License, v3+. -The goals of ShellCheck are:+The goals of ShellCheck are - - To point out and clarify typical beginner's syntax issues,+ - To point out and clarify typical beginner's syntax issues that causes a shell to give cryptic error messages. - - To point out and clarify typical intermediate level semantic problems,+ - To point out and clarify typical intermediate level semantic problems that causes 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! -ShellCheck is written in Haskell, and requires 2 GB of memory to compile. +## How to use+There are a variety of ways to use ShellCheck!+++#### On the web+Paste a shell script on http://www.shellcheck.net for instant feedback.++[ShellCheck.net](http://www.shellcheck.net) is always synchronized to the latest git commit, and is the simplest way to give ShellCheck a go. Tell your friends!+++#### From your terminal+Run `shellcheck yourscript` in your terminal for instant output, as seen above.+++#### In your editor++You can see ShellCheck suggestions directly in a variety of editors.++* Vim, through [Syntastic](https://github.com/scrooloose/syntastic):++.++* Emacs, through [Flycheck](https://github.com/flycheck/flycheck):++.++* Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck).++* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).++* Most other editors, through [GCC error compatibility](blob/master/shellcheck.1.md#user-content-formats).+++#### In your build or test suites+While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites.++Use ShellCheck's exit code, or it's [CheckStyle compatible XML output](blob/master/shellcheck.1.md#user-content-formats). There's also a simple JSON output format for easy integration.++ ## Installing -On systems with Cabal:+The easiest way to install ShellCheck locally is through your package manager. +On systems with Cabal (installs to `~/.cabal/bin`):+ cabal update cabal install shellcheck @@ -29,54 +70,60 @@ apt-get install shellcheck +On Fedora based distros:++ dnf install ShellCheck+ On OS X with homebrew: brew install shellcheck -ShellCheck is also available as an online service:+On OS X with MacPorts: - http://www.shellcheck.net+ port install shellcheck -## Building with Cabal+On openSUSE:Tumbleweed: -This sections describes how to build ShellCheck from a source directory.+ zypper in ShellCheck -First, make sure cabal is installed. On Debian based distros:+On other openSUSE distributions: - apt-get install cabal-install+add OBS devel:languages:haskell repository from https://build.opensuse.org/project/repositories/devel:languages:haskell -On Fedora:+ zypper ar http://download.opensuse.org/repositories/devel:/languages:/haskell/openSUSE_$(version)/devel:languages:haskell.repo+ zypper in ShellCheck - yum install cabal-install+or use OneClickInstall - https://software.opensuse.org/package/ShellCheck -On Mac OS X with homebrew (http://brew.sh/): - brew install cabal-install+## Compiling from source -On Mac OS X with MacPorts (http://www.macports.org/):+This sections describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile. - port install hs-cabal-install -On native Windows (https://www.haskell.org/platform/):+#### Installing Cabal - Download and install the latest version of the Haskell Platform.+ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `yum`, `zypper` or `brew`). -Let cabal update itself, in case your distro version is outdated:+On MacPorts, the package is instead called `hs-cabal-install`, while native Windows users should install the latest version of the Haskell platform from https://www.haskell.org/platform/ +Verify that `cabal` is installed and update its dependency list with+ $ cabal update- $ cabal install cabal-install -With cabal installed, cd to the ShellCheck source directory and:+#### Compiling ShellCheck +`git clone` this repository, and `cd` to the ShellCheck source directory to build/install:+ $ cabal install -This will install ShellCheck to your `~/.cabal/bin` directory.+This will compile ShellCheck and install it to your `~/.cabal/bin` directory. -Add the directory to your `PATH` (for bash, add this to your `~/.bashrc`):+Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`): export PATH="$HOME/.cabal/bin:$PATH" -Verify that your PATH is set up correctly:+Log out and in again, and verify that your PATH is set up correctly: $ which shellcheck ~/.cabal/bin/shellcheck@@ -93,18 +140,175 @@ > [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -## Running tests+#### Running tests To run the unit test suite: - cabal configure --enable-tests- cabal build- cabal test+ $ cabal test ++## Gallery of bad code+So what kind of things does ShellCheck look for? Here is an incomplete list of detected issues.++#### Quoting++ShellCheck can recognize several types of incorrect quoting:++ echo $1 # Unquoted variables+ find . -name *.ogg # Unquoted find/grep patterns+ rm "~/my file.txt" # Quoted tilde expansion+ v='--verbose="true"'; cmd $v # Literal quotes in variables+ for f in "*.ogg" # Incorrectly quoted 'for' loops+ touch $@ # Unquoted $@+ echo 'Don't forget to restart!' # Singlequote closed by apostrophe+ echo 'Don\'t try this at home' # Attempting to escape ' in ''+ echo 'Path is $PATH' # Variables in single quotes+ trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap+++#### Conditionals++ShellCheck can recognize many types of incorrect test statements.++ [[ n != 0 ]] # Constant test expressions+ [[ -e *.mpg ]] # Existence checks of globs+ [[ $foo==0 ]] # Always true due to missing spaces+ [[ -n "$foo " ]] # Always true due to literals+ [[ $foo =~ "fo+" ]] # Quoted regex in =~ + [ foo =~ re ] # Unsupported [ ] operators+ [ $1 -eq "shellcheck" ] # Numerical comparison of strings+ [ $n && $m ] # && in [ .. ]+ [ grep -q foo file ] # Command without $(..)+++#### Frequently misused commands++ShellCheck can recognize instances where commands are used incorrectly:++ grep '*foo*' file # Globs in regex contexts+ find . -exec foo {} && bar {} \; # Prematurely terminated find -exec+ sudo echo 'Var=42' > /etc/profile # Redirecting sudo+ time --format=%s sleep 10 # Passing time(1) flags to time builtin+ while read h; do ssh "$h" uptime # Commands eating while loop input+ alias archive='mv $1 /backup' # Defining aliases with arguments+ tr -cd '[a-zA-Z0-9]' # [] around ranges in tr+ exec foo; echo "Done!" # Misused 'exec'+ find -name \*.bak -o -name \*~ -delete # Implicit precedence in find+ f() { whoami; }; sudo f # External use of internal functions+++#### Common beginner's mistakes++ShellCheck recognizes many common beginner's syntax errors:++ var = 42 # Spaces around = in assignments+ $foo=42 # $ in assignments+ for $var in *; do ... # $ in for loop variables+ var$n="Hello" # Wrong indirect assignment+ echo ${var$n} # Wrong indirect reference+ var=(1, 2, 3) # Comma separated arrays+ echo "Argument 10 is $10" # Positional parameter misreference+ if $(myfunction); then ..; fi # Wrapping commands in $()+ else if othercondition; then .. # Using 'else if'+++#### Style++ShellCheck can make suggestions to improve style:++ [[ -z $(find /tmp | grep mpg) ]] # Use grep -q instead+ a >> log; b >> log; c >> log # Use a redirection block instead+ echo "The time is `date`" # Use $() instead+ cd dir; process *; cd ..; # Use subshells instead + echo $[1+2] # Use standard $((..)) instead of old $[]+ echo $(($RANDOM % 6)) # Don't use $ on variables in $((..))+ echo "$(date)" # Useless use of echo+ cat file | grep foo # Useless use of cat+++#### Data and typing errors++ShellCheck can recognize issues related to data and typing:++ args="$@" # Assigning arrays to strings+ files=(foo bar); echo "$files" # Referencing arrays as strings+ printf "%s\n" "Arguments: $@." # Concatenating strings and arrays.+ [[ $# > 2 ]] # Comparing numbers as strings+ var=World; echo "Hello " var # Unused lowercase variables+ echo "Hello $name" # Unassigned lowercase variables+ cmd | read bar; echo $bar # Assignments in subshells+++#### Robustness++ShellCheck can make suggestions for improving the robustness of a script:++ rm -rf "$STEAMROOT/"* # Catastrophic rm+ touch ./-l; ls * # Globs that could become options+ find . -exec sh -c 'a && b {}' \; # Find -exec shell injection+ printf "Hello $name" # Variables in printf format+ for f in $(ls *.txt); do # Iterating over ls output+ export MYVAR=$(cmd) # Masked exit codes+++#### Portability++ShellCheck will warn when using features not supported by the shebang. For example, if you set the shebang to `#!/bin/sh`, ShellCheck will warn about portability issues similar to `checkbashisms`:+++ echo {1..$n} # Works in ksh, but not bash/dash/sh+ echo {1..10} # Works in ksh and bash, but not dash/sh+ echo -n 42 # Works in ksh, bash and dash, undefined in sh+ trap 'exit 42' sigint # Unportable signal spec+ cmd &> file # Unportable redirection operator+ read foo < /dev/tcp/host/22 # Unportable intercepted files+ foo-bar() { ..; } # Undefined/unsupported function name+ [ $UID = 0 ] # Variable undefined in dash/sh+ local var=value # local is undefined in sh+ ++#### Miscellaneous++ShellCheck recognizes a menagerie of other issues:++ PS1='\e[0;32m\$\e[0m ' # PS1 colors not in \[..\]+ PATH="$PATH:~/bin" # Literal tilde in $PATH+ rm “file” # Unicode quotes+ echo "Hello world" # Carriage return / DOS line endings+ var=42 echo $var # Expansion of inlined environment+ #!/bin/bash -x -e # Common shebang errors+ echo $((n/180*100)) # Unnecessary loss of precision+ ls *[:digit:].txt # Bad character class globs+ sed 's/foo/bar/ file > file # Redirecting to input+++## Testimonials++> At first you're like "shellcheck is awesome" but then you're like "wtf are we still using bash"++Alexander Tarasikov,+[via Twitter](https://twitter.com/astarasikov/status/568825996532707330)++ ## Reporting bugs Please use the Github issue tracker for any bugs or feature suggestions: https://github.com/koalaman/shellcheck/issues+++## Contributing++Please submit patches to code or documentation as Github pull requests!++Contributions must be licensed under the GNU GPLv3.+The contributor retains the copyright.+++## Copyright++ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE).++Copyright 2012-2015, Vidar 'koala_man' Holen and contributors. Happy ShellChecking!
ShellCheck.cabal view
@@ -1,5 +1,5 @@ Name: ShellCheck-Version: 0.4.1+Version: 0.4.2 Synopsis: Shell script analysis tool License: GPL-3 License-file: LICENSE@@ -7,7 +7,7 @@ Author: Vidar Holen Maintainer: vidar@vidarholen.net Homepage: http://www.shellcheck.net/-Build-Type: Simple+Build-Type: Custom Cabal-Version: >= 1.8 Bug-reports: https://github.com/koalaman/shellcheck/issues Description:
ShellCheck/ASTLib.hs view
@@ -93,14 +93,17 @@ -- Turn a SimpleCommand foo -avz --bar=baz into args "a", "v", "z", "bar",--- each in a tuple of (token, stringFlag).+-- each in a tuple of (token, stringFlag). Non-flag arguments are added with+-- stringFlag == "". getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =- let textArgs = takeWhile (not . stopCondition . snd) $ map (\x -> (x, concat $ oversimplify x)) args in- concatMap flag textArgs+ let tokenAndText = map (\x -> (x, concat $ oversimplify x)) args+ (flagArgs, rest) = break (stopCondition . snd) tokenAndText+ in+ concatMap flag flagArgs ++ map (\(t, _) -> (t, "")) rest where flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ] flag (x, '-':args) = map (\v -> (x, [v])) args- flag _ = []+ flag (x, _) = [ (x, "") ] getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)" -- Get all flags in a GNU way, up until --@@ -223,6 +226,16 @@ T_Assignment {} -> True T_Annotation _ _ w -> isAssignment w otherwise -> False++isOnlyRedirection t =+ case t of+ T_Pipeline _ _ [x] -> isOnlyRedirection x+ T_Annotation _ _ w -> isOnlyRedirection w+ T_Redirecting _ (_:_) c -> isOnlyRedirection c+ T_SimpleCommand _ [] [] -> True+ otherwise -> False++isFunction t = case t of T_Function {} -> True; _ -> False -- Get the list of commands from tokens that contain them, such as -- the body of while loops and if statements.
ShellCheck/Analytics.hs view
@@ -74,6 +74,11 @@ ,checkTimeParameters ,checkForDecimals ]+checksFor Dash = [+ checkBashisms+ ,checkForDecimals+ ,checkLocalScope+ ] checksFor Ksh = [ checkEchoSed ]@@ -82,6 +87,7 @@ ,checkBraceExpansionVars ,checkEchoSed ,checkForDecimals+ ,checkLocalScope ] runAnalytics :: AnalysisSpec -> AnalysisResult@@ -210,9 +216,11 @@ ,checkReadWithoutR ,checkExportedExpansions ,checkLoopVariableReassignment+ ,checkTrailingBracket+ ,checkNonportableSignals+ ,checkMkdirDashPM ] - filterByAnnotation token = filter (not . shouldIgnore) where@@ -224,6 +232,7 @@ any hasNum anns where hasNum (DisableComment ts) = num == ts+ hasNum _ = False shouldIgnoreFor _ (T_Include {}) = True -- Ignore included files shouldIgnoreFor _ _ = False parents = getParentTree token@@ -577,17 +586,39 @@ 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_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"-checkBashisms _ = bashism+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"+checkBashisms params = bashism where- errMsg id s = err id 2040 $ "In sh, " ++ s ++ " not supported, even when sh is actually bash."- warnMsg id s = warn id 2039 $ "In POSIX sh, " ++ s ++ " not supported."- bashism (T_ProcSub id _ _) = errMsg id "process substitution is"+ isDash = shellType params == Dash+ warnMsg id s =+ if isDash+ then warn id 2169 $ "In dash, " ++ s ++ " not supported."+ else warn id 2039 $ "In POSIX sh, " ++ s ++ " undefined."++ bashism (T_ProcSub id _ _) = warnMsg id "process substitution is" bashism (T_Extglob id _ _) = warnMsg id "extglob is" bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is" bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\" is"@@ -599,8 +630,10 @@ bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is" bashism (T_HereString id _) = warnMsg id "here-strings are" bashism (TC_Binary id SingleBracket op _ _)- | op `elem` [ "-nt", "-ef", "\\<", "\\>", "==" ] =- warnMsg id $ op ++ " is"+ | op `elem` [ "-nt", "-ef", "\\<", "\\>"] =+ unless isDash $ warnMsg id $ op ++ " is"+ bashism (TC_Binary id SingleBracket "==" _ _) =+ warnMsg id "== in place of = is" bashism (TC_Unary id _ "-a" _) = warnMsg id "unary -a in place of -e is" bashism (TA_Unary id op _)@@ -621,10 +654,11 @@ warnMsg id $ fromJust str ++ " is" where str = getLiteralString t- isBashism = isJust str && fromJust str `elem` bashVars+ isBashism = isJust str && isBashVariable (fromJust str) bashism t@(T_DollarBraced id token) = do mapM_ check expansion- when (var `elem` bashVars) $ warnMsg id $ var ++ " is"+ when (isBashVariable var) $+ warnMsg id $ var ++ " is" where str = bracedString t var = getBracedReference str@@ -640,10 +674,23 @@ bashism (T_CoProc id _ _) = warnMsg id "coproc is" + bashism (T_Function id _ _ str _) | not (isVariableName str) =+ warnMsg id "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"++ bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =+ warnMsg id "$(<file) to read files is"+ bashism (T_Backticked id [x]) | isOnlyRedirection x =+ warnMsg id "`<file` to read files is"+ bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) | t `isCommand` "echo" && "-" `isPrefixOf` argString =- unless ("--" `isPrefixOf` argString) $ -- echo "-------"- warnMsg (getId arg) "echo flags are"+ 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 bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) | t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) =@@ -655,31 +702,47 @@ let name = fromMaybe "" $ getCommandName t flags = getLeadingFlags t in do- when (name `elem` bashCommands) $ warnMsg id $ "'" ++ name ++ "' is"+ when (name `elem` unsupportedCommands) $+ warnMsg id $ "'" ++ name ++ "' is" potentially $ do allowed <- Map.lookup name allowedFlags- (word, flag) <- listToMaybe $ filter (\x -> snd x `notElem` allowed) flags+ (word, flag) <- listToMaybe $+ filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is" when (name == "source") $ warnMsg id "'source' in place of '.' is" when (name == "trap") $ let check token = potentially $ do- word <- liftM (map toLower) $ getLiteralString token- guard $ word `elem` ["err", "debug", "return"]- return $ warnMsg (getId token) $ "trapping " ++ word ++ " is"+ str <- getLiteralString token+ let upper = map toUpper str+ return $ do+ when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $+ warnMsg (getId token) $ "trapping " ++ str ++ " is"+ when ("SIG" `isPrefixOf` upper) $+ warnMsg (getId token)+ "prefixing signal names with 'SIG' is"+ when (not isDash && upper /= str) $+ warnMsg (getId token)+ "using lower/mixed case for signal names is" in- mapM_ check (reverse rest)+ mapM_ check (drop 1 rest)++ when (name == "printf") $ potentially $ do+ format <- rest !!! 0 -- flags are covered by allowedFlags+ let literal = onlyLiteralString format+ guard $ "%q" `isInfixOf` literal+ return $ warnMsg (getId format) "printf %q is" where- bashCommands = [+ unsupportedCommands = [ "let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",- "enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend", "type",+ "enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend", "typeset"- ]+ ] ++ if not isDash then ["local", "type"] else [] allowedFlags = Map.fromList [- ("read", ["r"]),+ ("read", if isDash then ["r", "p"] else ["r"]), ("ulimit", ["f"]),- ("echo", []),+ ("printf", []), ("exec", []) ] @@ -687,6 +750,7 @@ varChars="_0-9a-zA-Z" expansion = let re = mkRegex in [+ (re $ "^![" ++ varChars ++ "]", "indirect expansion is"), (re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"), (re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"), (re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),@@ -694,10 +758,20 @@ (re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is") ] bashVars = [- "RANDOM", "LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",- "DIRSTACK", "EUID", "UID", "SECONDS", "SHLVL", "PIPESTATUS", "SHELLOPTS"+ "LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",+ "DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS" ]+ bashDynamicVars = [ "RANDOM", "SECONDS" ]+ isBashVariable var =+ var `elem` bashDynamicVars+ || var `elem` bashVars && not (isAssigned var)+ isAssigned var = any f (variableFlow params)+ where+ f x = case x of+ Assignment (_, _, name, _) -> name == var+ _ -> False + prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done" prop_checkForInQuoted2 = verifyNot checkForInQuoted "for f in \"$@\"; do echo foo; done" prop_checkForInQuoted2a = verifyNot checkForInQuoted "for f in *.mp3; do echo foo; done"@@ -822,6 +896,8 @@ prop_checkRedirectToSame3 = verifyNot checkRedirectToSame "cat lol | sed -e 's/a/b/g' > foo.bar && mv foo.bar lol" prop_checkRedirectToSame4 = verifyNot checkRedirectToSame "foo /dev/null > /dev/null" prop_checkRedirectToSame5 = verifyNot checkRedirectToSame "foo > bar 2> bar"+prop_checkRedirectToSame6 = verifyNot checkRedirectToSame "echo foo > foo"+prop_checkRedirectToSame7 = verifyNot checkRedirectToSame "sed 's/foo/bar/g' file | sponge file" checkRedirectToSame params s@(T_Pipeline _ _ list) = mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurrences x) l) (getAllRedirs list))) list where@@ -831,7 +907,8 @@ when (exceptId /= newId && x == y && not (isOutput t && isOutput u)- && not (special t)) $ do+ && not (special t)+ && not (any isHarmlessCommand [t,u])) $ do addComment $ note newId addComment $ note exceptId checkOccurrences _ _ = return ()@@ -854,6 +931,11 @@ T_DGREAT _ -> True _ -> False _ -> False+ isHarmlessCommand arg = fromMaybe False $ do+ cmd <- getClosestCommand (parentMap params) arg+ name <- getCommandBasename cmd+ return $ name `elem` ["echo", "printf", "sponge"]+ checkRedirectToSame _ _ = return () @@ -901,7 +983,7 @@ err (getId x) 2068 "Double quote array expansions to avoid re-splitting elements." where- -- Fixme: should detect whether the alterantive is quoted+ -- Fixme: should detect whether the alternative is quoted isAlternative b@(T_DollarBraced _ t) = ":+" `isInfixOf` bracedString b isAlternative _ = False checkUnquotedDollarAt _ _ = return ()@@ -944,24 +1026,26 @@ prop_checkArrayWithoutIndex3 = verifyTree checkArrayWithoutIndex "coproc foo while true; do echo cow; done; echo $foo" prop_checkArrayWithoutIndex4 = verifyTree checkArrayWithoutIndex "coproc tail -f log; echo $COPROC" prop_checkArrayWithoutIndex5 = verifyTree checkArrayWithoutIndex "a[0]=foo; echo $a"+prop_checkArrayWithoutIndex6 = verifyTree checkArrayWithoutIndex "echo $PIPESTATUS" checkArrayWithoutIndex params _ =- concat $ doVariableFlowAnalysis readF writeF Map.empty (variableFlow params)+ concat $ doVariableFlowAnalysis readF writeF defaultMap (variableFlow params) where+ defaultMap = Map.fromList $ map (\x -> (x,())) arrayVariables readF _ (T_DollarBraced id token) _ = do map <- get return . maybeToList $ do name <- getLiteralString token- assignment <- Map.lookup name map+ assigned <- Map.lookup name map return [makeComment WarningC id 2128 "Expanding an array without an index only gives the first element."] readF _ _ _ = return [] writeF _ t name (DataArray _) = do- modify (Map.insert name t)+ modify (Map.insert name ()) return [] writeF _ expr name _ = do if isIndexed expr- then modify (Map.insert name expr)+ then modify (Map.insert name ()) else modify (Map.delete name) return [] @@ -1028,9 +1112,7 @@ commandName = fromMaybe "" $ do cmd <- getClosestCommand parents t name <- getCommandBasename cmd- if name == "find"- then return $ getFindCommand cmd- else return name+ return $ if name == "find" then getFindCommand cmd else name isProbablyOk = any isOkAssignment (take 3 $ getPath parents t)@@ -1071,8 +1153,9 @@ prop_checkUnquotedN = verify checkUnquotedN "if [ -n $foo ]; then echo cow; fi" prop_checkUnquotedN2 = verify checkUnquotedN "[ -n $cow ]" prop_checkUnquotedN3 = verifyNot checkUnquotedN "[[ -n $foo ]] && echo cow"-checkUnquotedN _ (T_Condition _ SingleBracket (TC_Unary _ SingleBracket "-n" (T_NormalWord id [t]))) | willSplit t =- err id 2070 "Always true because you failed to quote. Use [[ ]] instead."+prop_checkUnquotedN4 = verify checkUnquotedN "[ -n $cow -o -t 1 ]"+checkUnquotedN _ (TC_Unary _ SingleBracket "-n" (T_NormalWord id [t])) | willSplit t =+ err id 2070 "-n doesn't work with unquoted arguments. Quote or use [[ ]]." checkUnquotedN _ _ = return () prop_checkNumberComparisons1 = verify checkNumberComparisons "[[ $foo < 3 ]]"@@ -1082,9 +1165,9 @@ prop_checkNumberComparisons5 = verify checkNumberComparisons "[[ $foo -le 2.72 ]]" prop_checkNumberComparisons6 = verify checkNumberComparisons "[[ 3.14 -eq $foo ]]" prop_checkNumberComparisons7 = verifyNot checkNumberComparisons "[[ 3.14 == $foo ]]"-prop_checkNumberComparisons8 = verify checkNumberComparisons "[[ foo <= bar ]]"+prop_checkNumberComparisons8 = verify checkNumberComparisons "[ foo <= bar ]" prop_checkNumberComparisons9 = verify checkNumberComparisons "[ foo \\>= bar ]"-prop_checkNumberComparisons11= verify checkNumberComparisons "[[ $foo -eq 'N' ]]"+prop_checkNumberComparisons11= verify checkNumberComparisons "[ $foo -eq 'N' ]" prop_checkNumberComparisons12= verify checkNumberComparisons "[ x$foo -gt x${N} ]" checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do if isNum lhs && not (isNonNum rhs)@@ -1106,15 +1189,15 @@ when (op `elem` ["-lt", "-gt", "-le", "-ge", "-eq"]) $ do mapM_ checkDecimals [lhs, rhs]- checkStrings [lhs, rhs]+ when (typ == SingleBracket) $+ checkStrings [lhs, rhs] where isLtGt = flip elem ["<", "\\<", ">", "\\>"] isLeGe = flip elem ["<=", "\\<=", ">=", "\\>="] - supportsDecimals = (shellType params) == Ksh checkDecimals hs =- when (isFraction hs && not supportsDecimals) $+ when (isFraction hs && not (hasFloatingPoint params)) $ err (getId hs) 2072 decimalError decimalError = "Decimals are not supported. " ++ "Either use integers only, or use bc or awk to compare."@@ -1127,8 +1210,8 @@ return . not . all numChar $ s numChar x = isDigit x || x `elem` "+-. " - stringError t = err (getId t) 2130 $- op ++ " is for integer comparisons. Use " ++ seqv op ++ " instead."+ stringError t = err (getId t) 2170 $+ "Numerical " ++ op ++ " does not dereference in [..]. Expand or use string operator." isNum t = case oversimplify t of@@ -1204,16 +1287,26 @@ otherwise -> return () -prop_checkQuotedCondRegex1 = verify checkQuotedCondRegex "[[ $foo =~ \"bar\" ]]"-prop_checkQuotedCondRegex2 = verify checkQuotedCondRegex "[[ $foo =~ 'cow' ]]"+prop_checkQuotedCondRegex1 = verify checkQuotedCondRegex "[[ $foo =~ \"bar.*\" ]]"+prop_checkQuotedCondRegex2 = verify checkQuotedCondRegex "[[ $foo =~ '(cow|bar)' ]]" prop_checkQuotedCondRegex3 = verifyNot checkQuotedCondRegex "[[ $foo =~ $foo ]]"+prop_checkQuotedCondRegex4 = verifyNot checkQuotedCondRegex "[[ $foo =~ \"bar\" ]]"+prop_checkQuotedCondRegex5 = verifyNot checkQuotedCondRegex "[[ $foo =~ 'cow bar' ]]" checkQuotedCondRegex _ (TC_Binary _ _ "=~" _ rhs) = case rhs of- T_NormalWord id [T_DoubleQuoted _ _] -> error id- T_NormalWord id [T_SingleQuoted _ _] -> error id+ T_NormalWord id [T_DoubleQuoted _ _] -> error rhs+ T_NormalWord id [T_SingleQuoted _ _] -> error rhs _ -> return () where- error id = err id 2076 "Don't quote rhs of =~, it'll match literally rather than as a regex."+ error t =+ unless (isConstantNonRe t) $+ err (getId t) 2076+ "Don't quote rhs of =~, it'll match literally rather than as a regex."+ re = mkRegex "[][*.+()]"+ hasMetachars s = s `matches` re+ isConstantNonRe t = fromMaybe False $ do+ s <- getLiteralString t+ return . not $ hasMetachars s checkQuotedCondRegex _ _ = return () prop_checkGlobbedRegex1 = verify checkGlobbedRegex "[[ $foo =~ *foo* ]]"@@ -1229,16 +1322,23 @@ prop_checkConstantIfs1 = verify checkConstantIfs "[[ foo != bar ]]"-prop_checkConstantIfs2 = verify checkConstantIfs "[[ n -le 4 ]]"-prop_checkConstantIfs3 = verify checkConstantIfs "[[ $n -le 4 && n -ge 2 ]]"+prop_checkConstantIfs2a= verify checkConstantIfs "[ n -le 4 ]"+prop_checkConstantIfs2b= verifyNot checkConstantIfs "[[ n -le 4 ]]"+prop_checkConstantIfs3 = verify checkConstantIfs "[[ $n -le 4 && n != 2 ]]" prop_checkConstantIfs4 = verifyNot checkConstantIfs "[[ $n -le 3 ]]" prop_checkConstantIfs5 = verifyNot checkConstantIfs "[[ $n -le $n ]]"-checkConstantIfs _ (TC_Binary id typ op lhs rhs)- | op `elem` [ "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~", ">", "<", "="] =- when (isJust lLit && isJust rLit) $ warn id 2050 "This expression is constant. Did you forget the $ on a variable?"- where- lLit = getLiteralString lhs- rLit = getLiteralString rhs+prop_checkConstantIfs6 = verifyNot checkConstantIfs "[[ a -ot b ]]"+prop_checkConstantIfs7 = verifyNot checkConstantIfs "[ a -nt b ]"+checkConstantIfs _ (TC_Binary id typ op lhs rhs) | not isDynamic =+ when (isJust lLit && isJust rLit) $+ warn id 2050 "This expression is constant. Did you forget the $ on a variable?"+ where+ lLit = getLiteralString lhs+ rLit = getLiteralString rhs+ isDynamic =+ op `elem` [ "-lt", "-gt", "-le", "-ge", "-eq", "-ne" ]+ && typ == DoubleBracket+ || op `elem` [ "-nt", "-ot", "-ef"] checkConstantIfs _ _ = return () prop_checkLiteralBreakingTest = verify checkLiteralBreakingTest "[[ a==$foo ]]"@@ -1249,13 +1349,13 @@ prop_checkLiteralBreakingTest6 = verify checkLiteralBreakingTest "[ -z $(true)z ]" prop_checkLiteralBreakingTest7 = verifyNot checkLiteralBreakingTest "[ -z $(true) ]" prop_checkLiteralBreakingTest8 = verifyNot checkLiteralBreakingTest "[ $(true)$(true) ]"+prop_checkLiteralBreakingTest10 = verify checkLiteralBreakingTest "[ -z foo ]" checkLiteralBreakingTest _ t = potentially $ case t of (TC_Noary _ _ w@(T_NormalWord _ l)) -> do- guard . not $ isConstant w+ guard . not $ isConstant w -- Covered by SC2078 comparisonWarning l `mplus` tautologyWarning w "Argument to implicit -n is always true due to literal strings."- (TC_Unary _ _ op w@(T_NormalWord _ l)) -> do- guard $ not $ isConstant w+ (TC_Unary _ _ op w@(T_NormalWord _ l)) -> case op of "-n" -> tautologyWarning w "Argument to -n is always true due to literal strings." "-z" -> tautologyWarning w "Argument to -z is always false due to literal strings."@@ -1312,7 +1412,8 @@ checkBraceExpansionVars _ _ = return () prop_checkForDecimals = verify checkForDecimals "((3.14*c))"-checkForDecimals _ t@(TA_Expansion id _) = potentially $ do+checkForDecimals params t@(TA_Expansion id _) = potentially $ do+ guard $ not (hasFloatingPoint params) str <- getLiteralString t first <- str !!! 0 guard $ isDigit first && '.' `elem` str@@ -1562,17 +1663,21 @@ -- Check whether a word is entirely output from a single command tokenIsJustCommandOutput t = case t of- T_NormalWord id [T_DollarExpansion _ _] -> True- T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ _]] -> True- T_NormalWord id [T_Backticked _ _] -> True- T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ _]] -> True+ T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds+ T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ cmds]] -> check cmds+ T_NormalWord id [T_Backticked _ cmds] -> check cmds+ T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ cmds]] -> check cmds _ -> False+ where+ check [x] = not $ isOnlyRedirection x+ check _ = False prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)" prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`" prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\"" prop_checkUuoeCmd4 = verify checkUuoeCmd "echo \"`date`\"" prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\""+prop_checkUuoeCmd6 = verifyNot checkUuoeCmd "echo \"$(<file)\"" checkUuoeCmd _ = checkUnqualifiedCommand "echo" (const f) where msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'." f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token)@@ -1586,6 +1691,7 @@ prop_checkUuoeVar6 = verifyNot checkUuoeVar "foo \"$(echo files: *.png)\"" prop_checkUuoeVar7 = verifyNot checkUuoeVar "foo $(echo $(bar))" -- covered by 2005 prop_checkUuoeVar8 = verifyNot checkUuoeVar "#!/bin/sh\nz=$(echo)"+prop_checkUuoeVar9 = verify checkUuoeVar "foo $(echo $(<file))" checkUuoeVar _ p = case p of T_Backticked id [cmd] -> check id cmd@@ -1723,8 +1829,22 @@ prop_checkTestRedirects1 = verify checkTestRedirects "test 3 > 1" prop_checkTestRedirects2 = verifyNot checkTestRedirects "test 3 \\> 1" prop_checkTestRedirects3 = verify checkTestRedirects "/usr/bin/test $var > $foo"-checkTestRedirects _ (T_Redirecting id redirs@(redir:_) cmd) | cmd `isCommand` "test" =- warn (getId redir) 2065 "This is interpretted as a shell file redirection, not a comparison."+prop_checkTestRedirects4 = verifyNot checkTestRedirects "test 1 -eq 2 2> file"+checkTestRedirects _ (T_Redirecting id redirs cmd) | cmd `isCommand` "test" =+ mapM_ check redirs+ where+ check t =+ when (suspicious t) $+ warn (getId t) 2065 "This is interpreted as a shell file redirection, not a comparison."+ suspicious t = -- Ignore redirections of stderr because these are valid for squashing e.g. int errors,+ case t of -- and >> and similar redirections because these are probably not comparisons.+ T_FdRedirect _ fd (T_IoFile _ op _) -> fd /= "2" && isComparison op+ otherwise -> False+ isComparison t =+ case t of+ T_Greater _ -> True+ T_Less _ -> True+ otherwise -> False checkTestRedirects _ _ = return () prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"@@ -1808,7 +1928,8 @@ prop_checkBackticks1 = verify checkBackticks "echo `foo`" prop_checkBackticks2 = verifyNot checkBackticks "echo $(foo)"-checkBackticks _ (T_Backticked id _) =+prop_checkBackticks3 = verifyNot checkBackticks "echo `#inlined comment` foo"+checkBackticks _ (T_Backticked id list) | not (null list) = style id 2006 "Use $(..) instead of legacy `..`." checkBackticks _ _ = return () @@ -2096,6 +2217,7 @@ lastCreatesSubshell = case shell of Bash -> True+ Dash -> True Sh -> True Ksh -> False @@ -2144,7 +2266,7 @@ "export" -> if "f" `elem` flags then [] else concatMap getReference rest- "declare" -> if "x" `elem` flags+ "declare" -> if any (`elem` flags) ["x", "p"] then concatMap getReference rest else [] "readonly" -> concatMap getReference rest@@ -2305,6 +2427,10 @@ TC_Unary id _ "-v" token -> getIfReference t token TC_Unary id _ "-R" token -> getIfReference t token+ TC_Binary id DoubleBracket op lhs rhs ->+ if isDereferencing op+ then concatMap (getIfReference t) [lhs, rhs]+ else [] t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo [(t, t, takeWhile (/= '}') var) | isClosingFileOp op]@@ -2330,6 +2456,8 @@ when (isDigit $ head str) $ fail "is a number" return (context, token, getBracedReference str) + isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])+ -- Try to get referenced variables from a literal string like "$foo" -- Ignores tons of cases like arithmetic evaluation and array indices. prop_getVariablesFromLiteral1 =@@ -2445,6 +2573,8 @@ prop_checkSpacefulness28= verifyNotTree checkSpacefulness "exec {n}>&1; echo $n" prop_checkSpacefulness29= verifyNotTree checkSpacefulness "n=$(stuff); exec {n}>&-;" prop_checkSpacefulness30= verifyTree checkSpacefulness "file='foo bar'; echo foo > $file;"+prop_checkSpacefulness31= verifyNotTree checkSpacefulness "echo \"`echo \\\"$1\\\"`\""+prop_checkSpacefulness32= verifyNotTree checkSpacefulness "var=$1; [ -v var ]" checkSpacefulness params t = doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)@@ -2461,7 +2591,7 @@ readF _ token name = do spaced <- hasSpaces name return [makeComment InfoC (getId token) 2086 warning |- spaced+ isExpansion token && spaced && not (isArrayExpansion token) -- There's another warning for this && not (isCounting token) && not (isQuoteFree parents token)@@ -2483,6 +2613,11 @@ parents = parentMap params + isExpansion t =+ case t of+ (T_DollarBraced _ _ ) -> True+ _ -> False+ isCounting (T_DollarBraced id token) = case concat $ oversimplify token of '#':_ -> True@@ -2525,6 +2660,7 @@ prop_checkQuotesInLiterals6a= verifyNotTree checkQuotesInLiterals "param='my\\ file'; cmd=\"rm ${#param}\"; $cmd" prop_checkQuotesInLiterals7 = verifyTree checkQuotesInLiterals "param='my\\ file'; rm $param" prop_checkQuotesInLiterals8 = verifyTree checkQuotesInLiterals "param=\"/foo/'bar baz'/etc\"; rm $param"+prop_checkQuotesInLiterals9 = verifyNotTree checkQuotesInLiterals "param=\"/foo/'bar baz'/etc\"; rm ${#param}" checkQuotesInLiterals params t = doVariableFlowAnalysis readF writeF Map.empty (variableFlow params) where@@ -2556,12 +2692,18 @@ then return $ getId t else Nothing + squashesQuotes t =+ case t of+ T_DollarBraced id _ -> "#" `isPrefixOf` bracedString t+ otherwise -> False+ readF _ expr name = do assignment <- getQuotes name return (if isJust assignment && not (isParamTo parents "eval" expr) && not (isQuoteFree parents expr)+ && not (squashesQuotes expr) then [ makeComment WarningC (fromJust assignment) 2089 "Quotes/backslashes will be treated literally. Use an array.",@@ -2643,6 +2785,9 @@ prop_checkUnused24= verifyNotTree checkUnusedAssignments "mapfile -C a b; echo ${b[@]}" prop_checkUnused25= verifyNotTree checkUnusedAssignments "readarray foo; echo ${foo[@]}" prop_checkUnused26= verifyNotTree checkUnusedAssignments "declare -F foo"+prop_checkUnused27= verifyTree checkUnusedAssignments "var=3; [ var -eq 3 ]"+prop_checkUnused28= verifyNotTree checkUnusedAssignments "var=3; [[ var -eq 3 ]]"+prop_checkUnused29= verifyNotTree checkUnusedAssignments "var=(a b); declare -p var" checkUnusedAssignments params t = execWriter (mapM_ warnFor unused) where flow = variableFlow params@@ -2753,7 +2898,7 @@ isGuarded _ = False match var candidate =- if var /= candidate && (map toLower var) == (map toLower candidate)+ if var /= candidate && map toLower var == map toLower candidate then 1 else dist var candidate @@ -2765,9 +2910,7 @@ mapM_ check $ takeWhile (not . isEndOfArgs) args where check v@(T_NormalWord _ (T_Glob id s:_)) | s == "*" || s == "?" =- info id 2035 $- "Use ./" ++ concat (oversimplify v)- ++ " so names with dashes won't become options."+ info id 2035 "Use ./*glob* or -- *glob* so names with dashes won't become options." check _ = return () isEndOfArgs t =@@ -2923,11 +3066,18 @@ subshellType t = case leadType (shellType params) (parentMap params) t of NoneScope -> Nothing SubshellScope str -> return str- isFunction t = case t of T_Function {} -> True; _ -> False relevant t = isLoop t || isFunction t || isJust (subshellType t) checkLoopKeywordScope _ _ = return () +prop_checkLocalScope1 = verify checkLocalScope "local foo=3"+prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }"+checkLocalScope params t | t `isCommand` "local" && not (isInFunction t) =+ err (getId t) 2168 "'local' is only valid in functions."+ where+ isInFunction t = any isFunction $ getPath (parentMap params) t+checkLocalScope _ _ = return ()+ prop_checkFunctionDeclarations1 = verify checkFunctionDeclarations "#!/bin/ksh\nfunction foo() { command foo --lol \"$@\"; }" prop_checkFunctionDeclarations2 = verify checkFunctionDeclarations "#!/bin/dash\nfunction foo { lol; }" prop_checkFunctionDeclarations3 = verifyNot checkFunctionDeclarations "foo() { echo bar; }"@@ -2938,7 +3088,11 @@ Ksh -> when (hasKeyword && hasParens) $ err id 2111 "ksh does not allow 'function' keyword and '()' at the same time."- Sh -> do+ Dash -> forSh+ Sh -> forSh++ where+ forSh = do when (hasKeyword && hasParens) $ warn id 2112 "'function' keyword is non-standard. Delete it." when (hasKeyword && not hasParens) $@@ -3417,6 +3571,8 @@ prop_checkUncheckedCd4 = verifyNotTree checkUncheckedCd "if cd foo; then rm foo; fi" prop_checkUncheckedCd5 = verifyTree checkUncheckedCd "if true; then cd foo; fi" prop_checkUncheckedCd6 = verifyNotTree checkUncheckedCd "cd .."+prop_checkUncheckedCd7 = verifyNotTree checkUncheckedCd "#!/bin/bash -e\ncd foo\nrm bar"+prop_checkUncheckedCd8 = verifyNotTree checkUncheckedCd "set -o errexit; cd foo; rm bar" checkUncheckedCd params root = if hasSetE then [] else execWriter $ doAnalysis checkElement root where@@ -3430,9 +3586,12 @@ hasSetE = isNothing $ doAnalysis (guard . not . isSetE) root isSetE t = case t of- T_SimpleCommand {} ->- t `isUnqualifiedCommand` "set" && "e" `elem` map snd (getAllFlags t)+ T_Script _ str _ -> str `matches` re+ T_SimpleCommand {} ->+ t `isUnqualifiedCommand` "set" &&+ ("errexit" `elem` oversimplify t || "e" `elem` map snd (getAllFlags t)) _ -> False+ re = mkRegex "[[:space:]]-[^-]*e" prop_checkLoopVariableReassignment1 = verify checkLoopVariableReassignment "for i in *; do for i in *.bar; do true; done; done" prop_checkLoopVariableReassignment2 = verify checkLoopVariableReassignment "for i in *; do for((i=0; i<3; i++)); do true; done; done"@@ -3448,7 +3607,7 @@ next <- listToMaybe $ filter (\x -> loopVariable x == Just str) path return $ do warn (getId token) 2165 "This nested loop overrides the index variable of its parent."- warn (getId next) 2165 "This parent loop has its index variable overridden."+ warn (getId next) 2167 "This parent loop has its index variable overridden." path = drop 1 $ getPath (parentMap params) token loopVariable :: Token -> Maybe String loopVariable t =@@ -3460,6 +3619,91 @@ (TA_Expansion _ [T_Literal _ var]) _]) _ _ _ -> return var _ -> fail "not loop"++prop_checkTrailingBracket1 = verify checkTrailingBracket "if -z n ]]; then true; fi "+prop_checkTrailingBracket2 = verifyNot checkTrailingBracket "if [[ -z n ]]; then true; fi "+prop_checkTrailingBracket3 = verify checkTrailingBracket "a || b ] && thing"+prop_checkTrailingBracket4 = verifyNot checkTrailingBracket "run [ foo ]"+prop_checkTrailingBracket5 = verifyNot checkTrailingBracket "run bar ']'"+checkTrailingBracket _ token =+ case token of+ T_SimpleCommand _ _ tokens@(_:_) -> check (last tokens) token+ otherwise -> return ()+ where+ check t command =+ case t of+ T_NormalWord id [T_Literal _ str] -> potentially $ do+ guard $ str `elem` [ "]]", "]" ]+ let opposite = invert str+ parameters = oversimplify command+ guard $ opposite `notElem` parameters+ return $ warn id 2171 $+ "Found trailing " ++ str ++ " outside test. Missing " ++ opposite ++ "?"+ otherwise -> return ()+ invert s =+ case s of+ "]]" -> "[["+ "]" -> "["+ x -> x++prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8"+prop_checkNonportableSignals2 = verifyNot checkNonportableSignals "trap f 0"+prop_checkNonportableSignals3 = verifyNot checkNonportableSignals "trap f 14"+prop_checkNonportableSignals4 = verify checkNonportableSignals "trap f SIGKILL"+prop_checkNonportableSignals5 = verify checkNonportableSignals "trap f 9"+prop_checkNonportableSignals6 = verify checkNonportableSignals "trap f stop"+checkNonportableSignals _ = checkUnqualifiedCommand "trap" (const f)+ where+ f = mapM_ check+ check param = potentially $ do+ str <- getLiteralString param+ let id = getId param+ return $ sequence_ $ mapMaybe (\f -> f id str) [+ checkNumeric,+ checkUntrappable+ ]++ checkNumeric id str = do+ guard $ not (null str)+ guard $ all isDigit str+ guard $ str /= "0" -- POSIX exit trap+ guard $ str `notElem` ["1", "2", "3", "6", "9", "14", "15" ] -- XSI+ return $ warn id 2172+ "Trapping signals by number is not well defined. Prefer signal names."++ checkUntrappable id str = do+ guard $ map toLower str `elem` ["kill", "9", "sigkill", "stop", "sigstop"]+ return $ err id 2173+ "SIGKILL/SIGSTOP can not be trapped."++prop_checkMkdirDashPM0 = verify checkMkdirDashPM "mkdir -p -m 0755 a/b"+prop_checkMkdirDashPM1 = verify checkMkdirDashPM "mkdir -pm 0755 $dir"+prop_checkMkdirDashPM2 = verify checkMkdirDashPM "mkdir -vpm 0755 a/b"+prop_checkMkdirDashPM3 = verify checkMkdirDashPM "mkdir -pm 0755 -v a/b"+prop_checkMkdirDashPM4 = verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b"+prop_checkMkdirDashPM5 = verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b"+prop_checkMkdirDashPM6 = verify checkMkdirDashPM "mkdir -p --mode=0755 a/b"+prop_checkMkdirDashPM7 = verify checkMkdirDashPM "mkdir --parents -m 0755 a/b"+prop_checkMkdirDashPM8 = verifyNot checkMkdirDashPM "mkdir -p a/b"+prop_checkMkdirDashPM9 = verifyNot checkMkdirDashPM "mkdir -m 0755 a/b"+prop_checkMkdirDashPM10 = verifyNot checkMkdirDashPM "mkdir a/b"+prop_checkMkdirDashPM11 = verifyNot checkMkdirDashPM "mkdir --parents a/b"+prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b"+prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b"+prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"+checkMkdirDashPM _ t@(T_SimpleCommand _ _ args) = potentially $ do+ name <- getCommandName t+ guard $ name == "mkdir"+ dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags+ dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags+ guard $ any couldHaveSubdirs (drop 1 args) -- mkdir -pm 0700 dir is fine, but dir/subdir is not.+ return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."+ where+ flags = getAllFlags t+ couldHaveSubdirs t = fromMaybe True $ do+ name <- getLiteralString t+ return $ '/' `elem` name+checkMkdirDashPM _ _ = return () return [] runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
ShellCheck/Checker.hs view
@@ -147,6 +147,9 @@ prop_cantSourceDynamic = [1090] == checkWithIncludes [("lib", "")] ". \"$1\"" +prop_cantSourceDynamic2 =+ [1090] == checkWithIncludes [("lib", "")] "source ~/foo"+ prop_canSourceDynamicWhenRedirected = null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
ShellCheck/Data.hs view
@@ -25,13 +25,14 @@ "FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE", "HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS", "IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",- "LC_MESSAGES", "LC_NUMERIC", "LINES", "MAIL", "MAILCHECK", "MAILPATH",- "OPTERR", "PATH", "POSIXLY_CORRECT", "PROMPT_COMMAND",- "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT",- "TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC",+ "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LINES", "MAIL",+ "MAILCHECK", "MAILPATH", "OPTERR", "PATH", "POSIXLY_CORRECT",+ "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL",+ "TIMEFORMAT", "TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC", -- Other- "USER", "TZ", "TERM"+ "USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",+ "HOSTNAME", "KRB5CCNAME", "XAUTHORITY" ] variablesWithoutSpaces = [@@ -41,6 +42,12 @@ "COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES" ] +arrayVariables = [+ "BASH_ALIASES", "BASH_ARGC", "BASH_ARGV", "BASH_CMDS", "BASH_LINENO",+ "BASH_REMATCH", "BASH_SOURCE", "BASH_VERSINFO", "COMP_WORDS", "COPROC",+ "DIRSTACK", "FUNCNAME", "GROUPS", "MAPFILE", "PIPESTATUS", "COMPREPLY"+ ]+ commonCommands = [ "admin", "alias", "ar", "asa", "at", "awk", "basename", "batch", "bc", "bg", "break", "c99", "cal", "cat", "cd", "cflow", "chgrp",@@ -76,13 +83,12 @@ ] shellForExecutable :: String -> Maybe Shell-shellForExecutable "sh" = return Sh-shellForExecutable "ash" = return Sh-shellForExecutable "dash" = return Sh--shellForExecutable "ksh" = return Ksh-shellForExecutable "ksh88" = return Ksh-shellForExecutable "ksh93" = return Ksh--shellForExecutable "bash" = return Bash-shellForExecutable _ = Nothing+shellForExecutable name =+ case name of+ "sh" -> return Sh+ "bash" -> return Bash+ "dash" -> return Dash+ "ksh" -> return Ksh+ "ksh88" -> return Ksh+ "ksh93" -> return Ksh+ otherwise -> Nothing
ShellCheck/Formatter/TTY.hs view
@@ -27,15 +27,15 @@ import System.Info import System.IO -format :: IO Formatter-format = return Formatter {+format :: FormatterOptions -> IO Formatter+format options = return Formatter { header = return (), footer = return (),- onFailure = outputError,- onResult = outputResult+ onFailure = outputError options,+ onResult = outputResult options } -colorForLevel level = +colorForLevel level = case level of "error" -> 31 -- red "warning" -> 33 -- yellow@@ -44,13 +44,13 @@ "message" -> 1 -- bold "source" -> 0 -- none otherwise -> 0 -- none- -outputError file error = do- color <- getColorFunc++outputError options file error = do+ color <- getColorFunc $ foColorOption options hPutStrLn stderr $ color "error" $ file ++ ": " ++ error -outputResult result contents = do- color <- getColorFunc+outputResult options result contents = do+ color <- getColorFunc $ foColorOption options let comments = crComments result let fileLines = lines contents let lineCount = fromIntegral $ length fileLines@@ -75,10 +75,15 @@ code code = "SC" ++ show code -getColorFunc = do+getColorFunc colorOption = do term <- hIsTerminalDevice stdout let windows = "mingw" `isPrefixOf` os- return $ if term && not windows then colorComment else const id+ let isUsableTty = term && not windows+ let useColor = case colorOption of+ ColorAlways -> True+ ColorNever -> False+ ColorAuto -> isUsableTty+ return $ if useColor then colorComment else const id where colorComment level comment = ansi (colorForLevel level) ++ comment ++ clear
ShellCheck/Interface.hs view
@@ -72,8 +72,15 @@ arComments :: [TokenComment] } ++-- Formatter options+data FormatterOptions = FormatterOptions {+ foColorOption :: ColorOption+}++ -- Supporting data types-data Shell = Ksh | Sh | Bash deriving (Show, Eq)+data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq) data ExecutionMode = Executed | Sourced deriving (Show, Eq) type ErrorMessage = String@@ -89,6 +96,12 @@ data Comment = Comment Severity Code String deriving (Show, Eq) data PositionedComment = PositionedComment Position Comment deriving (Show, Eq) data TokenComment = TokenComment Id Comment deriving (Show, Eq)++data ColorOption =+ ColorAuto+ | ColorAlways+ | ColorNever+ deriving (Ord, Eq, Show) -- For testing mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
ShellCheck/Parser.hs view
@@ -359,19 +359,36 @@ readCondBinaryOp = try $ do optional guardArithmetic id <- getNextId- op <- choice (map tryOp ["==", "!=", "<=", ">=", "=~", ">", "<", "=", "\\<=", "\\>=", "\\<", "\\>"]) <|> otherOp+ op <- getOp spacingOrLf return op where- tryOp s = try $ do- id <- getNextId- string s- return $ TC_Binary id typ s- otherOp = try $ do+ flaglessOps = [ "==", "!=", "<=", ">=", "=~", ">", "<", "=" ]++ getOp = do id <- getNextId+ op <- anyQuotedOp <|> anyEscapedOp <|> anyOp+ return $ TC_Binary id typ op++ -- hacks to read quoted operators without having to read a shell word+ anyEscapedOp = try $ do+ char '\\'+ escaped <$> anyOp+ anyQuotedOp = try $ do+ c <- oneOf "'\""+ s <- anyOp+ char c+ return s++ anyOp = flagOp <|> flaglessOp <|> fail+ "Expected comparison operator (don't wrap commands in []/[[]])"+ flagOp = try $ do s <- readOp when (s == "-a" || s == "-o") $ fail "Unexpected operator"- return $ TC_Binary id typ s+ return s+ flaglessOp =+ choice $ map (try . string) flaglessOps+ escaped s = if any (`elem` s) "<>" then '\\':s else s guardArithmetic = do try . lookAhead $ disregard (oneOf "+*/%") <|> disregard (string "- ")@@ -394,10 +411,17 @@ return $ TC_Unary id typ s readOp = try $ do- char '-'- s <- many1 letter+ char '-' <|> weirdDash+ s <- many1 letter <|> fail "Expected a test operator" return ('-':s) + weirdDash = do+ pos <- getPosition+ oneOf "\x058A\x05BE\x2010\x2011\x2012\x2013\x2014\x2015\xFE63\xFF0D"+ parseProblemAt pos ErrorC 1100+ "This is a unicode dash. Delete and retype as ASCII minus."+ return '-'+ readCondWord = do notFollowedBy2 (try (spacing >> string "]")) x <- readNormalWord@@ -429,6 +453,7 @@ return $ TC_Or id typ x readAndOrOp op requiresSpacing = do+ optional $ lookAhead weirdDash x <- string op condSpacing requiresSpacing return x@@ -598,7 +623,7 @@ readDoubleQuoted, readNormalDollar, readBraced,- readBackTicked,+ readUnquotedBackTicked, readNormalLiteral "+-*/=%^,]?:" ] spacing@@ -706,6 +731,10 @@ prop_readCondition11= isOk readCondition "[[ a == b ||\n c == d ]]" prop_readCondition12= isWarning readCondition "[ a == b \n -o c == d ]" prop_readCondition13= isOk readCondition "[[ foo =~ ^fo{1,3}$ ]]"+prop_readCondition14= isOk readCondition "[ foo '>' bar ]"+prop_readCondition15= isOk readCondition "[ foo \">=\" bar ]"+prop_readCondition16= isOk readCondition "[ foo \\< bar ]"+prop_readCondition17= isOk readCondition "[[ ${file::1} = [-.\\|/\\\\] ]]" readCondition = called "test expression" $ do opos <- getPosition id <- getNextId@@ -725,7 +754,7 @@ condition <- readConditionContents single cpos <- getPosition- close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here"+ close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here (don't wrap commands in []/[[]])" when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Did you mean ]] ?" when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?" spacing@@ -805,7 +834,7 @@ readGlob, readNormalDollar, readBraced,- readBackTicked,+ readUnquotedBackTicked, readProcSub, readNormalLiteral end, readLiteralCurlyBraces@@ -841,7 +870,7 @@ list <- many readDollarBracedPart return $ T_NormalWord id list -readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|> readExtglob <|> readNormalDollar <|> readBackTicked <|> readDollarBracedLiteral+readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|> readExtglob <|> readNormalDollar <|> readUnquotedBackTicked <|> readDollarBracedLiteral readDollarBracedLiteral = do id <- getNextId@@ -899,15 +928,18 @@ readSingleEscaped <|> many1 (noneOf "'\\\x2018\x2019") -prop_readBackTicked = isOk readBackTicked "`ls *.mp3`"-prop_readBackTicked2 = isOk readBackTicked "`grep \"\\\"\"`"-prop_readBackTicked3 = isWarning readBackTicked "´grep \"\\\"\"´"-prop_readBackTicked4 = isOk readBackTicked "`echo foo\necho bar`"++prop_readBackTicked = isOk (readBackTicked False) "`ls *.mp3`"+prop_readBackTicked2 = isOk (readBackTicked False) "`grep \"\\\"\"`"+prop_readBackTicked3 = isWarning (readBackTicked False) "´grep \"\\\"\"´"+prop_readBackTicked4 = isOk readSimpleCommand "`echo foo\necho bar`" prop_readBackTicked5 = isOk readSimpleCommand "echo `foo`bar" prop_readBackTicked6 = isWarning readSimpleCommand "echo `foo\necho `bar" prop_readBackTicked7 = isOk readSimpleCommand "`#inline comment`" prop_readBackTicked8 = isOk readSimpleCommand "echo `#comment` \\\nbar baz"-readBackTicked = called "backtick expansion" $ do+readQuotedBackTicked = readBackTicked True+readUnquotedBackTicked = readBackTicked False+readBackTicked quoted = called "backtick expansion" $ do id <- getNextId startPos <- getPosition backtick@@ -926,6 +958,7 @@ return $ T_Backticked id result where unEscape [] = []+ unEscape ('\\':'"':rest) | quoted = '"' : unEscape rest unEscape ('\\':x:rest) | x `elem` "$`\\" = x : unEscape rest unEscape ('\\':'\n':rest) = unEscape rest unEscape (c:rest) = c : unEscape rest@@ -993,7 +1026,7 @@ parseProblemAt endPos InfoC 1079 "This is actually an end quote, but due to next char it looks suspect." -doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readBackTicked+doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readQuotedBackTicked readDoubleQuotedLiteral = do doubleQuote@@ -1020,6 +1053,9 @@ prop_readGlob3 = isOk readGlob "[a[:alpha:]]" prop_readGlob4 = isOk readGlob "[[:alnum:]]" prop_readGlob5 = isOk readGlob "[^[:alpha:]1-9]"+prop_readGlob6 = isOk readGlob "[\\|]"+prop_readGlob7 = isOk readGlob "[^[]"+prop_readGlob8 = isOk readGlob "[*?]" readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral where readSimple = do@@ -1030,11 +1066,11 @@ readClass = try $ do id <- getNextId char '['- s <- many1 (predefined <|> liftM return (letter <|> digit <|> oneOf globchars))+ s <- many1 (predefined <|> readNormalLiteralPart "]" <|> globchars) char ']' return $ T_Glob id $ "[" ++ concat s ++ "]" where- globchars = "^-_:?*.,!~@#$%=+{}/~"+ globchars = liftM return . oneOf $ "!$[" ++ extglobStartChars predefined = do try $ string "[:" s <- many1 letter@@ -1152,12 +1188,19 @@ prop_readBraced4 = isOk readBraced "{1,{2,3}}" prop_readBraced5 = isOk readBraced "{JP{,E}G,jp{,e}g}" prop_readBraced6 = isOk readBraced "{foo,bar,$((${var}))}"+prop_readBraced7 = isNotOk readBraced "{}"+prop_readBraced8 = isNotOk readBraced "{foo}" readBraced = try braceExpansion where braceExpansion = T_BraceExpansion `withParser` do char '{' elements <- bracedElement `sepBy1` char ','+ guard $+ case elements of+ (_:_:_) -> True+ [t] -> ".." `isInfixOf` onlyLiteralString t+ [] -> False char '}' return elements bracedElement =@@ -1529,7 +1572,11 @@ readSource :: Monad m => SourcePos -> Token -> SCParser m Token readSource pos t@(T_Redirecting _ _ (T_SimpleCommand _ _ (cmd:file:_))) = do override <- getSourceOverride- let literalFile = override `mplus` getLiteralString file+ let literalFile = do+ name <- override `mplus` getLiteralString file+ -- Hack to avoid 'source ~/foo' trying to read from literal tilde+ guard . not $ "~/" `isPrefixOf` name+ return name case literalFile of Nothing -> do parseNoteAt pos WarningC 1090@@ -1933,6 +1980,9 @@ prop_readFunctionDefinition6 = isOk readFunctionDefinition "?(){ foo; }" prop_readFunctionDefinition7 = isOk readFunctionDefinition "..(){ cd ..; }" prop_readFunctionDefinition8 = isOk readFunctionDefinition "foo() (ls)"+prop_readFunctionDefinition9 = isOk readFunctionDefinition "function foo { true; }"+prop_readFunctionDefinition10= isOk readFunctionDefinition "function foo () { true; }"+prop_readFunctionDefinition11= isWarning readFunctionDefinition "function foo{\ntrue\n}" readFunctionDefinition = called "function" $ do functionSignature <- try readFunctionSignature allspacing@@ -1950,8 +2000,11 @@ whitespace spacing name <- readFunctionName- spacing+ spaces <- spacing hasParens <- wasIncluded readParens+ when (not hasParens && null spaces) $+ acceptButWarn (lookAhead (oneOf "{("))+ ErrorC 1095 "You need a space or linefeed between the function name and body." return $ T_Function id (FunctionKeyword True) (FunctionParentheses hasParens) name readWithoutFunction = try $ do@@ -2064,6 +2117,8 @@ prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42" prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= " prop_readAssignmentWord9a= isOk readAssignmentWord "foo="+prop_readAssignmentWord9b= isOk readAssignmentWord "foo= "+prop_readAssignmentWord9c= isOk readAssignmentWord "foo= #bar" prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42" prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )" prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'"@@ -2082,7 +2137,7 @@ isEndOfCommand <- liftM isJust $ optionMaybe (try . lookAhead $ (disregard (oneOf "\r\n;&|)") <|> eof)) if not hasLeftSpace && (hasRightSpace || isEndOfCommand) then do- when (variable /= "IFS" && hasRightSpace) $+ when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $ parseNoteAt pos WarningC 1007 "Remove space after = if trying to assign a value (for empty string, use var='' ... )." value <- readEmptyLiteral@@ -2334,6 +2389,7 @@ isWarning p s = parsesCleanly p s == Just False isOk p s = parsesCleanly p s == Just True+isNotOk p s = parsesCleanly p s == Nothing testParse string = runIdentity $ do (res, _) <- runParser (mockedSystemInterface []) readScript "-" string
shellcheck.1 view
@@ -1,7 +1,7 @@-.TH SHELLCHECK 1 "" "Shell script analysis tool"+.TH "SHELLCHECK" "1" "" "Shell script analysis tool" "" .SH NAME .PP-shellcheck - Shell script analysis tool+shellcheck \- Shell script analysis tool .SH SYNOPSIS .PP \f[B]shellcheck\f[] [\f[I]OPTIONS\f[]...] \f[I]FILES\f[]...@@ -14,7 +14,7 @@ advanced issues where corner cases can cause delayed failures. .PP ShellCheck gives shell specific advice.-Consider the line:+Consider this line: .IP .nf \f[C]@@ -23,47 +23,57 @@ .fi .IP \[bu] 2 For scripts starting with \f[C]#!/bin/sh\f[] (or when using-\f[C]-s\ sh\f[]), ShellCheck will warn that \f[C]((\ ..\ ))\f[] is not+\f[C]\-s\ sh\f[]), ShellCheck will warn that \f[C]((\ ..\ ))\f[] is not POSIX compliant (similar to checkbashisms). .IP \[bu] 2 For scripts starting with \f[C]#!/bin/bash\f[] (or using-\f[C]-s\ bash\f[]), ShellCheck will warn that decimals are not+\f[C]\-s\ bash\f[]), ShellCheck will warn that decimals are not supported. .IP \[bu] 2 For scripts starting with \f[C]#!/bin/ksh\f[] (or using-\f[C]-s\ ksh\f[]), ShellCheck will not warn at all, as \f[C]ksh\f[]+\f[C]\-s\ ksh\f[]), ShellCheck will not warn at all, as \f[C]ksh\f[] supports decimals in arithmetic contexts. .SH OPTIONS .TP-.B \f[B]-e\f[]\ \f[I]CODE1\f[][,\f[I]CODE2\f[]...],\ \f[B]--exclude=\f[]\f[I]CODE1\f[][,\f[I]CODE2\f[]...]+.B \f[B]\-C\f[][\f[I]WHEN\f[]],\ \f[B]\-\-color\f[][=\f[I]WHEN\f[]]+For TTY outut, enable colors \f[I]always\f[], \f[I]never\f[] or+\f[I]auto\f[].+The default is \f[I]auto\f[].+\f[B]\-\-color\f[] without an argument is equivalent to+\f[B]\-\-color=always\f[].+.RS+.RE+.TP+.B \f[B]\-e\f[]\ \f[I]CODE1\f[][,\f[I]CODE2\f[]...],\ \f[B]\-\-exclude=\f[]\f[I]CODE1\f[][,\f[I]CODE2\f[]...] Explicitly exclude the specified codes from the report.-Subsequent \f[B]-e\f[] options are cumulative, but all the codes can be-specified at once, comma-separated as a single argument.+Subsequent \f[B]\-e\f[] options are cumulative, but all the codes can be+specified at once, comma\-separated as a single argument. .RS .RE .TP-.B \f[B]-f\f[] \f[I]FORMAT\f[], \f[B]--format=\f[]\f[I]FORMAT\f[]+.B \f[B]\-f\f[] \f[I]FORMAT\f[], \f[B]\-\-format=\f[]\f[I]FORMAT\f[] Specify the output format of shellcheck, which prints its results in the standard output.-Subsequent \f[B]-f\f[] options are ignored, see \f[B]FORMATS\f[] below+Subsequent \f[B]\-f\f[] options are ignored, see \f[B]FORMATS\f[] below for more information. .RS .RE .TP-.B \f[B]-s\f[]\ \f[I]shell\f[],\ \f[B]--shell=\f[]\f[I]shell\f[]+.B \f[B]\-s\f[]\ \f[I]shell\f[],\ \f[B]\-\-shell=\f[]\f[I]shell\f[] Specify Bourne shell dialect.-Valid values are \f[I]sh\f[], \f[I]bash\f[] and \f[I]ksh\f[].+Valid values are \f[I]sh\f[], \f[I]bash\f[], \f[I]dash\f[] and+\f[I]ksh\f[]. The default is to use the file\[aq]s shebang, or \f[I]bash\f[] if the target shell can\[aq]t be determined. .RS .RE .TP-.B \f[B]-V\f[],\ \f[B]--version\f[]+.B \f[B]\-V\f[],\ \f[B]\-\-version\f[] Print version information and exit. .RS .RE .TP-.B \f[B]-x\f[],\ \f[B]-external-sources\f[]+.B \f[B]\-x\f[],\ \f[B]\-\-external\-sources\f[] Follow \[aq]source\[aq] statements even when the file is not specified as input. By default, \f[C]shellcheck\f[] will only follow files specified on the@@ -84,9 +94,10 @@ Useful for editors that support compiling and showing syntax errors. .RS .PP-For example, in Vim, \f[C]:set\ makeprg=shellcheck\\\ -f\\\ gcc\\\ %\f[]-will allow using \f[C]:make\f[] to check the script, and \f[C]:cnext\f[]-to jump to the next error.+For example, in Vim,+\f[C]:set\ makeprg=shellcheck\\\ \-f\\\ gcc\\\ %\f[] will allow using+\f[C]:make\f[] to check the script, and \f[C]:cnext\f[] to jump to the+next error. .IP .nf \f[C]@@ -103,7 +114,7 @@ .IP .nf \f[C]-<?xml\ version=\[aq]1.0\[aq]\ encoding=\[aq]UTF-8\[aq]?>+<?xml\ version=\[aq]1.0\[aq]\ encoding=\[aq]UTF\-8\[aq]?> <checkstyle\ version=\[aq]4.3\[aq]> \ \ <file\ name=\[aq]file\[aq]> \ \ \ \ <error@@ -150,7 +161,7 @@ .nf \f[C] #\ shellcheck\ key=value\ key=value-command-or-structure+command\-or\-structure \f[] .fi .PP@@ -211,12 +222,28 @@ .IP .nf \f[C]-export\ SHELLCHECK_OPTS=\[aq]--shell=bash\ --exclude=SC2016\[aq]+export\ SHELLCHECK_OPTS=\[aq]\-\-shell=bash\ \-\-exclude=SC2016\[aq] \f[] .fi .PP Its value will be split on spaces and prepended to the command line on each invocation.+.SH RETURN VALUES+.PP+ShellCheck uses the follow exit codes:+.IP \[bu] 2+0: All files successfully scanned with no issues.+.IP \[bu] 2+1: All files successfully scanned with some issues.+.IP \[bu] 2+2: Some files could not be processed (e.g.+file not found).+.IP \[bu] 2+3: ShellCheck was invoked with bad syntax (e.g.+unknown flag).+.IP \[bu] 2+4: ShellCheck was invoked with bad options (e.g.+unknown formatter). .SH AUTHOR .PP ShellCheck is written and maintained by Vidar Holen.@@ -227,7 +254,7 @@ https://github.com/koalaman/shellcheck/issues .SH COPYRIGHT .PP-Copyright 2012-2015, Vidar Holen.+Copyright 2012\-2015, Vidar Holen. Licensed under the GNU General Public License version 3 or later, see http://gnu.org/licenses/gpl.html .SH SEE ALSO
shellcheck.1.md view
@@ -16,7 +16,7 @@ strange behavior, but it also reports on a few more advanced issues where corner cases can cause delayed failures. -ShellCheck gives shell specific advice. Consider the line:+ShellCheck gives shell specific advice. Consider this line: (( area = 3.14*r*r )) @@ -32,6 +32,12 @@ # OPTIONS +**-C**[*WHEN*],\ **--color**[=*WHEN*]++: For TTY outut, enable colors *always*, *never* or *auto*. The default+ is *auto*. **--color** without an argument is equivalent to+ **--color=always**.+ **-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...] : Explicitly exclude the specified codes from the report. Subsequent **-e**@@ -46,7 +52,7 @@ **-s**\ *shell*,\ **--shell=***shell* -: Specify Bourne shell dialect. Valid values are *sh*, *bash* and *ksh*.+: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*. The default is to use the file's shebang, or *bash* if the target shell can't be determined. @@ -54,7 +60,7 @@ : Print version information and exit. -**-x**,\ **-external-sources**+**-x**,\ **--external-sources** : Follow 'source' statements even when the file is not specified as input. By default, `shellcheck` will only follow files specified on the command@@ -158,6 +164,16 @@ Its value will be split on spaces and prepended to the command line on each invocation.++# RETURN VALUES++ShellCheck uses the follow exit codes:+++ 0: All files successfully scanned with no issues.++ 1: All files successfully scanned with some issues.++ 2: Some files could not be processed (e.g. file not found).++ 3: ShellCheck was invoked with bad syntax (e.g. unknown flag).++ 4: ShellCheck was invoked with bad options (e.g. unknown formatter). # AUTHOR ShellCheck is written and maintained by Vidar Holen.
shellcheck.hs view
@@ -48,7 +48,6 @@ data Status = NoProblems | SomeProblems- | BadInput | SupportFailure | SyntaxFailure | RuntimeException@@ -60,12 +59,16 @@ data Options = Options { checkSpec :: CheckSpec,- externalSources :: Bool+ externalSources :: Bool,+ formatterOptions :: FormatterOptions } defaultOptions = Options { checkSpec = emptyCheckSpec,- externalSources = False+ externalSources = False,+ formatterOptions = FormatterOptions {+ foColorOption = ColorAuto+ } } usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."@@ -74,8 +77,11 @@ (ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings", Option "f" ["format"] (ReqArg (Flag "format") "FORMAT") "output format",+ Option "C" ["color"]+ (OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN")+ "Use color (auto, always, never)", Option "s" ["shell"]- (ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (bash,sh,ksh)",+ (ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (sh,bash,dash,ksh)", Option "x" ["external-sources"] (NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES.", Option "V" ["version"]@@ -92,12 +98,12 @@ printErr $ concat errors ++ "\n" ++ usageInfo usageHeader options throwError SyntaxFailure -formats :: Map.Map String (IO Formatter)-formats = Map.fromList [+formats :: FormatterOptions -> Map.Map String (IO Formatter)+formats options = Map.fromList [ ("checkstyle", ShellCheck.Formatter.CheckStyle.format), ("gcc", ShellCheck.Formatter.GCC.format), ("json", ShellCheck.Formatter.JSON.format),- ("tty", ShellCheck.Formatter.TTY.format)+ ("tty", ShellCheck.Formatter.TTY.format options) ] getOption [] _ = Nothing@@ -144,7 +150,6 @@ case status of NoProblems -> ExitSuccess SomeProblems -> ExitFailure 1- BadInput -> ExitFailure 5 SyntaxFailure -> ExitFailure 3 SupportFailure -> ExitFailure 4 RuntimeException -> ExitFailure 2@@ -154,12 +159,13 @@ options <- foldM (flip parseOption) defaultOptions flags verifyFiles files let format = fromMaybe "tty" $ getOption flags "format"+ let formatters = formats $ formatterOptions options formatter <-- case Map.lookup format formats of+ case Map.lookup format formatters of Nothing -> do printErr $ "Unknown format " ++ format printErr "Supported formats:"- mapM_ (printErr . write) $ Map.keys formats+ mapM_ (printErr . write) $ Map.keys formatters throwError SupportFailure where write s = " " ++ s Just f -> ExceptT $ fmap Right f@@ -197,6 +203,13 @@ then NoProblems else SomeProblems +parseColorOption colorOption =+ case colorOption of+ "auto" -> ColorAuto+ "always" -> ColorAlways+ "never" -> ColorNever+ _ -> error $ "Bad value for --color `" ++ colorOption ++ "'"+ parseOption flag options = case flag of Flag "shell" str ->@@ -221,9 +234,16 @@ liftIO printVersion throwError NoProblems - Flag "externals" _ -> do+ Flag "externals" _ -> return options { externalSources = True+ }++ Flag "color" color ->+ return options {+ formatterOptions = (formatterOptions options) {+ foColorOption = parseColorOption color+ } } _ -> return options