go exec cmd /c 处理空格和双引号问题,大家有什么好办法吗? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
LonnyWong
V2EX    程序员

go exec cmd /c 处理空格和双引号问题,大家有什么好办法吗?

  •  
  •   LonnyWong 2023-10-06 22:17:29 +08:00 2228 次点击
    这是一个创建于 743 天前的主题,其中的信息可能已经有所发展或是发生改变。

    开源 issue: https://github.com/trzsz/trzsz-ssh/issues/46

    复现 demo:

    package main import ( "fmt" "os/exec" ) func main() { command := `"C:\WINDOWS\System32\OpenSSH\ssh.exe" -V` // command = `"C:\Program Files\ssh.exe" -V` cmd := exec.Command("cmd", "/c", command) output, _ := cmd.CombinedOutput() fmt.Println(string(output)) } 

    错误输出:

    '\"C:\WINDOWS\System32\OpenSSH\ssh.exe\"' is not recognized as an internal or external command, operable program or batch file. 

    原始问题,用户在 ~/.ssh/config 中配置任意的 ProxyCommand,要用 os/exec 来执行它,并且获取它的标准输出。用 cmd /c 是想避免解释每一个具体的参数是什么,有双引号,有空格,有转义,要准确地解释出每一个参数不简单啊。

    大佬们有什么好想法吗?

    18 条回复    2023-10-07 09:56:26 +08:00
    geelaw
        1
    geelaw  
       2023-10-06 22:31:26 +08:00 via iPhone
    为什么要多此一举透过 cmd ?另外从 go 的 API 设计可以看出它必须用 Unix 的方式传入 argv ,而不是 Windows 的 command line 。而 cmd 是按照 command line 而不是 argv 读取命令的,因为同一组 argv 有无限种不同的 command line 表示,而且这些会被 cmd 理解为不同的意思,所以不存在可靠的用 go 的 exec 调用 cmd 的方法。

    如果你要调用 ssh 并传入 -V ,可以直接传入合适的 argv 。
    geelaw
        2
    geelaw  
       2023-10-06 22:34:44 +08:00 via iPhone
    例子:
    exec.Command("C:\\WINDOWS\\System32\|OpenSSH\\ssh.exe", "-V")

    go 和 ssh 理应 Windows 上能正确互转 command line 和 argv 。
    LonnyWong
        3
    LonnyWong  
    OP
       2023-10-06 22:46:24 +08:00
    @geelaw 因为 command 是未知的,如果你想输入准确的 argv ,那就需要先从一个字符串中解释出准确的 argv 来。

    只是简单的空格分隔是不够的,因为有些参数本身可能就存在空格,然后还会有双引号包起来,然后双引号自己又可能会存在转义。如何保证从字符串中解释出来的参数是准确的?
    geelaw
        4
    geelaw  
       2023-10-06 23:25:51 +08:00 via iPhone   2
    @LonnyWong Windows 上标准解析命令行的方式是有文档的,https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw

    解析方法如下(我随便瞎写的,不一定准确):
    0. 若字符串只有空格,则设置 argc 为 1 ,argv[0] 为空串,否则转到 1
    1. 去掉开头的空格,若已经没有字符,则结束,否则设置状态为无 quote ,设置当前实参为空字符串,转到 2
    2. 下一个字符
    2.1. 如果现在是无 quote 且这个字符是空格,则把当前实参加入 argv 并转到 1
    2.2. 如果接下来有 2n 个 \ 之后紧跟一个 ",则在当前实参中追加 n 个 \,反转 quote 状态,并转到 2
    2.3. (2n+1) \ + ",追加 n 个 \ 和 1 个 ",转到 2
    2.4. 其他字符,则追加当前字符并转到 2
    3. 把最后一个实参加入 argv
    geelaw
        5
    geelaw  
       2023-10-06 23:32:07 +08:00 via iPhone
    另外 cmd 本身还有自己的内部命令和转义,比如 ^、管道、重定向,除非你的目的就是完全按照 cmd 执行命令,否则用 cmd /c 是错误的。

    如果你想用 cmd /c ,那就不能用 argv 传参,而要用直接可以 ShellExecute/CreateProcess 的 API ,这样才能确保命令行准确传递(而不是经过 argv 互转,这个转换对 command line 来说不“往返”,对 argv 往返)。
    shimanooo
        6
    shimanooo  
       2023-10-06 23:51:17 +08:00
    当你想要解析字符串,而不是结构化地处理的时候,你往往在做错误的事情。

    尤其当是这个命令是外部传入的。看看 ImageMagick 2020 年的那个注入漏洞。
    LonnyWong
        7
    LonnyWong  
    OP
       2023-10-07 00:04:11 +08:00
    @geelaw #5 ssh ProxyCommand 的本意就是原样执行,我抽空看看这个 ShellExecute/CreateProcess 是否适用, 需要操作 stdin 和 stdout 。
    LonnyWong
        8
    LonnyWong  
    OP
       2023-10-07 00:05:28 +08:00
    @shimanooo ssh ProxyCommand 是用户自己配置,自己执行的,要注入也是用户自己搞自己。
    tool2d
        9
    tool2d  
       2023-10-07 00:12:06 +08:00
    动态生成一个临时 bat, 包含空格和双引号,用 cmd /c 调用应该是最简单的方法。
    patrickyoung
        10
    patrickyoung  
       2023-10-07 00:17:16 +08:00
    这个问题我之前写自己的玩具的时候遇到过,不过是在 linux 平台,我记得是 exec/cmd 的 godoc 里有写一些注意事项。首楼中 issue 提到的配置可用于测试吗?可以的话我试着调调看,有能力的话给你发个 PR
    lianyue
        11
    lianyue  
       2023-10-07 00:18:18 +08:00
    cmd := exec.Command("cmd", "/c", "C:\WINDOWS\System32\OpenSSH\ssh.exe", "-V")
    LonnyWong
        12
    LonnyWong  
    OP
       2023-10-07 00:30:37 +08:00
    @patrickyoung 可以测的,也可以直接用 ssh 来测:

    ProxyCommand C:\ssh.exe -W %h:%p jumpserver
    LonnyWong
        13
    LonnyWong  
    OP
       2023-10-07 00:33:53 +08:00
    @tool2d 临时 bat 不知会不会引起杀毒软件的误告。
    LonnyWong
        14
    LonnyWong  
    OP
       2023-10-07 01:31:02 +08:00
    ysc3839
        15
    ysc3839  
       2023-10-07 03:24:29 +08:00 via Android
    Windows 和 Unix 进程的一大区别是,Unix 进程参数是字符串数组,而 Windows 进程参数只是一个字符串。因此 Unix 程序无需自行解析参数,参数是由 shell 解析成字符串数组的,而 Windows 则需要程序自己解析参数成字符串数组。楼上几位似乎都没提到这个根本区别。
    @geelaw 有说法称 CommandLineToArgvW 和 MSVC CRT 内置的解析逻辑不同,cmd 的解析逻辑似乎也与前两者不同,我没有实际测试过情况如何,只是提醒一下可能遇到坑。
    geelaw
        16
    geelaw  
       2023-10-07 03:57:19 +08:00 via iPhone
    @ysc3839 #15 我以为之前已经算是提到了这个区别了。cmd 有自己的转义,但 cmd 当然不负责外部命令如何理解命令行。MSVC CRT 解析 argv 不是 CommandLineToArgvW 我倒是不知道,另外我刚发现 CommandLineToArgvW 读取空格开头的字符串时会把第一个 argv 设置为空串 orz

    除了提醒 lpCmdLine 不需要有 argv 的格式,还应该注意即使 lpCmdLine 解析为 argv ,第一个参数也不一定是程序的名字或者路径。
    Kisesy
        17
    Kisesy  
       2023-10-07 09:12:10 +08:00   1
    可以用 golang.org/x/sys/windows 下的 DecomposeCommandLine 函数,内部调用系统的 CommandLineToArgv 函数,所以兼容性非常高
    jorneyr
        18
    jorneyr  
       2023-10-07 09:56:26 +08:00   1
    我是把要执行的命令写入 bat / sh 文件,然后执行文件,这样可以方便的支持管道等复杂命令。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     970 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 19:09 PVG 03:09 LAX 12:09 JFK 15:09
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86