Comment appeler des commandes shell depuis Ruby

1077

Comment appeler des commandes shell depuis l'intérieur d'un programme Ruby? Comment puis-je récupérer la sortie de ces commandes dans Ruby?

CodageSans commentaires
la source
3
Bien que cette question soit utile, elle n'est pas bien posée. Ruby a de nombreuses façons d'appeler des sous-shells bien documentés et faciles à trouver en lisant la documentation du noyau et Open3 et en recherchant ici sur SO.
The Tin Man
1
Malheureusement, ce sujet est assez complexe. Open3( docs ) est le meilleur choix pour la plupart des situations, OMI, mais sur les anciennes versions de Ruby, il ne respectera pas une modification PATH( bugs.ruby-lang.org/issues/8004 ), et selon la façon dont vous passez les arguments (en particulier , si vous utilisez le hachage opts avec des non-mots clés), il peut se casser. Mais, si vous rencontrez ces situations, vous faites quelque chose d'assez avancé et vous pouvez savoir quoi faire en lisant l'implémentation de Open3.
Joshua Cheek
3
Je suis surpris que personne n'ait mentionné Shellwords.escape( doc ). Vous ne voulez pas insérer directement les entrées utilisateur dans les commandes shell - échappez-y d'abord! Voir aussi injection de commandes .
Kelvin

Réponses:

1319

Cette explication est basée sur un script Ruby commenté d'un de mes amis. Si vous souhaitez améliorer le script, n'hésitez pas à le mettre à jour sur le lien.

Tout d'abord, notez que lorsque Ruby appelle un shell, il appelle généralement /bin/sh, pas Bash. Certaines syntaxes Bash ne sont pas prises en charge par /bin/shsur tous les systèmes.

Voici comment exécuter un script shell:

cmd = "echo 'hi'" # Sample string that can be used
  1. Kernel#` , communément appelés backticks - `cmd`

    C'est comme beaucoup d'autres langages, dont Bash, PHP et Perl.

    Renvoie le résultat (c'est-à-dire la sortie standard) de la commande shell.

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

    value = `echo 'hi'`
    value = `#{cmd}`
  2. Syntaxe intégrée, %x( cmd )

    Le caractère qui suit xest un délimiteur, qui peut être n'importe quel caractère. Si le délimiteur est l' un des personnages (, [, {ou <, le littéral est composé des caractères jusqu'à la fermeture de delimiter correspondant, en tenant compte des paires de délimiteurs imbriquées. Pour tous les autres délimiteurs, le littéral comprend les caractères jusqu'à la prochaine occurrence du caractère délimiteur. L'interpolation de chaînes #{ ... }est autorisée.

    Renvoie le résultat (c'est-à-dire la sortie standard) de la commande shell, tout comme les backticks.

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

    value = %x( echo 'hi' )
    value = %x[ #{cmd} ]
  3. Kernel#system

    Exécute la commande donnée dans un sous-shell.

    Renvoie truesi la commande a été trouvée et exécutée correctement, falsesinon.

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

    wasGood = system( "echo 'hi'" )
    wasGood = system( cmd )
  4. Kernel#exec

    Remplace le processus en cours en exécutant la commande externe donnée.

    Renvoie none, le processus en cours est remplacé et ne se poursuit jamais.

    Documents: 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

Voici quelques conseils supplémentaires :, $?qui est le même que $CHILD_STATUS, accède à l'état de la dernière commande exécutée par le système si vous utilisez les raccourcis, system()ou %x{}. Vous pouvez ensuite accéder aux propriétés exitstatuset pid:

$?.exitstatus

Pour plus de lecture, voir:

Steve Willard
la source
4
Je dois enregistrer les sorties de mon exécutable sur le serveur de production mais je n'ai trouvé aucun moyen. J'ai utilisé des #{cmd}options de vente et logger.info ( #{cmd}). Existe-t-il un moyen de consigner leurs sorties sur la production?
Omer Aslam
5
Et IO # popen () et Open3 # popen3 (). mentalized.net/journal/2010/03/08/...
hughdbrown
6
Par souci d'exhaustivité (comme je pensais d'abord que ce serait aussi une commande Ruby): Rake a sh qui fait "Exécuter la commande système cmd. Si plusieurs arguments sont donnés, la commande n'est pas exécutée avec le shell (même sémantique que Kernel :: exec et Kernel :: system) ".
sschuberth
40
Les backticks ne capturent pas STDERR par défaut. Ajoutez «2> & 1» à la commande si vous voulez capturer
Andrei Botalov
14
Je pense que cette réponse serait légèrement améliorée si elle disait que les backticks et% x renvoyaient la "sortie", plutôt que le "résultat", de la commande donnée. Ce dernier pourrait être confondu avec le statut de sortie. Ou est-ce juste moi?
skagedal
275

Voici un organigramme basé sur " Quand utiliser chaque méthode de lancement d'un sous-processus dans Ruby ". Voir aussi, " Tromper une application en pensant que sa sortie standard est un terminal, pas un tuyau ".

entrez la description de l'image ici

Ian
la source
24
Wow haha. Très utile même si le fait que cela doit exister est regrettable
Josh Bodah
En remarque, je trouve la méthode spawn () trouvée dans de nombreux endroits différents (par exemple Kernelet Processpour être la plus polyvalente. C'est plus ou moins la même chose avec PTY.spawn(), mais plus générique.
Smar
160

La façon dont j'aime le faire est d'utiliser le %xlittéral, ce qui rend facile (et lisible!) L'utilisation de guillemets dans une commande, comme ceci:

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

Qui, dans ce cas, remplira la liste des fichiers avec tous les fichiers de test sous le répertoire actuel, que vous pouvez traiter comme prévu:

directorylist.each do |filename|
  filename.chomp!
  # work with file
end
cynique
la source
4
Vous %x[ cmd ]renvoie un tableau?
x-yuri
2
ce qui précède ne fonctionne pas pour moi. `` <main> ': méthode non définie each' for :String (NoMethodError) comment cela a-t-il fonctionné pour vous? J'utilise ruby -v ruby 1.9.3p484 (2013-11-22 revision 43786) [i686-linux]Êtes-vous sûr qu'un tableau est renvoyé par la commande pour que la boucle fonctionne réellement?
Nasser
% x [cmd] .split ("\ n") renverra une liste cependant :)
Ian Ellis
65

Voici le meilleur article à mon avis sur l'exécution de scripts shell dans Ruby: " 6 façons d'exécuter des commandes shell dans Ruby ".

Si vous avez seulement besoin d'obtenir la sortie, utilisez des astuces.

J'avais besoin de choses plus avancées comme STDOUT et STDERR, j'ai donc utilisé le joyau Open4. Vous avez toutes les méthodes expliquées ici.

Mihai A
la source
2
Le message décrit ici ne traite pas de l' %xoption de syntaxe.
Mei
+1 pour Open4. J'avais déjà commencé à essayer d'implémenter ma propre version de sa spawnméthode lorsque j'ai trouvé cela.
Brandan
40

Mon préféré est Open3

  require "open3"

  Open3.popen3('nroff -man') { |stdin, stdout, stderr| ... }
anshul
la source
3
J'aime aussi open3, en particulier Open3.capture3: ruby-doc.org/stdlib-1.9.3/libdoc/open3/rdoc/… -> stdout, stderr, status = Open3.capture3('nroff -man', :stdin_data => stdin)
severin
Existe-t-il de la documentation sur la façon d'effectuer des tests de spécifications et d'unités avec Open3 ou d'autres Open dans Ruby std-lib? Il est difficile de tester les coques à mon niveau actuel de compréhension.
FilBot3
29

Certaines choses à considérer lors du choix entre ces mécanismes sont:

  1. Voulez-vous simplement stdout ou avez-vous également besoin de stderr? Ou même séparé?
  2. Quelle est la taille de votre sortie? Voulez-vous garder le résultat entier en mémoire?
  3. Voulez-vous lire une partie de votre sortie pendant que le sous-processus est toujours en cours d'exécution?
  4. Avez-vous besoin de codes de résultat?
  5. Avez-vous besoin d'un objet Ruby qui représente le processus et vous permet de le tuer à la demande?

Vous pouvez avoir besoin de n'importe quoi, des simples backticks (``),, system()et IO.popenà part entière Kernel.fork/ Kernel.execavec IO.pipeet IO.select.

Vous voudrez peut-être aussi jeter des délais d'attente dans le mix si un sous-processus prend trop de temps à exécuter.

Malheureusement, c'est beaucoup dépend.

Nick Brosnahan
la source
25

Encore une option:

Quand vous:

  • besoin de stderr ainsi que stdout
  • ne peut pas / ne veut pas utiliser Open3 / Open4 (ils lèvent des exceptions dans NetBeans sur mon Mac, aucune idée pourquoi)

Vous pouvez utiliser la redirection shell:

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

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

La 2>&1syntaxe fonctionne sur Linux , Mac et Windows depuis les premiers jours de MS-DOS.

jg-faustus
la source
25

Je ne suis certainement pas un expert Ruby, mais je vais essayer:

$ irb 
system "echo Hi"
Hi
=> true

Vous devriez également pouvoir faire des choses comme:

cmd = 'ls'
system(cmd)
Steve Willard
la source
21

Les réponses ci-dessus sont déjà assez bonnes, mais je veux vraiment partager l'article récapitulatif suivant: " 6 façons d'exécuter des commandes shell dans Ruby "

Fondamentalement, il nous dit:

Kernel#exec:

exec 'echo "hello $HOSTNAME"'

systemet $?:

system 'false' 
puts $?

Pointes arrière (`):

today = `date`

IO#popen:

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

Open3#popen3 - stdlib:

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

Open4#popen4 -- une gemme:

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

Si vous avez vraiment besoin de Bash, selon la note de la "meilleure" réponse.

Tout d'abord, notez que lorsque Ruby appelle un shell, il appelle généralement /bin/sh, pas Bash. Certaines syntaxes Bash ne sont pas prises en charge par /bin/shsur tous les systèmes.

Si vous devez utiliser Bash, insérez-le à l' bash -c "your Bash-only command"intérieur de la méthode d'appel souhaitée:

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

Tester:

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

Ou si vous exécutez un fichier de script existant comme

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

Ruby devrait honorer le shebang, mais vous pouvez toujours utiliser

system("bash ./my_script.sh")

pour vous assurer, bien qu'il puisse y avoir un léger surcoût lié à l' /bin/shexécution /bin/bash, vous ne le remarquerez probablement pas.

dragon788
la source
11

Vous pouvez également utiliser les opérateurs backtick (`), similaires à Perl:

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

Pratique si vous avez besoin de quelque chose de simple.

La méthode que vous souhaitez utiliser dépend exactement de ce que vous essayez d'accomplir; consultez la documentation pour plus de détails sur les différentes méthodes.

Rufo Sanchez
la source
10

Nous pouvons y parvenir de plusieurs manières.

En utilisant Kernel#exec, rien après l'exécution de cette commande:

exec('ls ~')

En utilisant backticks or %x

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

En utilisant la Kernel#systemcommande, retourne en truecas de succès, en falsecas d'échec et retourne nilsi l'exécution de la commande échoue:

system('ls ~')
=> true
nkm
la source
10

La façon la plus simple est, par exemple:

reboot = `init 6`
puts reboot
Alex Lorsung
la source
9

En utilisant les réponses ici et liées dans la réponse de Mihai, j'ai mis en place une fonction qui répond à ces exigences:

  1. Capture parfaitement STDOUT et STDERR afin qu'ils ne "fuient" pas lorsque mon script est exécuté à partir de la console.
  2. Permet de passer des arguments au shell sous forme de tableau, donc vous n'avez pas à vous soucier de l'échappement.
  3. Capture l'état de sortie de la commande pour qu'il soit clair lorsqu'une erreur s'est produite.

En bonus, celui-ci renverra également STDOUT dans les cas où la commande shell se termine avec succès (0) et met quoi que ce soit sur STDOUT. De cette manière, il diffère de system, qui renvoie simplementtrue dans de tels cas.

Le code suit. La fonction spécifique est 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"
Ryan Tate
la source
9

N'oubliez pas la spawncommande pour créer un processus d'arrière-plan pour exécuter la commande spécifiée. Vous pouvez même attendre son achèvement en utilisant la Processclasse et le retourné 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

Le doc dit: Cette méthode est similaire #systemmais n'attend pas la fin de la commande.

MonsieurDart
la source
2
Kernel.spawn()semble être beaucoup plus polyvalent que toutes les autres options.
Kashyap
6

Si vous avez un cas plus complexe que le cas commun qui ne peut pas être traité ``, consultez Kernel.spawn() . Cela semble être le plus générique / le plus complet fourni par stock Ruby pour exécuter des commandes externes.

Vous pouvez l'utiliser pour:

  • créer des groupes de processus (Windows).
  • rediriger in, out, error vers des fichiers / les uns des autres.
  • définir env vars, umask.
  • changer le répertoire avant d'exécuter une commande.
  • définir des limites de ressources pour CPU / données / etc.
  • Faites tout ce qui peut être fait avec d'autres options dans d'autres réponses, mais avec plus de code.

La documentation Ruby contient suffisamment d'exemples:

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)
Kashyap
la source
6

La méthode backticks (`) est la plus simple pour appeler des commandes shell depuis Ruby. Il renvoie le résultat de la commande shell:

     url_request = 'http://google.com'
     result_of_shell_command = `curl #{url_request}`
ysk
la source
5

Étant donné une commande comme attrib:

require 'open3'

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

J'ai trouvé que même si cette méthode n'est pas aussi mémorable que

system("thecommand")

ou

`thecommand`

dans les backticks, une bonne chose à propos de cette méthode par rapport aux autres méthodes est que les backticks ne semblent pas me laisser putsla commande que j'exécute / stocke la commande que je veux exécuter dans une variable, etsystem("thecommand") ne semblent pas me permettre d'obtenir la sortie alors que cette méthode me permet de faire ces deux choses, et elle me permet d'accéder à stdin, stdout et stderr indépendamment.

Voir " Exécution de commandes dans ruby " et la documentation Ruby Open3 .

barlop
la source
3

Ce n'est pas vraiment une réponse mais peut-être que quelqu'un la trouvera utile:

Lorsque vous utilisez l'interface graphique TK sous Windows et que vous devez appeler des commandes shell à partir de rubyw, vous aurez toujours une fenêtre CMD ennuyeuse qui apparaît pendant moins d'une seconde.

Pour éviter cela, vous pouvez utiliser:

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

ou

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

Les deux stockeront la ipconfigsortie à l'intérieur log.txt, mais aucune fenêtre ne s'affichera.

Vous aurez besoin de l' require 'win32ole'intérieur de votre script.

system(), exec()Et spawn()va tous apparaître cette fenêtre gênant lors de l' utilisation des savoirs traditionnels et rubyw.

lucaortis
la source
-2

Voici un cool que j'utilise dans un script ruby ​​sur OS X (afin que je puisse démarrer un script et obtenir une mise à jour même après avoir quitté la fenêtre):

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