shellx

package
v0.44.0 Latest Latest
Warning

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

Go to latest
Published: Nov 10, 2026 License: MIT Imports: 3 Imported by: 1

Documentation

Overview

Package shellx 提供 POSIX shell 的参数转义与分词原语。

Quote 把任意字符串转义成**一个** shell word;QuoteArgv 把一组参数转义成 一条命令行;Split 是它们的逆运算——把一条命令行拆回 argv(未闭合引号、 尾随转义报错,不做尽力解析)。三者都是纯函数,不做策略判断——是否该拒绝 某个输入由调用方决定(换行、路径越界这类各有各的规则,见 pathx 与本包的文档)。

为什么需要它:把外部输入拼进 shell 命令时,不转义就等于把命令交给对方。 "a.txt; rm -rf /" 是**两条命令**而不是一个文件名;含空格的路径会被拆成 两个参数,于是文件被写到另一个地方——后者更隐蔽,因为命令看起来"成功"了。

转义方式用 POSIX 单引号:单引号内 shell 不做任何解释,所以唯一需要照顾的 是引号本身——关掉、转义、重新打开('\”)。这比反斜杠转义稳:反斜杠在 单引号内是字面量,双引号内又有一堆自己的规则,只有单引号是"里面什么都不 特殊"的。

空字符串是一个容易漏的边界:空参数仍然是一个参数,丢掉它会让 "cmd ” arg" 变成 "cmd arg",参数整体错位。所以 Quote("") 返回 "”"。

用法:

shellx.Quote("/tmp/a b.txt")   // "'/tmp/a b.txt'"
shellx.Quote("it's")            // "'it'\\''s'"
shellx.QuoteArgv("rm", "-f", p) // "rm '-f' '/tmp/x'" —— 逐个转义,程序名自己按需

与 pathx 的关系是两个不同的问题:pathx 问「这个路径合法吗」(越界即拒绝), shellx 问「这个字符串怎么写进命令行才不会被 shell 解释」。两者互补, 路径类输入通常两道都要过。

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrMissingRedirectTarget 表示重定向运算符后面没有目标词
	// (`echo a >`、`echo a > ;`)——与 bash 的语法错误同款,判据侧按拒绝处理。
	ErrMissingRedirectTarget = errors.New("shellx: redirect operator without target")
	// ErrBadRedirect 表示重定向的 fd 数字不是合法整数(位数过长等)。
	// 判不了就报错:宁可让判据拒绝,也不产出「fd 没看清」的半截事实。
	ErrBadRedirect = errors.New("shellx: bad file descriptor in redirect")
	// ErrUnclosedSubstitution 表示替换单元没有闭合(`$(rm -rf /`、
	// “ `whoami “、`<(cmd`、`${VAR`)。替换内容是执行时点的代码,闭合
	// 之前连「一个词在哪结束」都判不出来。
	ErrUnclosedSubstitution = errors.New("shellx: unclosed substitution in command line")
)

词法事实面的哨兵错误。判定纪律与 ErrUnclosedQuote 同口径:判不了就报错、 不尽力解析——尽力解析会把「没有重定向」的假事实交给判据,比不给事实更坏。

View Source
var (
	// ErrUnclosedQuote 表示命令行里有未闭合的引号('abc 或 "abc)。
	// Split 不做尽力解析:尽力解析会把两段判成一段,前缀判据因此被绕过。
	ErrUnclosedQuote = errors.New("shellx: unclosed quote in command line")
	// ErrTrailingEscape 表示命令行以悬挂反斜杠结尾(abc\)。
	ErrTrailingEscape = errors.New("shellx: trailing escape in command line")
)

Functions

func Quote

func Quote(s string) string

Quote quotes a string so that a POSIX shell passes it through as exactly one word, whatever it contains.

The result is always a single-quoted word. Nothing inside single quotes is special to the shell, so the only character that needs care is the quote itself: it is closed, escaped and reopened ("'\”"), which keeps the surrounding word intact.

An empty string becomes "”". An empty argument is still an argument, and dropping it would silently shift the whole command line: "cmd ” arg" must not become "cmd arg".

Quote does not decide what to do with a newline or a path that climbs out of its root -- it only escapes. Those are policy questions, and the caller is the one that knows them.

func QuoteArgv

func QuoteArgv(args ...string) string

QuoteArgv renders a command line from a list of arguments: every argument is quoted, and the results are joined with a single space.

The empty argument list renders as the empty string.

func Split added in v0.37.4

func Split(s string) ([]string, error)

Split 把一条命令行拆成 shell 词(argv),是 Quote/QuoteArgv 的逆运算:

Split(QuoteArgv(args...)) 必须逐项还原 args(含空参数、含引号/空格/
$ 等特殊字符——这条往返判据由用例钉住,Quote 的口径一改 Split 必须跟)。

口径(POSIX shell 词法,只拆词、不展开):

  • 单引号内一切原样(含反斜杠)——与 Quote 的单引号转义互逆;
  • 双引号内反斜杠仅在后跟 " \ $ ` 或换行时是转义(POSIX:\<char> 出该 char,\<newline> 是行拼接、整体消失),其余情况(\x 非 escape 时) 反斜杠与字符一并字面保留;
  • 未加引号的 ; | & 作为**独立 token** 返回,相邻同类合并成 && / ||—— 分隔符不与词粘连、也不丢弃:是否按「多条子命令」处理是判据侧的 严格度,不由语法层决定(#243);
  • 未闭合引号、尾随转义 → 返回 error,不做尽力解析;
  • 空参数保留:” 还原为空参数——丢弃它会让后续参数整体错位 (Quote("") = "”" 的逆);
  • 词内引号段与裸段粘连成一个词(a'b'c → abc,与 shell 一致);
  • 不做变量展开、命令替换、glob——与 Quote 的「只转义、不判策略」同一 立场:展开会让判定看到与执行时点不同的文本。

空白分隔符为空格/制表/换行/回车(CR 按空白处理:从配置/工单粘贴的 CRLF 命令行,行尾 \r 不粘进最后一个 token)。空串与纯空白输入返回空切片。

Types

type Analysis added in v0.44.0

type Analysis struct {
	// Segments 按未引号的 `;` `&&` `||` `|` `&` 机械切分:控制符不与词
	// 粘连、空段保留(首/尾控制符与空段都是事实的一部分,不清理);
	// 纯空白输入没有段。
	Segments []Segment
}

Analysis 是一条命令行的词法事实面(tokenize 结果):词、重定向、分段 三类事实一次给全,安全判定面不用再各自扫字符串。

func Analyze added in v0.44.0

func Analyze(s string) (*Analysis, error)

Analyze 把一条命令行拆成词法事实——Split 的加强面:在拆词之上区分 「词」与「重定向运算符+目标」、给动态词打标记、按控制符分段。给安全 判定面(前缀规则、白名单)当事实源:

a, _ := shellx.Analyze("git status > /etc/passwd")
a.Segments[0].Texts()        // ["git", "status"] —— argv 不含目标
a.Segments[0].Redirects[0]   // {Op: ">", Write: true, Target: "/etc/passwd"}

口径(POSIX shell 词法,只切词、不展开、不判策略):

  • Split 的引号/转义/空参数/控制符口径全部沿用;ErrUnclosedQuote、 ErrTrailingEscape 语义不动;
  • 未引号的 `<` `>` 是重定向运算符(`<(`/`>(` 除外——进程替换是词的 构成部分):`<` `<<` `<<-` `<<<` `<&` `>` `>>` `>|` `>&` `&>` `&>>` 十一种形态都识别;紧邻的裸数字串是显式 fd(`2>` ⇒ Fd 2);
  • 重定向目标是一个词(可引号、可替换);运算符后没有目标词报 ErrMissingRedirectTarget,不猜;
  • 动态词标记:词内含裸露或双引号内的 $( )、反引号、<( )、>( )、 $VAR 即 Dynamic(单引号内一切字面不算);替换单元按括号/引号配对 整体吞掉(bash 词法:`$(rm -rf /)` 是**一个**词,内部空格不切词), 闭合不了报 ErrUnclosedSubstitution,不做尽力解析;
  • 替换单元的内部命令不递归分析:`echo $(a > b)` 报的是「一个动态词」, 内部的 `>` 属于被替换的那段代码——要判它就对替换内容再调一次 Analyze(判据自己决定递归深度)。

与 Split 的分工:Split 是 Quote/QuoteArgv 的逆运算(纯拆词,`>` 是普通 词字符);Analyze 是给判据的事实面(`>` 是运算符)——两者对 `a>b` 的 切分不同是刻意的,往返判据只约束 Split。边界(如实声明):不处理 here-doc 正文(多行正文按普通词处理,判多行命令请按行切开)、不处理 `$'...'` ANSI-C 引用(按字面 `$` 处理)、不展开 `~` 与 glob。

func (*Analysis) HasDynamic added in v0.44.0

func (a *Analysis) HasDynamic() bool

HasDynamic 报告命令行里有没有动态词——普通参数或重定向目标里任一词含 替换单元即置位(`echo $(rm -rf /)` 与 `cat < $(cmd)` 都命中)。

func (*Analysis) HasRedirect added in v0.44.0

func (a *Analysis) HasRedirect() bool

HasRedirect 报告命令行里有没有重定向(任一段、任一侧)。

type Redirect added in v0.44.0

type Redirect struct {
	// Op 是运算符形态(不含 fd 数字):读侧 "<"、"<<"、"<<-"、"<<<"、"<&";
	// 写侧 ">"、">>"、">|"、">&"、"&>"、"&>>"。显式 fd 见 Fd。
	Op string
	// Fd 是紧邻运算符的显式 fd 数字(`2>` ⇒ 2,数字必须与运算符粘连且
	// 全为裸数字——`2 >x` 的 2 是普通参数,与 bash 的 IO_NUMBER 同口径);
	// 未显式为 -1。
	Fd int
	// Write 区分读写两侧:写侧(> 族,含 fd 复制 >&)为 true;读侧(< 族,
	// 含 here-doc/here-string)为 false。「有没有写重定向」看它——
	// `git status > /etc/passwd` 与 `git status >/etc/passwd` 都是写侧。
	Write bool
	// Target 是目标词(文件、here-string 内容、fd 号、"-")。目标是否字面
	// 看 Target.Dynamic:`cat < $(rm -rf build)` 的**读侧**目标含动态同样
	// 危险,只看 op 会放行它。
	Target Word
}

Redirect 是一条重定向的词法事实(词法层即可判定,不需要 AST)。

type Segment added in v0.44.0

type Segment struct {
	// Op 是结束本段的控制符:""(行尾)、";"、"&&"、"||"、"|"、"&"。
	Op string
	// Words 是段内普通参数(argv 语义):**不含**重定向运算符与目标词。
	// 把目标并回 argv 正是「git status > /etc/passwd」绕过前缀判据的根因:
	// 规则只看 argv 前缀,重定向以普通 token 身份排在其后就整类放行了写文件。
	Words []Word
	// Redirects 是段内重定向,按出现顺序。
	Redirects []Redirect
}

Segment 是按控制符切出的一段命令——前缀判据「逐段取最严」的单位。

func (*Segment) Texts added in v0.44.0

func (s *Segment) Texts() []string

Texts 返回段内普通参数的文本序列(前缀判据比对、QuoteArgv 拼装用)。 对它成立的往返判据:Split(QuoteArgv(seg.Texts()...)) == seg.Texts()。

type Word added in v0.44.0

type Word struct {
	// Text 是词文本:词自身的引号按 [Split] 同口径剥离粘连
	// (Split(QuoteArgv(texts...)) 往返判据对 Text 成立);命令替换/进程
	// 替换/变量替换的**原文**(含 $() / 反引号定界符)逐字保留——那是执行
	// 时点会被替换的代码,剥壳即失真。
	Text string
	// Dynamic 标记「执行时点文本可被替换」:词内含裸露或双引号内的 $( )、
	// 反引号、<( )、>( )、$VAR 时置位;单引号内一切字面(含 '$(rm -rf /)')
	// 不算。注意 Text 相同的两个词 Dynamic 可以不同($(rm -rf /) 裸露与
	// '$(rm -rf /)' 的 Text 相同、判据结论相反)——安全判定必须看本字段,
	// 不能只看 Text。
	Dynamic bool
}

Word 是一个 shell 词的词法事实。

Jump to

Keyboard shortcuts

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