mirror of
https://github.com/koalaman/shellcheck
synced 2025-07-07 13:31:36 -07:00
Warn about duplicate uses of stdin/out/err
This commit is contained in:
parent
8aa40c43ed
commit
2030b83607
2 changed files with 142 additions and 21 deletions
|
@ -1,4 +1,7 @@
|
||||||
## Git
|
## Git
|
||||||
|
### Added
|
||||||
|
- SC2259/SC2260: Warn when redirections override pipes
|
||||||
|
- SC2261: Warn about multiple competing redirections
|
||||||
|
|
||||||
## v0.7.1 - 2020-04-04
|
## v0.7.1 - 2020-04-04
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
@ -3282,25 +3282,92 @@ prop_checkPipeToNowhere6 = verifyNot checkPipeToNowhere "ls | echo $(cat)"
|
||||||
prop_checkPipeToNowhere7 = verifyNot checkPipeToNowhere "echo foo | var=$(cat) ls"
|
prop_checkPipeToNowhere7 = verifyNot checkPipeToNowhere "echo foo | var=$(cat) ls"
|
||||||
prop_checkPipeToNowhere8 = verify checkPipeToNowhere "foo | true"
|
prop_checkPipeToNowhere8 = verify checkPipeToNowhere "foo | true"
|
||||||
prop_checkPipeToNowhere9 = verifyNot checkPipeToNowhere "mv -i f . < /dev/stdin"
|
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"
|
||||||
|
prop_checkPipeToNowhere12 = verify checkPipeToNowhere "ls > foo > bar"
|
||||||
|
prop_checkPipeToNowhere13 = verify checkPipeToNowhere "ls > foo 2> bar > baz"
|
||||||
|
prop_checkPipeToNowhere14 = verify checkPipeToNowhere "ls > foo &> bar"
|
||||||
|
prop_checkPipeToNowhere15 = verifyNot checkPipeToNowhere "ls > foo 2> bar |& grep 'No space left'"
|
||||||
|
prop_checkPipeToNowhere16 = verifyNot checkPipeToNowhere "echo World | cat << EOF\nhello $(cat)\nEOF\n"
|
||||||
|
prop_checkPipeToNowhere17 = verify checkPipeToNowhere "echo World | cat << 'EOF'\nhello $(cat)\nEOF\n"
|
||||||
|
prop_checkPipeToNowhere18 = verifyNot checkPipeToNowhere "ls 1>&3 3>&1 3>&- | wc -l"
|
||||||
|
|
||||||
|
data PipeType = StdoutPipe | StdoutStderrPipe | NoPipe deriving (Eq)
|
||||||
checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity ()
|
checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity ()
|
||||||
checkPipeToNowhere _ t =
|
checkPipeToNowhere params t =
|
||||||
case t of
|
case t of
|
||||||
T_Pipeline _ _ (first:rest) -> mapM_ checkPipe rest
|
T_Pipeline _ pipes cmds ->
|
||||||
|
mapM_ checkPipe $ commandsWithContext pipes cmds
|
||||||
T_Redirecting _ redirects cmd | any redirectsStdin redirects -> checkRedir cmd
|
T_Redirecting _ redirects cmd | any redirectsStdin redirects -> checkRedir cmd
|
||||||
_ -> return ()
|
_ -> return ()
|
||||||
where
|
where
|
||||||
checkPipe redir = sequence_ $ do
|
checkPipe (input, stage, output) = do
|
||||||
cmd <- getCommand redir
|
let hasConsumers = hasAdditionalConsumers stage
|
||||||
name <- getCommandBasename cmd
|
let hasProducers = hasAdditionalProducers stage
|
||||||
guard $ name `elem` nonReadingCommands
|
|
||||||
guard . not $ hasAdditionalConsumers cmd
|
sequence_ $ do
|
||||||
-- Confusing echo for cat is so common that it's worth a special case
|
cmd <- getCommand stage
|
||||||
let suggestion =
|
name <- getCommandBasename cmd
|
||||||
if name == "echo"
|
guard $ name `elem` nonReadingCommands
|
||||||
then "Did you want 'cat' instead?"
|
guard $ not hasConsumers && input /= NoPipe
|
||||||
else "Wrong command or missing xargs?"
|
|
||||||
return $ warn (getId cmd) 2216 $
|
-- Confusing echo for cat is so common that it's worth a special case
|
||||||
"Piping to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion
|
let suggestion =
|
||||||
|
if name == "echo"
|
||||||
|
then "Did you want 'cat' instead?"
|
||||||
|
else "Wrong command or missing xargs?"
|
||||||
|
return $ warn (getId cmd) 2216 $
|
||||||
|
"Piping to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion
|
||||||
|
|
||||||
|
sequence_ $ do
|
||||||
|
T_Redirecting _ redirs cmd <- return stage
|
||||||
|
fds <- sequence $ map getRedirectionFds redirs
|
||||||
|
|
||||||
|
let fdAndToken :: [(Integer, Token)]
|
||||||
|
fdAndToken =
|
||||||
|
concatMap (\(list, redir) -> map (\n -> (n, redir)) list) $
|
||||||
|
zip fds redirs
|
||||||
|
|
||||||
|
let fdMap =
|
||||||
|
Map.fromListWith (++) $
|
||||||
|
map (\(a,b) -> (a,[b])) fdAndToken
|
||||||
|
|
||||||
|
let inputWarning = sequence_ $ do
|
||||||
|
guard $ input /= NoPipe && not hasConsumers
|
||||||
|
(override:_) <- Map.lookup 0 fdMap
|
||||||
|
return $ err (getOpId override) 2259 $
|
||||||
|
"This redirection overrides piped input. To use both, merge or pass filename."
|
||||||
|
|
||||||
|
-- Only produce output warnings for regular pipes, since these are
|
||||||
|
-- way more common, and `foo > out 2> err |& foo` can still write
|
||||||
|
-- to stderr if the files fail to open
|
||||||
|
let outputWarning = sequence_ $ do
|
||||||
|
guard $ output == StdoutPipe && not hasProducers
|
||||||
|
(override:_) <- Map.lookup 1 fdMap
|
||||||
|
return $ err (getOpId override) 2260 $
|
||||||
|
"This redirection overrides the output pipe. Use 'tee' to output to both."
|
||||||
|
|
||||||
|
return $ do
|
||||||
|
inputWarning
|
||||||
|
outputWarning
|
||||||
|
mapM_ warnAboutDupes $ Map.assocs fdMap
|
||||||
|
|
||||||
|
warnAboutDupes (n, list@(_:_:_)) =
|
||||||
|
forM_ list $ \c -> err (getOpId c) 2261 $
|
||||||
|
"Multiple redirections compete for " ++ str n ++ ". Combine, or use " ++ alternative ++ "."
|
||||||
|
warnAboutDupes _ = return ()
|
||||||
|
|
||||||
|
alternative =
|
||||||
|
if shellType params `elem` [Bash, Ksh]
|
||||||
|
then "process substitutions or temp files"
|
||||||
|
else "temporary files"
|
||||||
|
|
||||||
|
str n =
|
||||||
|
case n of
|
||||||
|
0 -> "stdin"
|
||||||
|
1 -> "stdout"
|
||||||
|
2 -> "stderr"
|
||||||
|
_ -> "FD " ++ show n
|
||||||
|
|
||||||
checkRedir cmd = sequence_ $ do
|
checkRedir cmd = sequence_ $ do
|
||||||
name <- getCommandBasename cmd
|
name <- getCommandBasename cmd
|
||||||
|
@ -3315,23 +3382,74 @@ checkPipeToNowhere _ t =
|
||||||
"Redirecting to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion
|
"Redirecting to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion
|
||||||
|
|
||||||
-- Could any words in a SimpleCommand consume stdin (e.g. echo "$(cat)")?
|
-- Could any words in a SimpleCommand consume stdin (e.g. echo "$(cat)")?
|
||||||
hasAdditionalConsumers t = isNothing $
|
hasAdditionalConsumers = treeContains mayConsume
|
||||||
doAnalysis (guard . not . mayConsume) t
|
-- Could any words in a SimpleCommand produce stdout? E.g. >(tee foo)
|
||||||
|
hasAdditionalProducers = treeContains mayProduce
|
||||||
|
treeContains pred t = isNothing $
|
||||||
|
doAnalysis (guard . not . pred) t
|
||||||
|
|
||||||
mayConsume t =
|
mayConsume t =
|
||||||
case t of
|
case t of
|
||||||
T_ProcSub {} -> True
|
T_ProcSub _ "<" _ -> True
|
||||||
T_Backticked {} -> True
|
T_Backticked {} -> True
|
||||||
T_DollarExpansion {} -> True
|
T_DollarExpansion {} -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
redirectsStdin t =
|
mayProduce t =
|
||||||
case t of
|
case t of
|
||||||
T_FdRedirect _ _ (T_IoFile _ T_Less {} _) -> True
|
T_ProcSub _ ">" _ -> True
|
||||||
T_FdRedirect _ _ T_HereDoc {} -> True
|
|
||||||
T_FdRedirect _ _ T_HereString {} -> True
|
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
|
getOpId t =
|
||||||
|
case t of
|
||||||
|
T_FdRedirect _ _ x -> getOpId x
|
||||||
|
T_IoFile _ op _ -> getId op
|
||||||
|
_ -> getId t
|
||||||
|
|
||||||
|
getRedirectionFds t =
|
||||||
|
case t of
|
||||||
|
T_FdRedirect _ "" x -> getDefaultFds x
|
||||||
|
T_FdRedirect _ "&" _ -> return [1, 2]
|
||||||
|
T_FdRedirect _ num x | all isDigit num ->
|
||||||
|
-- Don't report the number unless we know what it is.
|
||||||
|
-- This avoids triggering on 3>&1 1>&3
|
||||||
|
getDefaultFds x *> return [read num]
|
||||||
|
-- Don't bother with {fd}>42 and such
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
getDefaultFds redir =
|
||||||
|
case redir of
|
||||||
|
T_HereDoc {} -> return [0]
|
||||||
|
T_HereString {} -> return [0]
|
||||||
|
T_IoFile _ op _ ->
|
||||||
|
case op of
|
||||||
|
T_Less {} -> return [0]
|
||||||
|
T_Greater {} -> return [1]
|
||||||
|
T_DGREAT {} -> return [1]
|
||||||
|
T_GREATAND {} -> return [1, 2]
|
||||||
|
T_CLOBBER {} -> return [1]
|
||||||
|
T_IoDuplicate _ op "-" -> getDefaultFds op
|
||||||
|
_ -> Nothing
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
redirectsStdin t =
|
||||||
|
fromMaybe False $ do
|
||||||
|
fds <- getRedirectionFds t
|
||||||
|
return $ 0 `elem` fds
|
||||||
|
|
||||||
|
pipeType t =
|
||||||
|
case t of
|
||||||
|
T_Pipe _ "|" -> StdoutPipe
|
||||||
|
T_Pipe _ "|&" -> StdoutStderrPipe
|
||||||
|
_ -> NoPipe
|
||||||
|
|
||||||
|
commandsWithContext pipes cmds =
|
||||||
|
let pipeTypes = map pipeType pipes
|
||||||
|
inputs = NoPipe : pipeTypes
|
||||||
|
outputs = pipeTypes ++ [NoPipe]
|
||||||
|
in
|
||||||
|
zip3 inputs cmds outputs
|
||||||
|
|
||||||
prop_checkUseBeforeDefinition1 = verifyTree checkUseBeforeDefinition "f; f() { true; }"
|
prop_checkUseBeforeDefinition1 = verifyTree checkUseBeforeDefinition "f; f() { true; }"
|
||||||
prop_checkUseBeforeDefinition2 = verifyNotTree checkUseBeforeDefinition "f() { true; }; f"
|
prop_checkUseBeforeDefinition2 = verifyNotTree checkUseBeforeDefinition "f() { true; }; f"
|
||||||
prop_checkUseBeforeDefinition3 = verifyNotTree checkUseBeforeDefinition "if ! mycmd --version; then mycmd() { true; }; fi"
|
prop_checkUseBeforeDefinition3 = verifyNotTree checkUseBeforeDefinition "if ! mycmd --version; then mycmd() { true; }; fi"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue