homeASCIIcasts

242: Thor 

(view original Railscast)

Other translations: En Es

Other formats:

Written by Сергей Андронов

Каждый разработчик на рельсах иногда сталкивается с Rake. Rake первоначально была написана в качестве альтернативы make, но в Rails в основном используется для выполнения небольших административных скриптов. Rake имеет некоторые ограничения для написания скриптов, например, чтобы передать аргументы в Rake, обычно мы вынуждены использовать переменные среды. Другая проблема заключается в том, что Rake задачи нельзя легко сделать глобальными, они всегда локальные и привязаны к текущему проекту. Такие инструменты, как Sake решают эту проблему, но тогда вам нужно использовать дополнительный инструмент.

В этом эпизоде будет рассмотрен Thor. Thor – это альтернатива Rake, которая не имеет ограничений упомянутых выше. Он подключается по зависимостям в Rails, то есть если у нас установлены рельсы – значит, у вас есть Thor. Поэтому если вам нужно написать генератор для рельсов – используйте Thor, а как его использовать я сейчас покажу.

Thor запускается из командной строки, и если мы запустим thor help, мы получим список команд, которые он поддерживает.

$ thor help
Задачи:
  thor help [TASK]     # Описание задач или конкретной задачи
  thor install NAME    # Установить необязательного имени Thor файла в системные задачи
  thor installed       # Список установленных модулей и задач Thor
  thor list [SEARCH]   # Список установленных задач Thor 
  thor uninstall NAME  # Удаление именованного модуля Thor
  thor update NAME     # Обновление файла Thor из своего первоначального места
  thor version         # Версия Thor

У нас нет не одного собственного скрипта Thor, давайте его создадим.

Скрипт для копирования файлов конфигурации

Если уж мы собрались делать скрипт для Thor, давайте сделаем что-нибудь полезное. В приложениях Rails считается нормальным не ставить некоторые файлы конфигурации в систему контроля версий, так как они могут содержать важные данные, такие как пароли. Мы создадим пример конфигурационного файла в подкаталоге examples, затем создадим скрипт Thor , который будет копировать эти файлы в папку config.

Thor скрипт можно положить в папку lib/tasks, как скрипт Rake. Итак, создадим новый файл с именем setup.thor. Скрипт Thor – это класс, наследуемый от Thor, и название класса станет именем для его команды. Каждый метод, описанный в desc, становится командой. Методу desc нужно передать 2 параметра: имя команды и описание. Начнем с написания простой команды config, которая выведет строку текста.

/lib/tasks/setup.thor

class Setup < Thor
  
  desc "config", "copy configuration files"
  def config
    puts "running config"
  end
end

Мы можем проверить команду, запустив thor setup:config. Она вызывает метод config и мы видим нашу строчку в терминале.

$ thor setup:config
running config

Для того чтобы посмотреть список доступных команд запустим thor list.

$ thor list
setup
-----
thor setup:config  # copy configuration files

В выводе команды мы видим описание, которое мы только что написали.

Копирование файлов

Давайте сделаем так, чтобы команда config делала что-то полезное. Код перебирает каждый файл из папки /config/examples и копирует их в папку /config, пропуская уже существующие файлы.

/lib/tasks/setup.thor

class Setup < Thor
  
  desc "config", "copy configuration files"
  def config
    Dir["config/examples/*"].each do |source|
      destination = "config/#{File.basename(source)}"
      if File.exist?(destination)
        puts "Skipping #{destination} because it already exists"
      else
        puts "Generating #{destination}"
        FileUtils.cp(source, destination)
      end
    end
  end
end

Если мы снова запустим команду config, она скопирует файлы.

$ thor setup:config
Generating config/database.yml
Generating config/private.yml

Когда мы запустим команду снова, она уже не будет копировать существующие файлы в папке config

$ thor setup:config
Skipping config/database.yml because it already exists
Skipping config/private.yml because it already exists

Было бы полезным, если бы с опцией --force существующие файлы были перезаписаны. Мы можем сделать это вызвав method_options перед определением метода, для того чтобы задать опцию.

/lib/tasks/setup.thor

class Setup < Thor
  
  desc "config", "copy configuration files"
  method_options :force => :boolean
  def config
    Dir["config/examples/*"].each do |source|
      destination = "config/#{File.basename(source)}"
      FileUtils.rm(destination) if options[:force]
      if File.exist?(destination)
        puts "Skipping #{destination} because it already exists"
      else
        puts "Generating #{destination}"
        FileUtils.cp(source, destination)
      end
    end
  end
end

Мы можем добавить несколько опций и присвоить им разные типы: строковый, числовой и так далее. Чтобы получить значение данной опции мы вызываем options и в приведенном выше коде мы используем options[:force], чтобы получить опцию :force и удалить файлы, если параметр force был задан.

Если мы запустим файл с опцией --force все существующие файлы перезапишутся.

$ thor setup:config --force
Generating config/database.yml
Generating config/private.yml

Добавление параметров

Любые дополнительные параметры, которые мы передаем в thor, становятся методами. Предположим мы хотим скопировать только файл private.yml.

$ thor setup:config private.yml

Этот параметр будет передан методы config. Мы не хотим, чтобы всегда нужно было указывать имя файла, поэтому присвоим параметру значение по-умолчанию "*", чтобы копировались все файлы. Хорошо бы еще держать документацию в порядке, поэтому давайте изменим описание, чтобы в нем упоминался параметр NAME.

/lib/tasks/setup.thor

class Setup < Thor
  
  desc "config [NAME]", "copy configuration files"
  method_options :force => :boolean
  def config(name = "*")
    Dir["config/examples/#{name}"].each do |source|
      destination = "config/#{File.basename(source)}"
      FileUtils.rm(destination) if options[:force]
      if File.exist?(destination)
        puts "Skipping #{destination} because it already exists"
      else
        puts "Generating #{destination}"
        FileUtils.cp(source, destination)
      end
    end
  end
end

Теперь запустим команду с параметром и посмотрим, как она работает.

$ thor setup:config private.yml
Skipping config/private.yml because it already exists

Работает. А теперь запустим с опцией --force

$ thor setup:config  private.yml --force
Generating config/private.yml

Установка глобальных команд

Наш скрипт стал полезным, и мы хотим его использовать в других Rails проектах. Это делается легко: все что нужно - это запустить thor install <path_to_file> и наш скрипт будет в системных командах Thor.

$ thor install lib/tasks/setup.thor
Your Thorfile contains:
class Setup < Thor
  
  desc "config [NAME]", "copy configuration files"
  method_options :force => :boolean
  def config(name = "*")
    Dir["config/examples/#{name}"].each do |source|
      destination = "config/#{File.basename(source)}"
      FileUtils.rm(destination) if options[:force]
      if File.exist?(destination)
        puts "Skipping #{destination} because it already exists"
      else
        puts "Generating #{destination}"
        FileUtils.cp(source, destination)
      end
    end
  end
end
Do you wish to continue [y/N]? y
Please specify a name for lib/tasks/setup.thor in the system repository [setup.thor]: 
Storing thor file in your system repository

Однажды установив, мы можем запускать thor list в любой папке и увидим список команд.

$ cd ~
$ thor list
setup
-----
thor setup:config [NAME]  # copy configuration files

Наша команда теперь доступна глобально и мы можем ее использовать в любых Rails приложениях.

Доступ к Rails приложению из Thor

Есть еще несколько вещей в Thor, которые мы покажем, и для их демонстрации мы создадим новую команду в нашем классе Setup. Эта команда будет создавать несколько записей в нашей базе данных, поэтому мы назовем ее populate (заселение). Наше приложение - блог с моделью Article и populate создаст нам 10 записей.

/lib/tasks/setup.thor

class Setup < Thor
  
  desc "config [NAME]", "copy configuration files"
  method_options :force => :boolean
  def config(name = "*")
	# Тело метода Config пропущенно
  end
  
  desc "populate", "generate records"
  def populate
    10.times do |num|
      puts "Generating article #{num}"
      Article.create!(:name => "Article #{num}")
    end
  end
end

Если мы попробуем запустить команду сейчас, получим ошибку, что класс Article не найден, потому что команда не может загрузить модели приложения. Приложение Rails по-умолчанию не доступно для команд Thor, поэтому нам нужно загрузить приложение до того, как мы попробуем создать Article. К счастью это делается не сложно, все что нужно - это привязать файл config/environment.

/lib/tasks/setup.thor

class Setup < Thor
  
  desc "config [NAME]", "copy configuration files"
  method_options :force => :boolean
  def config(name = "*")
	# Тело метода Config пропущенно
  end
  
  desc "populate", "generate records"
  def populate
    require ‘./config/environment’
    10.times do |num|
      puts "Generating article #{num}"
      Article.create!(:name => "Article #{num}")
    end
  end
end

Если мы запустим команду еще раз, она создаст 10 статей, после того как среда загрузится.

$ thor setup:populate
Generating article 0
Generating article 1
Generating article 2
Generating article 3
Generating article 4
Generating article 5
Generating article 6
Generating article 7
Generating article 8
Generating article 9

Было бы хорошо, если бы мы могли сами задать количество статей, загружаемых в базу. Мы можем сделать это, используя method_options, так же как мы это делали для метода config.

/lib/tasks/setup.thor

class Setup < Thor
  
  desc "config [NAME]", "copy configuration files"
  method_options :force => :boolean
  def config(name = "*")
	# Тело метода Config пропущенно
  end
  
  desc "populate", "generate records"
  method_options :count => 10
  def populate
    require './config/environment'
    options[:count].times do |num|
      puts "Generating article #{num}"
      Article.create!(:name => "Article #{num}")
    end
  end
end

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

$ thor setup:populate --count 5
Generating article 0
Generating article 1
Generating article 2
Generating article 3
Generating article 4

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

Когда использовать Thor, а когда Rake? Если вы делаете простое Rails приложение, тогда лучше использовать Rake, так как он является более популярным и многие его знают. А если вам нужно для своего Rails приложения сделать какие-то задачи связанные с администрированием, тогда можно рассмотреть и Thor.