packages feed

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 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+![Screenshot of a terminal showing problematic shell script lines highlighted](doc/terminal.png). -The goals of ShellCheck are:+The goals of ShellCheck are -  - To point out and clarify typical beginner's syntax issues,+  - 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):++![Screenshot of vim showing inlined shellcheck feedback](doc/vim-syntastic.png).++* Emacs, through [Flycheck](https://github.com/flycheck/flycheck):++![Screenshot of emacs showing inlined shellcheck feedback](doc/emacs-flycheck.png).++* Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck).++* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).++* 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