如何在Crystal中执行一个Shell脚本并捕获输出?

7
我想在处理标准输出和标准错误输出时执行一个 shell 脚本。目前我使用 Process.run 命令来执行命令,其中 shell=false 并且有三个管道分别用于标准输入、标准输出和标准错误输出。我会生成 fiber 来读取标准输出和标准错误输出并将其记录或以其他方式处理。这对于单个命令运行得非常好,但对于脚本则失败了。
我可以在调用 Process.run 时将 shell=true,但看起来 Crystal 的源代码只是简单地将 "sh" 添加到命令行开头。我尝试了把 "bash" 添加到命令行开头,但是没有帮助。
像重定向 (>file) 和管道 (如 curl something | bash) 这样的事情似乎无法使用 Process.run 命令完成。
例如,要下载一个 shell 脚本并执行它,我尝试了以下命令:

cmd = %{bash -c "curl http://dist.crystal-lang.org/apt/setup.sh" | bash}

Process.run(cmd, ...)

在初始的 bash 中添加了管道操作符,希望它可以起作用,但看起来并没有帮助。我还尝试了逐个执行每个命令:

script.split("\n").reject(/^#/, "").each { Process.run(...) }

但是当一个命令使用重定向或管道时,这种方法仍然失败。例如,命令 echo "deb http://dist.crystal-lang.org/apt crystal main" >/etc/apt/sources.list.d/crystal.list 只会输出:

"deb http://dist.crystal-lang.org/apt crystal main" >/etc/apt/sources.list.d/crystal.list`

如果我使用反引号的方法 (``) 来执行,则可能可以运行。但那样我就无法实时捕获输出了。

你需要将管道移动到第一个情况的 bash -c 中才能调用它。但是,你要对抗 shell=false,这是允许你直接使用 shell 语法的东西。 - Anya Shenanigans
@Petesh 很遗憾,设置 shell=true 也没有帮助。 - Sod Almighty
@Petesh 将管道移入 bash -c 调用似乎可以解决问题。那么...为什么 shell=true 不起作用呢?它肯定几乎是一样的东西吧? - Sod Almighty
3个回答

13

这个问题是一个UNIX问题。父进程必须能够访问子进程的STDOUT。使用管道,您必须启动一个Shell进程,它将运行整个命令,包括| bash而不仅仅是curl $URL。在Crystal中,这是:

command = "curl http://dist.crystal-lang.org/apt/setup.sh | bash"
io = MemoryIO.new
Process.run(command, shell: true, output: io)
output = io.to_s

或者,如果您想复制Crystal为您所做的操作:

Process.run("sh", {"-c", command}, output: io)

5
我基于阅读run.cr文件的源代码来理解。它的行为方式与其他语言处理命令和参数的方式非常相似。
如果没有使用shell=trueProcess.run的默认行为是使用命令作为要运行的可执行文件。这意味着字符串需要是一个程序名称,没有任何参数,例如uname是一个有效的名称,因为我的系统中有一个在/usr/bin下的叫做uname的程序。
如果你曾经成功地使用%{bash -c "echo hello world"}shell=false,那么说明有问题——默认行为应该是尝试运行一个名为bash -c "echo hello world"的程序,这在任何系统上都不可能存在。
一旦传入了shell=true,它就会执行sh -c <command>,这将允许像echo hello world这样的字符串作为命令工作;这也将允许重定向和管道工作。 shell=true的行为通常可以解释为执行以下操作:
cmd = "sh"
args = [] of String
args << "-c" << "curl http://dist.crystal-lang.org/apt/setup.sh | bash"
Process.run(cmd, args, …)

请注意,这里使用了一个参数数组——如果没有参数数组,你就无法控制参数如何传递到shell中。
第一个版本无论是否使用shell=true都无法工作的原因是管道在-c之外,而-c是你发送给bash的命令。

我认为问题是由于我将命令字符串分割为“cmd”(字符串)和“args”(数组),然后调用Process.new(cmd,args,...)。即使使用shell=true,它也无法工作 - 可能是因为我已经将“|”拆分为其自己的参数... - Sod Almighty
基本上,我最终调用了 Process.new("curl", ["http://dist.crystal-lang.org/apt/setup.sh", "|", "bash"], shell: true)。当然这并没有起作用。为了支持 shell=true,我需要修改我的包装函数,以避免在 shell 为真时将命令拆分成单词。 - Sod Almighty

2

如果你想调用一个Shell脚本并获取输出,我刚刚尝试了Crystal 0.23.1,它可以正常工作!

def screen
    output = IO::Memory.new
     Process.run("bash", args: {"lib/bash_scripts/installation.sh"}, output: output)
     output.close
    output.to_s
end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接