news 2026/11/2 21:06:26

Elvish flag: 模块完全指南:掌握 Go 约定与 getopt 约定的命令行参数解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Elvish flag: 模块完全指南:掌握 Go 约定与 getopt 约定的命令行参数解析
  • CLI
  • 编程语言
  • 开发工具

【免费下载链接】elvish

Powerful scripting language & versatile interactive shell

项目地址:https://gitcode.com/gh_mirrors/el/elvish
点击查看免费下载

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:callGo 约定按某个函数的签名解析参数并直接调用该函数,最适合做脚本入口
flag:parseGo 约定按标志规格(specs)列表解析参数,返回标志值 map 与剩余参数
flag:parse-getoptgetopt 约定按规格列表解析短/长标志,返回解析出的标志列表与剩余参数

接下来先讲解两种约定的语法语义,再逐一剖析三个 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三个选项):

  • 可选:在见到标志终止符--后停止解析标志;
  • 可选:在见到第一个非标志参数之前停止解析标志。关闭此项对应 GNUgetopt_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'] ] ▶ [&times=(num 10) &v=$true] ▶ [foo] ~> flag:parse [] [ [v $false 'Verbose'] [times (num 1) 'How many times'] ] ▶ [&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

项目地址:https://gitcode.com/gh_mirrors/el/elvish
点击查看免费下载

相关推荐

上一篇:用 SCSS 插值(Interpolation)为 CSS 自定义属性赋值:TIL 仓库中的 Sass 实战笔记
下一篇:Ubuntu 24.04 ROCm 四步安装

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 22:00:33

在 Kotlin(Android)中使用 Xberg 递归提取 URL 文档链接

后端AI 应用NLP 【免费下载链接】xberg Polyglot document intelligence with a Rust core: extract text, metadata, images, tables, and structured data from 106 formats across 140 file extensions, plus code intelligence for 371 languages. Fifteen bindings, with …

作者头像 李华
网站建设 2026/11/2 21:06:09

pyprobml 软件学习路线指南:Python、ML 工具链与开发环境选型

机器学习深度学习 【免费下载链接】pyprobml Python code for "Probabilistic Machine learning" book by Kevin Murphy 项目地址: https://gitcode.com/gh_mirrors/py/pyprobml 点击查看 免费下载 本文是 pyprobml 项目(《Probabilistic Mac…

作者头像 李华
网站建设 2026/10/20 0:01:18

题解:洛谷 P1104 生日

本文分享的必刷题目是从蓝桥云课、洛谷、AcWing等知名刷题平台精心挑选而来,并结合各平台提供的算法标签和难度等级进行了系统分类。题目涵盖了从基础到进阶的多种算法和数据结构,旨在为不同阶段的编程学习者提供一条清晰、平稳的学习提升路径。 欢迎大家订阅我的专栏:算法…

作者头像 李华
网站建设 2026/10/20 0:01:17

零 GC 委托优化实战:游戏大厂的帧预算保卫战

背景:16.6ms 里没有 GC 的位置 AAA 手游和端游的帧预算是死线,不是建议。60fps 意味着每帧 16.6ms,其中渲染、物理、逻辑、音频都在争这块蛋糕。托管 GC(哪怕是 Unity 的增量式 GC)一旦触发,轻则 0.5ms&…

作者头像 李华
网站建设 2026/10/20 0:01:16

25天打卡训练营实况:从三次弃营到稳定输出的系统方法

这篇稿子的缘起很简单:我今天早上在训练营小程序里按下“1.25训练营打卡”的确认键时,发现自己已经连续打卡 25 天,而这期 30 天的营期只剩下最后 5 天。过去半年里,我报名过三次类似的目标型打卡训练营,分别坚持了 11…

作者头像 李华