Ruby: вызов внешних команд

Дока

Правило: в общем случае лучше не использовать вызов внешних команд, а попробовать положиться на standart libraries/gem/modules native ruby.

Недостаток в подходе вызова внешних команд очевиден – мы полагаемся на внешние пути, переменные в ОС и скрипты, будь то системные или написанные на других языках, а ошибки из-за этих проблем зачастую сложно обнаружить.

Исключения возможны, например:

  • если в ruby нет достойной библиотеки или она по какой-то причине хуже того, что можно вызывать
  • если задачу нужно решить быстро

Процесс вызова внешней команды делается используя подпроцесс (subprocess, child process) – ruby создает подпроцесс по отношению к основному (parent process) при вызове внешней команды. В этом процессе создается второй shell, который исполняет команду. Причем при использовании большинства методов вызова внешних процессов, пока внешний подпроцесс не отработает, основной процесс находится в режиме блокировки (blocked) – т.е. он не делает ничего, пока процесс не закончится. Только после того как child процесс закончится, flow control вернется к процессу parent и скрипт продолжит стандартное исполнение. Исключением из этого правила является вызов внешнего процесса через Fork – тогда Ruby интерпретатор не будет ждать отработки подпроцесса.

Для вызова внешний команд можно использовать:

System метод – берет имя команды как аргумент, возвраещает true если команда успешна (команда вернула 0 в exit status) и false если нет (exit status не равен 0). Отлично подходит если нужно знать только успешна команда или нет.

 result = System("chmod", "777", "file.txt") # аналог chmod 777 file.txt

 

`<command>` или %x(<command>) – запись команды в обратных ковычках. Отлично подходит на случай если нужен только STDOUT от child процесса и падение основного скрипта не критично. В отличии от System метода возвращает весь STDOUT после отработки child процесса, а true/false на основе exit status. Узнать exit статус в таком случае мы можем используя переменную $?, которая сохраняет значение exit status последнего child процесса. Применив к этой переменной метод .success можно получить информацию о успешности последней команды в виде true/false, по аналогии с System методом.

output = `ls` # аналог output = %x(ls)
puts output
puts "success" if $?.success?

При использовании подпроцессов через ковычки и %x, нужно учитывать что ошибка в подпроцессе (например, вызов утилиты, которая не установлена) по умолчанию приводит к exception не только в подпроцессе, но и в основном коде. Workaround есть – перенаправить STDERR в тот же поток что и STDOUT.

output = `not_existed_process 2>&1`
 puts output

При использовании этого workaround  нужно обязательно проверять exit status подпроцесса, иначе мы можем не знать о каких-то ошибках в скриптах (silent failure). silent failure нужно избегать всегда. Поэтому если нам не нужны ошибки основного процесса из-за падения подпроцесса лучше не использовать костыли, а использовать Open3.

Open3 (дока) – самый богатый способ вызова внешних команд т.к. он имеет метод capture3, который возвращает STDOUT, STDERR, EXIT code после завершения child process без каких то костылей в виде redirection, как в случае ковычек. Самый лучший вариант если нам нужны все три вывода или даже два из трех. Open3 является стандартной библиотекой, не требует установки, но требует require т.к. он не включен в код Ruby core. Еще одним преймуществом Open3 является то, что он не передает exception от subprocess в корневой, что делается при использовании ковычек. Для приема данных удобнее всего использовать multiple assignment – определение нескольких переменных одной командой.

require 'open3'

stdout, stderr, status = Open3.capture3("ls", "-l")
 puts stdout
 puts stderr
 puts status.success?

Leave a Reply