shell

package
v0.18.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 26, 2026 License: GPL-3.0 Imports: 7 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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").

View Source
var DownloadCommands = []string{"curl", "wget"}

DownloadCommands lists commands that download remote files.

View Source
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

func Basename(p string) string

Basename extracts the filename from a path, stripping quotes and handling both Unix and Windows separators.

func CommandNames

func CommandNames(script string) []string

CommandNames extracts all command names from a shell script. Uses VariantBash by default. Use CommandNamesWithVariant for other shells.

func CommandNamesWithVariant

func CommandNamesWithVariant(script string, variant Variant) []string

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

func ContainsCommand(script, command string) bool

ContainsCommand checks if a shell script contains a specific command. Uses VariantBash by default.

func ContainsCommandWithVariant

func ContainsCommandWithVariant(script, command string, variant Variant) bool

ContainsCommandWithVariant checks if a shell script contains a specific command using the specified shell variant for parsing.

func CountChainedCommands

func CountChainedCommands(script string, variant Variant) int

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

func DropQuotes(s string) string

DropQuotes removes surrounding single or double quotes from a string.

func ExtractChainSeparators added in v0.10.0

func ExtractChainSeparators(script string, variant Variant, commandCount int) []string

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

func ExtractChainedCommands(script string, variant Variant) []string

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

func ExtractCommandsBetweenCds(remaining string, variant Variant) string

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

func FormatChainedScript(script string, variant Variant) string

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

func FormatOctalMode(mode uint16) string

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

func FormatStatement(stmt *syntax.Stmt, variant Variant) string

FormatStatement formats a single statement using syntax.Printer. Returns a clean, single-line representation of the command.

func HasCdAtStart

func HasCdAtStart(script string, variant Variant) bool

HasCdAtStart returns true if the script has cd at the beginning of a command chain.

func HasExitCommand

func HasExitCommand(script string, variant Variant) bool

HasExitCommand checks if a script contains exit commands that would change control flow if merged with other commands.

func HasPipes

func HasPipes(script string, variant Variant) bool

HasPipes checks if a shell script contains any pipe operators (| or |&). Returns false for non-POSIX shells or unparseable scripts.

func HasStandaloneCd

func HasStandaloneCd(script string, variant Variant) bool

HasStandaloneCd returns true if the script contains a standalone cd command (one that isn't chained with other commands).

func IsArchiveFilename

func IsArchiveFilename(name string) bool

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

func IsArchiveURL(s string) bool

IsArchiveURL checks if a URL string points to an archive file. Strips query/fragment before checking extension. Requires http/https/ftp scheme.

func IsHeredocCandidate

func IsHeredocCandidate(script string, variant Variant, minCommands int) bool

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

func IsPureFileCreation(script string, variant Variant) bool

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

func IsSimpleScript(script string, variant Variant) bool

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 IsURL

func IsURL(s string) bool

IsURL checks if a string is a valid URL with an http, https, or ftp scheme.

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

func ParseOctalMode(s string) uint16

ParseOctalMode parses an octal mode string (e.g., "755", "0755") to uint16. Returns 0 for invalid input.

func ReconstructSourceText added in v0.11.0

func ReconstructSourceText(lines []string, cmdStartCol int) string

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

func ScriptHasInlineHeredoc(script string, variant Variant) bool

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

func SetsErrorFlag(cmd string, variant Variant) bool

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 SplitSimpleCommand

func SplitSimpleCommand(cmd string, variant Variant) ([]string, bool)

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

func FindCdCommands(script string, variant Variant) []CdCommand

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

func DetectStandaloneChmod(script string, variant Variant) *ChmodInfo

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

type CommandMatcher func(name string, args []string) bool

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.

const (
	// VariantBash is the GNU Bash shell (default for Docker).
	VariantBash Variant = iota
	// VariantPOSIX is the POSIX-compliant shell (sh, dash, ash).
	VariantPOSIX
	// VariantMksh is the MirBSD Korn Shell.
	VariantMksh
	// VariantNonPOSIX represents shells that are not POSIX-compatible.
	// When this variant is active, shell-specific linting rules are disabled.
	// Examples: powershell, cmd, pwsh
	VariantNonPOSIX
)

func VariantFromShell

func VariantFromShell(shell string) Variant

VariantFromShell returns the appropriate Variant for a shell name. Common shell mappings:

  • bash -> VariantBash
  • sh, dash, ash -> VariantPOSIX
  • mksh, ksh -> VariantMksh
  • zsh -> VariantBash (closest approximation)
  • powershell, pwsh, cmd -> VariantNonPOSIX (disables shell linting)
  • unknown -> VariantBash (safe default)

func VariantFromShellCmd

func VariantFromShellCmd(shellCmd []string) Variant

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) IsNonPOSIX

func (v Variant) IsNonPOSIX() bool

IsNonPOSIX returns true if this variant represents a non-POSIX shell. When true, shell-specific linting rules should be disabled because the shell syntax is incompatible with POSIX/Bash parsing.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL