Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ShellCheck.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ library
ShellCheck.Checks.Custom
ShellCheck.Checks.ShellSupport
ShellCheck.Data
ShellCheck.EditorConfig
ShellCheck.Fixer
ShellCheck.Formatter.Format
ShellCheck.Formatter.CheckStyle
Expand Down
31 changes: 31 additions & 0 deletions shellcheck.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
read the list from standard input. This option is processed in addition to
any files specified on the command line.

**--file-name** *FILE*

: When checking standard input (`-`), use *FILE* as the filename to resolve
`.shellcheckrc` and EditorConfig configuration, instead of `-`. This has no
effect when checking regular files.


# FORMATS

Expand Down Expand Up @@ -334,6 +340,31 @@ Use `shellcheckrc` without the dot instead.
Note for Docker users: ShellCheck will only be able to look for files that
are mounted in the container, so `~/.shellcheckrc` will not be read.

# EDITORCONFIG

Unless `--norc` is used, ShellCheck will also look for a file `.editorconfig`
in the script's directory and each parent directory. Any section whose glob
pattern matches the checked file will have its `shellcheck.*` keys read as
directives, with the `shellcheck.` prefix stripped. This uses the same
`key=value` syntax as `.shellcheckrc`.

For example:

[*.{ebuild,eclass}]
shellcheck.shell=bash
shellcheck.disable=SC2034

[{PKGBUILD,APKBUILD}]
shellcheck.shell=bash
shellcheck.disable=SC2034

If no matching directives are found in any `.editorconfig` in the parent
directories, ShellCheck will look in the global default
`$XDG_CONFIG_HOME/editorconfig.ini` (usually `~/.config/editorconfig.ini`).

Directives from `.shellcheckrc`/`shellcheckrc` and from `.editorconfig` are
both applied, with `.shellcheckrc` taking precedence in case of conflicts.


# ENVIRONMENT VARIABLES

Expand Down
127 changes: 118 additions & 9 deletions shellcheck.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import qualified ShellCheck.Analyzer
import ShellCheck.Checker
import ShellCheck.Data
import ShellCheck.EditorConfig
import ShellCheck.Interface
import ShellCheck.Regex

Expand Down Expand Up @@ -77,7 +78,8 @@ data Options = Options {
sourcePaths :: [FilePath],
formatterOptions :: FormatterOptions,
minSeverity :: Severity,
rcfile :: Maybe FilePath
rcfile :: Maybe FilePath,
fileNameOverride :: Maybe FilePath
}

defaultOptions = Options {
Expand All @@ -88,7 +90,8 @@ defaultOptions = Options {
foColorOption = ColorAuto
},
minSeverity = StyleC,
rcfile = Nothing
rcfile = Nothing,
fileNameOverride = Nothing
}

usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
Expand All @@ -110,7 +113,7 @@ options = [
Option "" ["list-optional"]
(NoArg $ Flag "list-optional" "true") "List checks disabled by default",
Option "" ["norc"]
(NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc files",
(NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc and .editorconfig files",
Option "" ["rcfile"]
(ReqArg (Flag "rcfile") "RCFILE")
"Prefer the specified configuration file over searching for one",
Expand All @@ -137,7 +140,10 @@ options = [
(NoArg $ Flag "help" "true") "Show this usage summary and exit",
Option "" ["files-from"]
(ReqArg (Flag "files-from") "FILE")
"Read input files from FILE (one per line, or '-' for stdin)"
"Read input files from FILE (one per line, or '-' for stdin)",
Option "" ["file-name"]
(ReqArg (Flag "file-name") "FILE")
"Use FILE as the filename for parsing EditorConfig configuration when input is stdin"
]
getUsageInfo = usageInfo usageHeader options

Expand Down Expand Up @@ -294,10 +300,19 @@ runFormatter sys format options files = do
}
result <- checkScript sys checkspec
onResult format result sys
-- A malformed EditorConfig (invalid 'root' or 'shellcheck.*'
-- directive) means shellcheck cannot apply the requested
-- configuration, so it fails rather than silently proceeding.
return $
if null (crComments result)
then NoProblems
else SomeProblems
if any editorConfigError (crComments result)
then SupportFailure
else if null (crComments result)
then NoProblems
else SomeProblems

editorConfigError pc =
cCode (pcComment pc) == 1134 &&
".editorconfig" `isSuffixOf` posFile (pcStartPos pc)

parseEnum name value list =
case lookup value list of
Expand Down Expand Up @@ -420,6 +435,11 @@ parseOption flag options =
rcfile = Just str
}

Flag "file-name" str -> do
return options {
fileNameOverride = Just str
}

Flag "enable" value ->
let cs = checkSpec options in return options {
checkSpec = cs {
Expand Down Expand Up @@ -514,8 +534,37 @@ ioInterface options files = do
fallback path _ = return path


-- Returns the name and contents of .shellcheckrc for the given file
getConfig cache filename =
-- Returns the name and contents of .shellcheckrc for the given file,
-- merged with any shellcheck.* directives found in applicable
-- EditorConfig files.
getConfig cache filename = do
let configFilename =
if filename == "-"
then fromMaybe filename (fileNameOverride options)
else filename
rcResult <- getRcConfig cache configFilename
ecResult <- getEditorConfig configFilename
-- A rejected EditorConfig blob (one that only contains invalid
-- 'root'/'shellcheck.*' rejections) is surfaced with the
-- EditorConfig source so its SC1134 is attributable to it; this
-- also lets the formatter treat it as a fatal config error.
return $ mergeConfigs filename rcResult ecResult

mergeConfigs filename rcResult ecResult =
case (rcResult, ecResult) of
(Nothing, Nothing) -> Nothing
(Just (rcPath, rc), Nothing) -> Just (rcPath, rc)
(Nothing, Just (ecPath, ec)) -> Just (ecPath, ec)
(Just (rcPath, rc), Just (ecPath, ec)) ->
if isEditorConfigRejection ec
then Just (ecPath, ec)
else Just (rcPath, rc ++ "\n" ++ ec)

isEditorConfigRejection ec =
all (\l -> null (trim l) || l == "invalid editorconfig value") (lines ec)


getRcConfig cache filename =
case rcfile options of
Just file -> do
-- We have a specified rcfile. Ignore normal rcfile resolution.
Expand All @@ -541,6 +590,66 @@ ioInterface options files = do
writeIORef cache (dir, result)
return result

-- Look for .editorconfig files in the target file's directory and
-- all its parents (as per the EditorConfig spec), plus the global
-- ${XDG_CONFIG_HOME}/editorconfig.ini default. shellcheck.* keys in
-- matching sections are turned into directives.
getEditorConfig filename = do
-- Resolve the directory (to find .editorconfig files) but keep
-- the leaf filename as-is so that globs match the symlink name
-- rather than the resolved target.
let name = takeFileName filename
dir <- normalize (takeDirectory filename)
let path = dir </> name
dirConfigs <- collectDirConfigs dir
globalConfig <- readGlobalEditorConfig
let allConfigs = dirConfigs ++ globalConfig
contributions = concatMap (directivesFor path) allConfigs
return $ case contributions of
[] -> Nothing
((sourceFile, _):_) -> Just (sourceFile, concatMap snd contributions)
where
-- For each EditorConfig file: report any invalid 'root'
-- declaration (which takes priority) as a rejected config blob,
-- otherwise yield the matching shellcheck.* directives
-- (invalid directives are reported by editorConfigDirectives as a
-- rejected blob so the .shellcheckrc parser emits SC1134).
directivesFor path (file, contents) =
let relative = makeRelativeTo (takeDirectory file) path
in case invalidRootLines contents of
(badLine:_) ->
[(file, replicate (badLine - 1) '\n' ++ "invalid editorconfig value\n")]
[] ->
case editorConfigDirectives contents relative of
Nothing -> []
Just blob -> [(file, blob)]

makeRelativeTo dir path =
case stripPrefix (addTrailingSlash dir) path of
Just rest -> rest
Nothing -> takeFileName path

addTrailingSlash dir
| null dir = dir
| last dir == '/' = dir
| otherwise = dir ++ "/"

collectDirConfigs dir = do
current <- readConfig (dir </> ".editorconfig")
let isRoot = maybe False (isEditorConfigRoot . snd) current
next = takeDirectory dir
rest <- if next /= dir && not isRoot
then collectDirConfigs next
else return []
return $ maybeToList current ++ rest

readGlobalEditorConfig = do
path <- (getXdgDirectory XdgConfig "editorconfig.ini")
`catch` ((const $ return "") :: IOException -> IO FilePath)
if null path
then return []
else maybeToList <$> readConfig path

findConfig paths =
case paths of
(file:rest) -> do
Expand Down
36 changes: 36 additions & 0 deletions src/ShellCheck/Checker.hs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where

import ShellCheck.Analyzer
import ShellCheck.ASTLib
import ShellCheck.EditorConfig
import ShellCheck.Interface
import ShellCheck.Parser

Expand Down Expand Up @@ -165,6 +166,41 @@ checkWithRcIncludesAndSourcePath rc includes mapper = getErrors
siFindSource = mapper
}

-- shellcheck.* directives extracted from an EditorConfig file are merged
-- into the same "key=value" blob as .shellcheckrc. We simulate that here
-- by feeding editorConfigDirectives' output through siGetConfig.
checkWithEditorConfig ec name src =
let sys = (mockedSystemInterface [("foo", src)]) {
siGetConfig = const . return $
Just (".editorconfig", fromMaybe "" $ editorConfigDirectives ec name)
}
in getErrors sys emptyCheckSpec {
csScript = src,
csExcludedWarnings = [2148]
}

prop_editorConfigAppliesKnownShell =
null $ checkWithEditorConfig "[foo]\nshellcheck.shell=bash\n" "foo"
"#!/bin/sh\necho \"hi\""
prop_editorConfigAppliesDisable =
null $ checkWithEditorConfig "[foo]\nshellcheck.disable=SC2086\n" "foo"
"#!/bin/sh\necho $1"
prop_editorConfigUnknownShellIsReported =
-- An unknown shell can't be applied silently; it surfaces as a
-- config parse error (SC1134) rather than being dropped.
[1134] == checkWithEditorConfig "[foo]\nshellcheck.shell=zsh\n" "foo"
"#!/bin/sh\necho \"hi\""
prop_editorConfigCommentValueIsReported =
-- EditorConfig has no inline comments, so a '#'-prefixed value is
-- reported as a config error (SC1134) instead of being eaten.
[1134] == checkWithEditorConfig "[foo]\nshellcheck.disable=#abc\n" "foo"
"#!/bin/sh\necho \"hi\""
prop_editorConfigNonMatchingSectionIgnored =
-- A directive in a section whose glob does not match the file is
-- not applied (and produces no config error).
[2086] == checkWithEditorConfig "[bar]\nshellcheck.disable=SC2086\n" "foo"
"#!/bin/sh\necho $1"

prop_findsParseIssue = check "echo \"$12\"" == [1037]

prop_commentDisablesParseIssue1 =
Expand Down
Loading
Loading