- CLI
- 编程语言
- 开发工具
【免费下载链接】elvish
Powerful scripting language & versatile interactive shell
flag:是 Elvish 标准库中用于解析命令行标志(command-line flags)的模块。本文以 website/ref/flag.md 为骨架,结合 pkg/mods/flag/flag.go、pkg/getopt/getopt.go 等源码与测试,完整讲解两种主流标志语法约定(Go 约定与 getopt 约定)、三个核心 API(flag:call、flag:parse、flag:parse-getopt)的用法与底层原理,帮助你为 Elvish 脚本写出健壮、可维护的命令行参数解析逻辑。
模块概览:一套工具,两种语法约定
flag:模块为 Elvish 脚本提供命令行标志解析能力,它同时支持两种不同的标志约定:
- Go 约定:每个标志形如
-flag=value,与 Elvish 自身命令(elvish)以及 Go 标准库flag包的风格一致,官方推荐用于 Elvish 脚本; - getopt 约定:标志分短形式(
-f)与长形式(--flag)两种,适合编写包装(wrap)那些遵循 GNU/BSD getopt 风格的外部程序的脚本。
从源码注册看,模块通过eval.BuildNsNamed("flag")构建命名空间,并向其中注册了三个 Go 函数:call、parse、parse-getopt(见 pkg/mods/flag/flag.go)。该命名空间随后在 pkg/mods/mods.go 中通过ev.AddModule("flag", flag.Ns)挂载为标准库模块,因此脚本中直接use flag即可使用。
三个函数的定位各不相同:
| 函数 | 语法约定 | 典型用途 |
|---|---|---|
flag:call | Go 约定 | 按某个函数的签名解析参数并直接调用该函数,最适合做脚本入口 |
flag:parse | Go 约定 | 按标志规格(specs)列表解析参数,返回标志值 map 与剩余参数 |
flag:parse-getopt | getopt 约定 | 按规格列表解析短/长标志,返回解析出的标志列表与剩余参数 |
接下来先讲解两种约定的语法语义,再逐一剖析三个 API。
Go 约定:-flag=value与解析停止规则
Go 约定下,每个标志看起来像-flag=value。它的核心规则如下:
布尔标志:-flag等价于-flag=true;非布尔标志:-flag会把紧随其后的下一个参数当作自己的值,即-flag value等价于-flag=value。
解析停止:标志解析在遇到第一个非标志参数之前停止;或者,在遇到标志终止符--之后停止(--之后的参数全部视为非标志参数,即使它们以-开头)。
假设-verbose是布尔标志、-port是非布尔标志,官方文档给出三个典型示例:
- 在
-port 10 foo -x中,port标志值为10,其余参数(foo -x)都是非标志参数; - 在
-verbose 10 foo -x中,verbose标志值为true(因为它是布尔标志,不会吞掉10),其余参数(10 foo -x)都是非标志参数; - 在
-port 10 -- -verbose foo中,port标志值为10,--之后的部分(-verbose foo)都是非标志参数。
此外还支持--flag写法,它与-flag完全等价。
注意:Go 约定不支持单字母标志的链式合并——-rf是一个名为rf的标志,而不是-r -f的简写。这是它与 getopt 约定最显著的区别之一。
从实现看,Go 约定直接复用了 Go 标准库的flag.FlagSet:pkg/mods/flag/flag.go 中的newFlagSet创建flag.NewFlagSet(name, flag.ContinueOnError)并将输出丢弃到io.Discard,随后调用fs.Parse(args)完成解析;addFlag则按默认值的 Go 类型分派到fs.Bool、fs.String或自定义fs.Var(详见下文flag:parse一节)。因此 Go 约定下的所有行为(含--终止、-flag value吞参等)都与 Go 标准库flag包一致。
getopt 约定:短标志、长标志与参数三元组
getopt 约定下,一个标志可以有以下两种形式(可以只有其中一种,也可以两者都有):
- 短形式:单个字符前面加
-,如-f; - 长形式:字符串前面加
--,如--flag。
一个标志可以携带三种参数形态:
- 无参数:如
-f或--flag; - 必需参数:如
-f value、-fvalue、--flag=value或--flag value; - 可选参数:如
-f、-fvalue、--flag或--flag=value。
短标志链式:不带参数的短标志后面可以紧跟另一个短标志。例如,若-r不带参数,则-rf等价于-r -f。链式可以继续延伸:后续的短标志如果也不带参数,就继续接下一个短标志;如果带参数,则后面剩余字符就是它的参数。假设-f和-v不带参数、-p带参数,则:
-rfv等价于-r -f -v;-rfp80等价于-r -f -p 80。
上述链式逻辑在 pkg/getopt/getopt.go 的parseShort中逐字符实现:遇到无参数标志就继续循环,遇到带参数标志则把剩余字符截取为其参数并停止。
getopt 的三个可配置行为
getopt 约定的部分行为可以按需开关(对应flag:parse-getopt的&stop-after-double-dash、&stop-before-non-flag、&long-only三个选项):
- 可选:在见到标志终止符
--后停止解析标志; - 可选:在见到第一个非标志参数之前停止解析标志。关闭此项对应 GNU
getopt_long的行为,开启此项对应 BSDgetopt_long的行为; - 可选:只支持长标志,且长标志允许以单个
-开头。开启此项对应getopt_long_only以及 Go 约定的行为。
在 pkg/getopt/getopt.go 中,这些行为被建模为Config位标志常量:StopAfterDoubleDash、StopBeforeFirstNonOption、LongOnly,并预置了两个组合值——GNU = StopAfterDoubleDash、BSD = StopAfterDoubleDash | StopBeforeFirstNonOption。flag:parse-getopt正是通过parseGetoptOptions.Config()(见 pkg/mods/flag/flag.go)把三个选项翻译成这些位标志,再交给getopt.Parse执行。
flag:call:按函数签名解析并调用
flag:call的完整签名(见 pkg/mods/flag/flag.d.elv)为:
flag:call $fn $args &on-parse-error=$nil它按照 Go 约定从$args中解析标志,然后调用用户自定义函数$fn。$fn必须是用户定义的函数(不能是内置函数或外部命令);$fn的每个选项参数(&opt)对应一个标志,标志的默认值决定其类型与行为(规则与下文flag:parse一致)。解析完成后,非标志参数按顺序作为$fn的位置参数传入。
官方示例:
~> use flag ~> fn f {|&verbose=$false &port=(num 8000) name| put $verbose $port $name } ~> flag:call $f~ [-verbose -port 80 a.c] ▶ $true ▶ (num 80) ▶ a.c ~> flag:call $f~ [-unknown-flag] &on-parse-error={|_| echo 'bad usage' } bad usage ~> flag:call $f~ [-verbose a b c] &on-parse-error={|_| echo 'bad usage' } bad usage注意第二个与第三个例子:前者是非法标志,后者是解析后参数个数与$fn期望不符——两者都会触发&on-parse-error。&on-parse-error是一个函数,会收到一个描述错误条件的参数(目前该参数应视为不透明值,未来会暴露更多有用字段);若不提供该选项,这类错误会直接作为异常抛出。
从源码看(pkg/mods/flag/flag.go),call先扫描$fn的选项名与默认值逐个添加标志,调用fs.Parse(args);解析出错时若提供了&on-parse-error则调用之。解析成功后,把标志值收集进 map 作为选项实参,非标志参数则通过convertStringArgs转成字符串列表作为位置实参,最终fn.Call(fm.Fork(), ...)完成调用;当调用因参数个数不匹配(errs.ArityMismatch)失败时,同样会转交&on-parse-error。
实战:用它编写脚本入口
flag:call最典型的用途是让 Elvish 脚本接受命令行参数。例如脚本a.elv包含(见 pkg/mods/flag/flag.d.elv):
use flag fn main { |&verbose=$false &port=(num 8000) name| # 脚本主体... } flag:call $main~ $args &on-parse-error={|_| echo 'Usage: '(src)[name]' [-verbose] [-port PORT-NUM] name' exit 1 }随后即可这样运行:
~> elvish a.elv -verbose -port 80 foo源码注释(pkg/mods/flag/flag.go)指出了该 API 的一个已知局限:&on-parse-error虽可输出自定义用法文本,但需要手工重复标志名与参数名;同时 lambda 签名语法中没有描述选项的合适位置,所有标志描述为空,无法依赖 Goflag.FlagSet.PrintDefaults的默认帮助文本。官方表示未来可能用不同的 API 解决。
仓库自身也大量使用该模式:构建脚本 tools/buildall.elv 中,main函数通过&name=elvish &variant='' &keep-bin=$false声明选项,末尾用flag:call $main~ $args &on-parse-error={|_| print $usage; exit 1}解析并调用,同时把$usage字符串作为出错时的用法提示输出。
flag:parse:按规格列表解析,返回标志值 map
flag:parse的完整签名(见 pkg/mods/flag/flag.d.elv)为:
flag:parse $args $specs$args是要解析的字符串列表;$specs是标志规格列表,每项形如:
[ [flag default-value 'description of the flag'] ... ]每个规格由三部分组成:标志名(不含开头的-)、默认值、描述文字。默认值决定标志字符串如何被转换:
- 布尔值:该标志是布尔标志(Go 约定的布尔语义生效)。标志值
0、f、F、false、False、FALSE转为$false;1、t、T、true、True、TRUE转为$true;其他值非法。 - 字符串:不做任何转换,原样保留。
- 类型化数字(见 website/ref/language.md):标志值通过
num转换。 - 列表:标志值按
,切分(等价于{|s| put [(str:split , $s)] })。 - 以上皆非:抛出异常。
成功时输出两个值:一个 map,包含$specs中定义的所有标志的值(无论是否出现在$args中,未出现的取默认值);一个列表,包含非标志参数。
官方示例:
~> flag:parse [-v -times 10 foo] [ [v $false 'Verbose'] [times (num 1) 'How many times'] ] ▶ [×=(num 10) &v=$true] ▶ [foo] ~> flag:parse [] [ [v $false 'Verbose'] [times (num 1) 'How many times'] ] ▶ [×=(num 1) &v=$false] ▶ []第一个例子演示了-v(布尔)与-times 10(数字)的解析;第二个例子演示空参数列表时输出默认值。
源码:默认值驱动的类型分派
pkg/mods/flag/flag.go 的addFlag按默认值类型分派:bool走fs.Bool,string走fs.String,int、*big.Int、*big.Rat、float64走自定义的numFlag(其Set通过vals.ScanToGo把字符串扫描为vals.Num,见 L154-L158),vals.List走listFlag(其Set把字符串按,切分为列表,见 L160-L168);其余类型直接返回bad value: flag default value must be boolean, number, string or list异常——这正是测试 pkg/mods/flag/flag_test.elvts 中 map 类型默认值被拒绝的原因。解析完成后,fs.VisitAll遍历所有标志,把flag.Getter.Get()的结果逐一Assoc进vals.EmptyMap(L123-L127),从而保证未出现的标志也带默认值出现在结果 map 中。
flag:parse-getopt:按 getopt 规格解析短/长标志
flag:parse-getopt的完整签名(见 pkg/mods/flag/flag.d.elv)为:
flag:parse-getopt $args $specs &stop-after-double-dash=$true &stop-before-non-flag=$false &long-only=$false$specs是标志规格列表,每项是一个 map:
[ [&short=f &long=flag &arg-optional=$false &arg-required=$false] ... ]规格字段规则:
&short与&long分别是不带-/--前缀的短形式与长形式。短形式若非空必须恰好一个字符;&short与&long至少一个非空。&arg-optional与&arg-required分别声明标志带可选参数还是必需参数;两者至多一个为真。- 规格 map 中出现多余键不是错误(会被原样保留并回显)。
三个选项的语义见上文"getopt 的三个可配置行为":&stop-after-double-dash(默认$true)控制是否在--后停止;&stop-before-non-flag(默认$false)控制是否在首个非标志参数前停止;&long-only(默认$false)控制是否只支持长标志且允许单-前缀。
成功时输出两个值:一个列表描述所有解析出的标志,以及一个包含非标志参数的列表。前者形如:
[ [&spec=... &arg=value &long=$false] ... ]其中每个条目包含:该标志的原始规格(&spec)、其参数(&arg,无参数时为'')、以及该标志是否以长形式出现(&long)。
官方示例(部分输出为可读性重新排版):
~> var specs = [ [&short=v &long=verbose] [&short=p &long=port &arg-required] ] ~> flag:parse-getopt [-v -p 80 foo] $specs ▶ [[&arg='' &long=$false &spec=[&long=verbose &short=v]] [&arg=80 &long=$false &spec=[&arg-required=$true &long=port &short=p]]] ▶ [foo] ~> flag:parse-getopt [--verbose] $specs ▶ [[&arg='' &long=$true &spec=[&long=verbose &short=v]]] ▶ [] ~> flag:parse-getopt [-v] [[&short=v &extra-info=foo]] # 规格中的多余键 ▶ [[&arg='' &long=$false &spec=[&extra-info=foo &short=v]]] ▶ []源码:规格校验与选项映射
pkg/mods/flag/flag.go 中,每个规格 map 被扫描进specStruct,再由OptionSpec()方法校验并转换为getopt.OptionSpec:&short与&long同时为空返回errShortLong(at least one of &short and &long must be non-empty);&arg-required与&arg-optional同时为真返回errArgRequiredArgOptional(at most one of &arg-required and &arg-optional may be true);参数形态映射为getopt.NoArgument/RequiredArgument/OptionalArgument三种Arity。解析通过getopt.Parse(args, specs, opts.Config())完成,随后把每个结果的Spec回指原始规格 map(originalSpecMap),并组装出包含&spec、&arg、&long的条目列表(L244-L258)。
关联:与编辑补全协作
flag:parse-getopt的规格设计与编辑器补全 APIedit:complete-getopt同源——后者同样接受$opt-specs(每个 map 含short、long、arg-optional、arg-required、desc、completer等键)和按位置排列的$arg-handlers函数列表,用于在命令行输入-/--时提供选项补全(见 pkg/edit/complete_getopt.d.elv)。两个 API 共享 getopt 规格心智模型,因此你可以在脚本中同时使用flag:parse-getopt解析、edit:complete-getopt补全,保持行为一致。
测试与验证
模块的行为由两组测试保证:
- pkg/mods/flag/flag_test.elvts:基于 Elvish 转录(transcript)的端到端测试,覆盖
flag:call的布尔/字符串/数字标志、$nil参数与错误回调;flag:parse的四种默认值类型、多标志与位置参数混用、非法标志与非法默认值类型;flag:parse-getopt的必需/可选参数、三个选项开关(&stop-after-double-dash、&stop-before-non-flag、&long-only)各自的开/关行为、规格校验错误(如[[&]]与&short=x &arg-optional &arg-required)以及未知选项错误(unknown option -x)。 - pkg/mods/flag/flag_test.go:通过
//go:embed *.elvts *.elv把上述转录文件嵌入测试二进制,用evaltest.TestTranscriptsInFS统一驱动执行。
此外,pkg/getopt/getopt.go 还额外提供Complete函数(L140-L184),能在参数不完整时返回上下文信息(Context及其ContextType),供补全引擎使用——这正是它与编辑器补全能力衔接的底层基础。
小结
flag:模块以两个小而精的语法约定覆盖了绝大多数命令行标志场景:Go 约定面向 Elvish 脚本自身(-flag=value、--终止、默认值驱动类型),getopt 约定面向包装外部程序(短/长标志、链式合并、三种参数形态、三个可开关行为)。配合flag:call的"按函数签名直接解析调用"模式和&on-parse-error的用法提示定制,你只需几行代码就能为脚本写出专业、健壮的命令行入口;edit:complete-getopt则可让同一套规格同时获得交互式补全体验。
- CLI
- 编程语言
- 开发工具
【免费下载链接】elvish
Powerful scripting language & versatile interactive shell
相关推荐
如何用League Akari智能辅助工具提升英雄联盟游戏效率:完整体验指南
如何用League Akari智能辅助工具提升英雄联盟游戏效率:完整体验指南 League Akari是一款基于GPL 3.0许可证的开源英雄联盟智能辅助工具,
编程语言语言运行时解释器标准库Go 1.18+自定义泛型终极指南:从零掌握类型参数与约束
Go 1.18+自定义泛型终极指南:从零掌握类型参数与约束 Go语言在1.18版本正式引入了期待已久的自定义泛型功能!🎉 本教程将带你从零开始,全面掌握Go泛
教程文档后端Jina CLI 完全指南:掌握 `jina` 命令行的架构、命令与参数
Jina CLI 完全指南:掌握 jina 命令行的架构、命令与参数 本文是 Jina 命令行接口(CLI)的权威技术指南。Jina 是一个用于构建多模态 AI
后端人工智能模型推理服务微服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考