如何在Elixir的System.cmd中执行带管道的USB设备查询命令?
在Elixir的System.cmd中执行带管道的命令
你遇到的问题其实是因为System.cmd和:os.cmd的工作方式不一样::os.cmd会默认通过系统shell来解析命令(包括管道、重定向这些shell语法),但System.cmd是直接调用指定的可执行文件,不会处理shell的特殊符号——所以你传的["|", "grep", ...]会被当成usb-devices的命令行参数,而不是用来触发管道的,自然就执行失败了。
下面给你两种解决办法,按需选择:
方法一:通过shell来执行整个命令
既然管道是shell的功能,那我们可以把整个命令字符串交给shell去执行。在类Unix系统中,通常用bash -c "你的命令"的方式:
case System.cmd("bash", ["-c", "usb-devices | grep -A 1 Product="]) do {output, 0} -> # 命令执行成功,处理输出 IO.puts("已连接的USB设备信息:\n#{output}") {error_msg, exit_code} -> # 命令执行失败,处理错误 IO.puts("获取设备信息失败,退出码:#{exit_code},错误信息:#{error_msg}") end
这种方法的好处是完全复用你原来的shell命令,不需要改逻辑,和:os.cmd的行为最接近;缺点是依赖系统的shell(比如bash),如果你的代码要跨不同系统(比如Windows),可能需要调整。
方法二:用Elixir自己处理输出(更推荐)
为了避免依赖shell,我们可以先单独执行usb-devices获取所有输出,再用Elixir的字符串/正则工具过滤出需要的内容。这样更符合Elixir的函数式风格,也更跨平台:
case System.cmd("usb-devices", []) do {full_output, 0} -> # 过滤出Product=开头的行,以及每行后面紧跟的一行(对应grep -A 1的效果) filtered_lines = full_output |> String.split("\n") |> Enum.reduce([], fn line, acc -> if String.starts_with?(line, "Product=") do # 找到当前行的索引,把当前行和下一行加入结果 index = Enum.find_index(String.split(full_output, "\n"), &(&1 == line)) next_line = Enum.at(String.split(full_output, "\n"), index + 1, "") acc ++ [line, next_line] else acc end end) |> Enum.reject(&(&1 == "")) # 去掉空行 IO.puts("已连接的USB设备信息:\n#{Enum.join(filtered_lines, "\n")}") {error_msg, exit_code} -> IO.puts("执行usb-devices失败,退出码:#{exit_code},错误信息:#{error_msg}") end
或者用正则表达式更简洁地匹配:
case System.cmd("usb-devices", []) do {full_output, 0} -> # 匹配Product=开头的行,以及后面的一行 matches = Regex.scan(~r/Product=.*?(?:\n.*?)?/, full_output) IO.puts("已连接的USB设备信息:\n#{Enum.join(List.flatten(matches), "\n")}") {error_msg, exit_code} -> IO.puts("执行usb-devices失败,退出码:#{exit_code},错误信息:#{error_msg}") end
这种方法的好处是不依赖shell,代码更可控,跨平台性更好;缺点是需要自己实现grep的过滤逻辑,不过对于简单的场景来说完全没问题。
为什么System.cmd不处理管道?
System.cmd的设计目标是直接调用可执行文件,避免shell注入的风险,同时让命令执行的行为更明确。如果需要shell的特性,就显式通过bash -c来调用,这样开发者能清楚知道自己在依赖shell的功能。
内容的提问来源于stack exchange,提问作者D.R
相关产品推荐
相关产品推荐

