如何从 Ruby 调用 shell 命令

Posted

技术标签:

【中文标题】如何从 Ruby 调用 shell 命令【英文标题】:How to call shell commands from Ruby 【发布时间】:2010-09-05 08:36:31 【问题描述】:

如何从 Ruby 程序内部调用 shell 命令?然后如何将这些命令的输出返回到 Ruby?

【问题讨论】:

虽然这个问题很有用,但问得不好。 Ruby 有许多调用子shell 的方法,这些方法都有很好的文档记录,通过阅读Kernel 和Open3 文档并在SO 上搜索即可轻松找到。 遗憾的是,这个话题相当复杂。 Open3 (docs) 是大多数情况下的最佳选择,IMO,但在旧版本的 Ruby 上,它不会尊重修改后的 PATH (bugs.ruby-lang.org/issues/8004),这取决于你如何传递参数(特别是,如果您使用带有非关键字的 opts 哈希),它可能会中断。但是,如果你遇到了这些情况,那么你正在做一些非常高级的事情,你可以通过阅读 Open3 的实现来弄清楚该怎么做。 我很惊讶没有人提到Shellwords.escape (doc)。您不想将用户输入直接插入到 shell 命令中 - 首先将其转义!另见command injection。 【参考方案1】:

您可以使用下面的format方法打印一些信息:

puts format('%s', `ps`)
puts format('%d MB', (`ps -o rss= -p #Process.pid`.to_i / 1024))

【讨论】:

【参考方案2】:

不确定 shell 命令。我使用以下方法将系统命令的输出捕获到变量 val

val = capture(:stdout) do
  system("pwd")
end

puts val

缩短版:

val = capture(:stdout)  system("pwd") 

capture方法由active_support/core_ext/kernel/reporting.rb

提供

同样,我们也可以使用:stderr 捕获标准错误

【讨论】:

【参考方案3】:

此解释基于我的一位朋友对Ruby script 的评论。如果您想改进脚本,请随时通过链接更新。

首先,请注意,当 Ruby 调用 shell 时,它通常调用 /bin/sh不是 Bash。 /bin/sh 并非在所有系统上都支持某些 Bash 语法。

以下是执行 shell 脚本的方法:

cmd = "echo 'hi'" # Sample string that can be used

    Kernel#`,俗称反引号——`cmd`

    这与许多其他语言一样,包括 Bash、php 和 Perl。

    返回 shell 命令的结果(即标准输出)。

    文档:http://ruby-doc.org/core/Kernel.html#method-i-60

    value = `echo 'hi'`
    value = `#cmd`
    

    内置语法,%x( cmd )

    x 字符后面是分隔符,可以是任何字符。 如果分隔符是字符([< 之一, 文字由直到匹配的结束分隔符的字符组成, 考虑到嵌套的分隔符对。对于所有其他分隔符, 文字包含直到下一次出现的字符 分隔符。允许字符串插值# ...

    返回 shell 命令的结果(即标准输出),就像反引号一样。

    文档:https://docs.ruby-lang.org/en/master/syntax/literals_rdoc.html#label-Percent+Strings

    value = %x( echo 'hi' )
    value = %x[ #cmd ]
    

    Kernel#system

    在子shell中执行给定的命令。

    如果找到并成功运行命令,则返回true,否则返回false

    文档:http://ruby-doc.org/core/Kernel.html#method-i-system

    wasGood = system( "echo 'hi'" )
    wasGood = system( cmd )
    

    Kernel#exec

    通过运行给定的外部命令替换当前进程。

    不返回,当前进程被替换,不再继续。

    文档:http://ruby-doc.org/core/Kernel.html#method-i-exec

    exec( "echo 'hi'" )
    exec( cmd ) # Note: this will never be reached because of the line above
    

这里有一些额外的建议: $?$CHILD_STATUS 相同,如果使用反引号system()%x,则访问最后一个系统执行命令的状态。 然后您可以访问exitstatuspid 属性:

$?.exitstatus

更多阅读请看:

http://www.elctech.com/blog/i-m-in-ur-commandline-executin-ma-commands http://blog.jayfields.com/2006/06/ruby-kernel-system-exec-and-x.html http://tech.natemurray.com/2007/03/ruby-shell-commands.html

【讨论】:

我需要在生产服务器上记录我的可执行文件的输出,但没有找到办法。我使用 puts #cmd 和 logger.info(#cmd)。有没有办法在生产中记录他们的输出? 还有 IO#popen() 和 Open3#popen3()。 mentalized.net/journal/2010/03/08/… 为了完整起见(我最初认为这也是一个 Ruby 命令):Rake 具有 sh,它执行“运行系统命令 cmd。如果给出多个参数该命令不与 shell 一起运行(与 Kernel::exec 和 Kernel::system 语义相同)”。 默认情况下,反引号不会捕获 STDERR。如果要捕获,请将 `2>&1` 添加到命令 如果它说反引号和 %x 返回给定命令的“输出”而不是“结果”,我认为这个答案会略有改进。后者可能被误认为是退出状态。还是只有我一个人?【参考方案4】:

这不是一个真正的答案,但也许有人会觉得它有用:

在 Windows 上使用 TK GUI 时,您需要从 ruby​​w 调用 shell 命令,您总是会弹出一个恼人的 CMD 窗口,持续时间不到一秒钟。

为避免这种情况,您可以使用:

WIN32OLE.new('Shell.Application').ShellExecute('ipconfig > log.txt','','','open',0)

WIN32OLE.new('WScript.Shell').Run('ipconfig > log.txt',0,0)

两者都将ipconfig 输出存储在log.txt 中,但不会出现任何窗口。

你需要在你的脚本中require 'win32ole'

system()exec()spawn() 在使用 TK 和 ruby​​w 时都会弹出那个烦人的窗口。

【讨论】:

【参考方案5】:

给定一个类似attrib的命令:

require 'open3'

a="attrib"
Open3.popen3(a) do |stdin, stdout, stderr|
  puts stdout.read
end

我发现虽然这种方法不如

system("thecommand")

`thecommand`

在反引号中,与其他方法相比,这种方法的一个好处是 反引号似乎不允许我 puts 运行命令/存储我想在变量中运行的命令,而 system("thecommand") 似乎不允许我获得输出,而这种方法让我可以做到这两个东西,它让我可以独立访问标准输入、标准输出和标准错误。

参见“Executing commands in ruby”和Ruby's Open3 documentation。

【讨论】:

【参考方案6】:

反引号 (`) 方法是从 Ruby 调用 shell 命令的最简单方法。它返回 shell 命令的结果:

     url_request = 'http://google.com'
     result_of_shell_command = `curl #url_request`

【讨论】:

【参考方案7】:

如果您有比普通情况更复杂且无法使用`` 处理的情况,请查看Kernel.spawn()。这似乎是 Ruby 提供的用于执行外部命令的最通用/功能最全面的。

你可以用它来:

创建进程组 (Windows)。 将输入、输出、错误重定向到文件/彼此。 设置环境变量、umask。 在执行命令之前更改目录。 为 CPU/数据/等设置资源限制。 做所有可以在其他答案中使用其他选项完成的事情,但需要更多代码。

Ruby documentation 有足够好的例子:

env: hash
  name => val : set the environment variable
  name => nil : unset the environment variable
command...:
  commandline                 : command line string which is passed to the standard shell
  cmdname, arg1, ...          : command name and one or more arguments (no shell)
  [cmdname, argv0], arg1, ... : command name, argv[0] and zero or more arguments (no shell)
options: hash
  clearing environment variables:
    :unsetenv_others => true   : clear environment variables except specified by env
    :unsetenv_others => false  : dont clear (default)
  process group:
    :pgroup => true or 0 : make a new process group
    :pgroup => pgid      : join to specified process group
    :pgroup => nil       : dont change the process group (default)
  create new process group: Windows only
    :new_pgroup => true  : the new process is the root process of a new process group
    :new_pgroup => false : dont create a new process group (default)
  resource limit: resourcename is core, cpu, data, etc.  See Process.setrlimit.
    :rlimit_resourcename => limit
    :rlimit_resourcename => [cur_limit, max_limit]
  current directory:
    :chdir => str
  umask:
    :umask => int
  redirection:
    key:
      FD              : single file descriptor in child process
      [FD, FD, ...]   : multiple file descriptor in child process
    value:
      FD                        : redirect to the file descriptor in parent process
      string                    : redirect to file with open(string, "r" or "w")
      [string]                  : redirect to file with open(string, File::RDONLY)
      [string, open_mode]       : redirect to file with open(string, open_mode, 0644)
      [string, open_mode, perm] : redirect to file with open(string, open_mode, perm)
      [:child, FD]              : redirect to the redirected file descriptor
      :close                    : close the file descriptor in child process
    FD is one of follows
      :in     : the file descriptor 0 which is the standard input
      :out    : the file descriptor 1 which is the standard output
      :err    : the file descriptor 2 which is the standard error
      integer : the file descriptor of specified the integer
      io      : the file descriptor specified as io.fileno
  file descriptor inheritance: close non-redirected non-standard fds (3, 4, 5, ...) or not
    :close_others => false : inherit fds (default for system and exec)
    :close_others => true  : dont inherit (default for spawn and IO.popen)

【讨论】:

【参考方案8】:

最简单的方法是,例如:

reboot = `init 6`
puts reboot

【讨论】:

【参考方案9】:

如果您真的需要 Bash,请按照“最佳”答案中的说明。

首先,请注意,当 Ruby 调用 shell 时,它通常调用/bin/sh不是 Bash。 /bin/sh 并非在所有系统上都支持某些 Bash 语法。

如果您需要使用 Bash,请在所需的调用方法中插入 bash -c "your Bash-only command"

quick_output = system("ls -la")
quick_bash = system("bash -c 'ls -la'")

测试:

system("echo $SHELL")
system('bash -c "echo $SHELL"')

或者,如果您正在运行现有的脚本文件,例如

script_output = system("./my_script.sh")

Ruby应该尊重shebang,但你总是可以使用

system("bash ./my_script.sh")

为了确保,虽然 /bin/sh 运行 /bin/bash 可能会产生轻微的开销,但您可能不会注意到。

【讨论】:

【参考方案10】:

在这些机制之间进行选择时需要考虑的一些事项是:

    你只是想要标准输出还是你想要 还需要标准错误吗?甚至 分开了吗? 您的输出有多大?你想要 将整个结果保存在内存中? 您想阅读一些您的 在子进程仍然存在时输出 跑步吗? 您需要结果代码吗? 您需要一个 Ruby 对象吗? 代表过程并让您 按需杀死它?

您可能需要任何东西,从简单的反引号 (``)、system()IO.popen 到完整的 Kernel.fork/Kernel.execIO.pipeIO.select

如果子流程执行时间过长,您可能还想在混合中加入超时。

不幸的是,它非常取决于

【讨论】:

【参考方案11】:

这是一个基于“When to use each method of launching a subprocess in Ruby”的流程图。另请参阅“Trick an application into thinking its stdout is a terminal, not a pipe”。

【讨论】:

哇哈哈。非常有用,即使它必须存在的事实是不幸的 附带说明,我发现 spawn() 方法在许多不同的地方(例如 KernelProcess 是最通用的。它或多或少与 PTY.spawn() 相同,但是更通用。 IMO、反引号和%x 仅适用于编写脚本,因为它们不会将 stderr 返回给您,并且您无法向它们传递参数(请注意,您可以使用 $? 来获取退出状态)。所以我几乎总是使用Open3.capture3 而不是反引号或%x【参考方案12】:

上面的答案已经很不错了,但是我真的很想分享以下总结文章:“6 Ways to Run Shell Commands in Ruby”

基本上,它告诉我们:

Kernel#exec:

exec 'echo "hello $HOSTNAME"'

system$?

system 'false' 
puts $?

反引号 (`):

today = `date`

IO#popen:

IO.popen("date")  |f| puts f.gets 

Open3#popen3 -- 标准库:

require "open3"
stdin, stdout, stderr = Open3.popen3('dc') 

Open4#popen4——一颗宝石:

require "open4" 
pid, stdin, stdout, stderr = Open4::popen4 "false" # => [26327, #<IO:0x6dff24>, #<IO:0x6dfee8>, #<IO:0x6dfe84>]

【讨论】:

【参考方案13】:

不要忘记spawn命令创建一个后台进程来执行指定的命令。您甚至可以使用Process 类和返回的pid 等待其完成:

pid = spawn("tar xf ruby-2.0.0-p195.tar.bz2")
Process.wait pid

pid = spawn(RbConfig.ruby, "-eputs'Hello, world!'")
Process.wait pid

文档说:此方法类似于#system,但它不等待命令完成。

【讨论】:

Kernel.spawn() 似乎比所有其他选项更通用。【参考方案14】:

这是我在 OS X 上的 ruby​​ 脚本中使用的一个很酷的脚本(这样即使在离开窗口后我也可以启动脚本并获得更新):

cmd = %Q|osascript -e 'display notification "Server was reset" with title "Posted Update"'|
system ( cmd )

【讨论】:

【参考方案15】:

我绝对不是 Ruby 专家,但我会试一试:

$ irb 
system "echo Hi"
Hi
=> true

您还应该能够执行以下操作:

cmd = 'ls'
system(cmd)

【讨论】:

【参考方案16】:

这是我认为关于在 Ruby 中运行 shell 脚本的最佳文章:“6 Ways to Run Shell Commands in Ruby”。

如果您只需要获取输出,请使用反引号。

我需要更高级的东西,比如 STDOUT 和 STDERR,所以我使用了 Open4 gem。那里解释了所有方法。

【讨论】:

这里描述的帖子没有讨论%x 语法选项。 +1 表示 Open4。当我发现这个时,我已经开始尝试实现我自己版本的 spawn 方法。【参考方案17】:

另一种选择:

当你:

需要标准错误和标准输出 不能/不会使用 Open3/Open4(它们在我的 Mac 上的 NetBeans 中抛出异常,不知道为什么)

你可以使用shell重定向:

puts %x[cat bogus.txt].inspect
  => ""

puts %x[cat bogus.txt 2>&1].inspect
  => "cat: bogus.txt: No such file or directory\n"

自 MS-DOS 早期以来,2&gt;&amp;1 语法适用于 Linux、Mac 和 Windows。

【讨论】:

【参考方案18】:

我们可以通过多种方式实现它。

使用Kernel#exec,这条命令执行后什么都没有:

exec('ls ~')

使用backticks or %x

`ls ~`
=> "Applications\nDesktop\nDocuments"
%x(ls ~)
=> "Applications\nDesktop\nDocuments"

使用Kernel#system命令,成功返回true,失败返回false,命令执行失败返回nil

system('ls ~')
=> true

【讨论】:

【参考方案19】:

使用这里的答案和米海的答案中的链接,我整理了一个满足这些要求的函数:

    整齐地捕获 STDOUT 和 STDERR,因此当我的脚本从控制台运行时它们不会“泄漏”。 允许将参数作为数组传递给 shell,因此无需担心转义。 捕获命令的退出状态,以便在发生错误时一目了然。

作为奖励,如果 shell 命令成功退出 (0) 并将任何内容放在 STDOUT 上,此命令也将返回 STDOUT。通过这种方式,它与system 不同,后者在这种情况下仅返回true

代码如下。具体函数为system_quietly

require 'open3'

class ShellError < StandardError; end

#actual function:
def system_quietly(*cmd)
  exit_status=nil
  err=nil
  out=nil
  Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thread|
    err = stderr.gets(nil)
    out = stdout.gets(nil)
    [stdin, stdout, stderr].each|stream| stream.send('close')
    exit_status = wait_thread.value
  end
  if exit_status.to_i > 0
    err = err.chomp if err
    raise ShellError, err
  elsif out
    return out.chomp
  else
    return true
  end
end

#calling it:
begin
  puts system_quietly('which', 'ruby')
rescue ShellError
  abort "Looks like you don't have the `ruby` command. Odd."
end

#output: => "/Users/me/.rvm/rubies/ruby-1.9.2-p136/bin/ruby"

【讨论】:

【参考方案20】:

我喜欢这样做的方式是使用 %x 文字,这使得在命令中使用引号变得容易(并且可读!),如下所示:

directorylist = %x[find . -name '*test.rb' | sort]

在这种情况下,它将使用当前目录下的所有测试文件填充文件列表,您可以按预期进行处理:

directorylist.each do |filename|
  filename.chomp!
  # work with file
end

【讨论】:

%x[ cmd ] 是否向您返回一个数组? 以上对我不起作用。 ``
': 未定义的方法each' for :String (NoMethodError) 它是如何为你工作的?我正在使用ruby -v ruby 1.9.3p484 (2013-11-22 revision 43786) [i686-linux]你确定从命令返回一个数组以便循环实际工作吗?
%x[ cmd ].split("\n") 将返回一个列表:)【参考方案21】:

我最喜欢的是Open3

  require "open3"

  Open3.popen3('nroff -man')  |stdin, stdout, stderr| ... 

【讨论】:

我也喜欢open3,尤其是Open3.capture3:ruby-doc.org/stdlib-1.9.3/libdoc/open3/rdoc/… -> stdout, stderr, status = Open3.capture3('nroff -man', :stdin_data =&gt; stdin) 是否有关于如何使用 Open3 或 Ruby 标准库中的其他 Open 执行规范和单元测试的文档?以我目前的理解水平很难测试外壳。 open3和open4有什么区别?【参考方案22】:

您也可以使用反引号运算符 (`),类似于 Perl:

directoryListing = `ls /`
puts directoryListing # prints the contents of the root directory

如果你需要一些简单的东西会很方便。

您要使用哪种方法取决于您要完成的具体任务;查看文档以了解有关不同方法的更多详细信息。

【讨论】:

以上是关于如何从 Ruby 调用 shell 命令的主要内容,如果未能解决你的问题,请参考以下文章

如何在命令行上使用相同的命令,通过Ruby shell命令运行app

如何从 Java 调用 Linux shell 命令

如何从 Java 调用 Linux shell 命令

如何使用 Ruby 编写 shell 脚本?

从命令提示符或 PowerShell 调用 MSYS2 Shell

ruby 超时和系统命令