Программирование на языке Ruby.

14.1. Запуск внешних программ.

Никакой язык не может использоваться в качестве «клея», если он не позволяет запускать внешние программы. В Ruby для этого есть несколько способов.

Не могу не обмолвиться о том, что перед запуском внешней программы неплохо бы понять, что она делает. Я имею в виду вирусы и другие потенциально разрушительные программы. Не запускайте произвольную командную строку, особенно поступившую из внешнего источника. Это касается не только приложений, ориентированных на Web.

14.1.1. Методы system и exec.

Метод system (из модуля Kernel) эквивалентен одноименной функции из библиотеки языка С. Он выполняет указанную команду в отдельной оболочке.

System("/usr/games/fortune").

# Вывод направляется, как обычно, на stdout...

Второй параметр, если он задан, должен содержать список аргументов; как правило, аргументы можно задавать и в командной строке — эффект будет тот же. Разница лишь в том, что алгоритм расширения имени файла применяется только к первой из переданных строк.

System("rm", "/tmp/file1").

System("rm /tmp/file2").

# Оба варианта годятся.

# А тут есть различие...

System("echo *")   # Печатается список всех файлов.

System("echo","*") # Печатается звездочка (расширение.

                   # имени файла не производится).

# Более сложные командные строки тоже работают.

System("ls -l | head -n |").

Посмотрим, как это будет работать в семействе операционных систем Windows. В случае с простой исполняемой программой поведение должно быть таким же, как в UNIX. В зависимости от варианта Ruby для вызова встроенных в оболочку команд может потребоваться запуск cmd.ехе  — интерпретатора команд в Windows (в некоторых версиях ОС он называется command.com). Ниже приведены примеры запуска внешней и встроенной команды:

System("notepad.ехе","myfile.txt") # Никаких проблем...

System("cmd /с dir","somefile")    # 'dir' - встроенная команда!

Другое решение — воспользоваться библиотекой Win32API и определить собственный вариант метода system.

Require "Win32API".

Def system(cmd).

 sys = Win32API.new("crtdll", "system", ['P'], 'L').

 sys.Call(cmd).

End.

System("dir") # cmd /с необязательно!

Таким образом, можно добиться более-менее системно-независимого поведения system. Но если вы хотите запомнить выведенную программой информацию (например, в переменной), то system — не лучший способ (см. следующий раздел).

Упомяну еще метод exec. Он ведет себя аналогично system с тем отличием, что новый процесс замещает текущий. Поэтому код, следующий за exec, исполняться не будет.

Puts "Содержимое каталога:".

Exec("ls", "-l").

Puts "Эта строка никогда не исполняется!".

14.1.2. Перехват вывода программы.

Простейший способ перехватить информацию, выведенную программой, — заключить команду в обратные кавычки, например:

Listing = `ls -l` # Одна строка будет содержать несколько строчек (lines).

Now = `date`      # "Mon Mar 12 16:50:11 CST 2001".

Обобщенный ограничитель %x вызывает оператор обратных кавычек (который в действительности является методом модуля Kernel). Работает он точно так же:

Listing = %x(ls -l).

Now = %x(date).

Применение %x бывает полезно, когда подлежащая исполнению строка содержит такие символы, как одиночные и двойные кавычки.

Поскольку обратные кавычки — это на самом деле метод (в некотором смысле), то его можно переопределить. Изменим его так, чтобы он возвращал не одну строку, а массив строк. Конечно, при этом мы создадим синоним старого метода, чтобы его можно было вызвать.

Alias old_execute `

Def `(cmd).

 out = old_execute(cmd) # Вызвать исходный метод обратной кавычки.

 out.split("\n") # Вернуть массив строк!

End.

Entries = `ls -l /tmp`

Num = entries.size          # 95.

First3lines = %x(ls -l | head -n 3).

How_many = first3lines.size # 3.

Как видите, при таком определении изменяется также поведение ограничителя %x.

В следующем примере мы добавили в конец команды конструкцию интерпретатора команд, которая перенаправляет стандартный вывод для ошибок в стандартный вывод:

Alias old_execute `

Def `(cmd).

 old_execute(cmd + " 2>&1").

End.

Entries = `ls -l /tmp/foobar`

# "/tmp/foobar: No such file or directory\n".

Есть, конечно, и много других способов изменить стандартное поведение обратных кавычек.

14.1.3. Манипулирование процессами.

В этом разделе мы обсудим манипулирование процессами, хотя создание нового процесса необязательно связано с запуском внешней программы. Основной способ создания нового процесса — это метод fork, название которого в соответствии с традицией UNIX подразумевает разветвление пути исполнения, напоминая развилку на дороге. (Отметим, что в базовом дистрибутиве Ruby метод fork на платформе Windows не поддерживается.).

Метод fork, находящийся в модуле Kernel (а также в модуле Process), не следует путать с одноименным методом экземпляра в классе Thread.

Существуют два способа вызвать метод fork. Первый похож на то, как это обычно делается в UNIX, — вызвать и проверить возвращенное значение. Если оно равно nil, мы находимся в дочернем процессе, в противном случае — в родительском. Родительскому процессу возвращается идентификатор дочернего процесса (pid).

Pid = fork.

If (pid == nil).

 puts "Ага, я, должно быть, потомок.".

 puts "Так и буду себя вести.".

Else.

 puts "Я родитель.".

 puts "Пора отказаться от детских штучек.".

End.

В этом не слишком реалистичном примере выводимые строки могут чередоваться, а может случиться и так, что строки, выведенные родителем, появятся раньше. Но сейчас это несущественно.

Следует также отметить, что процесс-потомок может пережить своего родителя. Для потоков в Ruby это не так, но системные процессы — совсем другое дело.

Во втором варианте вызова метод fork принимает блок. Заключенный в блок код выполняется в контексте дочернего процесса. Так, предыдущий вариант можно было бы переписать следующим образом:

Fork do.

 puts "Ага, я, должно быть, потомок.".

 puts "Так и буду себя вести.".

End.

Puts "Я родитель.".

Puts "Пора отказаться от детских штучек.".

Конечно, pid по-прежнему возвращается, мы просто не показали его.

Чтобы дождаться завершения процесса, мы можем вызвать метод wait из модуля Process. Он ждет завершения любого потомка и возвращает его идентификатор. Метод wait2 ведет себя аналогично, только возвращает массив, содержащий РМ, и сдвинутый влево код завершения.

Pid1 = fork { sleep 5; exit 3 }.

Pid2 = fork { sleep 2; exit 3 }.

Process.wait  # Возвращает pid2.

Process.wait2 # Возвращает [pid1,768].

Чтобы дождаться завершения конкретного потомка, применяются методы waitpid и waitpid2.

Pid3 = fork { sleep 5; exit 3 }.

Pid4 = fork { sleep 2; exit 3 }.

Process.waitpid(pid4,Process::WNOHANG)   # Возвращает pid4.

Process.waitpid2(pid3, Process::WNOHANG) # Возвращает [pid3,768].

Если второй параметр не задан, то вызов может блокировать программу (если такого потомка не существует). Второй параметр можно с помощью ИЛИ объединить с флагом Process::WUNTRACED, чтобы перехватывать остановленные процессы. Этот параметр системно зависим, поэкспериментируйте.

Метод exit! немедленно завершает процесс (не вызывая зарегистрированных обработчиков). Если задан целочисленный аргумент, то он возвращается в качестве кода завершения; по умолчанию подразумевается значение 1 (не 0).

Pid1 = fork { exit! }   # Вернуть код завершения -1.

Pid2 = fork { exit! 0 } # Вернуть код завершения 0.

Методы pid и ppid возвращают соответственно идентификатор текущего и родительского процессов.

Proc1 = Process.pid.

Fork do.

 if Process.ppid == proc1.

  puts "proc1 - мой родитель" # Печатается это сообщение.

 else.

  puts "Что происходит?".

 end.

End.

Метод kill служит для отправки процессу сигнала, как это понимается в UNIX. Первый параметр может быть целым числом, именем POSIX-сигнала с префиксом SIG или именем сигнала без префикса. Второй параметр — идентификатор процесса-получателя; если он равен нулю, подразумевается текущий процесс.

Process.kill(1,pid1)        # Послать сигнал 1 процессу pid1.

Process.kill ("HUP",pid2)   # Послать SIGHUP процессу pid2..

Process.kill("SIGHUP",pid2) # Послать SIGHUP процессу pid3.

Process.kill("SIGHUP",0)    # Послать SIGHUP самому себе.

Для обработки сигналов применяется метод Kernel.trap. Обычно он принимает номер или имя сигнала и подлежащий выполнению блок.

Trap(1) { puts "Перехвачен сигнал 1" }.

Sleep 2.

Process.kill(1,0) # Послать самому себе.

О применениях метода trap в более сложных ситуациях читайте в документации по Ruby и UNIX.

В модуле Process есть также методы для опроса и установки таких атрибутов процесса, как идентификатор пользователя, действующий идентификатор пользователя, приоритет и т.д. Дополнительную информацию вы отыщете в справочном руководстве по Ruby.

14.1.4. Стандартный ввод и вывод.

В главе 10 мы видели, как работают методы IO.popen и IO.pipe, но существует еще небольшая библиотека, которая иногда бывает удобна.

В библиотеке Open3.rb есть метод popen3, который возвращает массив из трех объектов IO. Они соответствуют стандартному вводу, стандартному выводу и стандартному выводу для ошибок того процесса, который был запущен методом popen3. Вот пример:

Require "open3".

Filenames = %w[ file1 file2 this that another one_more ].

Inp, out, err = Open3.popen3("xargs", "ls", "-l").

Filenames.each { |f| inp.puts f } # Писать в stdin процесса.

Inp.close                         # Закрывать обязательно!

Output = out.readlines            # Читать из stdout.

Errout = err.readlines            # Читать также из stderr.

Puts "Послано #{filenames.size} строк входных данных.".

Puts "Получено #{output.size} строк из stdout".

Puts "и #{errout.size} строк из stderr.".

В этом искусственном примере мы выполняем команду ls -l для каждого из заданных имен файлов и по отдельности перехватываем стандартный вывод и стандартный вывод для ошибок. Отметим, что вызов close необходим, чтобы порожденный процесс увидел конец файла. Также отметим, что в библиотеке Open3 используется метод fork, не реализованный на платформе Windows; для этой платформы придется пользоваться библиотекой win32-open3 (ее написали и поддерживают Дэниэль Бергер (Daniel Berger) и Парк Хисоб (Park Heesob)). См. также раздел 14.3.