Программистам часто приходится писать небольшие сценарии для запуска внешних программ и работы с операционной системой на достаточно высоком уровне. Особенно это относится к ОС UNIX, где для повседневной работы составляются многочисленные сценарии на языке интерпретатора команд (shell).
Ruby не всегда удобно использовать в качестве такого «склеивающего» языка, поскольку он задуман как более универсальный инструмент. Но в принципе все, что можно сделать на языке bash (и ему подобных), можно реализовать и на Ruby.
Нередко для этой цели можно воспользоваться каким-то более традиционным языком. Преимущества Ruby в его универсальности, богатстве функций и объектной ориентированности. Предполагая, что найдутся люди, желающие использовать Ruby для взаимодействия с операционной системой на таком уровне, мы продемонстрируем несколько полезных приемов.
Выстроить эту главу было трудно, поскольку есть много способов логически сгруппировать рассматриваемый материал. Если вы не смогли найти нужную тему там, где ожидали, попробуйте просмотреть другие разделы.
Кроме того, многие вопросы, которые можно было бы включить сюда, вошли в другие главы. Обратите внимание, в частности, на главу 10, где рассматриваются ввод/вывод и атрибуты файлов; эта информация часто бывает полезна при написании сценариев.
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.
14.2. Флаги и аргументы в командной строке
Слухи о кончине командной строки сильно преувеличены. Хоть мы и живем в век графических интерфейсов, ежедневно тысячи и тысячи программистов по тем или иным причинам обращаются к командным утилитам.
Мы уже говорили, что корнями своими Ruby уходит в UNIX. Но даже в Windows существует понятие командной строки, и, честно говоря, мы не думаем, что в обозримом будущем она исчезнет.
На этом уровне для управления работой программы применяются аргументы и флаги. О них мы и поговорим ниже.
14.2.1. Разбор флагов в командной строке
Для разбора командной строки чаще всего применяется библиотека getoptlong (библиотека getopts.rb, обладающая менее развитой функциональностью, считается устаревшей). Она понимает однобуквенные и длинные флаги и распознает двойной дефис (--) как признак конца флагов. В целом библиотека ведет себя так же, как соответствующие функции GNU.
Необходимо создать объект класса GetoptLong, который и будет выполнять функции анализатора. Затем ему передаются допустимые значения флагов, а он извлекает их по одному.
У объекта-анализатора есть метод set_options, который принимает список массивов. Каждый массив содержит один или несколько флагов (в виде строк) и один «признак наличия аргумента», который говорит, должны ли эти флаги сопровождаться аргументами. Все флаги в одном массиве считаются синонимами; первый из них является «каноническим именем», которое и возвращает операция get.
Предположим, что имеется программа, понимающая следующие флаги: -h или --help (печать справки), -f или --file (указание имени файла), -l или --lines (вывод не более указанного числа строк, по умолчанию 100).
Такая программа могла бы начинаться следующим образом:
require "getoptlong"
parser = GetoptLong.new
parser.set_options(
["-h", "--help", GetoptLong::NO_ARGUMENT],
["-f", "--file", GetoptLong::REQUIRED_ARGUMENT],
["-l", "--lines", GetoptLong::OPTIONAL_ARGUMENT])
Теперь можно в цикле вызвать метод get (см. листинг 14.1). Наличие операторных скобок begin-end имитирует цикл с проверкой условия в конце. У метода get есть синоним get_option, существуют также итераторы each и each_option, которые в точности идентичны.
Листинг 14.1. Получение флагов из командной строки
filename = nil
lines = 0 # По умолчанию вывод не усекается.
loop do
begin
opt, arg = parser.get
break if not opt
# Только для отладки...
puts (opt +" => " + arg)
case opt
when "-h"
puts "Usage: ..."
break # Прекратить обработку, если задан флаг -h.
when "-f"
filename = arg # Запомнить аргумент - имя файла.
when "-l"
if arg != ""
lines = arg # Запомнить аргумент - число строк (если задан).
else
lines = 100 # Оставляемое по умолчанию число строк.
end
end
rescue => err
puts err
break
end
end
puts "имя файла = #{filename}"
puts "число строк = #{lines}"
Метод get возвращает nil, если флаг отсутствует, но пустую строку, если для флага не задан аргумент. Возможно, это ошибка.
В этом примере мы перехватываем исключения. Всего их может быть четыре:
• AmbiguousOption — указано сокращенное длинное имя флага, но сокращение не уникально;
• InvalidOption — неизвестный флаг;
• MissingArgument — для флага не задан аргумент;
• NeedlessArgument — указан аргумент для флага, который не должен сопровождаться аргументом.
Сообщения об ошибках обычно выводятся на stderr, но вывод можно подавить, присвоив акцессору quiet= значение true.
Библиотека getoptlong располагает и другими возможностями, которых мы здесь не обсуждали. Подробности вы найдете в документации.
Существуют другие библиотеки, например OptionParser, предлагающие несколько иную функциональность. Дополнительная информация приведена в архиве приложений Ruby.
14.2.2. Константа ARGF
Глобальная константа ARGF представляет псевдофайл, получающийся в результате конкатенации всех имен файлов, заданных в командной строке. Во многих отношениях она ведет себя так же, как объект IO.
Когда в программе встречается «голый» метод ввода (без указания вызывающего объекта), обычно имеется в виду метод, подмешанный из модуля Kernel (например, gets и readlines). Если в командной строке не задано ни одного файла, то по умолчанию источником ввода является объект stdin. Но если файлы заданы, то данные читаются из них. Понятно, что конец файла достигается в конце последнего из указанных файлов.
Если хотите, можете обращаться к ARGF явно:
# Скопировать все файлы на stdout.
puts ARGF.readlines
Быть может, вопреки ожиданиям, признак конца файла устанавливается после каждого файла. Так, предыдущий код выведет все файлы, а следующий — только первый файл:
until ARGF.eof?
puts ARGF.gets
end
Является ли это ошибкой, предоставим судить вам. Впрочем, сюрпризы могут быть и приятными. Входные данные — не просто поток байтов; мы можем применять к ARGF операции seek и rewind, как если бы это был «настоящий файл».
С константой ARGF ассоциирован метод file — он возвращает объект IO, соответствующий файлу, обрабатываемому в данный момент. Естественно, возвращаемое значение изменяется по мере перехода от одного файла к другому.
А если мы не хотим интерпретировать имена аргументов в командной строке как имена файлов? Тогда не надо обращаться к методам ввода без указания вызывающего объекта. Если вы хотите читать из стандартного ввода, укажите в качестве такого объекта STDIN, и все будет работать правильно.
14.2.3. Константа ARGV
Глобальная константа ARGV представляет список аргументов, переданных в командной строке. По сути дела, это массив.
n = ARGV.size
argstr = '"' + ARGV*"," + '"'
puts "Мне было передано аргументов: #{n}..."
puts "Вот они: #{argstr}"
puts "Заметьте, что ARGV[0] = #{ARGV[0]}"
Если запустить эту программу с аргументами red green blue, то она напечатает:
Мне было передано аргументов: 3...
Вот они: "red,green,blue"
Заметьте, что ARGV[0] = red
Ясно, что отдельно передавать число аргументов, как в былые времена, не нужно; эта информация — часть массива.
Привычных к старым соглашениям программистов может смутить также тот факт, что нулевой элемент массива — настоящий аргумент (а не, скажем, имя сценария). Нумерация аргументов начинается с нуля, а не с единицы, как в языке С и в различных интерпретаторах команд.
14.3. Библиотека Shell
Не всегда Ruby удобен в качестве языка сценариев. Например, в языке bash для запуска внешней программы достаточно просто указать ее имя безо всякого дополнительного синтаксиса.
Оборотной стороной мощи и гибкости Ruby является более сложный синтаксис. Кроме того, функциональность разнесена по различным классам, модулям и библиотекам.
Это послужило основанием для создания библиотеки Shell, которая упрощает, к примеру, организацию конвейеров команд и перенаправление вывода в файл. Кроме того, она сводит воедино функциональность из разных источников, скрывая ее за интерфейсом объекта Shell. (На платформе Windows эта библиотека работает не во всех случаях.)
14.3.1. Использование библиотеки Shell для перенаправления ввода/вывода
В классе Shell для создания объектов есть два метода: new и cd. Первый создает объект, ассоциированный с текущим каталогом, второй — объект, для которого рабочим будет указанный каталог.
require "shell"
sh1 = Shell.new # Работать в текущем каталоге.
sh2 = Shell.cd("/tmp/hal") # Работать в каталоге /tmp/hal.
Библиотека Shell определяет несколько встроенных команд (например, echo, cat и tee) в виде методов. Они всегда возвращают объекты класса Filter (как и определяемые пользователем команды, с которыми мы вскоре познакомимся).
Класс Filter понимает, что такое перенаправление ввода/вывода. В нем определены методы (или операторы) <, > и |, которые ведут себя примерно так, как мы ожидаем по многолетнему опыту написания shell-сценариев.
Если методу перенаправления передать в качестве параметра строку, то она будет считаться именем файла. Если же параметром является объект IO, он используется для операций ввода/вывода. Примеры:
sh = Shell.new
# Вывести файл motd на stdout.
sh.cat("/etc/motd") > STDOUT
# Напечатать его еще раз.
(sh.cat < "/etc/motd") > STDOUT
(sh.echo "Это тест") > "myfile.txt"
# Добавить строку в конец файла /etc/motd.
sh.echo("Hello, world!") >> "/etc/motd"
# Вывести два файла на stdout и продублировать (tee) вывод в третий файл.
(sh.cat "file1" "file2") | (tee "file3") > STDOUT
Отметим, что у оператора > высокий приоритет. Скобки, которые вы видите в данном примере, в большинстве случаев обязательны. Вот два примера правильного использования и один — неправильного:
# Интерпретатор Ruby понимает такую конструкцию...
sh.cat("myfile.txt") > STDOUT
# ...и такую тоже.
(sh.cat "myfile.txt") > STDOUT
# TypeError! (ошибка связана с приоритетами).
sh.cat "myfile.txt" > STDOUT
Отметим еще, что можно «инсталлировать» системные команды по своему выбору. Для этого служит метод def_system_command. Ниже определяются два метода: ls и ll, которые выводят список файлов в текущем каталоге (в коротком и длинном формате).
# Имя метода совпадает с именем команды...
# Необходим только один параметр:
Shell.def_system_command "ls"
# А здесь должно быть два параметра:
Shell.def_system_command "ll", "ls -l"
sh = Shell.new
sh.ls > STDOUT # Короткий формат.
sh.ll > STDOUT # Длинный формат.
Вы, наверное, обратили внимание на то, что в большинстве случаев мы явно отправляем вывод объекту STDOUT. Связано это с тем, что объект Shell автоматически вывод команд никуда не направляет. Он просто ассоциирует его с объектом Filter, который уже может быть связан с файлом или с объектом IO.
14.3.2. Дополнительные замечания по поводу библиотеки shell.rb
Метод transact исполняет блок в контексте вызывающего объекта. Таким образом, допустима следующая сокращенная запись:
sh = Shell.new
sh.transact do
echo("Строка данных") > "somefile.txt"
cat("somefile.txt","otherfile.txt") > "thirdfile"
cat("thirdfile") | tee("file4") > STDOUT
end
Итератор foreach принимает в качестве параметра файл или каталог. Если это файл, он перебирает все его строки, а если каталог — все имена файлов в нем.
sh = Shell.new
# Напечатать все строки файла /tmp/foo.
sh.foreach("/tmp/foo") {|l| puts l }
# Вывести список файлов в каталоге /tmp.
sh.foreach("/tmp") {|f| puts f }
Метод pushdir запоминает текущий каталог, а метод popdir делает последний запомненный каталог текущим. У них есть синонимы pushd и popd. Метод pwd возвращает текущий рабочий каталог, его синонимы — getwd, cwd и dir.
sh = Shell.cd "/home"
puts sh.pwd # /home
sh.pushd "/tmp"
puts sh.pwd # /tmp
sh.popd
puts sh.pwd # /home
Для удобства в класс Shell импортируются методы из различных источников, в том числе из класса File, модуля FileTest и библиотеки ftools.rb. Это избавляет от необходимости выполнять require, include, создавать объекты, квалифицировать вызовы методов и т. д.
sh = Shell.new
flag1 = sh.exist? "myfile" # Проверить существование файла.
sh.delete "somefile" # Удалить файл.
sh.move "/tmp/foo", "/tmp/bar" # Переместить файл.
У библиотеки Shell есть и другие возможности, которые мы здесь не рассматриваем. Дополнительную информацию ищите в документации.
14.4. Переменные окружения
Иногда необходимо обращаться к переменным окружения, которые являются связующим звеном между программой и внешним миром. Переменные окружения — это просто метки, связанные с некоторым текстом (обычно небольшим); в них хранятся, например, пути к файлам, имена пользователей и т.п.
Переменные окружения широко применяются в ОС UNIX. Система Windows (а еще раньше MS-DOS) позаимствовала эту идею у UNIX, поэтому приведенные ниже коды будут работать на обеих платформах.
14.4.1. Чтение и установка переменных окружения
Глобальная константа ENV — это хэш, с помощью которого можно читать и изменять переменные окружения. В примере ниже мы читаем значение переменной PATH, (в Windows вместо двоеточия нужно употреблять точку с запятой):
bypath = ENV["PATH"]
# А теперь получим массив...
dirs = mypath.split(":")
А вот пример установки переменной. Новый процесс мы создали, чтобы проиллюстрировать две вещи. Во-первых, дочерний процесс наследует переменные окружения от своего родителя. Во-вторых, значение переменной окружения, установленное в дочернем процессе, родителю не видно.
ENV["alpha"] = "123"
ENV["beta"] = "456"
puts "Родитель: alpha = #{env['alpha']}"
puts "Родитель: beta = #(env['beta']}"
fork do # Код потомка...
x = ENV["alpha"]
ENV["beta"] = "789"
y = ENV["beta"]
puts " Потомок: alpha = #{x}"
puts " Потомок: beta = #{y}"
end
Process.wait
a = ENV["alpha"]
b = ENV["beta"]
puts "Родитель: alpha = #{a}"
puts "Родитель: beta = #{b}"
Программа выводит следующие строки:
Родитель: alpha = 123
Родитель: beta = 456
Потомок: alpha = 123
Потомок: beta = 789
Родитель: alpha = 123
Родитель: beta = 456
Это следствие того факта, что родитель ничего не знает о переменных окружения своих потомков. Поскольку программа на Ruby обычно исполняется в подоболочке, то после ее завершения все сделанные изменения переменных окружения не будут видны в текущей оболочке.
14.4.2. Хранение переменных окружения в виде массива или хэша
Важно понимать, что объект ENV — не настоящий хэш, а лишь выглядит как таковой. Например, мы не можем вызвать для него метод invert; будет возбуждено исключение NameError, поскольку такого метода не существует. Причина такой реализации в том, что существует тесная связь между объектом ENV и операционной системой; любое изменение хранящихся в нем значений отражается на состоянии ОС, а такое поведение с помощью простого хэша не смоделируешь.
Однако имеется метод to_hash, который вернет настоящий хэш, отражающим текущее состояние:
envhash = ENV.to_hash
val2var = envhash.invert
Получив такой хэш, мы можем преобразовать его к любому другому виду (например, в массив):
envarr = ENV.to_hash.to_a
Обратное присваивание объекту ENV недопустимо, но при необходимости можно пойти обходным путем:
envhash = env.to_hash
# Выполняем произвольные операции... и записываем обратно в ENV.
envhash.each {|k,v| ENV[k] = v }
14.4.3. Импорт переменных окружения как глобальных переменных
Существует библиотечка importenv.rb, которая импортирует все переменные окружения, сопоставляя им глобальные переменные программы:
require "importenv"
# Теперь переменные окружения стали глобальными переменными...
# Например, $PWD и $LOGNAME
where = $PWD
who = $LOGNAME
puts "В каталоге #{where}, вошел как #{who}"
Поскольку библиотека importenv пользуется библиотекой trace_var, отражение на самом деле двустороннее: если присвоить глобальной переменной новое значение, реальная переменная окружения получит то же значение.
require "importenv"
puts "Мой путь #$PATH"
# Печатается: /usr/local/bin:/usr/bin:/usr/ucb:/etc:.
$PATH = "/ruby-1.8.0:" + $PATH
puts "Моя переменная $PATH теперь равна #{ENV['PATH']}"
# Печатается: /ruby-1.8.0:/usr/local/bin:/usr/bin:/usr/ucb:/etc:.
Еще раз подчеркнем, что любые изменения переменных окружения, выполненные внутри программы на Ruby, не отражаются на их значениях, видимых вне этой программы.
14.5. Сценарии на платформе Microsoft Windows
Уже отмечалось, что Ruby больше любит ОС UNIX. В каком-то смысле это правда: язык разрабатывался в среде UNIX, в ней лучше всего и работает. Сейчас он, впрочем, перенесен на другие платформы, в том числе на Macintosh; ведется даже работа по переносу на Palm OS. Но если UNIX — основная платформа, то следующая по значимости — Windows.
Пользователи Windows не брошены на произвол судьбы. Существует немало инструментов и библиотек для этой платформы, а разрабатывается еще больше, многие аспекты Ruby, даже механизм потоков, изначально не зависят от платформы. Наибольшие трудности возникают при управлении процессами, выполнении ввода/вывода и других операций низкого уровня.
В прошлом существовало несколько вариантов Ruby для Windows. Интерпретатор мог быть собран компилятором gcc или Visual С, его работа могла зависеть от наличия библиотеки Cygwin DLL и т.д. Но в последние годы появился «моментальный» инсталлятор для Windows (см. раздел 14.6).
Среда изменяется слишком быстро, чтобы можно было ее сейчас документировать, однако в этом разделе мы все же рассмотрим некоторые вопросы написания сценариев и автоматизации на платформе Windows. Описанные приемы и утилиты должны работать в любой ОС. Если возникнут проблемы, сообщество придет на помощь.
14.5.1. Расширение Win32API
Расширение Win32API — исключительно мощный инструмент, если вы собираетесь программировать на относительно низком уровне. Оно позволяет вызывать из Ruby функции Windows API, находящиеся в любой DLL.
Указанная функция становится объектом, а методу new передаются параметры, точно описывающие функцию. Первый параметр — строка, идентифицирующая DLL, в которой находится функция (например, crtdll). Второй параметр — имя самой функции, третий — массив строк, описывающих типы параметров функции (массив импорта), а четвертый — строка, описывающая тип возвращаемого значения (строка экспорта).
Массив импорта может содержать следующие значения (регистр не играет роли):
I целое
L число
N число
P указатель на строку
Строка экспорта также может содержать любое из этих значений, а также значение «V», означающее «void».
После того как объект создан, можно обратиться к его методу call для вызова функции Windows. Синоним — Call.
В примере ниже мы вызываем функцию GetCursorPos, которая возвращает указатель на структуру POINT. Эта структура состоит из двух полей типа long. Чтобы получить их значения, мы можем воспользоваться методом unpack:
require 'Win32API'
result = "0"*8 # Восемь байтов (достаточно для двух long).
getCursorXY = Win32API.new("user32","GetCursorPos",["P"],"V")
getCursorXY.call(result)
x, y = result.unpack("LL") # Два long.
В данном случае функция вернула составные двоичные данные, а иногда такие данные нужно подать на вход функции. Понятно, что для этого нужно воспользоваться методом pack, который упакует данные в строку.
У описанной техники может быть много применений. Еще два примера приведены в разделах 10.1.20 и 14.1.1.
14.5.2. Расширение Win32OLE
Расширение Win32OLE (правильно писать его имя строчными буквами: win32ole) реализует интерфейс к OLE-автоматизации в Windows. Программа на Ruby может выступать в роли клиента любого сервера автоматизации, к числу которых относятся, например, Microsoft Word, Outlook, Internet Explorer, а также многие продукты третьих фирм.
Для того чтобы начать взаимодействие с внешним приложением, мы создаем объект класса WIN32OLE. С его помощью мы получим доступ ко всем свойствам и методам, которые раскрывает данное приложение. В примере ниже объект ассоциируется с редактором Microsoft Word. Атрибуту visible мы присвоим значение true, а в конце вызовем метод quit, чтобы завершить внешнюю программу.
require "win32ole"
word = WIN32OLE.new "Word.Application"
word.visible = true
# ...
word.quit
Свойства сервера автоматизации выглядят как атрибуты объекта. Их можно читать и устанавливать.
Имеется и альтернативная нотация, в которой для доступа к свойствам используется конструкция, напоминающая хэш.
player["FileName"] = "file.wav"
name = player["FileName"]
# Эквивалентно следующим предложениям:
# player.FileName = "file.wav"
# name = player.FileName
У этой нотации есть то преимущество, что она позволяет проще осуществлять динамический доступ к свойствам, как показано в искусственном примере ниже:
puts "Введите имя свойства"
prop = gets
puts "Введите новое значение"
val = gets
old = obj[prop]
obj[prop] = val
puts "#{prop} было #{old}... стало #{obj[prop]}"
Но обратимся к более жизненным примерам. Следующий код получает от пользователя имя файла, передает его Microsoft Word и распечатывает файл:
require "win32ole"
print "Введите имя файла для распечатки: "
docfile = gets
word = WIN32OLE.new "Word.Application"
word.visible = true
word.documents.open docfile
word.options.printBackground = false
# Можно было бы также установить свойство printBackground в true,
# но тогда пришлось бы дожидаться, пока весь файл будет
# скопирован в буфер принтера, и только потом вызывать quit...
word.activeDocument.printout
word.quit
В следующем примере проигрывается WAV-файл. Недостаток заключается в том, что в конце программы мы поставили sleep на случайно выбранное время, а не просто дожидаемся, когда воспроизведение закончится. Предлагаем читателю устранить этот недочет в качестве упражнения.
require "win32ole"
sound = WIN32OLE.new("MCI.MMcontrol")
wav = "с:\\windows\\media\\tada.wav"
sound.fileName = wav
sound.autoEnable = true
sound.command = "Open"
sound.command = "Play"
sleep 7
В листинге 14.2 мы просим Internet Explorer открыть диалог для ввода текста.
Листинг 14.2. Открытие диалога для ввода текста в браузере
require "win32ole"
def ieInputBox( msg, default )
ie = WIN32OLE.new("InternetExplorer.Application");
ie.visible = false
ie.navigate "about:blank"
sleep 0.01 while (ie.busy)
script = ie.Document.Script;
result = script.prompt(msg,default);
ie.quit
result
end
# Главная программа...
result = ieInputBox( "Введите свое имя",
"Дэйв Боумэн")
if result
puts result
else
puts "Пользователь нажал Cancel"
end
В листинге 14.3 мы открываем IE в небольшом окне и выводим в него HTML-документ.
Листинг 14.3. Для вывода в окно браузера требуется win32ole
html = <
A теперь что-нибудь
совсем
другое...
EOF
ie = WIN32OLE.new("InternetExplorer.Application");
ie.left = 150
ie.top = 150
ie.height = 200
ie.width = 300
ie.menubar = 0
ie.toolbar = 0
ie.navigate "about:blank"
ie.visible=TRUE;
ie.document.open
ie.document.write html
ie.document.close
sleep 5
ie.quit
В следующем примере открывается диалоговое окно, где пользователь может выбрать файл из списка:
require "win32ole"
cd = WIN32OLE.new("MSComDlg.CommonDialog")
# Задать фильтр файлов
cd.filter = "All Files(*.*)| *.*" +
"| Ruby Files(*.rb)|*.rb"
cd.filterIndex = 2
cd.maxFileSize = 128 # Установить MaxFileSize.
cd.showOpen()
file = cd.fileName # Получить путь к файлу.
if not file or file==""
puts "Файл не выбран."
else
puts "Пользователь выбрал: #{file}\n"
end
И, наконец, определим IP-адрес своего компьютера:
require "win32ole"
ws = WIN32OLE.new "MSWinsock.Winsock"
# Получить свойство LocalIP
ipAddress = ws.localIP
puts "Локальный IP-адрес равен : #{ipAddress}"
Как видите, возможности не ограничены. Развлекайтесь и не забывайте делиться своими программами с другими!
14.5.3. Использование ActiveScriptRuby
Наверняка вам приходилось открывать в браузере Internet Explorer страницы, содержащие код на языке JavaScript или VBScript. (Мы не будем здесь касаться различий между JScript и JavaScript.)
Но сценарий можно написать и на языке ActiveScriptRuby, представляющем собой мост между COM и Ruby. Вот как можно включить код на Ruby в HTML-страницу (листинг 14.4).
Листинг 14.4. Код на Ruby, встроенный в HTML-страницу
Это кнопка...
С помощью той же техники можно вызывать написанный на Ruby код из любого Windows-приложения, поддерживающего интерфейс IActiveScript, например из Explorer или WScript (исполняемый файл называется WSH). Дополнительную информацию вы можете найти на странице arton (http://vvww.geocities.co.jp/SiliconValley-PaolAlto/9251/rubymain.html).
14.6. Моментальный инсталлятор для Windows
С точки зрения пользователей Microsoft Windows одним из самых значительных шагов в развитии Ruby за последние годы стал так называемый «моментальный инсталлятор» (one-click installer). Главным разработчиком этого проекта (официально он называется Ruby Installer) является Курт Гиббс (Curt Hibbs). Процедура инсталляции выполнена в «родном» для Windows стиле.
Инсталлятор особенно ценен тем, что работает в полном соответствии с ожиданиями пользователей Windows. Он имеет графический интерфейс и выполняет шаги установки в строго определенном порядке. Разумеется, инсталлируется двоичная версия, так что компилятор не нужен. Но это не единственные его привлекательные черты.
Устанавливаемый дистрибутив весьма полон («батарейки в комплекте»). Он включает не только интерпретатор Ruby со всеми системными классами и стандартными библиотеками, но и целый ряд дополнительных библиотек и приложений. Многие из них предназначены исключительно для платформы Win32.
Устанавливаются следующие компоненты (некоторые из них необязательны):
• сам интерпретатор Ruby (пакет ruby-mswin32 и пакет RubySrc для тех, кто хочет познакомиться с исходными текстами на языке С);
• два часто используемых приложения: RubyGems и rake;
• бесплатная копия книги Дейва Томаса (Dave Thomas) и Энди Ханта (Andy Hunt) «Programming Ruby» — первое издание в формате Windows Help;
• библиотека fxruby (обычно несколько версий), реализующая привязки к комплекту инструментов для создания графических интерфейсов FOX;
• инструменты для разработки приложений трехмерной графики OpenGL и GLUT;
• утилиты fxirb и fxri — графические версии программ irb и ri, написанные с применением библиотеки FXRuby;
• FreeRIDE — интегрированная среда разработки для Ruby с встроенным редактором, обозревателем исходных текстов и отладчиком (работа над совершенствованием этой программы ведется постоянно);
• SciTE — текстовый редактор на базе Scintilla;
• SWin и VRuby — инструменты для обработки сообщений Windows и разработки графических интерфейсов (обе являются частью проекта VisualuRuby, во главе которого стоит Ясухира Насикава);
• два анализатора XML (XMLParser и Expat), а также HTMLParser;
• библиотеки для работы с базами данных RubyDBI и DBD/ODBC;
• прочие библиотеки и инструменты, в том числе log4r, zlib, OpenSSL, Iconv, readline и другие.
Планируются, но еще не готовы варианты этого инсталлятора и для других платформ.
14.7. Библиотеки, о которых полезно знать
Если вы программируете на Ruby в Windows, вам абсолютно необходим пакет, созданный Дэниэлем Бергером (Daniel Berger), одним из самых известных специалистов по Ruby на этой платформе. Библиотека win32-utils — в действительности целый набор мелких библиотек. Мы не можем рассмотреть их все подробно, но хотя бы перечислим.
• win32-changenotify — для мониторинга событий файловой системы;
• win32-clipboard — для взаимодействия с буфером обмена Windows;
• win32-etc — предоставляет аналоги таких UNIX-функций, как getpwnam и getpwuid;
• win32-event — интерфейс с событиями Windows (объектами Event);
• win32-eventlog — интерфейс с журналом событий;
• win32-ipc — базовый класс для всех объектов синхронизации в Windows (используется в библиотеке win32-event и др.);
• win32-mmap — интерфейс к файлам, проецируемым на память, в Windows;
• win32-open3 — библиотека open3 для Windows (запустить команды и получить три описателя файлов);
• win32-pipe — именованные каналы в Windows;
• win32-process — реализация для Windows методов fork, wait и kill, имеющихся в UNIX;
• win32-sapi — интерфейс к Microsoft Speech API;
• win32-service — интерфейс к службам Windows;
• win32-shortcut — интерфейс для создания и модификации ярлыков в Windows;
• win32-sound — интерфейс для воспроизведения звуковых файлов в Windows;
Вот еще несколько библиотек, которые полезно иметь под рукой:
• Win32::Console — это перенос пакетов Win32::Console и Win32::Console::ANSI, первоначально написанных на языке Perl. Эта библиотека значительно упрощает работу с консолью в Windows (изменение цветов, позиционирование курсора, запрос информации и эмуляцию управляющих символов ANSI);
• ActiveDirectory позволяет легко взаимодействовать с экземплярами Active Directory, работающими на серверах под управлением Microsoft Windows;
• ruby-inifile позволяет работать с ini-файлами (читать, разбирать и обновлять их).
В сети есть еще много библиотек, которые могут вам пригодиться. Ищите их на сайтах http://raa-ruby-lang.org и http://rubyforge.org.
14.8. Работа с файлами, каталогами и деревьями
При выполнении рутинных задач приходится много работать с файлами и каталогами, в том числе с целыми иерархиями каталогов. Немало материала на эту тему вошло в главу 4, но кое-какие важные моменты мы хотим осветить здесь.
Поскольку ввод/вывод — вещь системно-зависимая, то для различных систем приходится применять разные приемы. Если сомневаетесь, экспериментируйте!..
14.8.1. Несколько слов о текстовых фильтрах
Многие инструменты, которыми мы постоянно пользуемся (как поставляемые производителем, так и разрабатываемые собственными силами), — просто текстовые фильтры. Иными словами, они принимают на входе текст, каким-то образом преобразуют его и выводят. Классическими примерами текстовых фильтров в UNIX служат, в частности, программы sed и tr.
Иногда файл настолько мал, что целиком помещается в памяти. В этом случае возможны такие виды обработки, которые по-другому было бы сложно реализовать.
file = File.open(filename)
lines = file.readlines
# Какие-то операции...
lines.each { |x| puts x }
Бывает, что нужно обрабатывать файл построчно.
IO.foreach(filename) do |line|
# Какие-то операции...
puts line
end
Наконец, не забывайте, что все имена файлов, указанные в командной строке, автоматически собираются в объект ARGF, представляющий конкатенацию всех выходных данных (см. раздел 14.2.2). Мы можем вызывать, к примеру, метод ARGF.readlines, как если бы ARGF был объектом класса IO. Вся выходная информация будет, как обычно, направлена на стандартный вывод.
14.8.2. Копирование дерева каталогов (с символическими ссылками)
Пусть нужно скопировать целое дерево каталогов в новое место. Сделать это можно по-разному, но если в дереве есть символические ссылки, задача усложняется.
В листинге 14.5 приведено рекурсивное решение. Оно достаточно дружелюбно — контролирует входные данные и выводит информацию о порядке запуска.
Листинг 14.5. Копирование дерева каталогов
require "fileutils"
def recurse(src, dst)
Dir.mkdir(dst)
Dir.foreach(src) do |e|
# Пропустить . и ..
next if [".",".."].include? e
fullname = src + "/" + e
newname = fullname.sub(Regexp.new(Regexp.escape(src)),dst)
if FileTest:rdirectory?(fullname)
recurse(fullname,newname)
elsif FileTest::symlink?(fullname)
linkname = 'ls -l #{fullname}'.sub(/.* -> /,"").chomp
newlink = linkname.dup
n = newlink.index($oldname)
next if n == nil
n2 = n + $oldname.length - 1
newlink[n..n2] = $newname
newlink.sub!(/\/\//,"/")
# newlink = linkname.sub(Regexp.new(Regexp.escape(src)),dst)
File.symlink(newlink, newname)
elsif FileTest::file?(fullname)
FileUtils.copy(fullname, newname)
else
puts "??? : #{fullname}"
end
end
end
# "Главная программа"
if ARGV.size != 2
puts "Usage: copytree oldname newname"
exit
end
oldname = ARGV[0]
newname = ARGV[1]
if ! FileTest::directory?(oldname)
puts "Ошибка: первый параметр должен быть именем существующего каталога."
exit
end
if FileTest::exist? (newname)
puts "Ошибка: #{newname} уже существует."
exit
end
oldname = File.expand_path(oldname)
newname = File.expand_path(newname)
$оldname=oldname
$newname=newname
recurse(oldname, newname)
Возможно, и существуют варианты UNIX, в которых команда cp -R сохраняет символические ссылки, но нам о них ничего не известно. Программа, показанная в листинге 14.5, была написана для решения этой практической задачи.
14.8.3. Удаление файлов по времени модификации и другим критериям
Предположим, вы хотите удалить самые старые файлы из какого-то каталога. В нем могут, к примеру, храниться временные файлы, протоколы, кэш браузера и т.п.
Ниже представлена небольшая программа, удаляющая файлы, которые в последний раз модифицировались раньше указанного момента (заданного в виде объекта Time):
def delete_older(dir, time)
Dir.chdir(dir) do
Dir.foreach(".") do |entry|
# Каталоги не обрабатываются.
next if File.stat(entry).directory?
# Используем время модификации.
if File.mtime(entry) < time
File.unlink(entry)
end
end
end
end
delete_older("/tmp",Time.local(2001,3,29,18,38,0))
Неплохо, но можно обобщить. Создадим метод delete_if, который принимает блок, возвращающий значение true или false. И будем удалять те и только те файлы, которые удовлетворяют заданному критерию.
def delete_if(dir)
Dir.chdir(dir) do
Dir.foreach(".") do |entry|
# Каталоги не обрабатываются.
next if File.stat(entry).directory?
if yield entry
File.unlink(entry)
end
end
end
end
# Удалить файлы длиннее 3000 байтов.
delete_if("/tmp") { |f| File.size(f) > 3000 }
# Удалить файлы с расширениями LOG и BAK.
delete_if("/tmp") { |f| f =~ /(log|bak)$/i }
14.8.4. Вычисление свободного места на диске
Пусть нужно узнать, сколько байтов свободно на некотором устройстве. В следующем примере это делается по-простому, путем запуска системной утилиты:
def freespace(device=".")
lines = %x(df -k #{device}).split("\n")
n = lines.last.split[1].to_i * 1024
end
puts freespace("/tmp") # 16772204544
Эту задачу лучше решать, обернув метод statfs в расширение Ruby. Такие попытки в прошлом предпринимались, но, похоже, проект умер.
Для Windows имеется несколько более элегантное решение (предложено Дэниэлем Бергером):
require 'Win32API'
GetDiskFreeSpaceEx = Win32API.new('kernel32', 'GetDiskFreeSpaceEx',
'PPPP', 'I')
def freespace(dir=".")
total_bytes = [0].pack('Q')
total_free = [0].pack('Q')
GetDiskFreeSpaceEx.call(dir, 0, total_bytes, total_free)
total_bytes = total_bytes.unpack('Q').first
total_free = total_free.unpack('Q').first
end
puts freespace("С:") # 5340389376
Этот код должен работать во всех вариантах Windows.
14.9. Различные сценарии
Приведем еще несколько примеров. Не претендуя на оригинальность, мы отнесли их к категории «разное».
14.9.1. Ruby в виде одного файла
Иногда нужно быстро или временно установить Ruby. Или даже включить Ruby в состав собственной программы, поставляемой в виде одного исполняемого файла.
Мы уже познакомились с «моментальным инсталлятором» Ruby для Windows. Существуют планы (пока еще не оформившиеся) создать подобный инсталлятор для Linux и Mac OS X.
Эрик Веенстра (Erik Veenstra) недавно добился значительных успехов в создании пакетов, включающих как Ruby, так и написанные на нем приложения. Он автор пакетов AllInOneRuby, Tar2RubyScript и RubyScript2Exe (все они есть на его сайте http://www.erikveen.dds.nl).
AllInOneRuby — это дистрибутив Ruby в одном файле. В пакет входят интерпретатор Ruby, системные классы и стандартные библиотеки, упакованные в единый архив, который легко перемещать или копировать. Например, его можно записать на USB-диск, носить в кармане и «установить» на любую машину за считанные секунды. Работает AllInOneRuby на платформах Windows и Linux; имеется также экспериментальная поддержка для Mac OS X.
Что такое Tar2RubyScript, следует из самого названия. Программа получает на входе дерево каталогов и создает самораспаковывающийся архив, включающий написанную на Ruby программу и архив в формате tar. Идея та же, что у JAR-файлов в языке Java. Запускаемый сценарий должен называться init.rb; если сохраняется библиотека, а не автономное приложение, этот файл можно опустить.
Название RubyScript2Exe, наверное, не вполне удачно. Программа действительно преобразует написанное на Ruby приложение в один двоичный файл, однако работает она не только в Windows, но и в Linux и Mac OS X. Можете называть ее компилятором, хотя в действительности она им, конечно, не является. Она собирает файлы, являющиеся частью установленного дистрибутива Ruby на вашей машине, поэтому не нуждается в кросс-компиляции (даже если бы такая возможность имелась). Имейте в виду, что исполняемый файл «усечен» в том смысле, что неиспользуемые библиотеки Ruby в него не включаются.
Архив, созданный программой Tar2RubyScript, можно запустить на любой машине, где установлен Ruby (и программы, которые необходимы самому приложению). RubyScript2Exe не имеет такого ограничения, поскольку включает (наряду с вашим приложением) интерпретатор Ruby, всю среду исполнения и все необходимые внешние программы. Можете использовать эти инструменты вместе или порознь.
14.9.2. Подача входных данных Ruby по конвейеру
Поскольку интерпретатор Ruby — это однопроходный транслятор, можно подать ему на вход некий код и выполнить его. Это может оказаться полезным, когда обстоятельства вынуждают вас работать на традиционном языке сценариев, но для каких-то сложных задач вы хотите применить Ruby.
В листинге 14.6 представлен bash-сценарий, который вызывает Ruby (посредством вложенного документа) для вычисления интервала в секундах между двумя моментами времени. Ruby-программа печатает на стандартный вывод одно значение, которое перехватывается вызывающим сценарием.
Листинг 14.6. bash-сценарий, вызывающий Ruby
#!/usr/bin/bash
# Для вычисления разницы в секундах между двумя моментами временами
# bash вызывает Ruby...
export time1="2007-04-02 15:56:12"
export time2="2007-12-08 12:03:19"
cat <
require "parsedate"
time1 = ENV["time1"]
time2 = ENV["time2"]
args1 = ParseDate.parsedate(time1)
args2 = ParseDate.parsedate(time2)
args1 = args1[0..5]
args2 = args2[0..5]
t1 = Time.local(*args1)
t2 = Time.local(*args2)
diff = t2 — t1
puts diff
EOF
echo "Прошло секунд = " $elapsed
В данном случае оба исходных значения передаются в виде переменных окружения (которые необходимо экспортировать). Строки, читающие эти значения, можно было бы записать так:
time1="$time1" # Включить переменные оболочки непосредственно
time2="$time2" # в строку...
Но возникающие при этом проблемы очевидны. Очень трудно понять, имеется ли в виду переменная bash или глобальная переменная Ruby. Возможна также путаница при экранировании и расстановке кавычек.
Флаг -e позволяет создавать однострочные Ruby-сценарии. Вот пример обращения строки:
#!/usr/bin/bash
string="Francis Bacon"
ruby -e "puts '$string'.reverse" | read reversed
# $reversed теперь равно "nocaB sicnarF"
Знатоки UNIX заметят, что awk использовался подобным образом с незапамятных времен.
14.9.3. Получение и установка кодов завершения
Метод exit возбуждает исключение SystemExit и в конечном счете возвращает указанный код завершения операционной системе (или тому, кто его вызвал). Этот метод определен в модуле Kernel. Метод exit! отличается от него в двух отношениях: он не выполняет зарегистрированные обработчики завершения и по умолчанию возвращает -1.
# ...
if (all_OK)
exit # Нормально (0).
else
exit! # В спешке (-1).
end
Когда операционная система печатает возвращенный Ruby код (например, выполнив команду echo $?), мы видим то же самое число, что было указано в программе. Если завершается дочерний процесс, то код его завершения, полученный с помощью метода wait2 (или waitpid2), будет сдвинут влево на восемь битов. Это причуда стандарта POSIX, которую Ruby унаследовал.
child = fork { sleep 1; exit 3 }
pid, code = Process.wait2 # [12554,768]
status = code << 8 #3
14.9.4. Работает ли Ruby в интерактивном режиме?
Чтобы узнать, работает ли программа в интерактивном режиме, нужно проверить стандартный ввод. Метод isatty? возвращает true, если устройство интерактивное, а не диск или сокет. (Для Windows этот метод не реализован.)
if STDIN.isatty?
puts "Привет! Я вижу, вы печатаете"
puts "на клавиатуре."
else
puts "Входные данные поступают не с клавиатуры."
end
14.9.5. Определение текущей платформы или операционной системы
Если программа хочет знать, в какой операционной системе исполняется, то может опросить глобальную константу RUBY_PLATFORM. В ответ будет возвращена загадочная строка (что-то вроде i386-cygwin или sparc-solaris2.7), содержащая информацию о платформе, для которой был собран интерпретатор Ruby.
Поскольку мы в основном работаем с вариантами UNIX (Solaris, AIX, Linux) и Windows (98, NT, 2000, XP), то считаем полезным следующий очень грубый код. Он отличает UNIX от Windows (бесцеремонно отправляя всех остальных в категорию «прочие»).
def os_family
case RUBY_PLATFORM
when /ix/i, /ux/i, /gnu/i,
/sysv/i, /solaris/i,
/sunos/i, /bsd/i
"unix"
when /win/i, /ming/i
"windows"
else
"other"
end
end
Этот небольшой набор регулярных выражений корректно распознает абсолютное большинство платформ. Конечно, это весьма неуклюжий способ обработки системных зависимостей. Даже если вы правильно определите семейство ОС, отсюда еще не следует, что нужная вам функциональность имеется (или отсутствует).
14.9.6. Модуль Etc
Модуль Etc получает различную информацию из файлов /etc/passwd и /etc/group. Понятно, что полезен он только на платформе UNIX.
Метод getlogin возвращает имя пользователя, от имени которого запущена программа. Если он завершается неудачно, может помочь метод getpwuid (принимающий в качестве необязательного параметра идентификатор пользователя uid).
myself = getlogin # hal9000
myname = getpwuid(2001).name # hal9000
# Если параметр не задан, getpwuid вызывает getuid...
me2 = getpwuid.name # hal9000
Метод getpwnam возвращает структуру passwd, которая содержит поля name, dir (начальный каталог), shell (начальный интерпретатор команд) и др.
rootshell = getpwnam("root").shell # /sbin/sh
Методы getgrgid и getgrnam ведут себя аналогично, но по отношению к группам. Они возвращают структуру group, содержащую имя группы и т.д.
Итератор passwd обходит все записи в файле /etc/passwd. Запись передается в блок в виде структуры passwd.
all_users = []
passwd { |entry| all_users << entry.name }
Имеется также итератор group для обхода записей в файле /etc/group.
14.10. Заключение
На этом мы завершаем обсуждение применения Ruby для решения рутинных задач автоматизации. Мы видели, как передавать в программу и получать от нее информацию в виде переменных окружения и с помощью стандартного ввода/вывода. Мы познакомились с типичными операциями «склеивания», позволяющими разным программам взаимодействовать. Рассмотрели мы и различные уровни взаимодействия с операционной системой.
Поскольку значительная часть изложенного материала системно зависима, я призываю вас экспериментировать. Между платформами Windows и UNIX имеются серьезные отличия. Есть они и между разными операционными системами, относящимися к одному семейству.
Следующая тема, которую мы рассмотрим, тоже весьма широка. Речь пойдет о работе с данными в разных форматах, от графических до XML.