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

2023-10-06 22:17:29 +08:00
 LonnyWong

开源 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 是想避免解释每一个具体的参数是什么,有双引号,有空格,有转义,要准确地解释出每一个参数不简单啊。

大佬们有什么好想法吗?

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

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

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

只是简单的空格分隔是不够的,因为有些参数本身可能就存在空格,然后还会有双引号包起来,然后双引号自己又可能会存在转义。如何保证从字符串中解释出来的参数是准确的?
geelaw
2023-10-06 23:25:51 +08:00
@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
2023-10-06 23:32:07 +08:00
另外 cmd 本身还有自己的内部命令和转义,比如 ^、管道、重定向,除非你的目的就是完全按照 cmd 执行命令,否则用 cmd /c 是错误的。

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

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

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

除了提醒 lpCmdLine 不需要有 argv 的格式,还应该注意即使 lpCmdLine 解析为 argv ,第一个参数也不一定是程序的名字或者路径。
Kisesy
2023-10-07 09:12:10 +08:00
可以用 golang.org/x/sys/windows 下的 DecomposeCommandLine 函数,内部调用系统的 CommandLineToArgv 函数,所以兼容性非常高
jorneyr
2023-10-07 09:56:26 +08:00
我是把要执行的命令写入 bat / sh 文件,然后执行文件,这样可以方便的支持管道等复杂命令。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/979299

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX