Предисловие

Продолжая цикл статей о других направления разработки на Ruby, кроме веб-разработки. Пришла очередь многим известного Thor, который позволяет делать удобные cli-утилиты с применением Ruby.

Знакомство

Давайте сразу перейдем к простому примеру:

require 'thor'

class SayHi < Thor
  desc "hi NAME", "say hello to NAME"

  def hi(name)
    puts "Hi #{name}!"
  end
end

SayHi.start(ARGV)

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

Commands:
  first_steps.rb help [COMMAND]  # Describe available commands or one specific command
  first_steps.rb hi NAME         # say hello to NAME

С помощью всего нескольких строк кода у нас есть полное описание созданной нами команды! Конечно, если вы запустите скрипт с аргументами “Hi Danila”, вы должны получить ответ “Hello, Danila!”. Давайте разберем код.

Мы создали класс под названием “SayHi”, который является производным от класса Тhor. Затем в следующей строке говорится об описании конкретной команды. Первым аргументом функции “desc” является “hi NAME”, которое описывает, какая команда нам нужна. В этом случае в нем говорится, что мы хотим, чтобы пользователь мог ввести “hi”, а затем свое имя, которое будет передано в качестве переменной. Другим примером может быть “location LATITUDE LONGITUDE”, где пользователь может ввести “location 64.39 21.34”, и команда определения местоположения получит широту и долготу, указанные данными числами.

Что именно я подразумеваю под получением? Как только мы передаем аргумент, Thor
определяет, в какой формат он вписывается, и вызывает этот метод из нашего класса “SayHi”. В этом случае он вызывает метод “hi” с “именем” в качестве аргумента.

Наконец, у нас есть наш метод “hi”, который представляет собой просто стандартный скрипт на Ruby.

Приложения Thor обычно следуют такому шаблону; существует множество функций, которые можно использовать, но общие, основополагающие концепции остаются прежними.

Давайте напишем небольшую утилиту под названием file-op, в которой есть опция командной строки для вывода содержимого файла.

require 'thor'
class FileOp < Thor
	desc 'output FILE_NAME', 'print out the contents of FILE_NAME'
  
	def output(file_name)
		puts File.read(file_name)
	end
end

FileOp.start(ARGV)

Это почти та же концепция, что и в примере “SayHi”, но на этот раз, если вы запустите его с “output FILENAME”, он выведет содержимое файла (которое обрабатывается с помощью метода “вывод” класса “FileOp”).

Одной из самых удивительных функций Thor является автоматическая “help generation”. Метод “desc”, используемый ра��ее, имеет второй аргумент, который фактически описывает, для чего предназначена каждая команда.

Итак, сделаем это помощью нашей утилиты для работы с файлами, если запустить:

ruby file-op.rb help output

Thor выводит приятное уведомление об использовании:

Usage:
  file_op_v1.rb output FILE_NAME

print out the contents of FILE_NAME

Мы не только четко документируем команды для себя в скрипте, но и пользователь знает, что делает каждая команда!

Очевидно, что немногие значимые приложения командной строки будут удовлетворены такой базовой структурой опций. К счастью, Тhor может больше. Разве не было бы неплохо, если бы мы могли добавить флаг в нашу команду “вывод” для вывода файла в stderr? Я знаю, на самом деле это было бы не так уж приятно, но давай все равно сделаем это:

require 'thor'

class FileOp < Thor
  desc 'output FILE_NAME', 'print out the contents of FILE_NAME'
  option :stderr, :type => :boolean

  def output(file_name)
    #options[:stderr] is either true or false depending
    #on whether or not --stderr was passed
    contents = File.read(file_name)
    if options[:stderr]
      $stderr.puts contents
    else
      $stdout.puts contents 
    end
  end
end

FileOp.start(ARGV)

Мы добавили несколько строк, но самая важная - это опция :stderr. Эта строка сообщает Тору, что любая команда, которую мы только что определили (т.е. “вывод” в данном случае), может иметь флаг, переданный как логическое значение. Другими словами, он либо передается, либо не передается; к нему не привязано другое значение, как
--times15.

Итак, мы можем идти далее:

ruby file-ops-v2.rb output --stderr filename

который напечатал бы содержимое “filename” в stderr.

Как насчет команд, которые могут быть применены к любой команде? Что-то вроде:
some_utility.rb -v

У Thor и для этого есть решение. Эти параметры, которые не связаны с определенной командой, называются параметрами класса. Нам не помешала бы дополнительная информация, когда запустится наша утилита. Но обычно нам не нужна эта информация, поэтому мы включим опцию детализации:

require 'thor'

class FileOp < Thor
  class_option :verbose, :type => :boolean
  desc 'output FILE_NAME', 'print out the contents of FILE_NAME'
  option :stderr, :type => :boolean
  def output(file_name)
    log("Starting to read file...")
    #options[:stderr] is either true or false depending
    #on whether or not --stderr was passed
    contents = File.read(file_name)
    log("File contents:")
    if options[:stderr]
      log("(in stderr)")
      $stderr.puts contents
    else
      log("(in stdout)")
      $stdout.puts contents 
    end
  end

  no_commands do 
    def log(str)
      puts str if options[:verbose]
    end
  end

  desc 'touch FILE_NAME', 'creates an empty file named FILE_NAME'
  option :chmod, :type => :numeric
  def touch(file_name)
    log("Touching file...")
    f = File.new(file_name, "w")
    f.chmod(options[:chmod]) if options[:chmod]
  end
end

FileOp.start(ARGV)

Если вы запустите, выдаст это:

ruby file_op_v5.rb output some_file --verbose

Вы должны увидеть некоторые сообщения журнала в дополнение к содержимому файла. Давайте взглянем на код.

Мы добавили метод log, однако он содержится в блоке nocommands. Чтобы сообщить Тору, что журнал не связан с командой, мы должны поместить его в этот блок. В методе журнала мы распечатываем заданную строку, если Тор получает --подробный. Затем мы используем этот метод в командах вывода.

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

Источник для этой статьи можно найти здесь.