07.10.2009Некоторые тонкости стыковки ruby и bash
Введение
Последнее время больше занимаюсь работой системного администратора нежели программиста. Прошлую неделю даже пропустил написание статьи. Это, конечно, не означает, что совсем нечего рассказать.
Поскольку для быстроты я обычно пишу большинство скриптов на руби, необходимо чтобы они следовали некоторым тонкостям работы с командной строкой.
Условное выполнение
В bash кроме разделителей команд «&» и «;», существует ещё и условное выполнение списка команд с помощью «&&» и «||». Их работа зависит от кода, с которым произошёл выход. exit0:
#!/usr/bin/env ruby
puts "Выход с кодом 0"
exit 0
exit1:
#!/usr/bin/env ruby
puts "Выход с кодом 1"
exit 1
Теперь если запустить скрипты в следующем сочетании:
./exit0 && ./exit0 && ./exit1 && ./exit0
То вывод будет следующий:
Выход с кодом 0
Выход с кодом 0
Выход с кодом 1
А если запустить скрипты в следующем сочетании:
./exit1 || ./exit1 || ./exit0 || ./exit1
То вывод будет следующий:
Выход с кодом 1
Выход с кодом 1
Выход с кодом 0
То есть, «&&» выполняет следующую команду, если предыдущая вышла с кодом 0, а «||» выполняет следующую команду, если предыдущая вернула ненулевой код. Условно говоря, первый список выполняется пока всё срабатывает, а второй -- пока не срабатывает.
Перенаправление вывода с помощью pipeline
Тут всё просто. Если в bash команды разделены с помощью «|», то вывод первой команды будет перенаправлен на вход второй.
Простая демонстрация из трёх файлов. show:
#!/usr/bin/env ruby
$KCODE = "utf8"
$stdout.puts "stdin содержит: #{$stdin.read.inspect}"
out:
#!/usr/bin/env ruby
$stdout.puts "Привет из stdout!"
err:
#!/usr/bin/env ruby
$stderr.puts "[*stderr*] Привет из stderr!"
$stdout.puts "Привет из stdout!"
Думаю, не составит труда выяснить, что выводят запущенные отдельно out и err, но вот, что из этого можно сделать с помощью pipeline:
./out | ./show
выводит
stdin содержит: "Привет из stdout!\n"
Когда же есть обращение к другому каналу вывода, то
./err | ./show
выводит
[*stderr*] Привет из stderr!
stdin содержит: "Привет из stdout!\n"
Вывод stderr можно перенаправить в stdout:
./err 2>&1 | ./show
выводит
stdin содержит: "[*stderr*] Привет из stderr!\nПривет из stdout!\n"
Материалы для самостоятельного изучения
10.09.2009Создание init-скриптов с помощью руби
Предисловие
Сегодня у меня кроме всего прочего появился повод похвастаться! :) Когда вы смотрите широко обсуждаемые Яндекс-Панорамы, знайте, что их производством в составе моей любимой компании neq4 занимался и я.
В частности, флэш-просмотрщик, который кроме панорам и улиц показывает ещё и номера домов в соответствии с информацией, полученной от сервера, был разработан мной и передан на поддержку Яндексу. Когда происходит полная передача проекта с исходниками, не совсем справедливо забирать всю славу себе. Но следует отметить, что мои исходники исчерпывающе документированы. И разработчики обратились ко мне с вопросами лишь два раза. Но, может быть, они всё уже переписали с нуля, и я зря тешу своё самолюбие :)
Так же я, безусловно, планирую раскрывать некоторые секреты. Кстати, практически все записи в этом блоге основаны на работе над этим проектом. Например, программа, которая управляет камерами во время движения, а так же получает данные от приборов, была так же написана мной. И теперь вы знаете, что написана она на руби. :) За исключением библиотеки для работы с камерами, о которой я уже писал.
Это и подводит нас вплотную к теме сегодняшней записи.
Введение
Безусловно, руби — лучший из языков. Для меня выбор в его пользу обусловлен прежде всего высокой скоростью разработки. Поэтому при возникновении какой-то задачи, особенно если её нужно сделать быстро, я пробую решить её на руби. В прошлый раз я рассказал об автоподстановке. А сегодня — создание init-скриптов
Скрипты эти располагаются в папке /etc/init.d и запускаются при старте в определённых условиях. Они должны принимать параметры start, stop, restart и status. Как правило, они служат для запуска и остановки служб.
Решение
Написать программу, которая принимает текстовый ключ на входе, не составляет труда (/etc/init.d/my_script):
#!/usr/bin/env ruby
APP_NAME = "my_script"
case ARGV.first
when "status"
if...
...
exit 0
elsif ...
...
exit 1
else
exit ...
end
when "start"
...
when "stop"
...
when "restart"
...
else
puts "Usage: #{APP_NAME} {start|stop|restart|status}"
end
Круто, но до настоящего init-скрипта не дотягивает. Когда система проходит свой цикл жизни от запуска до перезагрузки или выключения, она проходит через шесть стадий. Нужно указать, в каких стадиях наш скрипт следует запустить с параметром start, а когда — с параметром stop. Для этого используется спецификация LSB (Linux Standard Base). Вот так:
#!/usr/bin/env ruby
# Start/stop my daemon
#
### BEGIN INIT INFO
# Provides: my_script
# Required-Start: $all
# Required-Stop: $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Initializes my daemon at boot time
# Description: In a Galaxy far, far away...
### END INIT INFO
APP_NAME = "my_script"
case ARGV.first
.....
Теперь лежащий в /etc/init.d скрипт нужно подрегулировать и зарегистрировать:
sudo chown root:root /etc/init.d/my_script
sudo chmod 755 /etc/init.d/my_script
sudo update-rc.d my_script defaults
Таким образом, вовсе необязательно изучать bash-скрипт. Вот, кстати, оболочка-альтернатива всяким башам, использующая синтаксис руби. Для таких, как я. :)
Для самостоятельного изучения
02.09.2009Автоподстановка задач rake в терминале с помощью ruby
Введение
Периодически натыкаюсь на хвалебные отзывы о zsh и о совершенной необходимости перехода с bash на него всему прогрессивному человечеству. В качестве демонстрации удобства демонстрируют лёгкость создания скриптов автоподстановки, например для задач rake. Неужели из-за этого следует сменить оболочку терминала?
Задача
Найти решение для автоподстановки задач rake с помощью bash несложно. Для этого используется complete. Огромное число примеров можно посмотреть у себя же в системе в /etc/bash_completion.
При ближайшем рассмотрении выяснилось, что complete может использовать не только функцию, но и команду. То есть отдельный запускаемый скрипт, который может быть написан и на руби в том числе.
Можно ли из этого извлечь пользу?
Решение
Итак, чтобы подключить наш скрипт, нужно добавить в файл ~/.bashrc такую строчку.
complete -C ~/.bash/autocomplete/rake_complete -o default rake
Для начала положим по адресу ~/.bash/autocomplete/rake_complete элементарную реализацию автоподстановки, которая будет запускать rake и фильтровать результат.
Следует знать два момента:
- Введенная строка, после которой была нажата табуляция, находится в ENV["COMP_LINE"].
- В задачах могут попасться пространства имён. Поэтому из окончательного результата нужно убрать часть введенной строки, содержащей двоеточие.
Элементарная реализация выглядит следующим образом:
#!/usr/bin/env ruby
class RakeComplete
def initialize(cmd)
@command = cmd[ /\s(.+)$/, 1 ] || ""
end
def search
`rake -T`.split("\n")[1..-1].map{ |l| l.split(/\s+/)[1] }.select{ |cmd| cmd[0, @command.length] == @command }.map{ |cmd| cmd.gsub(cmd_before_column, "") }
end
private
def cmd_before_column
@command[ /.+\:/ ] || ""
end
end
puts RakeComplete.new(ENV["COMP_LINE"]).search
exit 0
Вроде бы всё понятно: из результата вывода rake -T , отбросив паразитную первую строчку, вытаскиваем только названия задач, подбираем те, начало который совпадает с введенным названием задачи, выводим массив, предварительно удалив у элементов введённую до двоеточия часть. Но уж больно медленно работает. Конечно же, встает вопрос о кешировании.
Кеширование
Приведу сразу результат:
#!/usr/bin/env ruby
class RakeComplete
CACHE_NAME = ".rake_complete~"
def initialize(cmd)
@command = cmd[ /\s(.+)$/, 1 ] || ""
end
def search
exit 0 if rake_file.nil?
selected_tasks.map do |cmd|
cmd.gsub(cmd_before_column, "")
end
end
private
def cmd_before_column
@command[ /.+\:/ ] || ""
end
def rake_file
["Rakefile", "Rakefile.rb", "rakefile", "rakefile.rb"].detect do |name|
File.file?(File.join(Dir.pwd, name))
end
end
def cache_file
File.join(Dir.pwd, CACHE_NAME)
end
def generate_tasks
tasks = `rake -T`.split("\n")[1..-1].map{ |l| l.split(/\s+/)[1] }
File.open(cache_file, "w") do |f|
tasks.each do |task|
f.puts task
end
end
tasks
end
def cached_tasks
File.read(cache_file).split("\n")
end
def cache_valid?
File.exist?(cache_file) && (File.mtime(cache_file) >= File.mtime(rake_file))
end
def tasks
cache_valid? ? cached_tasks : generate_tasks
end
def selected_tasks
tasks.select do |cmd|
cmd[0, @command.length] == @command
end
end
end
puts RakeComplete.new(ENV["COMP_LINE"]).search
exit 0
Для самостоятельного изучения
Думаю, теперь не составит труда написать подобное для задач capistrano.