开始
今天的主题是如何解析ahk命令行,即如何将一条命令行拆分成开关、脚本路径及脚本参数。
- 首先,ahk支持的命令行格式如下:
AutoHotkey.exe [Switches] [Script Filename] [Script Parameters]
ahk源码中也有对应描述:
Any special flags (e.g. /force and /restart) must appear prior to the script filespec.
The script filespec (if present) must be the first non-backslash arg.
All args that appear after the filespec are considered to be parameters for the script
and will be stored in A_Args
- 然后,对于开关部分,有些无值,有些是有值的,比如
/include
,ahk是这样做的:
实际上,这些开关我常用的只有/force
和/restart
,所以在我的代码中没有特殊处理这些开关。
-
再是脚本名,它有些特殊处;比如可以是
*
号、可以省略,具体在文档中有介绍。 -
最后的脚本参数只有一个要注意的点,就是空格用引号包围。
代码
我的思路是这样的,逐行逐字符解析,当:
- 遇到转义符时,判断后面字符是否为引号
- 如果是,则记录引号
- 否则,记录转义符(意味着转义符无需再次转义)
- 遇到引号时
- 如果已经在引号内,记录,并切换引号状态(是否在引号内)
- 否则仅切换引号状态
- 遇到空格且不在引号内,说明读取完了部分参数,记录参数
- 其他情况记录当前字符
变成代码如下:
ParseCmdLine(cmdLine) {
if !cmdLine
throw Error('invalid command line:blank input')
switchs := [], args := [], _s := '', _q := false, i := 1
while i <= cmdLine.length {
if (_c := cmdLine.charAt(i)) = '\' && i < cmdLine.length && cmdLine.charAt(i + 1) = '"' {
_s .= '"', i++
} else if _c = '"' {
if _q
_s[1] = '/' ? switchs.push(_s) : args.push(_s), _s := ''
_q := !_q
} else if _c = ' ' && !_q {
if _s.length > 0
_s[1] = '/' ? switchs.push(_s) : args.push(_s), _s := ''
} else _s .= _c
i++
}
if _s.length > 0
args.push(_s)
if args.Length < 2
throw Error('invalid command line:bad command line')
return { ahkExePath: args.shift(), switchs: switchs, filespec: args.shift(), params: args }
}
在命名方面有些抽象,但是我写ahk总是这样。
应用
上面的解析代码不只可用于解析ahk命令行,我将它用在了MeowTool
的命令解析上。只是命令形式是下面这样:
cmd [-param] [-key=val] [target] [extra]
解析代码如下:
Parse(cmd) {
try ReplaceAlias(&cmd)
catch as e
return _fail(e.Message)
_esc := '\', _qc := "'"
args := [], switchs := [], p := [], kp := {}, _s := '', i := 1, _q := false
while i <= cmd.Length {
if (_c := cmd.charAt(i)) = _esc && i < cmd.length && cmd.charAt(i + 1) = _qc {
_s .= _qc, i++
} else if _c = _qc {
if _q
_push(_s), _s := ''
_q := !_q
} else if _c = A_Space && !_q {
if _s.length > 0
_push(_s)
_s := ''
} else _s .= _c
i++
}
if _s.length > 0
args.push(_s)
return _succ(args.shift(), {
params: p,
kvparams: kp,
target: args.Length ? args.shift() : '',
extra: args,
raw: cmd
})
_push(s) {
if s[1] != '-'
args.push(s)
if s.Length = 1
return
if _ := InStr(s, '=')
kp[s.substring(2, _)] := s.substring(_ + 1)
else p.Push(_s.substring(2))
}
_succ(w, o) => { valid: true, which: w, parsed: o }
_fail(msg) => { valid: false, msg: msg }
}
可以看到关键部分是一致的。