mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 16:59:20 +08:00
Compare commits
95 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a526ee0829 | ||
|
8c5fdc3522 | ||
|
ae199edb68 | ||
|
7cfcf6db8a | ||
|
a7c5be93dc | ||
|
8754c21244 | ||
|
985ca2530d | ||
|
3cae6cd6ab | ||
|
74b1745a19 | ||
|
495e34d101 | ||
|
2a16a4e8c1 | ||
|
0786b2bf3c | ||
|
84d8530f14 | ||
|
86e2b76730 | ||
|
b770984dfc | ||
|
d9c9e60fb0 | ||
|
14056a7f3a | ||
|
a524929b69 | ||
|
fa7943ac0e | ||
|
81c2ecaccb | ||
|
fcba462a99 | ||
|
43aca62ca7 | ||
|
128351f5ef | ||
|
d71d6ff294 | ||
|
bd65b67578 | ||
|
149b4dbd6f | ||
|
ef5f9a7af5 | ||
|
581981ba76 | ||
|
fcc473e27f | ||
|
0845b81183 | ||
|
966fb3e3dd | ||
|
f28462b01c | ||
|
ccab132b38 | ||
|
4806719035 | ||
|
0df9345142 | ||
|
77069f7445 | ||
|
04db46381f | ||
|
c76b8d9a32 | ||
|
d0dd81e1fa | ||
|
f440912279 | ||
|
3ce310e939 | ||
|
a30ac402eb | ||
|
4a27c9a8d5 | ||
|
b5f5e6347d | ||
|
c57e447c89 | ||
|
e9784fa9a7 | ||
|
f1148b8b41 | ||
|
982681fc05 | ||
|
52dac51cd4 | ||
|
30bb0e0093 | ||
|
d1d574c091 | ||
|
ea4e0091c7 | ||
|
81d9f7e640 | ||
|
69469c3603 | ||
|
5cf6e01ce9 | ||
|
f7857028f7 | ||
|
b261ec24f9 | ||
|
819470fa1d | ||
|
2f28847b08 | ||
|
e47480e93a | ||
|
9caeec104b | ||
|
95b3cbf071 | ||
|
e7f05d662a | ||
|
3ee4419ef4 | ||
|
8dc0fdb4cc | ||
|
da4885a71d | ||
|
642ad86125 | ||
|
f77a545282 | ||
|
7946bf5657 | ||
|
cc04b40119 | ||
|
c3bce51de3 | ||
|
a4042f7523 | ||
|
363c0633e0 | ||
|
7ceb1f1519 | ||
|
f1bdda54cb | ||
|
9aa4c22aa6 | ||
|
399c04cc17 | ||
|
fd595d1058 | ||
|
7c44e1060f | ||
|
2821552688 | ||
|
2034e3886e | ||
|
fa15c0a454 | ||
|
88cdb4e2c9 | ||
|
2292e852e5 | ||
|
ade2bf7b87 | ||
|
e6e558946c | ||
|
3a118246ef | ||
|
dd626686c4 | ||
|
866cbd0aa4 | ||
|
d7971dafd1 | ||
|
9092080a84 | ||
|
499c99372e | ||
|
d9a9d5db86 | ||
|
c5de58ae84 | ||
|
4c186c20b9 |
20
.github/workflows/build.yml
vendored
20
.github/workflows/build.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
sudo apt-get install cabal-install
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: source
|
||||
path: source/
|
||||
@@ -51,10 +51,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
|
||||
- name: Build source
|
||||
run: |
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} )
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: bin
|
||||
path: bin/
|
||||
@@ -74,10 +74,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
|
||||
- name: Work around GitHub permissions bug
|
||||
run: chmod +x bin/*/shellcheck*
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
rm -rf */ README* LICENSE*
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deploy
|
||||
path: deploy/
|
||||
@@ -104,10 +104,10 @@ jobs:
|
||||
environment: Deploy
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
|
||||
- name: Upload to GitHub
|
||||
env:
|
||||
|
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,3 +1,25 @@
|
||||
## v0.9.0 - 2022-12-12
|
||||
### Added
|
||||
- SC2316: Warn about 'local readonly foo' and similar (thanks, patrickxia!)
|
||||
- SC2317: Warn about unreachable commands
|
||||
- SC2318: Warn about backreferences in 'declare x=1 y=$x'
|
||||
- SC2319/SC2320: Warn when $? refers to echo/printf/[ ]/[[ ]]/test
|
||||
- SC2321: Suggest removing $((..)) in array[$((idx))]=val
|
||||
- SC2322: Suggest collapsing double parentheses in arithmetic contexts
|
||||
- SC2323: Suggest removing wrapping parentheses in a[(x+1)]=val
|
||||
|
||||
### Fixed
|
||||
- SC2086: Now uses DFA to make more accurate predictions about values
|
||||
- SC2086: No longer warns about values declared as integer with declare -i
|
||||
|
||||
### Changed
|
||||
- ShellCheck now has a Data Flow Analysis engine to make smarter decisions
|
||||
based on control flow rather than just syntax. Existing checks will
|
||||
gradually start using it, which may cause them to trigger differently
|
||||
(but more accurately).
|
||||
- Values in directives/shellcheckrc can now be quoted with '' or ""
|
||||
|
||||
|
||||
## v0.8.0 - 2021-11-06
|
||||
### Added
|
||||
- `disable=all` now conveniently disables all warnings
|
||||
|
10
LICENSE
10
LICENSE
@@ -1,13 +1,3 @@
|
||||
Employer mandated disclaimer:
|
||||
|
||||
I am providing code in the repository to you under an open source license.
|
||||
Because this is my personal repository, the license you receive to my code is
|
||||
from me and other individual contributors, and not my employer (Facebook).
|
||||
|
||||
- Vidar "koala_man" Holen
|
||||
|
||||
----
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
Name: ShellCheck
|
||||
Version: 0.8.0
|
||||
Version: 0.9.0
|
||||
Synopsis: Shell script analysis tool
|
||||
License: GPL-3
|
||||
License-file: LICENSE
|
||||
@@ -45,19 +45,26 @@ library
|
||||
build-depends:
|
||||
semigroups
|
||||
build-depends:
|
||||
aeson,
|
||||
array,
|
||||
-- The lower bounds are based on GHC 7.10.3
|
||||
-- The upper bounds are based on GHC 9.4.3
|
||||
aeson >= 1.4.0 && < 2.2,
|
||||
array >= 0.5.1 && < 0.6,
|
||||
base >= 4.8.0.0 && < 5,
|
||||
bytestring,
|
||||
containers >= 0.5,
|
||||
deepseq >= 1.4.0.0,
|
||||
Diff >= 0.2.0,
|
||||
directory >= 1.2.3.0,
|
||||
mtl >= 2.2.1,
|
||||
filepath,
|
||||
parsec,
|
||||
regex-tdfa,
|
||||
QuickCheck >= 2.7.4,
|
||||
bytestring >= 0.10.6 && < 0.12,
|
||||
containers >= 0.5.6 && < 0.7,
|
||||
deepseq >= 1.4.1 && < 1.5,
|
||||
Diff >= 0.4.0 && < 0.5,
|
||||
fgl >= 5.7.0 && < 5.9,
|
||||
filepath >= 1.4.0 && < 1.5,
|
||||
mtl >= 2.2.2 && < 2.3,
|
||||
parsec >= 3.1.14 && < 3.2,
|
||||
QuickCheck >= 2.14.2 && < 2.15,
|
||||
regex-tdfa >= 1.2.0 && < 1.4,
|
||||
transformers >= 0.4.2 && < 0.6,
|
||||
|
||||
-- getXdgDirectory from 1.2.3.0
|
||||
directory >= 1.2.3 && < 1.4,
|
||||
|
||||
-- When cabal supports it, move this to setup-depends:
|
||||
process
|
||||
exposed-modules:
|
||||
@@ -66,11 +73,15 @@ library
|
||||
ShellCheck.Analytics
|
||||
ShellCheck.Analyzer
|
||||
ShellCheck.AnalyzerLib
|
||||
ShellCheck.CFG
|
||||
ShellCheck.CFGAnalysis
|
||||
ShellCheck.Checker
|
||||
ShellCheck.Checks.Commands
|
||||
ShellCheck.Checks.ControlFlow
|
||||
ShellCheck.Checks.Custom
|
||||
ShellCheck.Checks.ShellSupport
|
||||
ShellCheck.Data
|
||||
ShellCheck.Debug
|
||||
ShellCheck.Fixer
|
||||
ShellCheck.Formatter.Format
|
||||
ShellCheck.Formatter.CheckStyle
|
||||
@@ -82,6 +93,7 @@ library
|
||||
ShellCheck.Formatter.Quiet
|
||||
ShellCheck.Interface
|
||||
ShellCheck.Parser
|
||||
ShellCheck.Prelude
|
||||
ShellCheck.Regex
|
||||
other-modules:
|
||||
Paths_ShellCheck
|
||||
@@ -94,17 +106,19 @@ executable shellcheck
|
||||
build-depends:
|
||||
aeson,
|
||||
array,
|
||||
base >= 4 && < 5,
|
||||
base,
|
||||
bytestring,
|
||||
containers,
|
||||
deepseq >= 1.4.0.0,
|
||||
Diff >= 0.2.0,
|
||||
directory >= 1.2.3.0,
|
||||
mtl >= 2.2.1,
|
||||
deepseq,
|
||||
Diff,
|
||||
directory,
|
||||
fgl,
|
||||
mtl,
|
||||
filepath,
|
||||
parsec >= 3.0,
|
||||
QuickCheck >= 2.7.4,
|
||||
parsec,
|
||||
QuickCheck,
|
||||
regex-tdfa,
|
||||
transformers,
|
||||
ShellCheck
|
||||
default-language: Haskell98
|
||||
main-is: shellcheck.hs
|
||||
@@ -114,17 +128,19 @@ test-suite test-shellcheck
|
||||
build-depends:
|
||||
aeson,
|
||||
array,
|
||||
base >= 4 && < 5,
|
||||
base,
|
||||
bytestring,
|
||||
containers,
|
||||
deepseq >= 1.4.0.0,
|
||||
Diff >= 0.2.0,
|
||||
directory >= 1.2.3.0,
|
||||
mtl >= 2.2.1,
|
||||
deepseq,
|
||||
Diff,
|
||||
directory,
|
||||
fgl,
|
||||
filepath,
|
||||
mtl,
|
||||
parsec,
|
||||
QuickCheck >= 2.7.4,
|
||||
QuickCheck,
|
||||
regex-tdfa,
|
||||
transformers,
|
||||
ShellCheck
|
||||
default-language: Haskell98
|
||||
main-is: test/shellcheck.hs
|
||||
|
@@ -1,5 +1,4 @@
|
||||
# DIGEST:sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76
|
||||
FROM liushuyu/osxcross:latest
|
||||
FROM liushuyu/osxcross@sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76
|
||||
|
||||
ENV TARGET x86_64-apple-darwin18
|
||||
ENV TARGETNAME darwin.x86_64
|
||||
|
@@ -52,6 +52,7 @@ RUN pirun apt-get install -y ghc cabal-install
|
||||
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--gcc-options;-Os -Wl,--gc-sections -ffunction-sections -fdata-sections"
|
||||
RUN pirun cabal update
|
||||
RUN IFS=";" && pirun cabal install --dependencies-only $CABALOPTS ShellCheck
|
||||
RUN IFS=';' && pirun cabal install $CABALOPTS --lib fgl
|
||||
|
||||
# Copy the build script
|
||||
WORKDIR /pi/scratch
|
||||
|
@@ -1,16 +1,10 @@
|
||||
FROM ubuntu:20.04
|
||||
FROM alpine:latest
|
||||
|
||||
ENV TARGETNAME linux.x86_64
|
||||
|
||||
# Install GHC and cabal
|
||||
USER root
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
RUN apt-get update && apt-get install -y ghc curl xz-utils
|
||||
|
||||
# So we'd like a later version of Cabal that supports --enable-executable-static,
|
||||
# but we can't use Ubuntu 20.10 because coreutils has switched to new syscalls that
|
||||
# the TravisCI kernel doesn't support. Download it manually.
|
||||
RUN curl "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/bin
|
||||
RUN apk add ghc cabal g++ libffi-dev curl bash
|
||||
|
||||
# Use ld.bfd instead of ld.gold due to
|
||||
# x86_64-linux-gnu/libpthread.a(pthread_cond_init.o)(.note.stapsdt+0x14): error:
|
||||
|
@@ -12,7 +12,7 @@ WORKDIR /haskell
|
||||
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-x86_64-unknown-mingw32.tar.xz" | tar xJ --strip-components=1
|
||||
WORKDIR /haskell/bin
|
||||
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-mingw32.zip" | busybox unzip -
|
||||
RUN curl -L "https://curl.se/windows/dl-7.75.0/curl-7.75.0-win64-mingw.zip" | busybox unzip - && mv curl-7.75.0-win64-mingw/bin/* .
|
||||
RUN curl -L "https://curl.se/windows/dl-7.84.0/curl-7.84.0-win64-mingw.zip" | busybox unzip - && mv curl-7.84.0-win64-mingw/bin/* .
|
||||
ENV WINEPATH /haskell/bin
|
||||
|
||||
# It's unknown whether Cabal on Windows suffers from the same issue
|
||||
|
294
doc/shellcheck_logo.svg
Normal file
294
doc/shellcheck_logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 244 KiB |
@@ -282,6 +282,9 @@ Here is an example `.shellcheckrc`:
|
||||
source-path=SCRIPTDIR
|
||||
source-path=/mnt/chroot
|
||||
|
||||
# Since 0.9.0, values can be quoted with '' or "" to allow spaces
|
||||
source-path="My Documents/scripts"
|
||||
|
||||
# Allow opening any 'source'd file, even if not specified as input
|
||||
external-sources=true
|
||||
|
||||
@@ -375,7 +378,7 @@ long list of wonderful contributors.
|
||||
|
||||
# COPYRIGHT
|
||||
|
||||
Copyright 2012-2021, Vidar Holen and contributors.
|
||||
Copyright 2012-2022, Vidar Holen and contributors.
|
||||
Licensed under the GNU General Public License version 3 or later,
|
||||
see https://gnu.org/licenses/gpl.html
|
||||
|
||||
|
@@ -34,6 +34,8 @@ import qualified ShellCheck.Formatter.Quiet
|
||||
|
||||
import Control.Exception
|
||||
import Control.Monad
|
||||
import Control.Monad.IO.Class
|
||||
import Control.Monad.Trans.Class
|
||||
import Control.Monad.Except
|
||||
import Data.Bits
|
||||
import Data.Char
|
||||
@@ -225,7 +227,7 @@ runFormatter sys format options files = do
|
||||
f :: Status -> FilePath -> IO Status
|
||||
f status file = do
|
||||
newStatus <- process file `catch` handler file
|
||||
return $ status `mappend` newStatus
|
||||
return $! status `mappend` newStatus
|
||||
handler :: FilePath -> IOException -> IO Status
|
||||
handler file e = reportFailure file (show e)
|
||||
reportFailure file str = do
|
||||
|
@@ -45,6 +45,7 @@ data InnerToken t =
|
||||
| Inner_TA_Variable String [t]
|
||||
| Inner_TA_Expansion [t]
|
||||
| Inner_TA_Sequence [t]
|
||||
| Inner_TA_Parenthesis t
|
||||
| Inner_TA_Trinary t t t
|
||||
| Inner_TA_Unary String t
|
||||
| Inner_TC_And ConditionType String t t
|
||||
@@ -141,7 +142,7 @@ data InnerToken t =
|
||||
| Inner_T_CoProcBody t
|
||||
| Inner_T_Include t
|
||||
| Inner_T_SourceCommand t t
|
||||
| Inner_T_BatsTest t t
|
||||
| Inner_T_BatsTest String t
|
||||
deriving (Show, Eq, Functor, Foldable, Traversable)
|
||||
|
||||
data Annotation =
|
||||
@@ -204,6 +205,7 @@ pattern T_Annotation id anns t = OuterToken id (Inner_T_Annotation anns t)
|
||||
pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c)
|
||||
pattern T_Array id t = OuterToken id (Inner_T_Array t)
|
||||
pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)
|
||||
pattern TA_Parentesis id t = OuterToken id (Inner_TA_Parenthesis t)
|
||||
pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value)
|
||||
pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3)
|
||||
pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1)
|
||||
@@ -256,7 +258,7 @@ pattern T_Subshell id l = OuterToken id (Inner_T_Subshell l)
|
||||
pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l)
|
||||
pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l)
|
||||
|
||||
{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-}
|
||||
{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, TA_Parentesis, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-}
|
||||
|
||||
instance Eq Token where
|
||||
OuterToken _ a == OuterToken _ b = a == b
|
||||
|
@@ -21,6 +21,7 @@
|
||||
module ShellCheck.ASTLib where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.Prelude
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Monad.Writer
|
||||
@@ -138,7 +139,7 @@ getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
|
||||
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
|
||||
flag (x, '-':args) = map (\v -> (x, [v])) args
|
||||
flag (x, _) = [ (x, "") ]
|
||||
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
|
||||
getFlagsUntil _ _ = error $ pleaseReport "getFlags on non-command"
|
||||
|
||||
-- Get all flags in a GNU way, up until --
|
||||
getAllFlags :: Token -> [(Token, String)]
|
||||
@@ -369,6 +370,21 @@ getGlobOrLiteralString = getLiteralStringExt f
|
||||
f (T_Glob _ str) = return str
|
||||
f _ = Nothing
|
||||
|
||||
|
||||
prop_getLiteralString1 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x01") == Just "\1"
|
||||
prop_getLiteralString2 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\xyz") == Just "\\xyz"
|
||||
prop_getLiteralString3 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x1") == Just "\x1"
|
||||
prop_getLiteralString4 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x1y") == Just "\x1y"
|
||||
prop_getLiteralString5 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\xy") == Just "\\xy"
|
||||
prop_getLiteralString6 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x") == Just "\\x"
|
||||
prop_getLiteralString7 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1x") == Just "\1x"
|
||||
prop_getLiteralString8 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\12x") == Just "\o12x"
|
||||
prop_getLiteralString9 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\123x") == Just "\o123x"
|
||||
prop_getLiteralString10 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1234") == Just "\o123\&4"
|
||||
prop_getLiteralString11 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1") == Just "\1"
|
||||
prop_getLiteralString12 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\12") == Just "\o12"
|
||||
prop_getLiteralString13 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\123") == Just "\o123"
|
||||
|
||||
-- Maybe get the literal value of a token, using a custom function
|
||||
-- to map unrecognized Tokens into strings.
|
||||
getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String
|
||||
@@ -401,14 +417,15 @@ getLiteralStringExt more = g
|
||||
'\\' -> '\\' : rest
|
||||
'x' ->
|
||||
case cs of
|
||||
(x:y:more) ->
|
||||
if isHexDigit x && isHexDigit y
|
||||
then chr (16*(digitToInt x) + (digitToInt y)) : rest
|
||||
else '\\':c:rest
|
||||
(x:y:more) | isHexDigit x && isHexDigit y ->
|
||||
chr (16*(digitToInt x) + (digitToInt y)) : decodeEscapes more
|
||||
(x:more) | isHexDigit x ->
|
||||
chr (digitToInt x) : decodeEscapes more
|
||||
more -> '\\' : 'x' : decodeEscapes more
|
||||
_ | isOctDigit c ->
|
||||
let digits = take 3 $ takeWhile isOctDigit (c:cs)
|
||||
num = parseOct digits
|
||||
in (if num < 256 then chr num else '?') : rest
|
||||
let (digits, more) = spanMax isOctDigit 3 (c:cs)
|
||||
num = (parseOct digits) `mod` 256
|
||||
in (chr num) : decodeEscapes more
|
||||
_ -> '\\' : c : rest
|
||||
where
|
||||
rest = decodeEscapes cs
|
||||
@@ -416,6 +433,11 @@ getLiteralStringExt more = g
|
||||
where
|
||||
f n "" = n
|
||||
f n (c:rest) = f (n * 8 + digitToInt c) rest
|
||||
spanMax f n list =
|
||||
let (first, second) = span f list
|
||||
(prefix, suffix) = splitAt n first
|
||||
in
|
||||
(prefix, suffix ++ second)
|
||||
decodeEscapes (c:cs) = c : decodeEscapes cs
|
||||
decodeEscapes [] = []
|
||||
|
||||
@@ -764,5 +786,122 @@ executableFromShebang = shellFor
|
||||
basename s = reverse . takeWhile (/= '/') . reverse $ s
|
||||
skipFlags = dropWhile ("-" `isPrefixOf`)
|
||||
|
||||
|
||||
-- Determining if a name is a variable
|
||||
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
|
||||
isVariableChar x = isVariableStartChar x || isDigit x
|
||||
isSpecialVariableChar = (`elem` "*@#?-$!")
|
||||
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
|
||||
|
||||
prop_isVariableName1 = isVariableName "_fo123"
|
||||
prop_isVariableName2 = not $ isVariableName "4"
|
||||
prop_isVariableName3 = not $ isVariableName "test: "
|
||||
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
|
||||
isVariableName _ = False
|
||||
|
||||
|
||||
-- Get the variable name from an expansion like ${var:-foo}
|
||||
prop_getBracedReference1 = getBracedReference "foo" == "foo"
|
||||
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
|
||||
prop_getBracedReference3 = getBracedReference "#" == "#"
|
||||
prop_getBracedReference4 = getBracedReference "##" == "#"
|
||||
prop_getBracedReference5 = getBracedReference "#!" == "!"
|
||||
prop_getBracedReference6 = getBracedReference "!#" == "#"
|
||||
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
|
||||
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
|
||||
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
|
||||
prop_getBracedReference10 = getBracedReference "foo: -1" == "foo"
|
||||
prop_getBracedReference11 = getBracedReference "!os*" == ""
|
||||
prop_getBracedReference11b = getBracedReference "!os@" == ""
|
||||
prop_getBracedReference12 = getBracedReference "!os?bar**" == ""
|
||||
prop_getBracedReference13 = getBracedReference "foo[bar]" == "foo"
|
||||
getBracedReference s = fromMaybe s $
|
||||
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
|
||||
where
|
||||
noPrefix = dropPrefix s
|
||||
dropPrefix (c:rest) | c `elem` "!#" = rest
|
||||
dropPrefix cs = cs
|
||||
takeName s = do
|
||||
let name = takeWhile isVariableChar s
|
||||
guard . not $ null name
|
||||
return name
|
||||
getSpecial (c:_) | isSpecialVariableChar c = return [c]
|
||||
getSpecial _ = fail "empty or not special"
|
||||
|
||||
nameExpansion ('!':next:rest) = do -- e.g. ${!foo*bar*}
|
||||
guard $ isVariableChar next -- e.g. ${!@}
|
||||
first <- find (not . isVariableChar) rest
|
||||
guard $ first `elem` "*?@"
|
||||
return ""
|
||||
nameExpansion _ = Nothing
|
||||
|
||||
-- Get the variable modifier like /a/b in ${var/a/b}
|
||||
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
|
||||
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
|
||||
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
|
||||
prop_getBracedModifier4 = getBracedModifier "foo[@]@Q" == "[@]@Q"
|
||||
prop_getBracedModifier5 = getBracedModifier "@@Q" == "@Q"
|
||||
getBracedModifier s = headOrDefault "" $ do
|
||||
let var = getBracedReference s
|
||||
a <- dropModifier s
|
||||
dropPrefix var a
|
||||
where
|
||||
dropPrefix [] t = return t
|
||||
dropPrefix (a:b) (c:d) | a == c = dropPrefix b d
|
||||
dropPrefix _ _ = []
|
||||
|
||||
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
|
||||
dropModifier x = [x]
|
||||
|
||||
-- Get the variables from indices like ["x", "y"] in ${var[x+y+1]}
|
||||
prop_getIndexReferences1 = getIndexReferences "var[x+y+1]" == ["x", "y"]
|
||||
getIndexReferences s = fromMaybe [] $ do
|
||||
match <- matchRegex re s
|
||||
index <- match !!! 0
|
||||
return $ matchAllStrings variableNameRegex index
|
||||
where
|
||||
re = mkRegex "(\\[.*\\])"
|
||||
|
||||
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
|
||||
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
|
||||
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
|
||||
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
|
||||
getOffsetReferences mods = fromMaybe [] $ do
|
||||
-- if mods start with [, then drop until ]
|
||||
match <- matchRegex re mods
|
||||
offsets <- match !!! 1
|
||||
return $ matchAllStrings variableNameRegex offsets
|
||||
where
|
||||
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
|
||||
|
||||
|
||||
-- Returns whether a token is a parameter expansion without any modifiers.
|
||||
-- True for $var ${var} $1 $#
|
||||
-- False for ${#var} ${var[x]} ${var:-0}
|
||||
isUnmodifiedParameterExpansion t =
|
||||
case t of
|
||||
T_DollarBraced _ False _ -> True
|
||||
T_DollarBraced _ _ list ->
|
||||
let str = concat $ oversimplify list
|
||||
in getBracedReference str == str
|
||||
_ -> False
|
||||
|
||||
--- A list of the element and all its parents up to the root node.
|
||||
getPath tree t = t :
|
||||
case Map.lookup (getId t) tree of
|
||||
Nothing -> []
|
||||
Just parent -> getPath tree parent
|
||||
|
||||
isClosingFileOp op =
|
||||
case op of
|
||||
T_IoDuplicate _ (T_GREATAND _) "-" -> True
|
||||
T_IoDuplicate _ (T_LESSAND _) "-" -> True
|
||||
_ -> False
|
||||
|
||||
getEnableDirectives root =
|
||||
case root of
|
||||
T_Annotation _ list _ -> [s | EnableComment s <- list]
|
||||
_ -> []
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{-
|
||||
Copyright 2012-2021 Vidar Holen
|
||||
Copyright 2012-2022 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
@@ -19,13 +19,16 @@
|
||||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
module ShellCheck.Analytics (runAnalytics, optionalChecks, ShellCheck.Analytics.runTests) where
|
||||
module ShellCheck.Analytics (checker, optionalChecks, ShellCheck.Analytics.runTests) where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.AnalyzerLib hiding (producesComments)
|
||||
import ShellCheck.CFG
|
||||
import qualified ShellCheck.CFGAnalysis as CF
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Prelude
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Regex
|
||||
|
||||
@@ -44,6 +47,7 @@ import Data.Ord
|
||||
import Data.Semigroup
|
||||
import Debug.Trace -- STRIP
|
||||
import qualified Data.Map.Strict as Map
|
||||
import qualified Data.Set as S
|
||||
import Test.QuickCheck.All (forAllProperties)
|
||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||
|
||||
@@ -52,7 +56,6 @@ treeChecks :: [Parameters -> Token -> [TokenComment]]
|
||||
treeChecks = [
|
||||
nodeChecksToTreeCheck nodeChecks
|
||||
,subshellAssignmentCheck
|
||||
,checkSpacefulness
|
||||
,checkQuotesInLiterals
|
||||
,checkShebangParameters
|
||||
,checkFunctionsUsedExternally
|
||||
@@ -68,29 +71,22 @@ treeChecks = [
|
||||
,checkArrayValueUsedAsIndex
|
||||
]
|
||||
|
||||
runAnalytics :: AnalysisSpec -> [TokenComment]
|
||||
runAnalytics options =
|
||||
runList options treeChecks ++ runList options optionalChecks
|
||||
checker spec params = mkChecker spec params treeChecks
|
||||
|
||||
mkChecker spec params checks =
|
||||
Checker {
|
||||
perScript = \(Root root) -> do
|
||||
tell $ concatMap (\f -> f params root) all,
|
||||
perToken = const $ return ()
|
||||
}
|
||||
where
|
||||
root = asScript options
|
||||
optionals = getEnableDirectives root ++ asOptionalChecks options
|
||||
optionalChecks =
|
||||
if "all" `elem` optionals
|
||||
all = checks ++ optionals
|
||||
optionalKeys = asOptionalChecks spec
|
||||
optionals =
|
||||
if "all" `elem` optionalKeys
|
||||
then map snd optionalTreeChecks
|
||||
else mapMaybe (\c -> Map.lookup c optionalCheckMap) optionals
|
||||
else mapMaybe (\c -> Map.lookup c optionalCheckMap) optionalKeys
|
||||
|
||||
runList :: AnalysisSpec -> [Parameters -> Token -> [TokenComment]]
|
||||
-> [TokenComment]
|
||||
runList spec list = notes
|
||||
where
|
||||
root = asScript spec
|
||||
params = makeParameters spec
|
||||
notes = concatMap (\f -> f params root) list
|
||||
|
||||
getEnableDirectives root =
|
||||
case root of
|
||||
T_Annotation _ list _ -> [s | EnableComment s <- list]
|
||||
_ -> []
|
||||
|
||||
checkList l t = concatMap (\f -> f t) l
|
||||
|
||||
@@ -199,6 +195,12 @@ nodeChecks = [
|
||||
,checkComparisonWithLeadingX
|
||||
,checkCommandWithTrailingSymbol
|
||||
,checkUnquotedParameterExpansionPattern
|
||||
,checkBatsTestDoesNotUseNegation
|
||||
,checkCommandIsUnreachable
|
||||
,checkSpacefulnessCfg
|
||||
,checkOverwrittenExitCode
|
||||
,checkUnnecessaryArithmeticExpansionIndex
|
||||
,checkUnnecessaryParens
|
||||
]
|
||||
|
||||
optionalChecks = map fst optionalTreeChecks
|
||||
@@ -217,7 +219,7 @@ optionalTreeChecks = [
|
||||
cdDescription = "Suggest quoting variables without metacharacters",
|
||||
cdPositive = "var=hello; echo $var",
|
||||
cdNegative = "var=hello; echo \"$var\""
|
||||
}, checkVerboseSpacefulness)
|
||||
}, nodeChecksToTreeCheck [checkVerboseSpacefulnessCfg])
|
||||
|
||||
,(newCheckDescription {
|
||||
cdName = "avoid-nullary-conditions",
|
||||
@@ -309,12 +311,12 @@ producesComments f s = not . null <$> runAndGetComments f s
|
||||
|
||||
runAndGetComments f s = do
|
||||
let pr = pScript s
|
||||
prRoot pr
|
||||
root <- prRoot pr
|
||||
let spec = defaultSpec pr
|
||||
let params = makeParameters spec
|
||||
return $
|
||||
filterByAnnotation spec params $
|
||||
runList spec [f]
|
||||
f params root
|
||||
|
||||
-- Copied from https://wiki.haskell.org/Edit_distance
|
||||
dist :: Eq a => [a] -> [a] -> Int
|
||||
@@ -544,6 +546,11 @@ prop_checkPipePitfalls15 = verifyNot checkPipePitfalls "foo | grep bar | wc -cmw
|
||||
prop_checkPipePitfalls16 = verifyNot checkPipePitfalls "foo | grep -r bar | wc -l"
|
||||
prop_checkPipePitfalls17 = verifyNot checkPipePitfalls "foo | grep -l bar | wc -l"
|
||||
prop_checkPipePitfalls18 = verifyNot checkPipePitfalls "foo | grep -L bar | wc -l"
|
||||
prop_checkPipePitfalls19 = verifyNot checkPipePitfalls "foo | grep -A2 bar | wc -l"
|
||||
prop_checkPipePitfalls20 = verifyNot checkPipePitfalls "foo | grep -B999 bar | wc -l"
|
||||
prop_checkPipePitfalls21 = verifyNot checkPipePitfalls "foo | grep --after-context 999 bar | wc -l"
|
||||
prop_checkPipePitfalls22 = verifyNot checkPipePitfalls "foo | grep -B 1 --after-context 999 bar | wc -l"
|
||||
prop_checkPipePitfalls23 = verifyNot checkPipePitfalls "ps -o pid,args -p $(pgrep java) | grep -F net.shellcheck.Test"
|
||||
checkPipePitfalls _ (T_Pipeline id _ commands) = do
|
||||
for ["find", "xargs"] $
|
||||
\(find:xargs:_) ->
|
||||
@@ -557,18 +564,25 @@ checkPipePitfalls _ (T_Pipeline id _ commands) = do
|
||||
]) $ warn (getId find) 2038
|
||||
"Use -print0/-0 or -exec + to allow for non-alphanumeric filenames."
|
||||
|
||||
for' ["ps", "grep"] $
|
||||
\x -> info x 2009 "Consider using pgrep instead of grepping ps output."
|
||||
for ["ps", "grep"] $
|
||||
\(ps:grep:_) ->
|
||||
let
|
||||
psFlags = maybe [] (map snd . getAllFlags) $ getCommand ps
|
||||
in
|
||||
-- There are many ways to specify a pid: 1, -1, p 1, wup 1, -q 1, -p 1, --pid 1.
|
||||
-- For simplicity we only deal with the most canonical looking flags:
|
||||
unless (any (`elem` ["p", "pid", "q", "quick-pid"]) psFlags) $
|
||||
info (getId ps) 2009 "Consider using pgrep instead of grepping ps output."
|
||||
|
||||
for ["grep", "wc"] $
|
||||
\(grep:wc:_) ->
|
||||
let flagsGrep = maybe [] (map snd . getAllFlags) $ getCommand grep
|
||||
flagsWc = maybe [] (map snd . getAllFlags) $ getCommand wc
|
||||
in
|
||||
unless (any (`elem` ["l", "files-with-matches", "L", "files-without-matches", "o", "only-matching", "r", "R", "recursive"]) flagsGrep
|
||||
unless (any (`elem` ["l", "files-with-matches", "L", "files-without-matches", "o", "only-matching", "r", "R", "recursive", "A", "after-context", "B", "before-context"]) flagsGrep
|
||||
|| any (`elem` ["m", "chars", "w", "words", "c", "bytes", "L", "max-line-length"]) flagsWc
|
||||
|| null flagsWc) $
|
||||
style (getId grep) 2126 "Consider using grep -c instead of grep|wc -l."
|
||||
style (getId grep) 2126 "Consider using 'grep -c' instead of 'grep|wc -l'."
|
||||
|
||||
didLs <- fmap or . sequence $ [
|
||||
for' ["ls", "grep"] $
|
||||
@@ -776,6 +790,7 @@ prop_checkUnquotedExpansions7 = verifyNot checkUnquotedExpansions "cat << foo\n$
|
||||
prop_checkUnquotedExpansions8 = verifyNot checkUnquotedExpansions "set -- $(seq 1 4)"
|
||||
prop_checkUnquotedExpansions9 = verifyNot checkUnquotedExpansions "echo foo `# inline comment`"
|
||||
prop_checkUnquotedExpansions10 = verify checkUnquotedExpansions "#!/bin/sh\nexport var=$(val)"
|
||||
prop_checkUnquotedExpansions11 = verifyNot checkUnquotedExpansions "ps -p $(pgrep foo)"
|
||||
checkUnquotedExpansions params =
|
||||
check
|
||||
where
|
||||
@@ -789,7 +804,7 @@ checkUnquotedExpansions params =
|
||||
warn (getId t) 2046 "Quote this to prevent word splitting."
|
||||
|
||||
shouldBeSplit t =
|
||||
getCommandNameFromExpansion t == Just "seq"
|
||||
getCommandNameFromExpansion t `elem` [Just "seq", Just "pgrep"]
|
||||
|
||||
|
||||
prop_checkRedirectToSame = verify checkRedirectToSame "cat foo > foo"
|
||||
@@ -801,6 +816,7 @@ prop_checkRedirectToSame6 = verifyNot checkRedirectToSame "echo foo > foo"
|
||||
prop_checkRedirectToSame7 = verifyNot checkRedirectToSame "sed 's/foo/bar/g' file | sponge file"
|
||||
prop_checkRedirectToSame8 = verifyNot checkRedirectToSame "while read -r line; do _=\"$fname\"; done <\"$fname\""
|
||||
prop_checkRedirectToSame9 = verifyNot checkRedirectToSame "while read -r line; do cat < \"$fname\"; done <\"$fname\""
|
||||
prop_checkRedirectToSame10 = verifyNot checkRedirectToSame "mapfile -t foo <foo"
|
||||
checkRedirectToSame params s@(T_Pipeline _ _ list) =
|
||||
mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurrences x) l) (getAllRedirs list))) list
|
||||
where
|
||||
@@ -846,7 +862,7 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) =
|
||||
isHarmlessCommand arg = fromMaybe False $ do
|
||||
cmd <- getClosestCommand (parentMap params) arg
|
||||
name <- getCommandBasename cmd
|
||||
return $ name `elem` ["echo", "printf", "sponge"]
|
||||
return $ name `elem` ["echo", "mapfile", "printf", "sponge"]
|
||||
containsAssignment arg = fromMaybe False $ do
|
||||
cmd <- getClosestCommand (parentMap params) arg
|
||||
return $ isAssignment cmd
|
||||
@@ -862,7 +878,7 @@ prop_checkShorthandIf5 = verifyNot checkShorthandIf "foo && rm || printf b"
|
||||
prop_checkShorthandIf6 = verifyNot checkShorthandIf "if foo && bar || baz; then true; fi"
|
||||
prop_checkShorthandIf7 = verifyNot checkShorthandIf "while foo && bar || baz; do true; done"
|
||||
prop_checkShorthandIf8 = verify checkShorthandIf "if true; then foo && bar || baz; fi"
|
||||
checkShorthandIf params x@(T_AndIf id _ (T_OrIf _ _ (T_Pipeline _ _ t)))
|
||||
checkShorthandIf params x@(T_OrIf _ (T_AndIf id _ _) (T_Pipeline _ _ t))
|
||||
| not (isOk t || inCondition) =
|
||||
info id 2015 "Note that A && B || C is not if-then-else. C may run when A is true."
|
||||
where
|
||||
@@ -1161,6 +1177,10 @@ prop_checkNumberComparisons18 = verify checkNumberComparisons "[[ foo -eq 2 ]]"
|
||||
prop_checkNumberComparisons19 = verifyNot checkNumberComparisons "foo=1; [[ foo -eq 2 ]]"
|
||||
prop_checkNumberComparisons20 = verify checkNumberComparisons "[[ 2 -eq / ]]"
|
||||
prop_checkNumberComparisons21 = verify checkNumberComparisons "[[ foo -eq foo ]]"
|
||||
prop_checkNumberComparisons22 = verify checkNumberComparisons "x=10; [[ $x > $z ]]"
|
||||
prop_checkNumberComparisons23 = verify checkNumberComparisons "x=0; if [[ -n $def ]]; then x=$def; fi; while [ $x > $z ]; do lol; done"
|
||||
prop_checkNumberComparisons24 = verify checkNumberComparisons "x=$RANDOM; [ $x > $z ]"
|
||||
prop_checkNumberComparisons25 = verify checkNumberComparisons "[[ $((n++)) > $x ]]"
|
||||
|
||||
checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do
|
||||
if isNum lhs || isNum rhs
|
||||
@@ -1236,6 +1256,17 @@ checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do
|
||||
numChar x = isDigit x || x `elem` "+-. "
|
||||
|
||||
isNum t =
|
||||
case getWordParts t of
|
||||
[T_DollarArithmetic {}] -> True
|
||||
[b@(T_DollarBraced id _ c)] ->
|
||||
let
|
||||
str = concat $ oversimplify c
|
||||
var = getBracedReference str
|
||||
in fromMaybe False $ do
|
||||
state <- CF.getIncomingState (cfgAnalysis params) id
|
||||
value <- Map.lookup var $ CF.variablesInScope state
|
||||
return $ CF.numericalStatus (CF.variableValue value) >= CF.NumericalStatusMaybe
|
||||
_ ->
|
||||
case oversimplify t of
|
||||
[v] -> all isDigit v
|
||||
_ -> False
|
||||
@@ -1450,7 +1481,8 @@ prop_checkArithmeticDeref7 = verifyNot checkArithmeticDeref "(( 10#$n ))"
|
||||
prop_checkArithmeticDeref8 = verifyNot checkArithmeticDeref "let i=$i+1"
|
||||
prop_checkArithmeticDeref9 = verifyNot checkArithmeticDeref "(( a[foo] ))"
|
||||
prop_checkArithmeticDeref10 = verifyNot checkArithmeticDeref "(( a[\\$foo] ))"
|
||||
prop_checkArithmeticDeref11= verifyNot checkArithmeticDeref "a[$foo]=wee"
|
||||
prop_checkArithmeticDeref11 = verify checkArithmeticDeref "a[$foo]=wee"
|
||||
prop_checkArithmeticDeref11b = verifyNot checkArithmeticDeref "declare -A a; a[$foo]=wee"
|
||||
prop_checkArithmeticDeref12 = verify checkArithmeticDeref "for ((i=0; $i < 3; i)); do true; done"
|
||||
prop_checkArithmeticDeref13 = verifyNot checkArithmeticDeref "(( $$ ))"
|
||||
prop_checkArithmeticDeref14 = verifyNot checkArithmeticDeref "(( $! ))"
|
||||
@@ -1467,6 +1499,7 @@ checkArithmeticDeref params t@(TA_Expansion _ [T_DollarBraced id _ l]) =
|
||||
T_Arithmetic {} -> return normalWarning
|
||||
T_DollarArithmetic {} -> return normalWarning
|
||||
T_ForArithmetic {} -> return normalWarning
|
||||
T_Assignment {} -> return normalWarning
|
||||
T_SimpleCommand {} -> return noWarning
|
||||
_ -> Nothing
|
||||
|
||||
@@ -1852,6 +1885,7 @@ prop_checkSpuriousExec7 = verifyNot checkSpuriousExec "exec file; echo failed; e
|
||||
prop_checkSpuriousExec8 = verifyNot checkSpuriousExec "exec {origout}>&1- >tmp.log 2>&1; bar"
|
||||
prop_checkSpuriousExec9 = verify checkSpuriousExec "for file in rc.d/*; do exec \"$file\"; done"
|
||||
prop_checkSpuriousExec10 = verifyNot checkSpuriousExec "exec file; r=$?; printf >&2 'failed\n'; return $r"
|
||||
prop_checkSpuriousExec11 = verifyNot checkSpuriousExec "exec file; :"
|
||||
checkSpuriousExec _ = doLists
|
||||
where
|
||||
doLists (T_Script _ _ cmds) = doList cmds False
|
||||
@@ -1867,7 +1901,7 @@ checkSpuriousExec _ = doLists
|
||||
|
||||
stripCleanup = reverse . dropWhile cleanup . reverse
|
||||
cleanup (T_Pipeline _ _ [cmd]) =
|
||||
isCommandMatch cmd (`elem` ["echo", "exit", "printf", "return"])
|
||||
isCommandMatch cmd (`elem` [":", "echo", "exit", "printf", "return"])
|
||||
|| isAssignment cmd
|
||||
cleanup _ = False
|
||||
|
||||
@@ -2001,107 +2035,6 @@ doVariableFlowAnalysis readFunc writeFunc empty flow = evalState (
|
||||
writeFunc base token name values
|
||||
doFlow _ = return []
|
||||
|
||||
---- Check whether variables could have spaces/globs
|
||||
prop_checkSpacefulness1 = verifyTree checkSpacefulness "a='cow moo'; echo $a"
|
||||
prop_checkSpacefulness2 = verifyNotTree checkSpacefulness "a='cow moo'; [[ $a ]]"
|
||||
prop_checkSpacefulness3 = verifyNotTree checkSpacefulness "a='cow*.mp3'; echo \"$a\""
|
||||
prop_checkSpacefulness4 = verifyTree checkSpacefulness "for f in *.mp3; do echo $f; done"
|
||||
prop_checkSpacefulness4a= verifyNotTree checkSpacefulness "foo=3; foo=$(echo $foo)"
|
||||
prop_checkSpacefulness5 = verifyTree checkSpacefulness "a='*'; b=$a; c=lol${b//foo/bar}; echo $c"
|
||||
prop_checkSpacefulness6 = verifyTree checkSpacefulness "a=foo$(lol); echo $a"
|
||||
prop_checkSpacefulness7 = verifyTree checkSpacefulness "a=foo\\ bar; rm $a"
|
||||
prop_checkSpacefulness8 = verifyNotTree checkSpacefulness "a=foo\\ bar; a=foo; rm $a"
|
||||
prop_checkSpacefulness10= verifyTree checkSpacefulness "rm $1"
|
||||
prop_checkSpacefulness11= verifyTree checkSpacefulness "rm ${10//foo/bar}"
|
||||
prop_checkSpacefulness12= verifyNotTree checkSpacefulness "(( $1 + 3 ))"
|
||||
prop_checkSpacefulness13= verifyNotTree checkSpacefulness "if [[ $2 -gt 14 ]]; then true; fi"
|
||||
prop_checkSpacefulness14= verifyNotTree checkSpacefulness "foo=$3 env"
|
||||
prop_checkSpacefulness15= verifyNotTree checkSpacefulness "local foo=$1"
|
||||
prop_checkSpacefulness16= verifyNotTree checkSpacefulness "declare foo=$1"
|
||||
prop_checkSpacefulness17= verifyTree checkSpacefulness "echo foo=$1"
|
||||
prop_checkSpacefulness18= verifyNotTree checkSpacefulness "$1 --flags"
|
||||
prop_checkSpacefulness19= verifyTree checkSpacefulness "echo $PWD"
|
||||
prop_checkSpacefulness20= verifyNotTree checkSpacefulness "n+='foo bar'"
|
||||
prop_checkSpacefulness21= verifyNotTree checkSpacefulness "select foo in $bar; do true; done"
|
||||
prop_checkSpacefulness22= verifyNotTree checkSpacefulness "echo $\"$1\""
|
||||
prop_checkSpacefulness23= verifyNotTree checkSpacefulness "a=(1); echo ${a[@]}"
|
||||
prop_checkSpacefulness24= verifyTree checkSpacefulness "a='a b'; cat <<< $a"
|
||||
prop_checkSpacefulness25= verifyTree checkSpacefulness "a='s/[0-9]//g'; sed $a"
|
||||
prop_checkSpacefulness26= verifyTree checkSpacefulness "a='foo bar'; echo {1,2,$a}"
|
||||
prop_checkSpacefulness27= verifyNotTree checkSpacefulness "echo ${a:+'foo'}"
|
||||
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 ]"
|
||||
prop_checkSpacefulness33= verifyTree checkSpacefulness "for file; do echo $file; done"
|
||||
prop_checkSpacefulness34= verifyTree checkSpacefulness "declare foo$n=$1"
|
||||
prop_checkSpacefulness35= verifyNotTree checkSpacefulness "echo ${1+\"$1\"}"
|
||||
prop_checkSpacefulness36= verifyNotTree checkSpacefulness "arg=$#; echo $arg"
|
||||
prop_checkSpacefulness37= verifyNotTree checkSpacefulness "@test 'status' {\n [ $status -eq 0 ]\n}"
|
||||
prop_checkSpacefulness37v = verifyTree checkVerboseSpacefulness "@test 'status' {\n [ $status -eq 0 ]\n}"
|
||||
prop_checkSpacefulness38= verifyTree checkSpacefulness "a=; echo $a"
|
||||
prop_checkSpacefulness39= verifyNotTree checkSpacefulness "a=''\"\"''; b=x$a; echo $b"
|
||||
prop_checkSpacefulness40= verifyNotTree checkSpacefulness "a=$((x+1)); echo $a"
|
||||
prop_checkSpacefulness41= verifyNotTree checkSpacefulness "exec $1 --flags"
|
||||
prop_checkSpacefulness42= verifyNotTree checkSpacefulness "run $1 --flags"
|
||||
prop_checkSpacefulness43= verifyNotTree checkSpacefulness "$foo=42"
|
||||
prop_checkSpacefulness44= verifyTree checkSpacefulness "#!/bin/sh\nexport var=$value"
|
||||
prop_checkSpacefulness45= verifyNotTree checkSpacefulness "wait -zzx -p foo; echo $foo"
|
||||
prop_checkSpacefulness46= verifyNotTree checkSpacefulness "x=0; (( x += 1 )); echo $x"
|
||||
|
||||
data SpaceStatus = SpaceSome | SpaceNone | SpaceEmpty deriving (Eq)
|
||||
instance Semigroup SpaceStatus where
|
||||
SpaceNone <> SpaceNone = SpaceNone
|
||||
SpaceSome <> _ = SpaceSome
|
||||
_ <> SpaceSome = SpaceSome
|
||||
SpaceEmpty <> x = x
|
||||
x <> SpaceEmpty = x
|
||||
instance Monoid SpaceStatus where
|
||||
mempty = SpaceEmpty
|
||||
mappend = (<>)
|
||||
|
||||
-- This is slightly awkward because we want to support structured
|
||||
-- optional checks based on nearly the same logic
|
||||
checkSpacefulness params = checkSpacefulness' onFind params
|
||||
where
|
||||
emit x = tell [x]
|
||||
onFind spaces token _ =
|
||||
when (spaces /= SpaceNone) $
|
||||
if isDefaultAssignment (parentMap params) token
|
||||
then
|
||||
emit $ makeComment InfoC (getId token) 2223
|
||||
"This default assignment may cause DoS due to globbing. Quote it."
|
||||
else
|
||||
unless (quotesMayConflictWithSC2281 params token) $
|
||||
emit $ makeCommentWithFix InfoC (getId token) 2086
|
||||
"Double quote to prevent globbing and word splitting."
|
||||
(addDoubleQuotesAround params token)
|
||||
|
||||
isDefaultAssignment parents token =
|
||||
let modifier = getBracedModifier $ bracedString token in
|
||||
any (`isPrefixOf` modifier) ["=", ":="]
|
||||
&& isParamTo parents ":" token
|
||||
|
||||
-- Given a T_DollarBraced, return a simplified version of the string contents.
|
||||
bracedString (T_DollarBraced _ _ l) = concat $ oversimplify l
|
||||
bracedString _ = error "Internal shellcheck error, please report! (bracedString on non-variable)"
|
||||
|
||||
prop_checkSpacefulness4v= verifyTree checkVerboseSpacefulness "foo=3; foo=$(echo $foo)"
|
||||
prop_checkSpacefulness8v= verifyTree checkVerboseSpacefulness "a=foo\\ bar; a=foo; rm $a"
|
||||
prop_checkSpacefulness28v = verifyTree checkVerboseSpacefulness "exec {n}>&1; echo $n"
|
||||
prop_checkSpacefulness36v = verifyTree checkVerboseSpacefulness "arg=$#; echo $arg"
|
||||
prop_checkSpacefulness44v = verifyNotTree checkVerboseSpacefulness "foo=3; $foo=4"
|
||||
checkVerboseSpacefulness params = checkSpacefulness' onFind params
|
||||
where
|
||||
onFind spaces token name =
|
||||
when (spaces == SpaceNone
|
||||
&& name `notElem` specialVariablesWithoutSpaces
|
||||
&& not (quotesMayConflictWithSC2281 params token)) $
|
||||
tell [makeCommentWithFix StyleC (getId token) 2248
|
||||
"Prefer double quoting even when variables don't contain special characters."
|
||||
(addDoubleQuotesAround params token)]
|
||||
|
||||
-- Don't suggest quotes if this will instead be autocorrected
|
||||
-- from $foo=bar to foo=bar. This is not pretty but ok.
|
||||
quotesMayConflictWithSC2281 params t =
|
||||
@@ -2111,75 +2044,125 @@ quotesMayConflictWithSC2281 params t =
|
||||
_ -> False
|
||||
|
||||
addDoubleQuotesAround params token = (surroundWith (getId token) params "\"")
|
||||
checkSpacefulness'
|
||||
:: (SpaceStatus -> Token -> String -> Writer [TokenComment] ()) ->
|
||||
Parameters -> Token -> [TokenComment]
|
||||
checkSpacefulness' onFind params t =
|
||||
doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
|
||||
|
||||
prop_checkSpacefulnessCfg1 = verify checkSpacefulnessCfg "a='cow moo'; echo $a"
|
||||
prop_checkSpacefulnessCfg2 = verifyNot checkSpacefulnessCfg "a='cow moo'; [[ $a ]]"
|
||||
prop_checkSpacefulnessCfg3 = verifyNot checkSpacefulnessCfg "a='cow*.mp3'; echo \"$a\""
|
||||
prop_checkSpacefulnessCfg4 = verify checkSpacefulnessCfg "for f in *.mp3; do echo $f; done"
|
||||
prop_checkSpacefulnessCfg4a = verifyNot checkSpacefulnessCfg "foo=3; foo=$(echo $foo)"
|
||||
prop_checkSpacefulnessCfg5 = verify checkSpacefulnessCfg "a='*'; b=$a; c=lol${b//foo/bar}; echo $c"
|
||||
prop_checkSpacefulnessCfg6 = verify checkSpacefulnessCfg "a=foo$(lol); echo $a"
|
||||
prop_checkSpacefulnessCfg7 = verify checkSpacefulnessCfg "a=foo\\ bar; rm $a"
|
||||
prop_checkSpacefulnessCfg8 = verifyNot checkSpacefulnessCfg "a=foo\\ bar; a=foo; rm $a"
|
||||
prop_checkSpacefulnessCfg10 = verify checkSpacefulnessCfg "rm $1"
|
||||
prop_checkSpacefulnessCfg11 = verify checkSpacefulnessCfg "rm ${10//foo/bar}"
|
||||
prop_checkSpacefulnessCfg12 = verifyNot checkSpacefulnessCfg "(( $1 + 3 ))"
|
||||
prop_checkSpacefulnessCfg13 = verifyNot checkSpacefulnessCfg "if [[ $2 -gt 14 ]]; then true; fi"
|
||||
prop_checkSpacefulnessCfg14 = verifyNot checkSpacefulnessCfg "foo=$3 env"
|
||||
prop_checkSpacefulnessCfg15 = verifyNot checkSpacefulnessCfg "local foo=$1"
|
||||
prop_checkSpacefulnessCfg16 = verifyNot checkSpacefulnessCfg "declare foo=$1"
|
||||
prop_checkSpacefulnessCfg17 = verify checkSpacefulnessCfg "echo foo=$1"
|
||||
prop_checkSpacefulnessCfg18 = verifyNot checkSpacefulnessCfg "$1 --flags"
|
||||
prop_checkSpacefulnessCfg19 = verify checkSpacefulnessCfg "echo $PWD"
|
||||
prop_checkSpacefulnessCfg20 = verifyNot checkSpacefulnessCfg "n+='foo bar'"
|
||||
prop_checkSpacefulnessCfg21 = verifyNot checkSpacefulnessCfg "select foo in $bar; do true; done"
|
||||
prop_checkSpacefulnessCfg22 = verifyNot checkSpacefulnessCfg "echo $\"$1\""
|
||||
prop_checkSpacefulnessCfg23 = verifyNot checkSpacefulnessCfg "a=(1); echo ${a[@]}"
|
||||
prop_checkSpacefulnessCfg24 = verify checkSpacefulnessCfg "a='a b'; cat <<< $a"
|
||||
prop_checkSpacefulnessCfg25 = verify checkSpacefulnessCfg "a='s/[0-9]//g'; sed $a"
|
||||
prop_checkSpacefulnessCfg26 = verify checkSpacefulnessCfg "a='foo bar'; echo {1,2,$a}"
|
||||
prop_checkSpacefulnessCfg27 = verifyNot checkSpacefulnessCfg "echo ${a:+'foo'}"
|
||||
prop_checkSpacefulnessCfg28 = verifyNot checkSpacefulnessCfg "exec {n}>&1; echo $n"
|
||||
prop_checkSpacefulnessCfg29 = verifyNot checkSpacefulnessCfg "n=$(stuff); exec {n}>&-;"
|
||||
prop_checkSpacefulnessCfg30 = verify checkSpacefulnessCfg "file='foo bar'; echo foo > $file;"
|
||||
prop_checkSpacefulnessCfg31 = verifyNot checkSpacefulnessCfg "echo \"`echo \\\"$1\\\"`\""
|
||||
prop_checkSpacefulnessCfg32 = verifyNot checkSpacefulnessCfg "var=$1; [ -v var ]"
|
||||
prop_checkSpacefulnessCfg33 = verify checkSpacefulnessCfg "for file; do echo $file; done"
|
||||
prop_checkSpacefulnessCfg34 = verify checkSpacefulnessCfg "declare foo$n=$1"
|
||||
prop_checkSpacefulnessCfg35 = verifyNot checkSpacefulnessCfg "echo ${1+\"$1\"}"
|
||||
prop_checkSpacefulnessCfg36 = verifyNot checkSpacefulnessCfg "arg=$#; echo $arg"
|
||||
prop_checkSpacefulnessCfg37 = verifyNot checkSpacefulnessCfg "@test 'status' {\n [ $status -eq 0 ]\n}"
|
||||
prop_checkSpacefulnessCfg37v = verify checkVerboseSpacefulnessCfg "@test 'status' {\n [ $status -eq 0 ]\n}"
|
||||
prop_checkSpacefulnessCfg38 = verify checkSpacefulnessCfg "a=; echo $a"
|
||||
prop_checkSpacefulnessCfg39 = verifyNot checkSpacefulnessCfg "a=''\"\"''; b=x$a; echo $b"
|
||||
prop_checkSpacefulnessCfg40 = verifyNot checkSpacefulnessCfg "a=$((x+1)); echo $a"
|
||||
prop_checkSpacefulnessCfg41 = verifyNot checkSpacefulnessCfg "exec $1 --flags"
|
||||
prop_checkSpacefulnessCfg42 = verifyNot checkSpacefulnessCfg "run $1 --flags"
|
||||
prop_checkSpacefulnessCfg43 = verifyNot checkSpacefulnessCfg "$foo=42"
|
||||
prop_checkSpacefulnessCfg44 = verify checkSpacefulnessCfg "#!/bin/sh\nexport var=$value"
|
||||
prop_checkSpacefulnessCfg45 = verifyNot checkSpacefulnessCfg "wait -zzx -p foo; echo $foo"
|
||||
prop_checkSpacefulnessCfg46 = verifyNot checkSpacefulnessCfg "x=0; (( x += 1 )); echo $x"
|
||||
prop_checkSpacefulnessCfg47 = verifyNot checkSpacefulnessCfg "x=0; (( x-- )); echo $x"
|
||||
prop_checkSpacefulnessCfg48 = verifyNot checkSpacefulnessCfg "x=0; (( ++x )); echo $x"
|
||||
prop_checkSpacefulnessCfg49 = verifyNot checkSpacefulnessCfg "for i in 1 2 3; do echo $i; done"
|
||||
prop_checkSpacefulnessCfg50 = verify checkSpacefulnessCfg "for i in 1 2 *; do echo $i; done"
|
||||
prop_checkSpacefulnessCfg51 = verify checkSpacefulnessCfg "x='foo bar'; x && x=1; echo $x"
|
||||
prop_checkSpacefulnessCfg52 = verifyNot checkSpacefulnessCfg "x=1; if f; then x='foo bar'; exit; fi; echo $x"
|
||||
prop_checkSpacefulnessCfg53 = verifyNot checkSpacefulnessCfg "s=1; f() { local s='a b'; }; f; echo $s"
|
||||
prop_checkSpacefulnessCfg54 = verifyNot checkSpacefulnessCfg "s='a b'; f() { s=1; }; f; echo $s"
|
||||
prop_checkSpacefulnessCfg55 = verify checkSpacefulnessCfg "s='a b'; x && f() { s=1; }; f; echo $s"
|
||||
prop_checkSpacefulnessCfg56 = verifyNot checkSpacefulnessCfg "s=1; cat <(s='a b'); echo $s"
|
||||
prop_checkSpacefulnessCfg57 = verifyNot checkSpacefulnessCfg "declare -i s=0; s=$(f); echo $s"
|
||||
prop_checkSpacefulnessCfg58 = verify checkSpacefulnessCfg "f() { declare -i s; }; f; s=$(var); echo $s"
|
||||
prop_checkSpacefulnessCfg59 = verifyNot checkSpacefulnessCfg "f() { declare -gi s; }; f; s=$(var); echo $s"
|
||||
prop_checkSpacefulnessCfg60 = verify checkSpacefulnessCfg "declare -i s; declare +i s; s=$(foo); echo $s"
|
||||
prop_checkSpacefulnessCfg61 = verify checkSpacefulnessCfg "declare -x X; y=foo$X; echo $y;"
|
||||
prop_checkSpacefulnessCfg62 = verifyNot checkSpacefulnessCfg "f() { declare -x X; y=foo$X; echo $y; }"
|
||||
prop_checkSpacefulnessCfg63 = verify checkSpacefulnessCfg "f && declare -i s; s='x + y'; echo $s"
|
||||
prop_checkSpacefulnessCfg64 = verifyNot checkSpacefulnessCfg "declare -i s; s='x + y'; x=$s; echo $x"
|
||||
prop_checkSpacefulnessCfg65 = verifyNot checkSpacefulnessCfg "f() { s=$?; echo $s; }; f"
|
||||
prop_checkSpacefulnessCfg66 = verifyNot checkSpacefulnessCfg "f() { s=$?; echo $s; }"
|
||||
|
||||
checkSpacefulnessCfg = checkSpacefulnessCfg' True
|
||||
checkVerboseSpacefulnessCfg = checkSpacefulnessCfg' False
|
||||
|
||||
checkSpacefulnessCfg' :: Bool -> (Parameters -> Token -> Writer [TokenComment] ())
|
||||
checkSpacefulnessCfg' dirtyPass params token@(T_DollarBraced id _ list) =
|
||||
when (needsQuoting && (dirtyPass == not isClean)) $
|
||||
unless (name `elem` specialVariablesWithoutSpaces || quotesMayConflictWithSC2281 params token) $
|
||||
if dirtyPass
|
||||
then
|
||||
if isDefaultAssignment (parentMap params) token
|
||||
then
|
||||
info (getId token) 2223
|
||||
"This default assignment may cause DoS due to globbing. Quote it."
|
||||
else
|
||||
infoWithFix id 2086 "Double quote to prevent globbing and word splitting." $
|
||||
addDoubleQuotesAround params token
|
||||
else
|
||||
styleWithFix id 2248 "Prefer double quoting even when variables don't contain special characters." $
|
||||
addDoubleQuotesAround params token
|
||||
|
||||
where
|
||||
defaults = zip variablesWithoutSpaces (repeat SpaceNone)
|
||||
|
||||
hasSpaces name = gets (Map.findWithDefault SpaceSome name)
|
||||
|
||||
setSpaces name status =
|
||||
modify $ Map.insert name status
|
||||
|
||||
readF _ token name = do
|
||||
spaces <- hasSpaces name
|
||||
let needsQuoting =
|
||||
isExpansion token
|
||||
&& not (isArrayExpansion token) -- There's another warning for this
|
||||
name = getBracedReference $ concat $ oversimplify list
|
||||
parents = parentMap params
|
||||
needsQuoting =
|
||||
not (isArrayExpansion token) -- There's another warning for this
|
||||
&& not (isCountingReference token)
|
||||
&& not (isQuoteFree (shellType params) parents token)
|
||||
&& not (isQuotedAlternativeReference token)
|
||||
&& not (usedAsCommandName parents token)
|
||||
|
||||
return . execWriter $ when needsQuoting $ onFind spaces token name
|
||||
isClean = fromMaybe False $ do
|
||||
state <- CF.getIncomingState (cfgAnalysis params) id
|
||||
value <- Map.lookup name $ CF.variablesInScope state
|
||||
return $ isCleanState value
|
||||
|
||||
where
|
||||
emit x = tell [x]
|
||||
isCleanState state =
|
||||
(all (S.member CFVPInteger) $ CF.variableProperties state)
|
||||
|| CF.spaceStatus (CF.variableValue state) == CF.SpaceStatusClean
|
||||
|
||||
writeF _ (TA_Assignment {}) name _ = setSpaces name SpaceNone >> return []
|
||||
writeF _ _ name (DataString SourceExternal) = setSpaces name SpaceSome >> return []
|
||||
writeF _ _ name (DataString SourceInteger) = setSpaces name SpaceNone >> return []
|
||||
isDefaultAssignment parents token =
|
||||
let modifier = getBracedModifier $ bracedString token in
|
||||
any (`isPrefixOf` modifier) ["=", ":="]
|
||||
&& isParamTo parents ":" token
|
||||
|
||||
writeF _ _ name (DataString (SourceFrom vals)) = do
|
||||
map <- get
|
||||
setSpaces name
|
||||
(isSpacefulWord (\x -> Map.findWithDefault SpaceSome x map) vals)
|
||||
return []
|
||||
-- Given a T_DollarBraced, return a simplified version of the string contents.
|
||||
bracedString (T_DollarBraced _ _ l) = concat $ oversimplify l
|
||||
bracedString _ = error $ pleaseReport "bracedString on non-variable"
|
||||
|
||||
writeF _ _ _ _ = return []
|
||||
checkSpacefulnessCfg' _ _ _ = return ()
|
||||
|
||||
parents = parentMap params
|
||||
|
||||
isExpansion t =
|
||||
case t of
|
||||
(T_DollarBraced _ _ _ ) -> True
|
||||
_ -> False
|
||||
|
||||
isSpacefulWord :: (String -> SpaceStatus) -> [Token] -> SpaceStatus
|
||||
isSpacefulWord f = mconcat . map (isSpaceful f)
|
||||
isSpaceful :: (String -> SpaceStatus) -> Token -> SpaceStatus
|
||||
isSpaceful spacefulF x =
|
||||
case x of
|
||||
T_DollarExpansion _ _ -> SpaceSome
|
||||
T_Backticked _ _ -> SpaceSome
|
||||
T_Glob _ _ -> SpaceSome
|
||||
T_Extglob {} -> SpaceSome
|
||||
T_DollarArithmetic _ _ -> SpaceNone
|
||||
T_Literal _ s -> fromLiteral s
|
||||
T_SingleQuoted _ s -> fromLiteral s
|
||||
T_DollarBraced _ _ l -> spacefulF $ getBracedReference $ concat $ oversimplify l
|
||||
T_NormalWord _ w -> isSpacefulWord spacefulF w
|
||||
T_DoubleQuoted _ w -> isSpacefulWord spacefulF w
|
||||
_ -> SpaceEmpty
|
||||
where
|
||||
globspace = "*?[] \t\n"
|
||||
containsAny s = any (`elem` s)
|
||||
fromLiteral "" = SpaceEmpty
|
||||
fromLiteral s | s `containsAny` globspace = SpaceSome
|
||||
fromLiteral _ = SpaceNone
|
||||
|
||||
prop_CheckVariableBraces1 = verify checkVariableBraces "a='123'; echo $a"
|
||||
prop_CheckVariableBraces2 = verifyNot checkVariableBraces "a='123'; echo ${a}"
|
||||
@@ -2296,7 +2279,7 @@ checkFunctionsUsedExternally params t =
|
||||
let args = skipOver t argv
|
||||
let argStrings = map (\x -> (fromMaybe "" $ getLiteralString x, x)) args
|
||||
let candidates = getPotentialCommands name argStrings
|
||||
mapM_ (checkArg name) candidates
|
||||
mapM_ (checkArg name (getId t)) candidates
|
||||
_ -> return ()
|
||||
checkCommand _ _ = return ()
|
||||
|
||||
@@ -2322,14 +2305,19 @@ checkFunctionsUsedExternally params t =
|
||||
|
||||
functionsAndAliases = Map.union (functions t) (aliases t)
|
||||
|
||||
checkArg cmd (_, arg) = sequence_ $ do
|
||||
patternContext id =
|
||||
case posLine . fst <$> Map.lookup id (tokenPositions params) of
|
||||
Just l -> " on line " <> show l <> "."
|
||||
_ -> "."
|
||||
|
||||
checkArg cmd cmdId (_, arg) = sequence_ $ do
|
||||
literalArg <- getUnquotedLiteral arg -- only consider unquoted literals
|
||||
definitionId <- Map.lookup literalArg functionsAndAliases
|
||||
return $ do
|
||||
warn (getId arg) 2033
|
||||
"Shell functions can't be passed to external commands."
|
||||
"Shell functions can't be passed to external commands. Use separate script or sh -c."
|
||||
info definitionId 2032 $
|
||||
"Use own script or sh -c '..' to run this from " ++ cmd ++ "."
|
||||
"This function can't be invoked via " ++ cmd ++ patternContext cmdId
|
||||
|
||||
prop_checkUnused0 = verifyNotTree checkUnusedAssignments "var=foo; echo $var"
|
||||
prop_checkUnused1 = verifyTree checkUnusedAssignments "var=foo; echo $bar"
|
||||
@@ -2383,6 +2371,7 @@ prop_checkUnused47= verifyNotTree checkUnusedAssignments "a=1; alias hello='echo
|
||||
prop_checkUnused48 = verifyNotTree checkUnusedAssignments "_a=1"
|
||||
prop_checkUnused49 = verifyNotTree checkUnusedAssignments "declare -A array; key=a; [[ -v array[$key] ]]"
|
||||
prop_checkUnused50 = verifyNotTree checkUnusedAssignments "foofunc() { :; }; typeset -fx foofunc"
|
||||
prop_checkUnused51 = verifyTree checkUnusedAssignments "x[y[z=1]]=1; echo ${x[@]}"
|
||||
|
||||
checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
|
||||
where
|
||||
@@ -3313,6 +3302,7 @@ checkReturnAgainstZero params token =
|
||||
_:next@(TA_Unary _ "!" _):_ -> isOnlyTestInCommand next
|
||||
_:next@(TC_Group {}):_ -> isOnlyTestInCommand next
|
||||
_:next@(TA_Sequence _ [_]):_ -> isOnlyTestInCommand next
|
||||
_:next@(TA_Parentesis _ _):_ -> isOnlyTestInCommand next
|
||||
_ -> False
|
||||
|
||||
-- TODO: Do better $? tracking and filter on whether
|
||||
@@ -3595,7 +3585,6 @@ prop_checkPipeToNowhere4 = verify checkPipeToNowhere "printf 'Lol' << eof\nlol\n
|
||||
prop_checkPipeToNowhere5 = verifyNot checkPipeToNowhere "echo foo | xargs du"
|
||||
prop_checkPipeToNowhere6 = verifyNot checkPipeToNowhere "ls | echo $(cat)"
|
||||
prop_checkPipeToNowhere7 = verifyNot checkPipeToNowhere "echo foo | var=$(cat) ls"
|
||||
prop_checkPipeToNowhere8 = verify checkPipeToNowhere "foo | true"
|
||||
prop_checkPipeToNowhere9 = verifyNot checkPipeToNowhere "mv -i f . < /dev/stdin"
|
||||
prop_checkPipeToNowhere10 = verify checkPipeToNowhere "ls > file | grep foo"
|
||||
prop_checkPipeToNowhere11 = verify checkPipeToNowhere "ls | grep foo < file"
|
||||
@@ -4115,13 +4104,6 @@ checkAliasUsedInSameParsingUnit params root =
|
||||
checkUnit :: [Token] -> Writer [TokenComment] ()
|
||||
checkUnit unit = evalStateT (mapM_ (doAnalysis findCommands) unit) (Map.empty)
|
||||
|
||||
isSourced t =
|
||||
let
|
||||
f (T_SourceCommand {}) = True
|
||||
f _ = False
|
||||
in
|
||||
any f $ getPath (parentMap params) t
|
||||
|
||||
findCommands :: Token -> StateT (Map.Map String Token) (Writer [TokenComment]) ()
|
||||
findCommands t = case t of
|
||||
T_SimpleCommand _ _ (cmd:args) ->
|
||||
@@ -4132,7 +4114,7 @@ checkAliasUsedInSameParsingUnit params root =
|
||||
cmd <- gets (Map.lookup name)
|
||||
case cmd of
|
||||
Just alias ->
|
||||
unless (isSourced t || shouldIgnoreCode params 2262 alias) $ do
|
||||
unless (isSourced params t || shouldIgnoreCode params 2262 alias) $ do
|
||||
warn (getId alias) 2262 "This alias can't be defined and used in the same parsing unit. Use a function instead."
|
||||
info (getId t) 2263 "Since they're in the same parsing unit, this command will not refer to the previously mentioned alias."
|
||||
_ -> return ()
|
||||
@@ -4143,6 +4125,14 @@ checkAliasUsedInSameParsingUnit params root =
|
||||
when (isVariableName name && not (null value)) $
|
||||
modify (Map.insertWith (\new old -> old) name arg)
|
||||
|
||||
isSourced params t =
|
||||
let
|
||||
f (T_SourceCommand {}) = True
|
||||
f _ = False
|
||||
in
|
||||
any f $ getPath (parentMap params) t
|
||||
|
||||
|
||||
-- Like groupBy, but compares pairs of adjacent elements, rather than against the first of the span
|
||||
prop_groupByLink1 = groupByLink (\a b -> a+1 == b) [1,2,3,2,3,7,8,9] == [[1,2,3], [2,3], [7,8,9]]
|
||||
prop_groupByLink2 = groupByLink (==) ([] :: [()]) == []
|
||||
@@ -4706,6 +4696,7 @@ prop_checkSetESuppressed15 = verifyTree checkSetESuppressed "set -e; f(){ :;
|
||||
prop_checkSetESuppressed16 = verifyTree checkSetESuppressed "set -e; f(){ :; }; until set -e; f; do :; done"
|
||||
prop_checkSetESuppressed17 = verifyNotTree checkSetESuppressed "set -e; f(){ :; }; g(){ :; }; g f"
|
||||
prop_checkSetESuppressed18 = verifyNotTree checkSetESuppressed "set -e; shopt -s inherit_errexit; f(){ :; }; x=$(f)"
|
||||
prop_checkSetESuppressed19 = verifyNotTree checkSetESuppressed "set -e; set -o posix; f(){ :; }; x=$(f)"
|
||||
checkSetESuppressed params t =
|
||||
if hasSetE params then runNodeAnalysis checkNode params t else []
|
||||
where
|
||||
@@ -4783,8 +4774,12 @@ prop_checkExtraMaskedReturns32 = verifyNotTree checkExtraMaskedReturns "false <
|
||||
prop_checkExtraMaskedReturns33 = verifyNotTree checkExtraMaskedReturns "{ false || true; } | true"
|
||||
prop_checkExtraMaskedReturns34 = verifyNotTree checkExtraMaskedReturns "{ false || :; } | true"
|
||||
prop_checkExtraMaskedReturns35 = verifyTree checkExtraMaskedReturns "f() { local -r x=$(false); }"
|
||||
prop_checkExtraMaskedReturns36 = verifyNotTree checkExtraMaskedReturns "time false"
|
||||
prop_checkExtraMaskedReturns37 = verifyNotTree checkExtraMaskedReturns "time $(time false)"
|
||||
prop_checkExtraMaskedReturns38 = verifyTree checkExtraMaskedReturns "x=$(time time time false) time $(time false)"
|
||||
|
||||
checkExtraMaskedReturns params t = runNodeAnalysis findMaskingNodes params t
|
||||
checkExtraMaskedReturns params t =
|
||||
runNodeAnalysis findMaskingNodes params (removeTransparentCommands t)
|
||||
where
|
||||
findMaskingNodes _ (T_Arithmetic _ list) = findMaskedNodesInList [list]
|
||||
findMaskingNodes _ (T_Array _ list) = findMaskedNodesInList $ allButLastSimpleCommands list
|
||||
@@ -4817,6 +4812,13 @@ checkExtraMaskedReturns params t = runNodeAnalysis findMaskingNodes params t
|
||||
where
|
||||
simpleCommands = filter containsSimpleCommand cmds
|
||||
|
||||
removeTransparentCommands t =
|
||||
doTransform go t
|
||||
where
|
||||
go cmd@(T_SimpleCommand id assigns (_:args)) | isTransparentCommand cmd
|
||||
= T_SimpleCommand id assigns args
|
||||
go t = t
|
||||
|
||||
inform t = info (getId t) 2312 ("Consider invoking this command "
|
||||
++ "separately to avoid masking its return value (or use '|| true' "
|
||||
++ "to ignore).")
|
||||
@@ -4849,6 +4851,10 @@ checkExtraMaskedReturns params t = runNodeAnalysis findMaskingNodes params t
|
||||
,"shopt"
|
||||
]
|
||||
|
||||
isTransparentCommand t = fromMaybe False $ do
|
||||
basename <- getCommandBasename t
|
||||
return $ basename == "time"
|
||||
|
||||
parentChildPairs t = go $ parents params t
|
||||
where
|
||||
go (child:parent:rest) = (parent, child):go (parent:rest)
|
||||
@@ -4857,5 +4863,152 @@ checkExtraMaskedReturns params t = runNodeAnalysis findMaskingNodes params t
|
||||
hasParent pred t = any (uncurry pred) (parentChildPairs t)
|
||||
|
||||
|
||||
-- hard error on negated command that is not last
|
||||
prop_checkBatsTestDoesNotUseNegation1 = verify checkBatsTestDoesNotUseNegation "#!/usr/bin/env/bats\n@test \"name\" { ! true; false; }"
|
||||
prop_checkBatsTestDoesNotUseNegation2 = verify checkBatsTestDoesNotUseNegation "#!/usr/bin/env/bats\n@test \"name\" { ! [[ -e test ]]; false; }"
|
||||
prop_checkBatsTestDoesNotUseNegation3 = verify checkBatsTestDoesNotUseNegation "#!/usr/bin/env/bats\n@test \"name\" { ! [ -e test ]; false; }"
|
||||
-- acceptable formats:
|
||||
-- using run
|
||||
prop_checkBatsTestDoesNotUseNegation4 = verifyNot checkBatsTestDoesNotUseNegation "#!/usr/bin/env/bats\n@test \"name\" { run ! true; }"
|
||||
-- using || false
|
||||
prop_checkBatsTestDoesNotUseNegation5 = verifyNot checkBatsTestDoesNotUseNegation "#!/usr/bin/env/bats\n@test \"name\" { ! [[ -e test ]] || false; }"
|
||||
prop_checkBatsTestDoesNotUseNegation6 = verifyNot checkBatsTestDoesNotUseNegation "#!/usr/bin/env/bats\n@test \"name\" { ! [ -e test ] || false; }"
|
||||
-- only style warning when last command
|
||||
prop_checkBatsTestDoesNotUseNegation7 = verifyCodes checkBatsTestDoesNotUseNegation [2314] "#!/usr/bin/env/bats\n@test \"name\" { ! true; }"
|
||||
prop_checkBatsTestDoesNotUseNegation8 = verifyCodes checkBatsTestDoesNotUseNegation [2315] "#!/usr/bin/env/bats\n@test \"name\" { ! [[ -e test ]]; }"
|
||||
prop_checkBatsTestDoesNotUseNegation9 = verifyCodes checkBatsTestDoesNotUseNegation [2315] "#!/usr/bin/env/bats\n@test \"name\" { ! [ -e test ]; }"
|
||||
|
||||
checkBatsTestDoesNotUseNegation params t =
|
||||
case t of
|
||||
T_BatsTest _ _ (T_BraceGroup _ commands) -> mapM_ (check commands) commands
|
||||
_ -> return ()
|
||||
where
|
||||
check commands t =
|
||||
case t of
|
||||
T_Banged id (T_Pipeline _ _ [T_Redirecting _ _ (T_Condition idCondition _ _)]) ->
|
||||
if t `isLastOf` commands
|
||||
then style id 2315 "In Bats, ! will not fail the test if it is not the last command anymore. Fold the `!` into the conditional!"
|
||||
else err id 2315 "In Bats, ! does not cause a test failure. Fold the `!` into the conditional!"
|
||||
|
||||
T_Banged id cmd -> if t `isLastOf` commands
|
||||
then styleWithFix id 2314 "In Bats, ! will not fail the test if it is not the last command anymore. Use `run ! ` (on Bats >= 1.5.0) instead."
|
||||
(fixWith [replaceStart id params 0 "run "])
|
||||
else errWithFix id 2314 "In Bats, ! does not cause a test failure. Use 'run ! ' (on Bats >= 1.5.0) instead."
|
||||
(fixWith [replaceStart id params 0 "run "])
|
||||
_ -> return ()
|
||||
isLastOf t commands =
|
||||
case commands of
|
||||
[x] -> x == t
|
||||
x:rest -> isLastOf t rest
|
||||
[] -> False
|
||||
|
||||
|
||||
prop_checkCommandIsUnreachable1 = verify checkCommandIsUnreachable "foo; bar; exit; baz"
|
||||
prop_checkCommandIsUnreachable2 = verify checkCommandIsUnreachable "die() { exit; }; foo; bar; die; baz"
|
||||
prop_checkCommandIsUnreachable3 = verifyNot checkCommandIsUnreachable "foo; bar || exit; baz"
|
||||
checkCommandIsUnreachable params t =
|
||||
case t of
|
||||
T_Pipeline {} -> sequence_ $ do
|
||||
state <- CF.getIncomingState (cfgAnalysis params) id
|
||||
guard . not $ CF.stateIsReachable state
|
||||
guard . not $ isSourced params t
|
||||
return $ info id 2317 "Command appears to be unreachable. Check usage (or ignore if invoked indirectly)."
|
||||
_ -> return ()
|
||||
where id = getId t
|
||||
|
||||
|
||||
prop_checkOverwrittenExitCode1 = verify checkOverwrittenExitCode "x; [ $? -eq 1 ] || [ $? -eq 2 ]"
|
||||
prop_checkOverwrittenExitCode2 = verifyNot checkOverwrittenExitCode "x; [ $? -eq 1 ]"
|
||||
prop_checkOverwrittenExitCode3 = verify checkOverwrittenExitCode "x; echo \"Exit is $?\"; [ $? -eq 0 ]"
|
||||
prop_checkOverwrittenExitCode4 = verifyNot checkOverwrittenExitCode "x; [ $? -eq 0 ] && echo Success"
|
||||
prop_checkOverwrittenExitCode5 = verify checkOverwrittenExitCode "x; if [ $? -eq 0 ]; then var=$?; fi"
|
||||
prop_checkOverwrittenExitCode6 = verify checkOverwrittenExitCode "x; [ $? -gt 0 ] && fail=$?"
|
||||
prop_checkOverwrittenExitCode7 = verifyNot checkOverwrittenExitCode "[ 1 -eq 2 ]; status=$?"
|
||||
prop_checkOverwrittenExitCode8 = verifyNot checkOverwrittenExitCode "[ 1 -eq 2 ]; exit $?"
|
||||
checkOverwrittenExitCode params t =
|
||||
case t of
|
||||
T_DollarBraced id _ val | getLiteralString val == Just "?" -> check id
|
||||
_ -> return ()
|
||||
where
|
||||
check id = sequence_ $ do
|
||||
state <- CF.getIncomingState (cfgAnalysis params) id
|
||||
let exitCodeIds = CF.exitCodes state
|
||||
guard . not $ S.null exitCodeIds
|
||||
|
||||
let idToToken = idMap params
|
||||
exitCodeTokens <- sequence $ map (\k -> Map.lookup k idToToken) $ S.toList exitCodeIds
|
||||
return $ do
|
||||
when (all isCondition exitCodeTokens && not (usedUnconditionally t exitCodeIds)) $
|
||||
warn id 2319 "This $? refers to a condition, not a command. Assign to a variable to avoid it being overwritten."
|
||||
when (all isPrinting exitCodeTokens) $
|
||||
warn id 2320 "This $? refers to echo/printf, not a previous command. Assign to variable to avoid it being overwritten."
|
||||
|
||||
isCondition t =
|
||||
case t of
|
||||
T_Condition {} -> True
|
||||
T_SimpleCommand {} -> getCommandName t == Just "test"
|
||||
_ -> False
|
||||
|
||||
-- If we don't do anything based on the condition, assume we wanted the condition itself
|
||||
-- This helps differentiate `x; [ $? -gt 0 ] && exit $?` vs `[ cond ]; exit $?`
|
||||
usedUnconditionally t testIds =
|
||||
all (\c -> CF.doesPostDominate (cfgAnalysis params) (getId t) c) testIds
|
||||
|
||||
isPrinting t =
|
||||
case getCommandBasename t of
|
||||
Just "echo" -> True
|
||||
Just "printf" -> True
|
||||
_ -> False
|
||||
|
||||
|
||||
prop_checkUnnecessaryArithmeticExpansionIndex1 = verify checkUnnecessaryArithmeticExpansionIndex "a[$((1+1))]=n"
|
||||
prop_checkUnnecessaryArithmeticExpansionIndex2 = verifyNot checkUnnecessaryArithmeticExpansionIndex "a[1+1]=n"
|
||||
prop_checkUnnecessaryArithmeticExpansionIndex3 = verifyNot checkUnnecessaryArithmeticExpansionIndex "a[$(echo $((1+1)))]=n"
|
||||
prop_checkUnnecessaryArithmeticExpansionIndex4 = verifyNot checkUnnecessaryArithmeticExpansionIndex "declare -A a; a[$((1+1))]=val"
|
||||
checkUnnecessaryArithmeticExpansionIndex params t =
|
||||
case t of
|
||||
T_Assignment _ mode var [TA_Sequence _ [ TA_Expansion _ [expansion@(T_DollarArithmetic id _)]]] val ->
|
||||
styleWithFix id 2321 "Array indices are already arithmetic contexts. Prefer removing the $(( and ))." $ fix id
|
||||
_ -> return ()
|
||||
|
||||
where
|
||||
fix id =
|
||||
fixWith [
|
||||
replaceStart id params 3 "", -- Remove "$(("
|
||||
replaceEnd id params 2 "" -- Remove "))"
|
||||
]
|
||||
|
||||
|
||||
prop_checkUnnecessaryParens1 = verify checkUnnecessaryParens "echo $(( ((1+1)) ))"
|
||||
prop_checkUnnecessaryParens2 = verify checkUnnecessaryParens "x[((1+1))+1]=1"
|
||||
prop_checkUnnecessaryParens3 = verify checkUnnecessaryParens "x[(1+1)]=1"
|
||||
prop_checkUnnecessaryParens4 = verify checkUnnecessaryParens "$(( (x) ))"
|
||||
prop_checkUnnecessaryParens5 = verify checkUnnecessaryParens "(( (x) ))"
|
||||
prop_checkUnnecessaryParens6 = verifyNot checkUnnecessaryParens "x[(1+1)+1]=1"
|
||||
prop_checkUnnecessaryParens7 = verifyNot checkUnnecessaryParens "(( (1*1)+1 ))"
|
||||
prop_checkUnnecessaryParens8 = verifyNot checkUnnecessaryParens "(( (1)+1 ))"
|
||||
checkUnnecessaryParens params t =
|
||||
case t of
|
||||
T_DollarArithmetic _ t -> checkLeading "$(( (x) )) is the same as $(( x ))" t
|
||||
T_ForArithmetic _ x y z _ -> mapM_ (checkLeading "for (((x); (y); (z))) is the same as for ((x; y; z))") [x,y,z]
|
||||
T_Assignment _ _ _ [t] _ -> checkLeading "a[(x)] is the same as a[x]" t
|
||||
T_Arithmetic _ t -> checkLeading "(( (x) )) is the same as (( x ))" t
|
||||
TA_Parentesis _ (TA_Sequence _ [ TA_Parentesis id _ ]) ->
|
||||
styleWithFix id 2322 "In arithmetic contexts, ((x)) is the same as (x). Prefer only one layer of parentheses." $ fix id
|
||||
_ -> return ()
|
||||
where
|
||||
|
||||
checkLeading str t =
|
||||
case t of
|
||||
TA_Sequence _ [TA_Parentesis id _ ] -> styleWithFix id 2323 (str ++ ". Prefer not wrapping in additional parentheses.") $ fix id
|
||||
_ -> return ()
|
||||
|
||||
fix id =
|
||||
fixWith [
|
||||
replaceStart id params 1 "", -- Remove "("
|
||||
replaceEnd id params 1 "" -- Remove ")"
|
||||
]
|
||||
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{-
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
Copyright 2012-2022 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
@@ -25,6 +25,7 @@ import ShellCheck.Interface
|
||||
import Data.List
|
||||
import Data.Monoid
|
||||
import qualified ShellCheck.Checks.Commands
|
||||
import qualified ShellCheck.Checks.ControlFlow
|
||||
import qualified ShellCheck.Checks.Custom
|
||||
import qualified ShellCheck.Checks.ShellSupport
|
||||
|
||||
@@ -34,19 +35,21 @@ analyzeScript :: AnalysisSpec -> AnalysisResult
|
||||
analyzeScript spec = newAnalysisResult {
|
||||
arComments =
|
||||
filterByAnnotation spec params . nub $
|
||||
runAnalytics spec
|
||||
++ runChecker params (checkers spec params)
|
||||
runChecker params (checkers spec params)
|
||||
}
|
||||
where
|
||||
params = makeParameters spec
|
||||
|
||||
checkers spec params = mconcat $ map ($ params) [
|
||||
ShellCheck.Analytics.checker spec,
|
||||
ShellCheck.Checks.Commands.checker spec,
|
||||
ShellCheck.Checks.ControlFlow.checker spec,
|
||||
ShellCheck.Checks.Custom.checker,
|
||||
ShellCheck.Checks.ShellSupport.checker
|
||||
]
|
||||
|
||||
optionalChecks = mconcat $ [
|
||||
ShellCheck.Analytics.optionalChecks,
|
||||
ShellCheck.Checks.Commands.optionalChecks
|
||||
ShellCheck.Checks.Commands.optionalChecks,
|
||||
ShellCheck.Checks.ControlFlow.optionalChecks
|
||||
]
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{-
|
||||
Copyright 2012-2021 Vidar Holen
|
||||
Copyright 2012-2022 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
@@ -23,13 +23,16 @@ module ShellCheck.AnalyzerLib where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import qualified ShellCheck.CFGAnalysis as CF
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Prelude
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Arrow (first)
|
||||
import Control.DeepSeq
|
||||
import Control.Monad
|
||||
import Control.Monad.Identity
|
||||
import Control.Monad.RWS
|
||||
import Control.Monad.State
|
||||
@@ -87,6 +90,8 @@ data Parameters = Parameters {
|
||||
hasPipefail :: Bool,
|
||||
-- A linear (bad) analysis of data flow
|
||||
variableFlow :: [StackData],
|
||||
-- A map from Id to Token
|
||||
idMap :: Map.Map Id Token,
|
||||
-- A map from Id to parent Token
|
||||
parentMap :: Map.Map Id Token,
|
||||
-- The shell type, such as Bash or Ksh
|
||||
@@ -96,7 +101,9 @@ data Parameters = Parameters {
|
||||
-- The root node of the AST
|
||||
rootNode :: Token,
|
||||
-- map from token id to start and end position
|
||||
tokenPositions :: Map.Map Id (Position, Position)
|
||||
tokenPositions :: Map.Map Id (Position, Position),
|
||||
-- Result from Control Flow Graph analysis (including data flow analysis)
|
||||
cfgAnalysis :: CF.CFGAnalysis
|
||||
} deriving (Show)
|
||||
|
||||
-- TODO: Cache results of common AST ops here
|
||||
@@ -189,35 +196,42 @@ makeCommentWithFix severity id code str fix =
|
||||
}
|
||||
in force withFix
|
||||
|
||||
makeParameters spec =
|
||||
let params = Parameters {
|
||||
makeParameters spec = params
|
||||
where
|
||||
params = Parameters {
|
||||
rootNode = root,
|
||||
shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec,
|
||||
hasSetE = containsSetE root,
|
||||
hasLastpipe =
|
||||
case shellType params of
|
||||
Bash -> containsLastpipe root
|
||||
Bash -> isOptionSet "lastpipe" root
|
||||
Dash -> False
|
||||
Sh -> False
|
||||
Ksh -> True,
|
||||
hasInheritErrexit =
|
||||
case shellType params of
|
||||
Bash -> containsInheritErrexit root
|
||||
Bash -> isOptionSet "inherit_errexit" root
|
||||
Dash -> True
|
||||
Sh -> True
|
||||
Ksh -> False,
|
||||
hasPipefail =
|
||||
case shellType params of
|
||||
Bash -> containsPipefail root
|
||||
Bash -> isOptionSet "pipefail" root
|
||||
Dash -> True
|
||||
Sh -> True
|
||||
Ksh -> containsPipefail root,
|
||||
Ksh -> isOptionSet "pipefail" root,
|
||||
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
|
||||
idMap = getTokenMap root,
|
||||
parentMap = getParentTree root,
|
||||
variableFlow = getVariableFlow params root,
|
||||
tokenPositions = asTokenPositions spec
|
||||
} in params
|
||||
where root = asScript spec
|
||||
tokenPositions = asTokenPositions spec,
|
||||
cfgAnalysis = CF.analyzeControlFlow cfParams root
|
||||
}
|
||||
cfParams = CF.CFGParameters {
|
||||
CF.cfLastpipe = hasLastpipe params,
|
||||
CF.cfPipefail = hasPipefail params
|
||||
}
|
||||
root = asScript spec
|
||||
|
||||
|
||||
-- Does this script mention 'set -e' anywhere?
|
||||
@@ -234,13 +248,14 @@ containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
|
||||
_ -> False
|
||||
re = mkRegex "[[:space:]]-[^-]*e"
|
||||
|
||||
containsPipefail root = isNothing $ doAnalysis (guard . not . isPipefail) root
|
||||
|
||||
containsSetOption opt root = isNothing $ doAnalysis (guard . not . isPipefail) root
|
||||
where
|
||||
isPipefail t =
|
||||
case t of
|
||||
T_SimpleCommand {} ->
|
||||
t `isUnqualifiedCommand` "set" &&
|
||||
("pipefail" `elem` oversimplify t ||
|
||||
(opt `elem` oversimplify t ||
|
||||
"o" `elem` map snd (getAllFlags t))
|
||||
_ -> False
|
||||
|
||||
@@ -254,12 +269,8 @@ containsShopt shopt root =
|
||||
(shopt `elem` oversimplify t)
|
||||
_ -> False
|
||||
|
||||
-- Does this script mention 'shopt -s inherit_errexit' anywhere?
|
||||
containsInheritErrexit = containsShopt "inherit_errexit"
|
||||
|
||||
-- Does this script mention 'shopt -s lastpipe' anywhere?
|
||||
-- Also used as a hack.
|
||||
containsLastpipe = containsShopt "lastpipe"
|
||||
-- Does this script mention 'shopt -s $opt' or 'set -o $opt' anywhere?
|
||||
isOptionSet opt root = containsShopt opt root || containsSetOption opt root
|
||||
|
||||
|
||||
prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh
|
||||
@@ -351,7 +362,7 @@ isQuoteFreeNode strict shell tree t =
|
||||
T_SelectIn {} -> return (not strict)
|
||||
_ -> Nothing
|
||||
|
||||
-- Check whether this assigment is self-quoting due to being a recognized
|
||||
-- Check whether this assignment is self-quoting due to being a recognized
|
||||
-- assignment passed to a Declaration Utility. This will soon be required
|
||||
-- by POSIX: https://austingroupbugs.net/view.php?id=351
|
||||
assignmentIsQuoting t = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand t)
|
||||
@@ -408,12 +419,6 @@ usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
||||
getId word == currentId || getId (getCommandTokenOrThis t) == currentId
|
||||
go _ _ = False
|
||||
|
||||
-- A list of the element and all its parents up to the root node.
|
||||
getPath tree t = t :
|
||||
case Map.lookup (getId t) tree of
|
||||
Nothing -> []
|
||||
Just parent -> getPath tree parent
|
||||
|
||||
-- Version of the above taking the map from the current context
|
||||
-- Todo: give this the name "getPath"
|
||||
getPathM t = do
|
||||
@@ -511,13 +516,11 @@ getModifiedVariables t =
|
||||
T_SimpleCommand {} ->
|
||||
getModifiedVariableCommand t
|
||||
|
||||
TA_Unary _ "++|" v@(TA_Variable _ name _) ->
|
||||
[(t, v, name, DataString $ SourceFrom [v])]
|
||||
TA_Unary _ "|++" v@(TA_Variable _ name _) ->
|
||||
[(t, v, name, DataString $ SourceFrom [v])]
|
||||
TA_Unary _ op v@(TA_Variable _ name _) | "--" `isInfixOf` op || "++" `isInfixOf` op ->
|
||||
[(t, v, name, DataString SourceInteger)]
|
||||
TA_Assignment _ op (TA_Variable _ name _) rhs -> do
|
||||
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
||||
return (t, t, name, DataString $ SourceFrom [rhs])
|
||||
return (t, t, name, DataString SourceInteger)
|
||||
|
||||
T_BatsTest {} -> [
|
||||
(t, t, "lines", DataArray SourceExternal),
|
||||
@@ -561,12 +564,6 @@ getModifiedVariables t =
|
||||
return (place, t, str, DataString SourceChecked)
|
||||
_ -> Nothing
|
||||
|
||||
isClosingFileOp op =
|
||||
case op of
|
||||
T_IoDuplicate _ (T_GREATAND _) "-" -> True
|
||||
T_IoDuplicate _ (T_LESSAND _) "-" -> True
|
||||
_ -> False
|
||||
|
||||
|
||||
-- Consider 'export/declare -x' a reference, since it makes the var available
|
||||
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
||||
@@ -748,13 +745,6 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
|
||||
|
||||
getModifiedVariableCommand _ = []
|
||||
|
||||
getIndexReferences s = fromMaybe [] $ do
|
||||
match <- matchRegex re s
|
||||
index <- match !!! 0
|
||||
return $ matchAllStrings variableNameRegex index
|
||||
where
|
||||
re = mkRegex "(\\[.*\\])"
|
||||
|
||||
-- Given a NormalWord like foo or foo[$bar], get foo.
|
||||
-- Primarily used to get references for [[ -v foo[bar] ]]
|
||||
getVariableForTestDashV :: Token -> Maybe String
|
||||
@@ -769,18 +759,6 @@ getVariableForTestDashV t = do
|
||||
-- in a non-constant expression (while filtering out foo$x[$y])
|
||||
toStr _ = return "\0"
|
||||
|
||||
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
|
||||
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
|
||||
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
|
||||
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
|
||||
getOffsetReferences mods = fromMaybe [] $ do
|
||||
-- if mods start with [, then drop until ]
|
||||
match <- matchRegex re mods
|
||||
offsets <- match !!! 1
|
||||
return $ matchAllStrings variableNameRegex offsets
|
||||
where
|
||||
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
|
||||
|
||||
getReferencedVariables parents t =
|
||||
case t of
|
||||
T_DollarBraced id _ l -> let str = concat $ oversimplify l in
|
||||
@@ -859,17 +837,6 @@ isConfusedGlobRegex ('*':_) = True
|
||||
isConfusedGlobRegex [x,'*'] | x `notElem` "\\." = True
|
||||
isConfusedGlobRegex _ = False
|
||||
|
||||
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
|
||||
isVariableChar x = isVariableStartChar x || isDigit x
|
||||
isSpecialVariableChar = (`elem` "*@#?-$!")
|
||||
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
|
||||
|
||||
prop_isVariableName1 = isVariableName "_fo123"
|
||||
prop_isVariableName2 = not $ isVariableName "4"
|
||||
prop_isVariableName3 = not $ isVariableName "test: "
|
||||
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
|
||||
isVariableName _ = False
|
||||
|
||||
getVariablesFromLiteralToken token =
|
||||
getVariablesFromLiteral (getLiteralStringDef " " token)
|
||||
|
||||
@@ -882,73 +849,6 @@ getVariablesFromLiteral string =
|
||||
where
|
||||
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
|
||||
|
||||
-- Get the variable name from an expansion like ${var:-foo}
|
||||
prop_getBracedReference1 = getBracedReference "foo" == "foo"
|
||||
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
|
||||
prop_getBracedReference3 = getBracedReference "#" == "#"
|
||||
prop_getBracedReference4 = getBracedReference "##" == "#"
|
||||
prop_getBracedReference5 = getBracedReference "#!" == "!"
|
||||
prop_getBracedReference6 = getBracedReference "!#" == "#"
|
||||
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
|
||||
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
|
||||
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
|
||||
prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
|
||||
prop_getBracedReference11= getBracedReference "!os*" == ""
|
||||
prop_getBracedReference11b= getBracedReference "!os@" == ""
|
||||
prop_getBracedReference12= getBracedReference "!os?bar**" == ""
|
||||
prop_getBracedReference13= getBracedReference "foo[bar]" == "foo"
|
||||
getBracedReference s = fromMaybe s $
|
||||
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
|
||||
where
|
||||
noPrefix = dropPrefix s
|
||||
dropPrefix (c:rest) | c `elem` "!#" = rest
|
||||
dropPrefix cs = cs
|
||||
takeName s = do
|
||||
let name = takeWhile isVariableChar s
|
||||
guard . not $ null name
|
||||
return name
|
||||
getSpecial (c:_) | isSpecialVariableChar c = return [c]
|
||||
getSpecial _ = fail "empty or not special"
|
||||
|
||||
nameExpansion ('!':next:rest) = do -- e.g. ${!foo*bar*}
|
||||
guard $ isVariableChar next -- e.g. ${!@}
|
||||
first <- find (not . isVariableChar) rest
|
||||
guard $ first `elem` "*?@"
|
||||
return ""
|
||||
nameExpansion _ = Nothing
|
||||
|
||||
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
|
||||
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
|
||||
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
|
||||
prop_getBracedModifier4 = getBracedModifier "foo[@]@Q" == "[@]@Q"
|
||||
prop_getBracedModifier5 = getBracedModifier "@@Q" == "@Q"
|
||||
getBracedModifier s = headOrDefault "" $ do
|
||||
let var = getBracedReference s
|
||||
a <- dropModifier s
|
||||
dropPrefix var a
|
||||
where
|
||||
dropPrefix [] t = return t
|
||||
dropPrefix (a:b) (c:d) | a == c = dropPrefix b d
|
||||
dropPrefix _ _ = []
|
||||
|
||||
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
|
||||
dropModifier x = [x]
|
||||
|
||||
-- Useful generic functions.
|
||||
|
||||
-- Get element 0 or a default. Like `head` but safe.
|
||||
headOrDefault _ (a:_) = a
|
||||
headOrDefault def _ = def
|
||||
|
||||
-- Get the last element or a default. Like `last` but safe.
|
||||
lastOrDefault def [] = def
|
||||
lastOrDefault _ list = last list
|
||||
|
||||
--- Get element n of a list, or Nothing. Like `!!` but safe.
|
||||
(!!!) list i =
|
||||
case drop i list of
|
||||
[] -> Nothing
|
||||
(r:_) -> Just r
|
||||
|
||||
-- Run a command if the shell is in the given list
|
||||
whenShell l c = do
|
||||
@@ -1001,17 +901,6 @@ isBashLike params =
|
||||
Dash -> False
|
||||
Sh -> False
|
||||
|
||||
-- Returns whether a token is a parameter expansion without any modifiers.
|
||||
-- True for $var ${var} $1 $#
|
||||
-- False for ${#var} ${var[x]} ${var:-0}
|
||||
isUnmodifiedParameterExpansion t =
|
||||
case t of
|
||||
T_DollarBraced _ False _ -> True
|
||||
T_DollarBraced _ _ list ->
|
||||
let str = concat $ oversimplify list
|
||||
in getBracedReference str == str
|
||||
_ -> False
|
||||
|
||||
isTrueAssignmentSource c =
|
||||
case c of
|
||||
DataString SourceChecked -> False
|
||||
|
1307
src/ShellCheck/CFG.hs
Normal file
1307
src/ShellCheck/CFG.hs
Normal file
File diff suppressed because it is too large
Load Diff
1424
src/ShellCheck/CFGAnalysis.hs
Normal file
1424
src/ShellCheck/CFGAnalysis.hs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
{-
|
||||
Copyright 2012-2020 Vidar Holen
|
||||
Copyright 2012-2022 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
@@ -20,9 +20,10 @@
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
|
||||
|
||||
import ShellCheck.Analyzer
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Analyzer
|
||||
|
||||
import Data.Either
|
||||
import Data.Functor
|
||||
@@ -85,7 +86,7 @@ checkScript sys spec = do
|
||||
asCheckSourced = csCheckSourced spec,
|
||||
asExecutionMode = Executed,
|
||||
asTokenPositions = tokenPositions,
|
||||
asOptionalChecks = csOptionalChecks spec
|
||||
asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec
|
||||
} where as = newAnalysisSpec root
|
||||
let analysisMessages =
|
||||
maybe []
|
||||
@@ -243,6 +244,9 @@ prop_canStripPrefixAndSource2 =
|
||||
prop_canSourceDynamicWhenRedirected =
|
||||
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
|
||||
|
||||
prop_canRedirectWithSpaces =
|
||||
null $ checkWithIncludes [("my file", "")] "#shellcheck source=\"my file\"\n. \"$1\""
|
||||
|
||||
prop_recursiveAnalysis =
|
||||
[2086] == checkRecursive [("lib", "echo $1")] "source lib"
|
||||
|
||||
@@ -412,6 +416,15 @@ prop_sourcePathAddsAnnotation = result == [2086]
|
||||
csCheckSourced = True
|
||||
}
|
||||
|
||||
prop_sourcePathWorksWithSpaces = result == [2086]
|
||||
where
|
||||
f "dir/myscript" _ ["my path"] "lib" = return "foo/lib"
|
||||
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
|
||||
csScript = "#!/bin/bash\n# shellcheck source-path='my path'\nsource lib",
|
||||
csFilename = "dir/myscript",
|
||||
csCheckSourced = True
|
||||
}
|
||||
|
||||
prop_sourcePathRedirectsDirective = result == [2086]
|
||||
where
|
||||
f "dir/myscript" _ _ "lib" = return "foo/lib"
|
||||
@@ -483,6 +496,17 @@ prop_fileCannotEnableExternalSources2 = result == [1144]
|
||||
csCheckSourced = True
|
||||
}
|
||||
|
||||
prop_rcCanSuppressEarlyProblems1 = null result
|
||||
where
|
||||
result = checkWithRc "disable=1071" emptyCheckSpec {
|
||||
csScript = "#!/bin/zsh\necho $1"
|
||||
}
|
||||
|
||||
prop_rcCanSuppressEarlyProblems2 = null result
|
||||
where
|
||||
result = checkWithRc "disable=1104" emptyCheckSpec {
|
||||
csScript = "!/bin/bash\necho 'hello world'"
|
||||
}
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{-
|
||||
Copyright 2012-2021 Vidar Holen
|
||||
Copyright 2012-2022 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
@@ -27,21 +27,28 @@ module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Co
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.AnalyzerLib
|
||||
import ShellCheck.CFG
|
||||
import qualified ShellCheck.CFGAnalysis as CF
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Prelude
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.RWS
|
||||
import Data.Char
|
||||
import Data.Functor.Identity
|
||||
import qualified Data.Graph.Inductive.Graph as G
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import qualified Data.Map.Strict as Map
|
||||
import qualified Data.Map.Strict as M
|
||||
import qualified Data.Set as S
|
||||
import Test.QuickCheck.All (forAllProperties)
|
||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||
|
||||
import Debug.Trace -- STRIP
|
||||
|
||||
data CommandName = Exactly String | Basename String
|
||||
deriving (Eq, Ord)
|
||||
|
||||
@@ -98,8 +105,10 @@ commandChecks = [
|
||||
,checkUnquotedEchoSpaces
|
||||
,checkEvalArray
|
||||
]
|
||||
++ map checkArgComparison declaringCommands
|
||||
++ map checkArgComparison ("alias" : declaringCommands)
|
||||
++ map checkMaskedReturns declaringCommands
|
||||
++ map checkMultipleDeclaring declaringCommands
|
||||
++ map checkBackreferencingDeclaration declaringCommands
|
||||
|
||||
|
||||
optionalChecks = map fst optionalCommandChecks
|
||||
@@ -112,7 +121,7 @@ optionalCommandChecks = [
|
||||
cdNegative = "command -v javac"
|
||||
}, checkWhich)
|
||||
]
|
||||
optionalCheckMap = Map.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks
|
||||
optionalCheckMap = M.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks
|
||||
|
||||
prop_verifyOptionalExamples = all check optionalCommandChecks
|
||||
where
|
||||
@@ -161,27 +170,27 @@ prop_checkGenericOptsT1 = checkGetOpts "-x -- -y" ["x"] ["-y"] $ return . getGen
|
||||
prop_checkGenericOptsT2 = checkGetOpts "-xy --" ["x", "y"] [] $ return . getGenericOpts
|
||||
|
||||
|
||||
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
||||
buildCommandMap = foldl' addCheck Map.empty
|
||||
buildCommandMap :: [CommandCheck] -> M.Map CommandName (Token -> Analysis)
|
||||
buildCommandMap = foldl' addCheck M.empty
|
||||
where
|
||||
addCheck map (CommandCheck name function) =
|
||||
Map.insertWith composeAnalyzers name function map
|
||||
M.insertWith composeAnalyzers name function map
|
||||
|
||||
|
||||
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
|
||||
checkCommand :: M.Map CommandName (Token -> Analysis) -> Token -> Analysis
|
||||
checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do
|
||||
name <- getLiteralString cmd
|
||||
return $
|
||||
if '/' `elem` name
|
||||
then
|
||||
Map.findWithDefault nullCheck (Basename $ basename name) map t
|
||||
M.findWithDefault nullCheck (Basename $ basename name) map t
|
||||
else if name == "builtin" && not (null rest) then
|
||||
let t' = T_SimpleCommand id cmdPrefix rest
|
||||
selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest
|
||||
in Map.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
|
||||
in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
|
||||
else do
|
||||
Map.findWithDefault nullCheck (Exactly name) map t
|
||||
Map.findWithDefault nullCheck (Basename name) map t
|
||||
M.findWithDefault nullCheck (Exactly name) map t
|
||||
M.findWithDefault nullCheck (Basename name) map t
|
||||
|
||||
where
|
||||
basename = reverse . takeWhile (/= '/') . reverse
|
||||
@@ -203,7 +212,7 @@ checker spec params = getChecker $ commandChecks ++ optionals
|
||||
optionals =
|
||||
if "all" `elem` keys
|
||||
then map snd optionalCommandChecks
|
||||
else mapMaybe (\x -> Map.lookup x optionalCheckMap) keys
|
||||
else mapMaybe (\x -> M.lookup x optionalCheckMap) keys
|
||||
|
||||
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
|
||||
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
|
||||
@@ -472,9 +481,16 @@ prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\n
|
||||
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
|
||||
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
|
||||
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
|
||||
prop_checkUnusedEchoEscapes6 = verify checkUnusedEchoEscapes "echo '\\506'"
|
||||
prop_checkUnusedEchoEscapes7 = verify checkUnusedEchoEscapes "echo '\\5a'"
|
||||
prop_checkUnusedEchoEscapes8 = verifyNot checkUnusedEchoEscapes "echo '\\8a'"
|
||||
prop_checkUnusedEchoEscapes9 = verifyNot checkUnusedEchoEscapes "echo '\\d5a'"
|
||||
prop_checkUnusedEchoEscapes10 = verify checkUnusedEchoEscapes "echo '\\x4a'"
|
||||
prop_checkUnusedEchoEscapes11 = verify checkUnusedEchoEscapes "echo '\\xat'"
|
||||
prop_checkUnusedEchoEscapes12 = verifyNot checkUnusedEchoEscapes "echo '\\xth'"
|
||||
checkUnusedEchoEscapes = CommandCheck (Basename "echo") f
|
||||
where
|
||||
hasEscapes = mkRegex "\\\\[rnt]"
|
||||
hasEscapes = mkRegex "\\\\([rntabefv\\']|[0-7]{1,3}|x([0-9]|[A-F]|[a-f]){1,2})"
|
||||
f cmd =
|
||||
whenShell [Sh, Bash, Ksh] $
|
||||
unless (cmd `hasFlag` "e") $
|
||||
@@ -674,6 +690,7 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||
let formats = getPrintfFormats string
|
||||
let formatCount = length formats
|
||||
let argCount = length more
|
||||
let pluraliseIfMany word n = if n > 1 then word ++ "s" else word
|
||||
|
||||
return $ if
|
||||
| argCount == 0 && formatCount == 0 ->
|
||||
@@ -689,7 +706,8 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||
return () -- Great: a suitable number of arguments
|
||||
| otherwise ->
|
||||
warn (getId format) 2183 $
|
||||
"This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments."
|
||||
"This format string has " ++ show formatCount ++ " " ++ pluraliseIfMany "variable" formatCount ++
|
||||
", but is passed " ++ show argCount ++ pluraliseIfMany " argument" argCount ++ "."
|
||||
|
||||
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
|
||||
info (getId format) 2059
|
||||
@@ -735,7 +753,7 @@ getPrintfFormats = getFormats
|
||||
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \______ /
|
||||
-- V V V V V
|
||||
-- flags field width precision format character rest
|
||||
-- field width and precision can be specified with a '*' instead of a digit,
|
||||
-- field width and precision can be specified with an '*' instead of a digit,
|
||||
-- in which case printf will accept one more argument for each '*' used
|
||||
|
||||
|
||||
@@ -941,6 +959,22 @@ checkLocalScope = CommandCheck (Exactly "local") $ \t ->
|
||||
unless (any isFunctionLike path) $
|
||||
err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
|
||||
|
||||
prop_checkMultipleDeclaring1 = verify (checkMultipleDeclaring "local") "q() { local readonly var=1; }"
|
||||
prop_checkMultipleDeclaring2 = verifyNot (checkMultipleDeclaring "local") "q() { local var=1; }"
|
||||
prop_checkMultipleDeclaring3 = verify (checkMultipleDeclaring "readonly") "readonly local foo=5"
|
||||
prop_checkMultipleDeclaring4 = verify (checkMultipleDeclaring "export") "export readonly foo=5"
|
||||
prop_checkMultipleDeclaring5 = verifyNot (checkMultipleDeclaring "local") "f() { local -r foo=5; }"
|
||||
prop_checkMultipleDeclaring6 = verifyNot (checkMultipleDeclaring "declare") "declare -rx foo=5"
|
||||
prop_checkMultipleDeclaring7 = verifyNot (checkMultipleDeclaring "readonly") "readonly 'local' foo=5"
|
||||
checkMultipleDeclaring cmd = CommandCheck (Exactly cmd) (mapM_ check . arguments)
|
||||
where
|
||||
check t = sequence_ $ do
|
||||
lit <- getUnquotedLiteral t
|
||||
guard $ lit `elem` declaringCommands
|
||||
return $ err (getId $ getCommandTokenOrThis t) 2316 $
|
||||
"This applies " ++ cmd ++ " to the variable named " ++ lit ++
|
||||
", which is probably not what you want. Use a separate command or the appropriate `declare` options instead."
|
||||
|
||||
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
|
||||
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
|
||||
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
|
||||
@@ -987,20 +1021,20 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
|
||||
|
||||
check :: Id -> [String] -> Token -> Analysis
|
||||
check optId opts (T_CaseExpression id _ list) = do
|
||||
unless (Nothing `Map.member` handledMap) $ do
|
||||
mapM_ (warnUnhandled optId id) $ catMaybes $ Map.keys notHandled
|
||||
unless (Nothing `M.member` handledMap) $ do
|
||||
mapM_ (warnUnhandled optId id) $ catMaybes $ M.keys notHandled
|
||||
|
||||
unless (any (`Map.member` handledMap) [Just "*",Just "?"]) $
|
||||
unless (any (`M.member` handledMap) [Just "*",Just "?"]) $
|
||||
warn id 2220 "Invalid flags are not handled. Add a *) case."
|
||||
|
||||
mapM_ warnRedundant $ Map.toList notRequested
|
||||
mapM_ warnRedundant $ M.toList notRequested
|
||||
|
||||
where
|
||||
handledMap = Map.fromList (concatMap getHandledStrings list)
|
||||
requestedMap = Map.fromList $ map (\x -> (Just x, ())) opts
|
||||
handledMap = M.fromList (concatMap getHandledStrings list)
|
||||
requestedMap = M.fromList $ map (\x -> (Just x, ())) opts
|
||||
|
||||
notHandled = Map.difference requestedMap handledMap
|
||||
notRequested = Map.difference handledMap requestedMap
|
||||
notHandled = M.difference requestedMap handledMap
|
||||
notRequested = M.difference handledMap requestedMap
|
||||
|
||||
warnUnhandled optId caseId str =
|
||||
warn caseId 2213 $ "getopts specified -" ++ (e4m str) ++ ", but it's not handled by this 'case'."
|
||||
@@ -1253,6 +1287,7 @@ prop_checkArgComparison3 = verifyNot (checkArgComparison "declare") "declare a=b
|
||||
prop_checkArgComparison4 = verify (checkArgComparison "export") "export a +=b"
|
||||
prop_checkArgComparison7 = verifyNot (checkArgComparison "declare") "declare -a +i foo"
|
||||
prop_checkArgComparison8 = verify (checkArgComparison "let") "let x = 0"
|
||||
prop_checkArgComparison9 = verify (checkArgComparison "alias") "alias x =0"
|
||||
-- This mirrors checkSecondArgIsComparison but for arguments to local/readonly/declare/export
|
||||
checkArgComparison cmd = CommandCheck (Exactly cmd) wordsWithEqual
|
||||
where
|
||||
@@ -1353,10 +1388,10 @@ checkUnquotedEchoSpaces = CommandCheck (Basename "echo") check
|
||||
m <- asks tokenPositions
|
||||
redir <- getClosestCommandM t
|
||||
sequence_ $ do
|
||||
let positions = mapMaybe (\c -> Map.lookup (getId c) m) args
|
||||
let positions = mapMaybe (\c -> M.lookup (getId c) m) args
|
||||
let pairs = zip positions (drop 1 positions)
|
||||
(T_Redirecting _ redirTokens _) <- redir
|
||||
let redirPositions = mapMaybe (\c -> fst <$> Map.lookup (getId c) m) redirTokens
|
||||
let redirPositions = mapMaybe (\c -> fst <$> M.lookup (getId c) m) redirTokens
|
||||
guard $ any (hasSpacesBetween redirPositions) pairs
|
||||
return $ info (getId t) 2291 "Quote repeated spaces to avoid them collapsing into one."
|
||||
|
||||
@@ -1386,5 +1421,51 @@ checkEvalArray = CommandCheck (Exactly "eval") (mapM_ check . concatMap getWordP
|
||||
_ -> False
|
||||
|
||||
|
||||
prop_checkBackreferencingDeclaration1 = verify (checkBackreferencingDeclaration "declare") "declare x=1 y=foo$x"
|
||||
prop_checkBackreferencingDeclaration2 = verify (checkBackreferencingDeclaration "readonly") "readonly x=1 y=$((1+x))"
|
||||
prop_checkBackreferencingDeclaration3 = verify (checkBackreferencingDeclaration "local") "local x=1 y=$(echo $x)"
|
||||
prop_checkBackreferencingDeclaration4 = verify (checkBackreferencingDeclaration "local") "local x=1 y[$x]=z"
|
||||
prop_checkBackreferencingDeclaration5 = verify (checkBackreferencingDeclaration "declare") "declare x=var $x=1"
|
||||
prop_checkBackreferencingDeclaration6 = verify (checkBackreferencingDeclaration "declare") "declare x=var $x=1"
|
||||
prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration "declare") "declare x=var $k=$x"
|
||||
checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check
|
||||
where
|
||||
check t = foldM_ perArg M.empty $ arguments t
|
||||
|
||||
perArg leftArgs t =
|
||||
case t of
|
||||
T_Assignment id _ name idx t -> do
|
||||
warnIfBackreferencing leftArgs $ t:idx
|
||||
return $ M.insert name id leftArgs
|
||||
t -> do
|
||||
warnIfBackreferencing leftArgs [t]
|
||||
return leftArgs
|
||||
|
||||
warnIfBackreferencing backrefs l = do
|
||||
references <- findReferences l
|
||||
let reused = M.intersection backrefs references
|
||||
mapM msg $ M.toList reused
|
||||
|
||||
msg (name, id) = warn id 2318 $ "This assignment is used again in this '" ++ cmd ++ "', but won't have taken effect. Use two '" ++ cmd ++ "'s."
|
||||
|
||||
findReferences list = do
|
||||
cfga <- asks cfgAnalysis
|
||||
let graph = CF.graph cfga
|
||||
let nodesMap = CF.tokenToNodes cfga
|
||||
let nodes = S.unions $ map (\id -> M.findWithDefault S.empty id nodesMap) $ map getId $ list
|
||||
let labels = mapMaybe (G.lab graph) $ S.toList nodes
|
||||
let references = M.fromList $ concatMap refFromLabel labels
|
||||
return references
|
||||
|
||||
refFromLabel lab =
|
||||
case lab of
|
||||
CFApplyEffects effects -> mapMaybe refFromEffect effects
|
||||
_ -> []
|
||||
refFromEffect e =
|
||||
case e of
|
||||
IdTagged id (CFReadVariable name) -> return (name, id)
|
||||
_ -> Nothing
|
||||
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||
|
101
src/ShellCheck/Checks/ControlFlow.hs
Normal file
101
src/ShellCheck/Checks/ControlFlow.hs
Normal file
@@ -0,0 +1,101 @@
|
||||
{-
|
||||
Copyright 2022 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
-- Checks that run on the Control Flow Graph (as opposed to the AST)
|
||||
-- This is scaffolding for a work in progress.
|
||||
|
||||
module ShellCheck.Checks.ControlFlow (checker, optionalChecks, ShellCheck.Checks.ControlFlow.runTests) where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.CFG hiding (cfgAnalysis)
|
||||
import ShellCheck.CFGAnalysis
|
||||
import ShellCheck.AnalyzerLib
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.Reader
|
||||
import Data.Graph.Inductive.Graph
|
||||
import qualified Data.Map as M
|
||||
import qualified Data.Set as S
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
|
||||
import Test.QuickCheck.All (forAllProperties)
|
||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||
|
||||
|
||||
optionalChecks :: [CheckDescription]
|
||||
optionalChecks = []
|
||||
|
||||
-- A check that runs on the entire graph
|
||||
type ControlFlowCheck = Analysis
|
||||
-- A check invoked once per node, with its (pre,post) data
|
||||
type ControlFlowNodeCheck = LNode CFNode -> (ProgramState, ProgramState) -> Analysis
|
||||
-- A check invoked once per effect, with its node's (pre,post) data
|
||||
type ControlFlowEffectCheck = IdTagged CFEffect -> Node -> (ProgramState, ProgramState) -> Analysis
|
||||
|
||||
|
||||
checker :: AnalysisSpec -> Parameters -> Checker
|
||||
checker spec params = Checker {
|
||||
perScript = const $ sequence_ controlFlowChecks,
|
||||
perToken = const $ return ()
|
||||
}
|
||||
|
||||
controlFlowChecks :: [ControlFlowCheck]
|
||||
controlFlowChecks = [
|
||||
runNodeChecks controlFlowNodeChecks
|
||||
]
|
||||
|
||||
controlFlowNodeChecks :: [ControlFlowNodeCheck]
|
||||
controlFlowNodeChecks = [
|
||||
runEffectChecks controlFlowEffectChecks
|
||||
]
|
||||
|
||||
controlFlowEffectChecks :: [ControlFlowEffectCheck]
|
||||
controlFlowEffectChecks = [
|
||||
]
|
||||
|
||||
runNodeChecks :: [ControlFlowNodeCheck] -> ControlFlowCheck
|
||||
runNodeChecks perNode = do
|
||||
cfg <- asks cfgAnalysis
|
||||
runOnAll cfg
|
||||
where
|
||||
getData datas n@(node, label) = do
|
||||
(pre, post) <- M.lookup node datas
|
||||
return (n, (pre, post))
|
||||
|
||||
runOn :: (LNode CFNode, (ProgramState, ProgramState)) -> Analysis
|
||||
runOn (node, prepost) = mapM_ (\c -> c node prepost) perNode
|
||||
runOnAll cfg = mapM_ runOn $ mapMaybe (getData $ nodeToData cfg) $ labNodes (graph cfg)
|
||||
|
||||
runEffectChecks :: [ControlFlowEffectCheck] -> ControlFlowNodeCheck
|
||||
runEffectChecks list = checkNode
|
||||
where
|
||||
checkNode (node, label) prepost =
|
||||
case label of
|
||||
CFApplyEffects effects -> mapM_ (\effect -> mapM_ (\c -> c effect node prepost) list) effects
|
||||
_ -> return ()
|
||||
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -25,6 +25,7 @@ import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.AnalyzerLib
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Prelude
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Monad
|
||||
@@ -134,6 +135,8 @@ prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
|
||||
prop_checkBashisms50 = verify checkBashisms "#!/bin/sh\ncmd >& file"
|
||||
prop_checkBashisms51 = verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
||||
prop_checkBashisms52 = verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
||||
prop_checkBashisms52b = verifyNot checkBashisms "#!/bin/sh\ncmd >& $var"
|
||||
prop_checkBashisms52c = verify checkBashisms "#!/bin/sh\ncmd >& $dir/$var"
|
||||
prop_checkBashisms53 = verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
||||
prop_checkBashisms54 = verify checkBashisms "#!/bin/sh\nfoo+=bar"
|
||||
prop_checkBashisms55 = verify checkBashisms "#!/bin/sh\necho ${@%foo}"
|
||||
@@ -224,7 +227,8 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
warnMsg id 3018 $ filter (/= '|') op ++ " is"
|
||||
bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
|
||||
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id 3020 "&> is"
|
||||
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id 3021 ">& is"
|
||||
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) file)) =
|
||||
unless (all isDigit $ onlyLiteralString file) $ warnMsg id 3021 ">& filename (as opposed to >& fd) is"
|
||||
bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are"
|
||||
bashism (T_FdRedirect id num _)
|
||||
| all isDigit num && length num > 1 = warnMsg id 3023 "FDs outside 0-9 are"
|
||||
|
@@ -2,33 +2,54 @@ module ShellCheck.Data where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import Data.Version (showVersion)
|
||||
import Paths_ShellCheck (version)
|
||||
|
||||
|
||||
{-
|
||||
If you are here because you saw an error about Paths_ShellCheck in this file,
|
||||
simply comment out the import below and define the version as a constant string.
|
||||
|
||||
Instead of:
|
||||
|
||||
import Paths_ShellCheck (version)
|
||||
shellcheckVersion = showVersion version
|
||||
|
||||
Use:
|
||||
|
||||
-- import Paths_ShellCheck (version)
|
||||
shellcheckVersion = "kludge"
|
||||
|
||||
-}
|
||||
|
||||
import Paths_ShellCheck (version)
|
||||
shellcheckVersion = showVersion version -- VERSIONSTRING
|
||||
|
||||
|
||||
internalVariables = [
|
||||
-- Generic
|
||||
"", "_", "rest", "REST",
|
||||
|
||||
-- Bash
|
||||
"BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
|
||||
"BASH_ARGV", "BASH_CMDS", "BASH_COMMAND", "BASH_EXECUTION_STRING",
|
||||
"BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL",
|
||||
"BASH_VERSINFO", "BASH_VERSION", "COMP_CWORD", "COMP_KEY",
|
||||
"COMP_LINE", "COMP_POINT", "COMP_TYPE", "COMP_WORDBREAKS",
|
||||
"COMP_WORDS", "COPROC", "DIRSTACK", "EUID", "FUNCNAME", "GROUPS",
|
||||
"HISTCMD", "HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE",
|
||||
"OLDPWD", "OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD",
|
||||
"RANDOM", "READLINE_LINE", "READLINE_POINT", "REPLY", "SECONDS",
|
||||
"SHELLOPTS", "SHLVL", "UID", "BASH_ENV", "BASH_XTRACEFD", "CDPATH",
|
||||
"COLUMNS", "COMPREPLY", "EMACS", "ENV", "FCEDIT", "FIGNORE",
|
||||
"BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND",
|
||||
"BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_LOADABLES_PATH",
|
||||
"BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL", "BASH_VERSINFO",
|
||||
"BASH_VERSION", "COMP_CWORD", "COMP_KEY", "COMP_LINE", "COMP_POINT",
|
||||
"COMP_TYPE", "COMP_WORDBREAKS", "COMP_WORDS", "COPROC", "DIRSTACK",
|
||||
"EPOCHREALTIME", "EPOCHSECONDS", "EUID", "FUNCNAME", "GROUPS", "HISTCMD",
|
||||
"HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE", "OLDPWD",
|
||||
"OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD", "RANDOM",
|
||||
"READLINE_ARGUMENT", "READLINE_LINE", "READLINE_MARK", "READLINE_POINT",
|
||||
"REPLY", "SECONDS", "SHELLOPTS", "SHLVL", "SRANDOM", "UID", "BASH_COMPAT",
|
||||
"BASH_ENV", "BASH_XTRACEFD", "CDPATH", "CHILD_MAX", "COLUMNS",
|
||||
"COMPREPLY", "EMACS", "ENV", "EXECIGNORE", "FCEDIT", "FIGNORE",
|
||||
"FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",
|
||||
"HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",
|
||||
"IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
|
||||
"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",
|
||||
"IGNOREEOF", "INPUTRC", "INSIDE_EMACS", "LANG", "LC_ALL", "LC_COLLATE",
|
||||
"LC_CTYPE", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME",
|
||||
"LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH",
|
||||
"POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1",
|
||||
"PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR",
|
||||
"auto_resume", "histchars",
|
||||
|
||||
-- Other
|
||||
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
|
||||
@@ -43,13 +64,18 @@ internalVariables = [
|
||||
"flags_error", "flags_return"
|
||||
]
|
||||
|
||||
specialVariablesWithoutSpaces = [
|
||||
"$", "-", "?", "!", "#"
|
||||
specialIntegerVariables = [
|
||||
"$", "?", "!", "#"
|
||||
]
|
||||
|
||||
specialVariablesWithoutSpaces = "-" : specialIntegerVariables
|
||||
|
||||
variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
|
||||
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
|
||||
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
|
||||
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
|
||||
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID",
|
||||
"EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM",
|
||||
"READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS",
|
||||
"SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE",
|
||||
"HISTSIZE", "LINES"
|
||||
|
||||
-- shflags
|
||||
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
|
||||
@@ -95,10 +121,10 @@ commonCommands = [
|
||||
|
||||
nonReadingCommands = [
|
||||
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
|
||||
"cp", "du", "echo", "export", "false", "fg", "fuser", "getconf",
|
||||
"cp", "du", "echo", "export", "fg", "fuser", "getconf",
|
||||
"getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
|
||||
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
|
||||
"set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname"
|
||||
"set", "sleep", "touch", "trap", "ulimit", "unalias", "uname"
|
||||
]
|
||||
|
||||
sampleWords = [
|
||||
@@ -138,5 +164,6 @@ shellForExecutable name =
|
||||
_ -> Nothing
|
||||
|
||||
flagsForRead = "sreu:n:N:i:p:a:t:"
|
||||
flagsForMapfile = "d:n:O:s:u:C:c:t"
|
||||
|
||||
declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"]
|
||||
|
313
src/ShellCheck/Debug.hs
Normal file
313
src/ShellCheck/Debug.hs
Normal file
@@ -0,0 +1,313 @@
|
||||
{-
|
||||
|
||||
This file contains useful functions for debugging and developing ShellCheck.
|
||||
|
||||
To invoke them interactively, run:
|
||||
|
||||
cabal repl
|
||||
|
||||
At the ghci prompt, enter:
|
||||
|
||||
:load ShellCheck.Debug
|
||||
|
||||
You can now invoke the functions. Here are some examples:
|
||||
|
||||
shellcheckString "echo $1"
|
||||
stringToAst "(( x+1 ))"
|
||||
stringToCfg "if foo; then bar; else baz; fi"
|
||||
writeFile "/tmp/test.dot" $ stringToCfgViz "while foo; do bar; done"
|
||||
|
||||
The latter file can be rendered to png with GraphViz:
|
||||
|
||||
dot -Tpng /tmp/test.dot > /tmp/test.png
|
||||
|
||||
To run all unit tests in a module:
|
||||
|
||||
ShellCheck.Parser.runTests
|
||||
ShellCheck.Analytics.runTests
|
||||
|
||||
To run a specific test:
|
||||
|
||||
:load ShellCheck.Analytics
|
||||
prop_checkUuoc3
|
||||
|
||||
If you make code changes, reload in seconds at any time with:
|
||||
|
||||
:r
|
||||
|
||||
===========================================================================
|
||||
|
||||
Crash course in printf debugging in Haskell:
|
||||
|
||||
import Debug.Trace
|
||||
|
||||
greet 0 = return ()
|
||||
-- Print when a function is invoked
|
||||
greet n | trace ("calling greet " ++ show n) False = undefined
|
||||
greet n = do
|
||||
putStrLn "Enter name"
|
||||
name <- getLine
|
||||
-- Print at some point in any monadic function
|
||||
traceM $ "user entered " ++ name
|
||||
putStrLn $ "Hello " ++ name
|
||||
-- Print a value before passing it on
|
||||
greet $ traceShowId (n - 1)
|
||||
|
||||
|
||||
===========================================================================
|
||||
|
||||
If you want to invoke `ghci` directly, such as on `shellcheck.hs`, to
|
||||
debug all of ShellCheck including I/O, you may see an error like this:
|
||||
|
||||
src/ShellCheck/Data.hs:5:1: error:
|
||||
Could not load module ‘Paths_ShellCheck’
|
||||
it is a hidden module in the package ‘ShellCheck-0.8.0’
|
||||
|
||||
This can easily be circumvented by running `./setgitversion` or manually
|
||||
editing src/ShellCheck/Data.hs to replace the auto-deduced version number
|
||||
with a constant string as indicated.
|
||||
|
||||
Afterwards, you can run the ShellCheck tool, as if from the shell, with:
|
||||
|
||||
$ ghci shellcheck.hs
|
||||
ghci> runMain ["-x", "file.sh"]
|
||||
|
||||
-}
|
||||
|
||||
module ShellCheck.Debug () where
|
||||
|
||||
import ShellCheck.Analyzer
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.CFG
|
||||
import ShellCheck.Checker
|
||||
import ShellCheck.CFGAnalysis as CF
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Prelude
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.Identity
|
||||
import Control.Monad.RWS
|
||||
import Control.Monad.Writer
|
||||
import Data.Graph.Inductive.Graph as G
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import qualified Data.Map as M
|
||||
import qualified Data.Set as S
|
||||
|
||||
|
||||
-- Run all of ShellCheck (minus output formatters)
|
||||
shellcheckString :: String -> CheckResult
|
||||
shellcheckString scriptString =
|
||||
runIdentity $ checkScript dummySystemInterface checkSpec
|
||||
where
|
||||
checkSpec :: CheckSpec
|
||||
checkSpec = emptyCheckSpec {
|
||||
csScript = scriptString
|
||||
}
|
||||
|
||||
dummySystemInterface :: SystemInterface Identity
|
||||
dummySystemInterface = mockedSystemInterface [
|
||||
-- A tiny, fake filesystem for sourced files
|
||||
("lib/mylib1.sh", "foo=$(cat $1 | wc -l)"),
|
||||
("lib/mylib2.sh", "bar=42")
|
||||
]
|
||||
|
||||
-- Parameters used when generating Control Flow Graphs
|
||||
cfgParams :: CFGParameters
|
||||
cfgParams = CFGParameters {
|
||||
cfLastpipe = False,
|
||||
cfPipefail = False
|
||||
}
|
||||
|
||||
-- An example script to play with
|
||||
exampleScript :: String
|
||||
exampleScript = unlines [
|
||||
"#!/bin/sh",
|
||||
"count=0",
|
||||
"for file in *",
|
||||
"do",
|
||||
" (( count++ ))",
|
||||
"done",
|
||||
"echo $count"
|
||||
]
|
||||
|
||||
-- Parse the script string into ShellCheck's ParseResult
|
||||
parseScriptString :: String -> ParseResult
|
||||
parseScriptString scriptString =
|
||||
runIdentity $ parseScript dummySystemInterface parseSpec
|
||||
where
|
||||
parseSpec :: ParseSpec
|
||||
parseSpec = newParseSpec {
|
||||
psFilename = "myscript",
|
||||
psScript = scriptString
|
||||
}
|
||||
|
||||
|
||||
-- Parse the script string into an Abstract Syntax Tree
|
||||
stringToAst :: String -> Token
|
||||
stringToAst scriptString =
|
||||
case maybeRoot of
|
||||
Just root -> root
|
||||
Nothing -> error $ "Script failed to parse: " ++ show parserWarnings
|
||||
where
|
||||
parseResult :: ParseResult
|
||||
parseResult = parseScriptString scriptString
|
||||
|
||||
maybeRoot :: Maybe Token
|
||||
maybeRoot = prRoot parseResult
|
||||
|
||||
parserWarnings :: [PositionedComment]
|
||||
parserWarnings = prComments parseResult
|
||||
|
||||
|
||||
astToCfgResult :: Token -> CFGResult
|
||||
astToCfgResult = buildGraph cfgParams
|
||||
|
||||
astToDfa :: Token -> CFGAnalysis
|
||||
astToDfa = analyzeControlFlow cfgParams
|
||||
|
||||
astToCfg :: Token -> CFGraph
|
||||
astToCfg = cfGraph . astToCfgResult
|
||||
|
||||
stringToCfg :: String -> CFGraph
|
||||
stringToCfg = astToCfg . stringToAst
|
||||
|
||||
stringToDfa :: String -> CFGAnalysis
|
||||
stringToDfa = astToDfa . stringToAst
|
||||
|
||||
cfgToGraphViz :: CFGraph -> String
|
||||
cfgToGraphViz = cfgToGraphVizWith show
|
||||
|
||||
stringToCfgViz :: String -> String
|
||||
stringToCfgViz = cfgToGraphViz . stringToCfg
|
||||
|
||||
stringToDfaViz :: String -> String
|
||||
stringToDfaViz = dfaToGraphViz . stringToDfa
|
||||
|
||||
-- Dump a Control Flow Graph as GraphViz with extended information
|
||||
stringToDetailedCfgViz :: String -> String
|
||||
stringToDetailedCfgViz scriptString = cfgToGraphVizWith nodeLabel graph
|
||||
where
|
||||
ast :: Token
|
||||
ast = stringToAst scriptString
|
||||
|
||||
cfgResult :: CFGResult
|
||||
cfgResult = astToCfgResult ast
|
||||
|
||||
graph :: CFGraph
|
||||
graph = cfGraph cfgResult
|
||||
|
||||
idToToken :: M.Map Id Token
|
||||
idToToken = M.fromList $ execWriter $ doAnalysis (\c -> tell [(getId c, c)]) ast
|
||||
|
||||
idToNode :: M.Map Id (Node, Node)
|
||||
idToNode = cfIdToRange cfgResult
|
||||
|
||||
nodeToStartIds :: M.Map Node (S.Set Id)
|
||||
nodeToStartIds =
|
||||
M.fromListWith S.union $
|
||||
map (\(id, (start, _)) -> (start, S.singleton id)) $
|
||||
M.toList idToNode
|
||||
|
||||
nodeToEndIds :: M.Map Node (S.Set Id)
|
||||
nodeToEndIds =
|
||||
M.fromListWith S.union $
|
||||
map (\(id, (_, end)) -> (end, S.singleton id)) $
|
||||
M.toList idToNode
|
||||
|
||||
formatId :: Id -> String
|
||||
formatId id = fromMaybe ("Unknown " ++ show id) $ do
|
||||
(OuterToken _ token) <- M.lookup id idToToken
|
||||
firstWord <- words (show token) !!! 0
|
||||
-- Strip off "Inner_"
|
||||
(_ : tokenName) <- return $ dropWhile (/= '_') firstWord
|
||||
return $ tokenName ++ " " ++ show id
|
||||
|
||||
formatGroup :: S.Set Id -> String
|
||||
formatGroup set = intercalate ", " $ map formatId $ S.toList set
|
||||
|
||||
nodeLabel (node, label) = unlines [
|
||||
show node ++ ". " ++ show label,
|
||||
"Begin: " ++ formatGroup (M.findWithDefault S.empty node nodeToStartIds),
|
||||
"End: " ++ formatGroup (M.findWithDefault S.empty node nodeToEndIds)
|
||||
]
|
||||
|
||||
|
||||
-- Dump a Control Flow Graph with Data Flow Analysis as GraphViz
|
||||
dfaToGraphViz :: CF.CFGAnalysis -> String
|
||||
dfaToGraphViz analysis = cfgToGraphVizWith label $ CF.graph analysis
|
||||
where
|
||||
label (node, label) =
|
||||
let
|
||||
desc = show node ++ ". " ++ show label
|
||||
in
|
||||
fromMaybe ("No DFA available\n\n" ++ desc) $ do
|
||||
(pre, post) <- M.lookup node $ CF.nodeToData analysis
|
||||
return $ unlines [
|
||||
"Precondition: " ++ show pre,
|
||||
"",
|
||||
desc,
|
||||
"",
|
||||
"Postcondition: " ++ show post
|
||||
]
|
||||
|
||||
|
||||
-- Dump an Control Flow Graph to GraphViz with a given node formatter
|
||||
cfgToGraphVizWith :: (LNode CFNode -> String) -> CFGraph -> String
|
||||
cfgToGraphVizWith nodeLabel graph = concat [
|
||||
"digraph {\n",
|
||||
concatMap dumpNode (labNodes graph),
|
||||
concatMap dumpLink (labEdges graph),
|
||||
tagVizEntries graph,
|
||||
"}\n"
|
||||
]
|
||||
where
|
||||
dumpNode l@(node, label) = show node ++ " [label=" ++ quoteViz (nodeLabel l) ++ "]\n"
|
||||
dumpLink (from, to, typ) = show from ++ " -> " ++ show to ++ " [style=" ++ quoteViz (edgeStyle typ) ++ "]\n"
|
||||
edgeStyle CFEFlow = "solid"
|
||||
edgeStyle CFEExit = "bold"
|
||||
edgeStyle CFEFalseFlow = "dotted"
|
||||
|
||||
quoteViz str = "\"" ++ escapeViz str ++ "\""
|
||||
escapeViz [] = []
|
||||
escapeViz (c:rest) =
|
||||
case c of
|
||||
'\"' -> '\\' : '\"' : escapeViz rest
|
||||
'\n' -> '\\' : 'l' : escapeViz rest
|
||||
'\\' -> '\\' : '\\' : escapeViz rest
|
||||
_ -> c : escapeViz rest
|
||||
|
||||
|
||||
-- Dump an Abstract Syntax Tree (or branch thereof) to GraphViz format
|
||||
astToGraphViz :: Token -> String
|
||||
astToGraphViz token = concat [
|
||||
"digraph {\n",
|
||||
formatTree token,
|
||||
"}\n"
|
||||
]
|
||||
where
|
||||
formatTree :: Token -> String
|
||||
formatTree t = snd $ execRWS (doStackAnalysis push pop t) () []
|
||||
|
||||
push :: Token -> RWS () String [Int] ()
|
||||
push (OuterToken (Id n) inner) = do
|
||||
stack <- get
|
||||
put (n : stack)
|
||||
case stack of
|
||||
[] -> return ()
|
||||
(top:_) -> tell $ show top ++ " -> " ++ show n ++ "\n"
|
||||
tell $ show n ++ " [label=" ++ quoteViz (show n ++ ": " ++ take 32 (show inner)) ++ "]\n"
|
||||
|
||||
pop :: Token -> RWS () String [Int] ()
|
||||
pop _ = modify tail
|
||||
|
||||
|
||||
-- For each entry point, set the rank so that they'll align in the graph
|
||||
tagVizEntries :: CFGraph -> String
|
||||
tagVizEntries graph = "{ rank=same " ++ rank ++ " }"
|
||||
where
|
||||
entries = mapMaybe find $ labNodes graph
|
||||
find (node, CFEntryPoint name) = return (node, name)
|
||||
find _ = Nothing
|
||||
rank = unwords $ map (\(c, _) -> show c) entries
|
@@ -22,6 +22,8 @@
|
||||
module ShellCheck.Fixer (applyFix, removeTabStops, mapPositions, Ranged(..), runTests) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Prelude
|
||||
import Control.Monad
|
||||
import Control.Monad.State
|
||||
import Data.Array
|
||||
import Data.List
|
||||
@@ -35,7 +37,7 @@ class Ranged a where
|
||||
end :: a -> Position
|
||||
overlap :: a -> a -> Bool
|
||||
overlap x y =
|
||||
(yStart >= xStart && yStart < xEnd) || (yStart < xStart && yEnd > xStart)
|
||||
xEnd > yStart && yEnd > xStart
|
||||
where
|
||||
yStart = start y
|
||||
yEnd = end y
|
||||
@@ -86,6 +88,7 @@ instance Ranged Replacement where
|
||||
instance Monoid Fix where
|
||||
mempty = newFix
|
||||
mappend = (<>)
|
||||
mconcat = foldl mappend mempty -- fold left to right since <> discards right on overlap
|
||||
|
||||
instance Semigroup Fix where
|
||||
f1 <> f2 =
|
||||
@@ -228,7 +231,7 @@ applyReplacement2 rep string = do
|
||||
|
||||
let (l1, l2) = tmap posLine originalPos in
|
||||
when (l1 /= 1 || l2 /= 1) $
|
||||
error "ShellCheck internal error, please report: bad cross-line fix"
|
||||
error $ pleaseReport "bad cross-line fix"
|
||||
|
||||
let replacer = repString rep
|
||||
let shift = (length replacer) - (oldEnd - oldStart)
|
||||
|
@@ -203,10 +203,9 @@ formatDoc color (DiffDoc name lf regions) =
|
||||
buildFixMap :: [Fix] -> M.Map String Fix
|
||||
buildFixMap fixes = perFile
|
||||
where
|
||||
splitFixes = concatMap splitFixByFile fixes
|
||||
splitFixes = splitFixByFile $ mconcat fixes
|
||||
perFile = groupByMap (posFile . repStartPos . head . fixReplacements) splitFixes
|
||||
|
||||
-- There are currently no multi-file fixes, but let's handle it anyways
|
||||
splitFixByFile :: Fix -> [Fix]
|
||||
splitFixByFile fix = map makeFix $ groupBy sameFile (fixReplacements fix)
|
||||
where
|
||||
|
@@ -23,6 +23,7 @@ module ShellCheck.Formatter.JSON (format) where
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Control.DeepSeq
|
||||
import Data.Aeson
|
||||
import Data.IORef
|
||||
import Data.Monoid
|
||||
@@ -103,7 +104,7 @@ collectResult ref cr sys = mapM_ f groups
|
||||
comments = crComments cr
|
||||
groups = groupWith sourceFile comments
|
||||
f :: [PositionedComment] -> IO ()
|
||||
f group = modifyIORef ref (\x -> comments ++ x)
|
||||
f group = deepseq comments $ modifyIORef ref (\x -> comments ++ x)
|
||||
|
||||
finish ref = do
|
||||
list <- readIORef ref
|
||||
|
@@ -23,6 +23,7 @@ module ShellCheck.Formatter.JSON1 (format) where
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Control.DeepSeq
|
||||
import Data.Aeson
|
||||
import Data.IORef
|
||||
import Data.Monoid
|
||||
@@ -120,7 +121,7 @@ collectResult ref cr sys = mapM_ f groups
|
||||
result <- siReadFile sys (Just True) filename
|
||||
let contents = either (const "") id result
|
||||
let comments' = makeNonVirtual comments contents
|
||||
modifyIORef ref (\x -> comments' ++ x)
|
||||
deepseq comments' $ modifyIORef ref (\x -> comments' ++ x)
|
||||
|
||||
finish ref = do
|
||||
list <- readIORef ref
|
||||
|
@@ -23,6 +23,7 @@ import ShellCheck.Fixer
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Control.DeepSeq
|
||||
import Control.Monad
|
||||
import Data.Array
|
||||
import Data.Foldable
|
||||
@@ -88,7 +89,7 @@ rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err)
|
||||
appendComments errRef comments max = do
|
||||
previous <- readIORef errRef
|
||||
let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments
|
||||
writeIORef errRef . take max . nubBy equal . sort $ previous ++ current
|
||||
writeIORef errRef $! force . take max . nubBy equal . sort $ previous ++ current
|
||||
where
|
||||
fst3 (x,_,_) = x
|
||||
equal x y = fst3 x == fst3 y
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{-
|
||||
Copyright 2012-2021 Vidar Holen
|
||||
Copyright 2012-2022 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
@@ -27,6 +27,7 @@ import ShellCheck.AST
|
||||
import ShellCheck.ASTLib hiding (runTests)
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Prelude
|
||||
|
||||
import Control.Applicative ((<*), (*>))
|
||||
import Control.Monad
|
||||
@@ -37,7 +38,6 @@ import Data.Functor
|
||||
import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub, find)
|
||||
import Data.Maybe
|
||||
import Data.Monoid
|
||||
import Debug.Trace -- STRIP
|
||||
import GHC.Exts (sortWith)
|
||||
import Prelude hiding (readList)
|
||||
import System.IO
|
||||
@@ -46,7 +46,7 @@ import Text.Parsec.Error
|
||||
import Text.Parsec.Pos
|
||||
import qualified Control.Monad.Reader as Mr
|
||||
import qualified Control.Monad.State as Ms
|
||||
import qualified Data.Map as Map
|
||||
import qualified Data.Map.Strict as Map
|
||||
|
||||
import Test.QuickCheck.All (quickCheckAll)
|
||||
|
||||
@@ -210,7 +210,7 @@ getNextIdSpanningTokenList list =
|
||||
-- Get the span covered by an id
|
||||
getSpanForId :: Monad m => Id -> SCParser m (SourcePos, SourcePos)
|
||||
getSpanForId id =
|
||||
Map.findWithDefault (error "Internal error: no position for id. Please report!") id <$>
|
||||
Map.findWithDefault (error $ pleaseReport "no parser span for id") id <$>
|
||||
getMap
|
||||
|
||||
-- Create a new id with the same span as an existing one
|
||||
@@ -457,8 +457,8 @@ called s p = do
|
||||
pos <- getPosition
|
||||
withContext (ContextName pos s) p
|
||||
|
||||
withAnnotations anns =
|
||||
withContext (ContextAnnotation anns)
|
||||
withAnnotations anns p =
|
||||
if null anns then p else withContext (ContextAnnotation anns) p
|
||||
|
||||
readConditionContents single =
|
||||
readCondContents `attempting` lookAhead (do
|
||||
@@ -556,7 +556,7 @@ readConditionContents single =
|
||||
notFollowedBy2 (try (spacing >> string "]"))
|
||||
x <- readNormalWord
|
||||
pos <- getPosition
|
||||
when (endedWith "]" x && notArrayIndex x) $ do
|
||||
when (notArrayIndex x && endedWith "]" x && not (x `containsLiteral` "[")) $ do
|
||||
parseProblemAt pos ErrorC 1020 $
|
||||
"You need a space before the " ++ (if single then "]" else "]]") ++ "."
|
||||
fail "Missing space before ]"
|
||||
@@ -572,6 +572,7 @@ readConditionContents single =
|
||||
endedWith _ _ = False
|
||||
notArrayIndex (T_NormalWord id s@(_:T_Literal _ t:_)) = t /= "["
|
||||
notArrayIndex _ = True
|
||||
containsLiteral x s = s `isInfixOf` onlyLiteralString x
|
||||
|
||||
readCondAndOp = readAndOrOp TC_And "&&" False <|> readAndOrOp TC_And "-a" True
|
||||
|
||||
@@ -819,11 +820,13 @@ readArithmeticContents =
|
||||
return $ TA_Expansion id pieces
|
||||
|
||||
readGroup = do
|
||||
start <- startSpan
|
||||
char '('
|
||||
s <- readSequence
|
||||
char ')'
|
||||
id <- endSpan start
|
||||
spacing
|
||||
return s
|
||||
return $ TA_Parentesis id s
|
||||
|
||||
readArithTerm = readGroup <|> readVariable <|> readExpansion
|
||||
|
||||
@@ -941,6 +944,9 @@ prop_readCondition23 = isOk readCondition "[[ -v arr[$var] ]]"
|
||||
prop_readCondition25 = isOk readCondition "[[ lex.yy.c -ot program.l ]]"
|
||||
prop_readCondition26 = isOk readScript "[[ foo ]]\\\n && bar"
|
||||
prop_readCondition27 = not $ isOk readConditionCommand "[[ x ]] foo"
|
||||
prop_readCondition28 = isOk readCondition "[[ x = [\"$1\"] ]]"
|
||||
prop_readCondition29 = isOk readCondition "[[ x = [*] ]]"
|
||||
|
||||
readCondition = called "test expression" $ do
|
||||
opos <- getPosition
|
||||
start <- startSpan
|
||||
@@ -985,6 +991,10 @@ prop_readAnnotation5 = isOk readAnnotation "# shellcheck disable=SC2002 # All ca
|
||||
prop_readAnnotation6 = isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\n"
|
||||
prop_readAnnotation7 = isOk readAnnotation "# shellcheck disable=SC1000,SC2000-SC3000,SC1001\n"
|
||||
prop_readAnnotation8 = isOk readAnnotation "# shellcheck disable=all\n"
|
||||
prop_readAnnotation9 = isOk readAnnotation "# shellcheck source='foo bar' source-path=\"baz etc\"\n"
|
||||
prop_readAnnotation10 = isOk readAnnotation "# shellcheck disable='SC1234,SC2345' enable=\"foo\" shell='bash'\n"
|
||||
prop_readAnnotation11 = isOk (readAnnotationWithoutPrefix False) "external-sources='true'"
|
||||
|
||||
readAnnotation = called "shellcheck directive" $ do
|
||||
try readAnnotationPrefix
|
||||
many1 linewhitespace
|
||||
@@ -1000,12 +1010,19 @@ readAnnotationWithoutPrefix sandboxed = do
|
||||
many linewhitespace
|
||||
return $ concat values
|
||||
where
|
||||
plainOrQuoted p = quoted p <|> p
|
||||
quoted p = do
|
||||
c <- oneOf "'\""
|
||||
start <- getPosition
|
||||
str <- many1 $ noneOf (c:"\n")
|
||||
char c <|> fail "Missing terminating quote for directive."
|
||||
subParse start p str
|
||||
readKey = do
|
||||
keyPos <- getPosition
|
||||
key <- many1 (letter <|> char '-')
|
||||
char '=' <|> fail "Expected '=' after directive key"
|
||||
annotations <- case key of
|
||||
"disable" -> readElement `sepBy` char ','
|
||||
"disable" -> plainOrQuoted $ readElement `sepBy` char ','
|
||||
where
|
||||
readElement = readRange <|> readAll
|
||||
readAll = do
|
||||
@@ -1020,21 +1037,21 @@ readAnnotationWithoutPrefix sandboxed = do
|
||||
int <- many1 digit
|
||||
return $ read int
|
||||
|
||||
"enable" -> readName `sepBy` char ','
|
||||
"enable" -> plainOrQuoted $ readName `sepBy` char ','
|
||||
where
|
||||
readName = EnableComment <$> many1 (letter <|> char '-')
|
||||
|
||||
"source" -> do
|
||||
filename <- many1 $ noneOf " \n"
|
||||
filename <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n")
|
||||
return [SourceOverride filename]
|
||||
|
||||
"source-path" -> do
|
||||
dirname <- many1 $ noneOf " \n"
|
||||
dirname <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n")
|
||||
return [SourcePath dirname]
|
||||
|
||||
"shell" -> do
|
||||
pos <- getPosition
|
||||
shell <- many1 $ noneOf " \n"
|
||||
shell <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n")
|
||||
when (isNothing $ shellForExecutable shell) $
|
||||
parseNoteAt pos ErrorC 1103
|
||||
"This shell type is unknown. Use e.g. sh or bash."
|
||||
@@ -1042,7 +1059,7 @@ readAnnotationWithoutPrefix sandboxed = do
|
||||
|
||||
"external-sources" -> do
|
||||
pos <- getPosition
|
||||
value <- many1 letter
|
||||
value <- plainOrQuoted $ many1 letter
|
||||
case value of
|
||||
"true" ->
|
||||
if sandboxed
|
||||
@@ -1914,7 +1931,7 @@ readPendingHereDocs = do
|
||||
-- The end token is just a prefix
|
||||
skipLine
|
||||
| hasTrailer ->
|
||||
error "ShellCheck bug, please report (here doc trailer)."
|
||||
error $ pleaseReport "unexpected heredoc trailer"
|
||||
|
||||
-- The following cases assume no trailing text:
|
||||
| dashed == Undashed && (not $ null leadingSpace) -> do
|
||||
@@ -2095,6 +2112,7 @@ prop_readSimpleCommand11 = isOk readSimpleCommand "/\\* foo"
|
||||
prop_readSimpleCommand12 = isWarning readSimpleCommand "elsif foo"
|
||||
prop_readSimpleCommand13 = isWarning readSimpleCommand "ElseIf foo"
|
||||
prop_readSimpleCommand14 = isWarning readSimpleCommand "elseif[$i==2]"
|
||||
prop_readSimpleCommand15 = isWarning readSimpleCommand "trap 'foo\"bar' INT"
|
||||
readSimpleCommand = called "simple command" $ do
|
||||
prefix <- option [] readCmdPrefix
|
||||
skipAnnotationAndWarn
|
||||
@@ -2124,9 +2142,12 @@ readSimpleCommand = called "simple command" $ do
|
||||
id2 <- getNewIdFor id1
|
||||
|
||||
let result = makeSimpleCommand id1 id2 prefix [cmd] suffix
|
||||
if isCommand ["source", "."] cmd
|
||||
then readSource result
|
||||
else return result
|
||||
case () of
|
||||
_ | isCommand ["source", "."] cmd -> readSource result
|
||||
_ | isCommand ["trap"] cmd -> do
|
||||
syntaxCheckTrap result
|
||||
return result
|
||||
_ -> return result
|
||||
where
|
||||
isCommand strings (T_NormalWord _ [T_Literal _ s]) = s `elem` strings
|
||||
isCommand _ _ = False
|
||||
@@ -2146,6 +2167,17 @@ readSimpleCommand = called "simple command" $ do
|
||||
parseProblemAtId (getId cmd) ErrorC 1131 "Use 'elif' to start another branch."
|
||||
_ -> return ()
|
||||
|
||||
syntaxCheckTrap cmd =
|
||||
case cmd of
|
||||
(T_Redirecting _ _ (T_SimpleCommand _ _ (cmd:arg:_))) -> checkArg arg (getLiteralString arg)
|
||||
_ -> return ()
|
||||
where
|
||||
checkArg _ Nothing = return ()
|
||||
checkArg arg (Just ('-':_)) = return ()
|
||||
checkArg arg (Just str) = do
|
||||
(start,end) <- getSpanForId (getId arg)
|
||||
subParse start (tryWithErrors (readCompoundListOrEmpty >> verifyEof) <|> return ()) str
|
||||
|
||||
commentWarning id =
|
||||
parseProblemAtId id ErrorC 1127 "Was this intended as a comment? Use # in sh."
|
||||
|
||||
@@ -2282,7 +2314,7 @@ readAndOr = do
|
||||
parseProblemAt apos ErrorC 1123 "ShellCheck directives are only valid in front of complete compound commands, like 'if', not e.g. individual 'elif' branches."
|
||||
|
||||
andOr <- withAnnotations annotations $
|
||||
chainr1 readPipeline $ do
|
||||
chainl1 readPipeline $ do
|
||||
op <- g_AND_IF <|> g_OR_IF
|
||||
readLineBreak
|
||||
return $ case op of T_AND_IF id -> T_AndIf id
|
||||
@@ -2483,16 +2515,29 @@ readBraceGroup = called "brace group" $ do
|
||||
spacing
|
||||
return $ T_BraceGroup id list
|
||||
|
||||
prop_readBatsTest = isOk readBatsTest "@test 'can parse' {\n true\n}"
|
||||
prop_readBatsTest1 = isOk readBatsTest "@test 'can parse' {\n true\n}"
|
||||
prop_readBatsTest2 = isOk readBatsTest "@test random text !(@*$Y&! {\n true\n}"
|
||||
prop_readBatsTest3 = isOk readBatsTest "@test foo { bar { baz {\n true\n}"
|
||||
prop_readBatsTest4 = isNotOk readBatsTest "@test foo \n{\n true\n}"
|
||||
readBatsTest = called "bats @test" $ do
|
||||
start <- startSpan
|
||||
try $ string "@test "
|
||||
spacing
|
||||
name <- readNormalWord
|
||||
name <- readBatsName
|
||||
spacing
|
||||
test <- readBraceGroup
|
||||
id <- endSpan start
|
||||
return $ T_BatsTest id name test
|
||||
where
|
||||
readBatsName = do
|
||||
line <- try . lookAhead $ many1 $ noneOf "\n"
|
||||
let name = reverse $ f $ reverse line
|
||||
string name
|
||||
|
||||
-- We want everything before the last " {" in a string, so we find everything after "{ " in its reverse
|
||||
f ('{':' ':rest) = dropWhile isSpace rest
|
||||
f (a:rest) = f rest
|
||||
f [] = ""
|
||||
|
||||
prop_readWhileClause = isOk readWhileClause "while [[ -e foo ]]; do sleep 1; done"
|
||||
readWhileClause = called "while loop" $ do
|
||||
@@ -2521,7 +2566,7 @@ readDoGroup kwId = do
|
||||
parseProblem ErrorC 1058 "Expected 'do'."
|
||||
return "Expected 'do'"
|
||||
|
||||
acceptButWarn g_Semi ErrorC 1059 "No semicolons directly after 'do'."
|
||||
acceptButWarn g_Semi ErrorC 1059 "Semicolon is not allowed directly after 'do'. You can just delete it."
|
||||
allspacing
|
||||
|
||||
optional (do
|
||||
@@ -3240,23 +3285,30 @@ prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world"
|
||||
prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=("
|
||||
prop_readScript5 = isOk readScript "#!/bin/bash\n#This is an empty script\n\n"
|
||||
prop_readScript6 = isOk readScript "#!/usr/bin/env -S X=FOO bash\n#This is an empty script\n\n"
|
||||
prop_readScript7 = isOk readScript "#!/bin/zsh\n# shellcheck disable=SC1071\nfor f (a b); echo $f\n"
|
||||
readScriptFile sourced = do
|
||||
start <- startSpan
|
||||
pos <- getPosition
|
||||
optional $ do
|
||||
readUtf8Bom
|
||||
parseProblem ErrorC 1082
|
||||
"This file has a UTF-8 BOM. Remove it with: LC_CTYPE=C sed '1s/^...//' < yourscript ."
|
||||
shebang <- readShebang <|> readEmptyLiteral
|
||||
let (T_Literal _ shebangString) = shebang
|
||||
allspacing
|
||||
annotationStart <- startSpan
|
||||
fileAnnotations <- readAnnotations
|
||||
rcAnnotations <- if sourced
|
||||
then return []
|
||||
else do
|
||||
filename <- Mr.asks currentFilename
|
||||
readConfigFile filename
|
||||
|
||||
-- Put the rc annotations on the stack so that one can ignore e.g. SC1084 in .shellcheckrc
|
||||
withAnnotations rcAnnotations $ do
|
||||
hasBom <- wasIncluded readUtf8Bom
|
||||
shebang <- readShebang <|> readEmptyLiteral
|
||||
let (T_Literal _ shebangString) = shebang
|
||||
allspacing
|
||||
annotationStart <- startSpan
|
||||
fileAnnotations <- readAnnotations
|
||||
|
||||
-- Similarly put the filewide annotations on the stack to allow earlier suppression
|
||||
withAnnotations fileAnnotations $ do
|
||||
when (hasBom) $
|
||||
parseProblemAt pos ErrorC 1082
|
||||
"This file has a UTF-8 BOM. Remove it with: LC_CTYPE=C sed '1s/^...//' < yourscript ."
|
||||
let annotations = fileAnnotations ++ rcAnnotations
|
||||
annotationId <- endSpan annotationStart
|
||||
let shellAnnotationSpecified =
|
||||
@@ -3268,7 +3320,7 @@ readScriptFile sourced = do
|
||||
verifyShebang pos (executableFromShebang shebangString)
|
||||
if ignoreShebang || isValidShell (executableFromShebang shebangString) /= Just False
|
||||
then do
|
||||
commands <- withAnnotations annotations readCompoundListOrEmpty
|
||||
commands <- readCompoundListOrEmpty
|
||||
id <- endSpan start
|
||||
verifyEof
|
||||
let script = T_Annotation annotationId annotations $
|
||||
@@ -3370,16 +3422,6 @@ parsesCleanly parser string = runIdentity $ do
|
||||
return $ Just . null $ parseNotes userState ++ parseProblems systemState
|
||||
(Left _, _) -> return Nothing
|
||||
|
||||
-- For printf debugging: print the value of an expression
|
||||
-- Example: return $ dump $ T_Literal id [c]
|
||||
dump :: Show a => a -> a -- STRIP
|
||||
dump x = trace (show x) x -- STRIP
|
||||
|
||||
-- Like above, but print a specific expression:
|
||||
-- Example: return $ dumps ("Returning: " ++ [c]) $ T_Literal id [c]
|
||||
dumps :: Show x => x -> a -> a -- STRIP
|
||||
dumps t = trace (show t) -- STRIP
|
||||
|
||||
parseWithNotes parser = do
|
||||
item <- parser
|
||||
state <- getState
|
||||
@@ -3459,9 +3501,9 @@ notesForContext list = zipWith ($) [first, second] $ filter isName list
|
||||
|
||||
-- Go over all T_UnparsedIndex and reparse them as either arithmetic or text
|
||||
-- depending on declare -A statements.
|
||||
reparseIndices root =
|
||||
analyze blank blank f root
|
||||
reparseIndices root = process root
|
||||
where
|
||||
process = analyze blank blank f
|
||||
associative = getAssociativeArrays root
|
||||
isAssociative s = s `elem` associative
|
||||
f (T_Assignment id mode name indices value) = do
|
||||
@@ -3486,8 +3528,9 @@ reparseIndices root =
|
||||
|
||||
fixAssignmentIndex name word =
|
||||
case word of
|
||||
T_UnparsedIndex id pos src ->
|
||||
parsed name pos src
|
||||
T_UnparsedIndex id pos src -> do
|
||||
idx <- parsed name pos src
|
||||
process idx -- Recursively parse for cases like x[y[z=1]]=1
|
||||
_ -> return word
|
||||
|
||||
parsed name pos src =
|
||||
|
51
src/ShellCheck/Prelude.hs
Normal file
51
src/ShellCheck/Prelude.hs
Normal file
@@ -0,0 +1,51 @@
|
||||
{-
|
||||
Copyright 2022 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
|
||||
-- Generic basic utility functions
|
||||
module ShellCheck.Prelude where
|
||||
|
||||
import Data.Semigroup
|
||||
|
||||
|
||||
-- Get element 0 or a default. Like `head` but safe.
|
||||
headOrDefault _ (a:_) = a
|
||||
headOrDefault def _ = def
|
||||
|
||||
-- Get the last element or a default. Like `last` but safe.
|
||||
lastOrDefault def [] = def
|
||||
lastOrDefault _ list = last list
|
||||
|
||||
--- Get element n of a list, or Nothing. Like `!!` but safe.
|
||||
(!!!) list i =
|
||||
case drop i list of
|
||||
[] -> Nothing
|
||||
(r:_) -> Just r
|
||||
|
||||
|
||||
-- Like mconcat but for Semigroups
|
||||
sconcat1 :: (Semigroup t) => [t] -> t
|
||||
sconcat1 [x] = x
|
||||
sconcat1 (x:xs) = x <> sconcat1 xs
|
||||
|
||||
sconcatOrDefault def [] = def
|
||||
sconcatOrDefault _ list = sconcat1 list
|
||||
|
||||
-- For more actionable "impossible" errors
|
||||
pleaseReport str = "ShellCheck internal error, please report: " ++ str
|
@@ -22,6 +22,7 @@ fi
|
||||
|
||||
cabal install --dependencies-only --enable-tests "${flags[@]}" ||
|
||||
cabal install --dependencies-only "${flags[@]}" ||
|
||||
cabal install --dependencies-only --max-backjumps -1 "${flags[@]}" ||
|
||||
die "can't install dependencies"
|
||||
cabal configure --enable-tests "${flags[@]}" ||
|
||||
die "configure failed"
|
||||
|
@@ -25,6 +25,13 @@ exit 0
|
||||
echo "Deleting 'dist' and 'dist-newstyle'..."
|
||||
rm -rf dist dist-newstyle
|
||||
|
||||
execs=$(find . -name shellcheck)
|
||||
|
||||
if [ -n "$execs" ]
|
||||
then
|
||||
die "Found unexpected executables. Remove and try again: $execs"
|
||||
fi
|
||||
|
||||
log=$(mktemp) || die "Can't create temp file"
|
||||
date >> "$log" || die "Can't write to log"
|
||||
|
||||
@@ -63,14 +70,17 @@ debian:testing apt-get update && apt-get install -y cabal-install
|
||||
ubuntu:latest apt-get update && apt-get install -y cabal-install
|
||||
haskell:latest true
|
||||
opensuse/leap:latest zypper install -y cabal-install ghc
|
||||
fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils
|
||||
fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils libstdc++-static gcc-c++
|
||||
archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
|
||||
|
||||
# Ubuntu LTS
|
||||
ubuntu:22.04 apt-get update && apt-get install -y cabal-install
|
||||
ubuntu:20.04 apt-get update && apt-get install -y cabal-install
|
||||
ubuntu:18.04 apt-get update && apt-get install -y cabal-install
|
||||
ubuntu:16.04 apt-get update && apt-get install -y cabal-install
|
||||
|
||||
# Stack on Ubuntu LTS
|
||||
ubuntu:20.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
|
||||
ubuntu:22.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
|
||||
EOF
|
||||
|
||||
exit "$final"
|
||||
|
@@ -5,8 +5,11 @@ import System.Exit
|
||||
import qualified ShellCheck.Analytics
|
||||
import qualified ShellCheck.AnalyzerLib
|
||||
import qualified ShellCheck.ASTLib
|
||||
import qualified ShellCheck.CFG
|
||||
import qualified ShellCheck.CFGAnalysis
|
||||
import qualified ShellCheck.Checker
|
||||
import qualified ShellCheck.Checks.Commands
|
||||
import qualified ShellCheck.Checks.ControlFlow
|
||||
import qualified ShellCheck.Checks.Custom
|
||||
import qualified ShellCheck.Checks.ShellSupport
|
||||
import qualified ShellCheck.Fixer
|
||||
@@ -19,8 +22,11 @@ main = do
|
||||
ShellCheck.Analytics.runTests
|
||||
,ShellCheck.AnalyzerLib.runTests
|
||||
,ShellCheck.ASTLib.runTests
|
||||
,ShellCheck.CFG.runTests
|
||||
,ShellCheck.CFGAnalysis.runTests
|
||||
,ShellCheck.Checker.runTests
|
||||
,ShellCheck.Checks.Commands.runTests
|
||||
,ShellCheck.Checks.ControlFlow.runTests
|
||||
,ShellCheck.Checks.Custom.runTests
|
||||
,ShellCheck.Checks.ShellSupport.runTests
|
||||
,ShellCheck.Fixer.runTests
|
||||
|
@@ -3,7 +3,7 @@
|
||||
# various resolvers. It's run via distrotest.
|
||||
|
||||
resolvers=(
|
||||
nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
|
||||
# nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
|
||||
)
|
||||
|
||||
die() { echo "$*" >&2; exit 1; }
|
||||
|
Reference in New Issue
Block a user