• 14.1. Запуск внешних программ
  • 14.1.1. Методы system и exec
  • 14.1.2. Перехват вывода программы
  • 14.1.3. Манипулирование процессами
  • 14.1.4. Стандартный ввод и вывод
  • 14.2. Флаги и аргументы в командной строке
  • 14.2.1. Разбор флагов в командной строке
  • 14.2.2. Константа ARGF
  • 14.2.3. Константа ARGV
  • 14.3. Библиотека Shell
  • 14.3.1. Использование библиотеки Shell для перенаправления ввода/вывода
  • 14.3.2. Дополнительные замечания по поводу библиотеки shell.rb
  • 14.4. Переменные окружения
  • 14.4.1. Чтение и установка переменных окружения
  • 14.4.2. Хранение переменных окружения в виде массива или хэша
  • 14.4.3. Импорт переменных окружения как глобальных переменных
  • 14.5. Сценарии на платформе Microsoft Windows
  • 14.5.1. Расширение Win32API
  • 14.5.2. Расширение Win32OLE
  • 14.5.3. Использование ActiveScriptRuby
  • 14.6. Моментальный инсталлятор для Windows
  • 14.7. Библиотеки, о которых полезно знать
  • 14.8. Работа с файлами, каталогами и деревьями
  • 14.8.1. Несколько слов о текстовых фильтрах
  • 14.8.2. Копирование дерева каталогов (с символическими ссылками)
  • 14.8.3. Удаление файлов по времени модификации и другим критериям
  • 14.8.4. Вычисление свободного места на диске
  • 14.9. Различные сценарии
  • 14.9.1. Ruby в виде одного файла
  • 14.9.2. Подача входных данных Ruby по конвейеру
  • 14.9.3. Получение и установка кодов завершения
  • 14.9.4. Работает ли Ruby в интерактивном режиме?
  • 14.9.5. Определение текущей платформы или операционной системы
  • 14.9.6. Модуль Etc
  • 14.10. Заключение
  • Глава 14. Сценарии и системное администрирование

    И сказал главный программист: «Даже если программа состоит всего из трех строчек, когда-то ее придется сопровождать».

    (Джеффри Джеймс, «Дао программирования»)

    Программистам часто приходится писать небольшие сценарии для запуска внешних программ и работы с операционной системой на достаточно высоком уровне. Особенно это относится к ОС 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 = <<EOF

    <html>

     <body>

      <h3>A теперь что-нибудь</h3>

      <h2>совсем</h2>

      <h1>другое...</h1>

     </body>

    </html>

    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-страницу

    <html>

     <script language="RubyScript">

      # Это код на Ruby...

      def helloMethod

       @window.alert "Работает Ruby!"

      end

     </script>


     <body>


      Это кнопка...

      <input id=Hello type=button onclick="helloMethod" language="RubyScript">

     </body>

    </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 <<EOF | ruby | read elapsed


    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.









    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх