Documentation
¶
Overview ¶
Package shell provides shell script parsing utilities for Dockerfile linting.
Package shell provides shell script parsing utilities for Dockerfile linting.
Package shell provides shell script parsing utilities for Dockerfile linting.
Package shell provides shell script parsing utilities for Dockerfile linting.
Package shell provides shell script parsing utilities for Dockerfile linting. It wraps mvdan.cc/sh/v3/syntax to provide a simple API for extracting command names from shell scripts, similar to how hadolint uses ShellCheck.
Index ¶
- Variables
- func Basename(p string) string
- func CommandNames(script string) []string
- func CommandNamesWithVariant(script string, variant Variant) []string
- func ContainsCommand(script, command string) bool
- func ContainsCommandWithVariant(script, command string, variant Variant) bool
- func CountChainedCommands(script string, variant Variant) int
- func DownloadOutputFile(cmd *CommandInfo) string
- func DownloadURL(cmd *CommandInfo) string
- func DropQuotes(s string) string
- func ExtractChainSeparators(script string, variant Variant, commandCount int) []string
- func ExtractChainedCommands(script string, variant Variant) []string
- func ExtractCommandsBetweenCds(remaining string, variant Variant) string
- func FormatChainedScript(script string, variant Variant) string
- func FormatOctalMode(mode uint16) string
- func FormatStatement(stmt *syntax.Stmt, variant Variant) string
- func HasCdAtStart(script string, variant Variant) bool
- func HasExitCommand(script string, variant Variant) bool
- func HasPipes(script string, variant Variant) bool
- func HasStandaloneCd(script string, variant Variant) bool
- func IsArchiveFilename(name string) bool
- func IsArchiveURL(s string) bool
- func IsHeredocCandidate(script string, variant Variant, minCommands int) bool
- func IsPureFileCreation(script string, variant Variant) bool
- func IsSimpleScript(script string, variant Variant) bool
- func IsTarExtract(cmd *CommandInfo) bool
- func IsURL(s string) bool
- func IterateWrapperArgs(args []*syntax.Word, wrapperName string, callback func(WrapperArg) bool)
- func ParseOctalMode(s string) uint16
- func ReconstructSourceText(lines []string, cmdStartCol int) string
- func ScriptHasInlineHeredoc(script string, variant Variant) bool
- func SetsErrorFlag(cmd string, variant Variant) bool
- func ShellFromShebang(line string) (string, bool)
- func SplitSimpleCommand(cmd string, variant Variant) ([]string, bool)
- func TarDestination(cmd *CommandInfo) string
- type CdCommand
- type ChainBoundary
- type ChainPosition
- type ChmodInfo
- type CommandInfo
- type CommandMatcher
- type CommandOccurrence
- type FileCreationInfo
- type PackageInstallInfo
- type PackageManager
- type Variant
- type WrapperArg
Constants ¶
This section is empty.
Variables ¶
var ArchiveExtensions = []string{
".tar.lzma",
".tar.bz2",
".tar.gz",
".tar.xz",
".tar.zst",
".tar.lz",
".tar.Z",
".lzma",
".tbz2",
".tzst",
".tar",
".tbz",
".tb2",
".tgz",
".tlz",
".tpz",
".txz",
".bz2",
".tZ",
".gz",
".lz",
".xz",
".Z",
}
ArchiveExtensions is the unified superset of archive file extensions recognized by both DL3010 (hadolint) and prefer-add-unpack (tally). Sorted longest-first so suffix matching is greedy (e.g. ".tar.gz" is checked before ".gz").
var DownloadCommands = []string{"curl", "wget"}
DownloadCommands lists commands that download remote files.
var ExtractionCommands = []string{
"bunzip2",
"gzcat",
"gunzip",
"uncompress",
"unlzma",
"unxz",
"unzip",
"zcat",
"zgz",
}
ExtractionCommands lists commands that extract archive files (excluding tar, which needs separate flag checking via IsTarExtract).
Functions ¶
func Basename ¶
Basename extracts the filename from a path, stripping quotes and handling both Unix and Windows separators.
func CommandNames ¶
CommandNames extracts all command names from a shell script. Uses VariantBash by default. Use CommandNamesWithVariant for other shells.
func CommandNamesWithVariant ¶
CommandNamesWithVariant extracts all command names from a shell script using the specified shell variant for parsing.
It parses the script and walks the AST to find all CallExpr nodes, returning the first word of each (the command name). It also handles command wrappers (env, nice, xargs, etc.) and shell wrappers (sh -c, bash -c).
This matches hadolint's behavior using ShellCheck.findCommandNames.
func ContainsCommand ¶
ContainsCommand checks if a shell script contains a specific command. Uses VariantBash by default.
func ContainsCommandWithVariant ¶
ContainsCommandWithVariant checks if a shell script contains a specific command using the specified shell variant for parsing.
func CountChainedCommands ¶
CountChainedCommands counts the number of commands in && chains within a shell script. Pipelines (|) count as a single logical command. Top-level statements separated by semicolons or newlines are counted individually.
func DownloadOutputFile ¶
func DownloadOutputFile(cmd *CommandInfo) string
DownloadOutputFile extracts the output filename from a curl or wget CommandInfo. For curl: -o <file>, -o<file>, --output <file>, --output=<file> For wget: -O <file>, -O<file>, --output-document <file>, --output-document=<file> Returns "" if no output file is specified or if output is stdout ("-").
func DownloadURL ¶
func DownloadURL(cmd *CommandInfo) string
DownloadURL extracts the first URL argument (http/https/ftp) from a download CommandInfo. Returns "" if no URL is found.
func DropQuotes ¶
DropQuotes removes surrounding single or double quotes from a string.
func ExtractChainSeparators ¶ added in v0.10.0
ExtractChainSeparators returns the raw separator text between commands in an && chain (for example " && " or " && \\\n "). The result length is commandCount-1 on success; otherwise nil.
func ExtractChainedCommands ¶
ExtractChainedCommands extracts individual command strings from && chains. Each command is formatted cleanly using the shell printer. Returns nil if parsing fails or for non-POSIX shells.
func ExtractCommandsBetweenCds ¶
ExtractCommandsBetweenCds parses the remaining commands after a cd and extracts commands that come before the next cd. This properly handles quoted paths. For "make && cd /tmp && build", if we're looking for commands before "cd /tmp", this returns "make".
func FormatChainedScript ¶ added in v0.11.0
FormatChainedScript formats a shell script so that each top-level &&/|| chain operator starts on its own line. Uses the mvdan.cc/sh/v3 printer with BinaryNextLine for correct shell formatting. Returns the original script text (trimmed) if parsing fails or there are no chain operators.
The output uses tab indentation for continuation lines (Indent(0) = tabs).
func FormatOctalMode ¶
FormatOctalMode formats a chmod mode as a 4-digit octal string. E.g., 0o755 -> "0755", 0o644 -> "0644" Returns empty string for 0 (no mode).
func FormatStatement ¶
FormatStatement formats a single statement using syntax.Printer. Returns a clean, single-line representation of the command.
func HasCdAtStart ¶
HasCdAtStart returns true if the script has cd at the beginning of a command chain.
func HasExitCommand ¶
HasExitCommand checks if a script contains exit commands that would change control flow if merged with other commands.
func HasPipes ¶
HasPipes checks if a shell script contains any pipe operators (| or |&). Returns false for non-POSIX shells or unparseable scripts.
func HasStandaloneCd ¶
HasStandaloneCd returns true if the script contains a standalone cd command (one that isn't chained with other commands).
func IsArchiveFilename ¶
IsArchiveFilename checks if a filename has a recognized archive extension. Extensions are case-sensitive (e.g. .Z and .tZ use uppercase Z for Unix compress format).
func IsArchiveURL ¶
IsArchiveURL checks if a URL string points to an archive file. Strips query/fragment before checking extension. Requires http/https/ftp scheme.
func IsHeredocCandidate ¶
IsHeredocCandidate checks if a shell script would be a good candidate for heredoc conversion by the prefer-run-heredoc rule. This is used by other rules (like DL3003) to avoid generating fixes that would interfere with heredoc conversion.
A script is a heredoc candidate if:
- It uses a POSIX shell
- It has at least minCommands commands (from && chains or separate statements)
- It's a simple script (no complex control flow like if/for/while)
This function parses the script once and reuses the AST for all checks.
func IsPureFileCreation ¶
IsPureFileCreation checks if a shell script is PURELY for creating files. Returns true only if every command in the script is for file creation (echo/cat/printf > file) or chmod on the created file. Returns false if there are any other commands mixed in. This is used by prefer-run-heredoc to yield to prefer-copy-heredoc.
func IsSimpleScript ¶
IsSimpleScript checks if a script contains only simple commands that can be safely merged into a heredoc. Returns false for scripts with compound commands (if, for, while, case), control flow (return, break, continue, exec), functions, or subshells. `exit` is allowed so that common guard patterns like `cd dir || exit` (often suggested by ShellCheck) don't block heredoc conversion.
func IsTarExtract ¶
func IsTarExtract(cmd *CommandInfo) bool
IsTarExtract checks if a tar CommandInfo has extraction flags (-x, --extract, --get).
func IterateWrapperArgs ¶
func IterateWrapperArgs(args []*syntax.Word, wrapperName string, callback func(WrapperArg) bool)
IterateWrapperArgs iterates through wrapper command arguments, skipping flags and their values, and calls the callback for each potential command argument found. This handles the common pattern of finding commands within sudo, env, etc.
The callback should return true to break iteration, false to continue looking for nested wrappers.
func ParseOctalMode ¶
ParseOctalMode parses an octal mode string (e.g., "755", "0755") to uint16. Returns 0 for invalid input.
func ReconstructSourceText ¶ added in v0.11.0
ReconstructSourceText reconstructs the shell command source text from Dockerfile source lines. Backslash-newline continuations are kept intact because the shell parser (mvdan.cc/sh) handles them natively. This preserves line/col positions for mapping back to the Dockerfile.
cmdStartCol is the byte offset in the first line where the command starts (after RUN + flags). Continuation lines are included in full.
func ScriptHasInlineHeredoc ¶ added in v0.11.0
ScriptHasInlineHeredoc checks whether a shell script contains inline heredocs (e.g., cat <<EOF ... EOF && other_cmd). Such scripts should not have their chain boundaries reformatted because the heredoc body positions would break.
func SetsErrorFlag ¶
SetsErrorFlag checks if a command is a "set" builtin that enables the -e flag. Uses shell AST to properly detect any flag combination containing 'e' (e.g., "set -e", "set -ex", "set -euo pipefail").
func ShellFromShebang ¶ added in v0.19.0
ShellFromShebang extracts the shell name from a shebang line. It delegates to fileutil.Shebang for the common cases (sh, bash, mksh, bats, zsh) and adds ksh support for Dockerfile compatibility.
Returns the normalized shell name (e.g., "bash", "sh", "ksh") and true if a known shell shebang was found. The returned name can be passed directly to VariantFromShell.
func SplitSimpleCommand ¶
SplitSimpleCommand parses a shell command string and returns its argv words.
This is intentionally conservative: it only succeeds for a single simple command without redirections, pipelines, boolean operators, variable expansions, command substitutions, or other shell-specific constructs.
This is useful for suggesting "exec form" JSON arrays for Dockerfile instructions like CMD/ENTRYPOINT when the shell form is trivially tokenizable.
func TarDestination ¶
func TarDestination(cmd *CommandInfo) string
TarDestination extracts the target directory from a tar CommandInfo. Checks -C <dir>, --directory=<dir>, --directory <dir>. Returns "" if none found.
Types ¶
type CdCommand ¶
type CdCommand struct {
// TargetDir is the directory argument passed to cd.
TargetDir string
// IsStandalone is true if cd is the only command (not chained with && or ;).
IsStandalone bool
// IsAtStart is true if cd is at the beginning of a command chain.
// e.g., "cd /foo && make" has cd at start, "make && cd /foo" does not.
IsAtStart bool
// PrecedingCommands contains the commands before cd if it's not at the start.
// e.g., for "mkdir /tmp && cd /tmp && make", this would be "mkdir /tmp".
PrecedingCommands string
// RemainingCommands contains the commands after "cd /foo &&" if IsAtStart is true.
// Empty if IsStandalone is true or cd is not at start.
RemainingCommands string
// StartCol is the 0-based column where cd starts.
StartCol int
// Line is the 0-based line number.
Line int
}
CdCommand represents a cd command found in a shell script.
func FindCdCommands ¶
FindCdCommands finds all cd commands in a shell script and analyzes their context.
type ChainBoundary ¶ added in v0.11.0
type ChainBoundary struct {
// LeftEndLine is the 1-based line (in the parsed text) where the left command ends.
LeftEndLine int
// LeftEndCol is the 1-based column where the left command ends.
LeftEndCol int
// RightStartLine is the 1-based line where the right command starts.
RightStartLine int
// RightStartCol is the 1-based column where the right command starts.
RightStartCol int
// Op is the operator text ("&&" or "||").
Op string
// SameLine is true when left end and right start are on the same source line.
SameLine bool
}
ChainBoundary represents the position of a chain operator (&&/||) between two commands in a shell script's source text.
func CollectChainBoundaries ¶ added in v0.11.0
func CollectChainBoundaries(scriptText string, variant Variant) ([]ChainBoundary, int)
CollectChainBoundaries parses a shell script and returns all top-level chain boundaries (&& and ||) along with the maximum per-chain command count. The per-chain count reflects the longest &&/|| chain in any single statement, not the sum across semicolon-separated statements. This is the correct value for comparing against a minCommands threshold.
The script text should include backslash continuations exactly as they appear in the Dockerfile source so that line/col positions map correctly.
Returns nil, 0 if parsing fails or for non-POSIX shells.
type ChainPosition ¶
type ChainPosition struct {
// IsStandalone is true if this is the only command (not chained).
IsStandalone bool
// HasOtherStatements is true when the script contains multiple top-level
// statements separated by semicolons or newlines. In this case,
// PrecedingCommands and RemainingCommands only cover the chain within
// the matched statement and do NOT include commands from other statements.
// Callers building replacement text for the entire script must not use
// this position alone, as it would silently drop sibling statements.
HasOtherStatements bool
// PrecedingCommands contains the commands before this one in the chain.
// Empty when the command is at the start or standalone.
PrecedingCommands string
// RemainingCommands contains the commands after this one in the chain.
// Empty when the command is at the end or standalone.
RemainingCommands string
}
ChainPosition describes a command's position within a && chain.
func FindCommandInChain ¶
func FindCommandInChain(script string, variant Variant, match CommandMatcher) *ChainPosition
FindCommandInChain locates the first command matching the predicate in a shell script and returns its chain context (preceding/remaining commands). Returns nil if no matching command is found or the script fails to parse.
type ChmodInfo ¶
type ChmodInfo struct {
// Mode is the octal mode (e.g., 0o755, 0o644, 0o4755).
Mode uint16
// Target is the file path being chmod'd.
Target string
}
ChmodInfo describes a standalone chmod command.
func DetectStandaloneChmod ¶
DetectStandaloneChmod checks if a shell script is a standalone chmod command. Returns nil if it's not a pure chmod or if the chmod cannot be converted (e.g., symbolic mode, recursive chmod, multiple commands).
type CommandInfo ¶
type CommandInfo struct {
// Name is the base command name (e.g., "apt-get", "yum").
Name string
// Subcommand is the first non-flag argument (e.g., "install" in "apt-get install").
Subcommand string
// Args contains all arguments including flags.
Args []string
// Position information for the command name.
Line int // 0-based line within the script
StartCol int // 0-based column where command starts
EndCol int // 0-based column where command name ends
// Position information for the subcommand (if present).
// These are only set when Subcommand is non-empty.
SubcommandLine int // 0-based line within the script
SubcommandStartCol int // 0-based column where subcommand starts
SubcommandEndCol int // 0-based column where subcommand ends
}
CommandInfo represents a parsed command with its arguments and flags.
func FindCommands ¶
func FindCommands(script string, variant Variant, names ...string) []CommandInfo
FindCommands extracts all commands matching the given name(s) from a shell script. It returns detailed CommandInfo for each matching command.
func (*CommandInfo) CountFlag ¶
func (c *CommandInfo) CountFlag(flag string) int
CountFlag counts how many times a flag appears in the command. Useful for checking flags like -q -q (equivalent to -qq).
func (*CommandInfo) GetArgValue ¶
func (c *CommandInfo) GetArgValue(flag string) string
GetArgValue returns the value following a flag (e.g., "-q=2" returns "2"). Returns empty string if not found or no value.
func (*CommandInfo) HasAnyArg ¶
func (c *CommandInfo) HasAnyArg(args ...string) bool
HasAnyArg checks if any of the specified arguments are present as the subcommand.
func (*CommandInfo) HasAnyFlag ¶
func (c *CommandInfo) HasAnyFlag(flags ...string) bool
HasAnyFlag checks if the command has any of the specified flags.
func (*CommandInfo) HasFlag ¶
func (c *CommandInfo) HasFlag(flag string) bool
HasFlag checks if the command has a specific flag. Handles both short flags (-y) and long flags (--yes). For short flags, also checks combined flags (e.g., -yq contains -y).
type CommandMatcher ¶
CommandMatcher is a predicate that decides whether a shell call expression is the command to locate. name is the base command name (path stripped), args are all arguments (flags and positional).
type CommandOccurrence ¶
type CommandOccurrence struct {
// Name is the command name (e.g., "apt", "sudo").
Name string
// Subcommand is the first argument if it looks like a subcommand (e.g., "install" in "apt install").
// Empty if the first argument looks like a flag or there are no arguments.
Subcommand string
// StartCol is the 0-based column offset where the command starts.
StartCol int
// EndCol is the 0-based column offset where the command name ends (exclusive).
EndCol int
// Line is the 0-based line number within the script where the command appears.
Line int
}
CommandOccurrence represents a command with its exact position in the script.
func FindAllCommandOccurrences ¶
func FindAllCommandOccurrences(script, command string, variant Variant) []CommandOccurrence
FindAllCommandOccurrences finds all occurrences of a specific command.
func FindCommandOccurrence ¶
func FindCommandOccurrence(script, command string, variant Variant) *CommandOccurrence
FindCommandOccurrence finds the first occurrence of a specific command. Returns nil if the command is not found.
func FindCommandOccurrences ¶
func FindCommandOccurrences(script string, variant Variant) []CommandOccurrence
FindCommandOccurrences extracts all command positions from a shell script. It returns occurrences with precise byte offsets for each command found.
type FileCreationInfo ¶
type FileCreationInfo struct {
// TargetPath is the absolute path to the target file.
TargetPath string
// Content is the literal content to write.
Content string
// ChmodMode is the octal chmod mode (e.g., 0o755, 0o644), or 0 if no chmod.
ChmodMode uint16
// IsAppend is true if ALL writes in the chain use >> (append) mode.
// If true, converting to COPY would lose existing file content.
// A later > (overwrite) clears this flag since content no longer depends on existing data.
IsAppend bool
// HasUnsafeVariables is true if the script uses variables that cannot be
// converted to COPY heredoc (e.g., shell variables, command substitution).
HasUnsafeVariables bool
// PrecedingCommands contains commands before the file creation (for mixed scripts).
// Empty if file creation is at the start or script is pure file creation.
PrecedingCommands string
// RemainingCommands contains commands after the file creation (for mixed scripts).
// Empty if file creation is at the end or script is pure file creation.
RemainingCommands string
}
FileCreationInfo describes a detected file creation pattern in a shell script. This is used to coordinate between prefer-copy-heredoc and prefer-run-heredoc rules.
func DetectFileCreation ¶
func DetectFileCreation(script string, variant Variant, knownVars func(name string) bool) *FileCreationInfo
DetectFileCreation analyzes a shell script for file creation patterns. Returns nil if the script is not primarily a file creation operation.
Detected patterns:
- echo "content" > /path/to/file
- echo "content" >> /path/to/file (append)
- cat <<EOF > /path/to/file ... EOF
- printf "content" > /path/to/file (limited support)
Also detects chmod chaining: echo "x" > /file && chmod 0755 /file
The knownVars function is called to check if a variable is a known ARG/ENV. If nil, all variables are considered unsafe.
type PackageInstallInfo ¶
type PackageInstallInfo struct {
Manager PackageManager
Packages []string
}
PackageInstallInfo represents a detected package installation.
func ExtractPackageInstalls ¶
func ExtractPackageInstalls(script string, variant Variant) []PackageInstallInfo
ExtractPackageInstalls parses a shell script and extracts package installations.
type PackageManager ¶
type PackageManager string
PackageManager identifies a system package manager.
const ( PackageManagerApt PackageManager = "apt" PackageManagerApk PackageManager = "apk" PackageManagerYum PackageManager = "yum" PackageManagerDnf PackageManager = "dnf" PackageManagerZypper PackageManager = "zypper" PackageManagerPacman PackageManager = "pacman" PackageManagerEmerge PackageManager = "emerge" PackageManagerUnknown PackageManager = "" )
type Variant ¶
type Variant int
Variant represents a shell variant for parsing and rule gating. Each variant expresses what the shell can do, enabling rules to query capabilities via intent-based methods rather than negation tests.
const ( // VariantBash is the GNU Bash shell (default for Docker Linux containers). VariantBash Variant = iota // VariantPOSIX is the POSIX-compliant shell (sh, dash, ash). VariantPOSIX // VariantMksh is the MirBSD Korn Shell. VariantMksh // VariantPowerShell is PowerShell (cross-platform: powershell on Windows, pwsh on Linux/macOS). VariantPowerShell // VariantCmd is the Windows cmd.exe command interpreter. VariantCmd // VariantUnknown is an unrecognized shell. Treated conservatively: not parseable, // not ShellCheck-compatible, not PowerShell. VariantUnknown )
func VariantFromShell ¶
VariantFromShell returns the appropriate Variant for a shell name. Common shell mappings:
- bash, zsh -> VariantBash
- sh, dash, ash -> VariantPOSIX
- mksh, ksh -> VariantMksh
- powershell, pwsh -> VariantPowerShell
- cmd -> VariantCmd
- unknown -> VariantUnknown
func VariantFromShellCmd ¶
VariantFromShellCmd returns the appropriate Variant from a SHELL command array. The first element is typically the shell path (e.g., ["/bin/bash", "-c"]).
func (Variant) IsParseable ¶ added in v0.19.0
IsParseable returns true for shells whose scripts can be parsed by mvdan.cc/sh. Use this to guard shell AST analysis, command extraction, and chained-command counting.
func (Variant) IsPowerShell ¶ added in v0.19.0
IsPowerShell returns true for PowerShell variants (powershell, pwsh). Use this to gate PowerShell-specific lint rules (tally/powershell/*).
func (Variant) IsShellCheckCompatible ¶ added in v0.19.0
IsShellCheckCompatible returns true for shells that ShellCheck can analyze. Use this to guard ShellCheck WASM invocation.
func (Variant) SupportsHeredoc ¶ added in v0.19.0
SupportsHeredoc returns true for shells compatible with BuildKit heredoc syntax (RUN <<EOF). Use this to guard heredoc suggestions and fixes.
type WrapperArg ¶
type WrapperArg struct {
// Arg is the syntax.Word representing this argument
Arg *syntax.Word
// Index is the position in the args slice
Index int
// Name is the base name of the command (path.Base applied)
Name string
// RemainingArgs are the args after this command
RemainingArgs []*syntax.Word
}
WrapperArg represents a potential command argument found within wrapper arguments.