Thor – божественный фреймворк для создания задач

июня 28, 2012  |  Published in Ruby, Ruby Gems, Ruby on Rails, Ruby on Rails 3  |  3 Comments

– это альтернатива для всем известного Rake. Автором Thor является небезызвестный , хотя если посмотреть на коммиты в репозитории Thor, то Катц находится только на 4 месте, но сейчас не об этом. Thor – создавался как более изящная альтернатива фреймворку Rake и это вполне удалось его авторам. Я надеюсь, что по прочтению данной статьи вы не только научитесь работать с Thor, но и замените им Rake.

Для установки thor воспользуйтесь командой:

$ gem install thor

Если вы программируете с использованием фреймворка Ruby on Rails, то Thor уже уже установлен так как находится в зависимостях у Rails и используется внутри Rails для создания генераторов.
Установив Thor у вас появится возможность использовать в консоли команду thor:

$ thor -v
Thor 0.15.3

$ thor
Tasks:
thor help [TASK] # Describe available tasks or one specific task
thor install NAME # Install an optionally named Thor file into your syst…
thor installed # List the installed Thor modules and tasks
thor list [SEARCH] # List the available thor tasks (—substring means .*S…
thor uninstall NAME # Uninstall a named Thor module
thor update NAME # Update a Thor file from its original location
thor version # Show Thor version

Прежде чем создать первую задачу Thor вы должны быть знакомы с некоторыми соглашениями:

  1. Задачи Thor имеют расширение .thor
  2. Имена файлов в которых имеются задачи должны соответствовать именам классов.
  3. Классы используются в для создания пространств имен.
  4. Каждая задача – это просто метод.

Теперь мы можем приступить к созданию первой задачи. В файл first_task.thor поместим следующий код на Ruby:

class FirstTask < Thor
  desc 'hello_world', "prints \"Hello, World!\""
  def hello_world
    puts "Hello, World!"
  end
end

После того, как задача создана, убедимся, что Thor ее «видит»:

$ thor list
first_task
———-
thor first_task:hello_world # prints «Hello, World!»

Команда thor list просто выводит список всех доступных задач с описанием, которое вы даете им используя метод desc. В качестве первого аргумента метод desc принимает название задачи (метода), а в качестве второго – строку описания того, что эта задача делает.
Когда мы убедились в том, что Thor «видит» нашу задачу давайте выполним ее:

$ thor first_task:hello_world
Hello, World!

Задачи Thor могут принимать передаваемые в них пользователем параметры, давайте напишем такую задачу, которая будет принимать параметр.

class FirstTask < Thor
  desc 'hello_world', "prints \"Hello, World!\""
  def hello_world
    puts "Hello, World!"
  end

  desc 'hello USER_NAME', "prints \"Hello, %user_name%!\""
  def hello(user_name)
    puts "Hello, #{user_name}!"
  end
end

Теперь попробуем ее запустить:

$ thor list
first_task
———-
thor first_task:hello USER_NAME # prints «Hello, %user_name%!»
thor first_task:hello_world # prints «Hello, World!»

$ thor first_task:hello
thor hello requires at least 1 argument: «thor first_task:hello USER_NAME».

$ thor first_task:hello Vladimir
Hello, Vladimir!

Как вы уже, наверное, догадались, в метод desc в первом его аргументе следует передавать не только имя задачи, но и все необходимые для ее выполнения параметры.

Следующей важной возможностью Thor является возможность использования опций (еще их называют флагами). Для объявления опций следует использовать метод method_option. Ниже приведен пример задачи использующей опции.

class FirstTask < Thor
  desc 'hello_world', "prints \"Hello, World!\""
  def hello_world
    puts "Hello, World!"
  end

desc 'hello USER_NAME', "prints \"Hello, %user_name%!\""
def hello(user_name)
puts "Hello, #{user_name}!"
end

  desc 'authenticate USER_NAME TOKEN', 'authenticate user though token'
  method_option :admin, :aliases => '-a', :desc => 'authenticate as admin', :type => :boolean
  def authenticate(user_name, token)
    role = options[:admin] ? 'admin' : 'guest'
    puts <<-MESSAGE
    #{user_name}, you have been successfully authenticated as
    #{role} with #{token} token.
    MESSAGE
  end
end

Вызываем задачу:

$ thor first_task:authenticate Vladimir ToKeN
Vladimir, you have been successfully authenticated as
guest with ToKeN token.

$ thor first_task:authenticate Vladimir ToKeN –a
Vladimir, you have been successfully authenticated as
admin with ToKeN token.

Помимо метода method_option вы можете использовать также метод method_options, который позволяет объявить сразу несколько опций. Доступ к опциям, как видно из примера осуществляется через хэш options.

При объявлении опций задачи вы можете пользоваться следующими опциями (простите за тавтологию) методов method_options и method_option:

  • :aliases – псевдонимы опций
  • :default – значение по умолчанию для опции
  • :desc – описание опции (показывается при thor help )
  • :banner – краткое описание опции, по умолчанию это название опции набранное заглавными буквами.
  • :required – показывает, что передача опции при вызове задачи обязательна
  • :type – тип опции. Тип может быть следующим: :boolean, :string, :numeric, :array, :hash.

В зависимости от того, каким типом обладают опции изменяется и синтаксис передачи их значений. Например, для опции типа :Boolean значение можно упускать. Если опция передана, то options[:option_name] будет true, а в противном случае false. Можно, конечно, явно передать значение true или false, например, так:

$ thor namespace:task_name —option=true

Передача значений опций типа :string и :numeric аналогично явной передаче значения опции типа :bolean, а вот для :array и :hash синтаксис несколько другой, соответственно:

$ thor namespace:task_name —option=1 2 3 4 5
$ thor namespace:task_name —option=login:string password:string

На этот момент вы уже умеете писать простые задачи, которые покроят 70% ваших нужд. Теперь давайте познакомимся с более сложными возможностями Thor.

Вызов других задач

Thor позволяет в контексте задач вызывать другие задачи, причем объявленные не только в текущем классе, но в любом другом. Для вызова другой задачи используется метод invoke:

class FirstTask < Thor
  desc 'authenticate_as_admin USER_NAME TOKEN', "prints \"Hello, %user_name%!\""
  def authenticate_as_admin(user_name, token)
    puts "Hello, #{user_name}!"
    invoke :authenticate, [user_name, token], :admin => true
  end

  desc 'authenticate USER_NAME TOKEN', 'authenticate user though token'
  method_option :admin, :aliases => '-a', :desc => 'authenticate as admin', :type => :boolean
  def authenticate(user_name, token)
    role = options[:admin] ? 'admin' : 'guest'
    puts <<-MESSAGE
    #{user_name}, you have been successfully authenticated as
    #{role} with #{token} token.
    MESSAGE
  end
end

$ thor first_task:authenticate_as_admin Vladimir ToKeN
Hello, Vladimir!
Vladimir, you have been successfully authenticated as
admin with ToKeN token.

Если задача, которую вы хотите вызвать находится в другом классе, то вы должны указать и имя класса:

invoke "second_task:parse_something"

Группы

Следующее с чем следует познакомиться – это группы. Группами называются классы, которые наследуются от Thor::Group. В группах задачи выполняются все вместе, в том порядке, в котором они определены. Давайте перепишем наши задачи с использованием Thor::Group.

class FirstTask < Thor::Group desc "authenticate user though token" argument :user_name, :type => :string
  argument :token, :type => :string
  class_option :admin, :aliases => "-a", :type => :boolean, :default => true

  def authenticate_as_admin
    puts "Hello, #{user_name}!"
  end

  def authenticate
    role = options[:admin] ? 'admin' : 'guest'
    puts "#{user_name}, you have been successfully authenticated as #{role} with #{token} token."
  end
end

Запускаем:

$ thor list
root
—-
thor first_task USER_NAME TOKEN # authenticate user though token

$ thor first_task Vladimir Token
Hello, Vladimir!
Vladimir, you have been successfully authenticated as admin with Token token.

Обратите внимание на то, что мы используем метод argument для объявления параметров и метод class_option для объявления опций.

Экшены и хэлперы Thor

Чтобы сделать работу более удобной Thor поставляется с набором экшенов – так называются методы хэлперы, которые выполняют некоторые полезные функции.

Чтобы спользовать экшены Thor необходимо включить в подмешать в класс с задачами Thor::Actions. Ниже приведен краткий список экшенов с кратким описанием.

append_to_file(path, *args, &block)
Добавляет текст в конец файла.

chmod(path, mode, config = {})
Изменяет права доступа к файлу или директории.

copy_file(source, *args, &block)
Копирует файл по относительному пути в текущую директорию.

create_file(destination, *args, &block) (also: #add_file)
Создает файл и помещает в него отпределенный текст.

directory(source, *args, &block)
Полностью копирует структуру директорий и файлы из source директории в root директорию.

empty_directory(destination, config = {})
Создает пустую директорию.

inject_into_class(path, klass, *args, &block)
Помещает текст в файл сразу после объявления класса.

insert_into_file(destination, *args, &block) (или #inject_into_file)
Помещает текст в файл.

prepend_to_file(path, *args, &block) (also: #prepend_file)
Добавляет текст в начало файла.

run(command, config = {})
Выполняет команду и возвращает результат ее выполнения.

Помимо экшенов из Thor::Actions, с некоторыми из которых мы только что познакомились существует еще одно расширение — Thor::Shell::Basic. Это расширение предаставляет хэлперы для работой с консолью, например печати вопросов пользователю. Ниже приведены некоторые из хэлперов:

ask(statement, *args)
Спрашивает что-то у пользователя и сохраняет ответ.

error(statement)
Вызывается при возникновении ошибки во время исполнения.

file_collision(destination)
Спрашивает у пользователя можно ли перезаписать файл в случае необходимости.

yes?(statement, color = nil)
Делает вопрос пользователю и возвращает true, если ответ y — yes.

no?(statement, color = nil)
Антипод yes?. Возвращает true если ответ n — no.

print_table(table, options = {})
Печатает таблицу.

print_wrapped(message, options = {})
Печатает большой кусок текста с переносом строк по достижению приделов окна консоли.

say(message = «», color = nil, force_new_line = (message.to_s !~ /( |\t)$/))
Печатает что-то пользователю

Когда мы знаем столько экшенов и консольных хелперов, давайте напишем простую задачу с их применением.

class FirstTask < Thor
  include Thor::Actions

  desc 'print_dir', "It prints contents of directory."
  def print_dir
    if yes?("Do you want me print contents of directory?")
      run('dir')
    else
      error("You don't want to print contents of directory!")
    end
  end
end

Запускаем:

$ thor first_task:print_dir
Do you want me print contents of directory? y
run dir from «.»
A5client A5server first_task.thor flagus rails spine.site

$ thor first_task:print_dir
Do you want me print contents of directory? n
You don’t want to print contents of directory!

Tips & Tricks (штуки-дрюки)

Thor задачи можно вызывать не только из консоли, но и из кода. Наследуясь от Thor классы задач получают метод start, вызов которого для класса реализует запуск задачи на выполнение, наприме:

FirstTask.start

Так вы можете запускать задачи по расписанию или использовать их как «обработчики событий».

Заключение

Я имел опыт работы с Rake и с Thor и хочу сказать, что Thor более удобен в пользовании. Вместо использования Rake’ового ущербного DSL, которое местами выглядит не очивидно (некоторые моменты в работе следует запоминать) я рекомендую вам переходить на использование замечательной альтернативы в стиле ООП от замечательных Ехуды Катца (wycatz), Жозе Валима (josevalim) и дугих.

Tags: , , , ,

Responses

  1. Serg says:

    июня 29, 2012 at 00:44 (#)

    расширение у файла должно быть *.rake(«В файл first_task.rake поместим следующий код на Ruby:»), не *.thor???

  2. admin says:

    июня 29, 2012 at 01:14 (#)

    Serg, абсолютно верно, это я чего-то отвлекся и перепутал. Спасибо, поправил.

  3. prijutme4ty says:

    июля 11, 2012 at 15:51 (#)

    Спасибо за статью!
    Пара вопросов:
    Вот например, я хочу иметь возможность запускать задачи так:
    thor benchmark (== thor benchmark:run), thor benchmark:show
    1)Как мне сделать односложный синоним по имени нэймспейса, и как заставить его принимать опции и аргументы командной строки?
    2)Как эти опции и аргументы задачи передавать при invoke’е зависимостей?

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.