Статьи

Введение в Тор

256px-Ruby_logo.svg

Глядя на список драгоценных камней RubyGem , мы видим много знакомых имен: «rake», «rails», «rack», «actionmailer» и т. Д. Но «thor» является исключением; немногие из нас слышали об этом, и еще меньше написали код с ним. Итак, что именно Тор?

Это способ написания мощных утилит командной строки в Ruby. Это делает анализ аргументов действительно простым и задает определенный формат для аргументов командной строки. Тонны проектов Ruby используют Thor для того, чтобы сделать утилиты командной строки (например, команду «rails») написанными легко, быстро и увлекательно.

Но чтобы понять, почему Тор такой классный, нам нужно сначала понять, какую проблему он решает.

Старые дни

Назад, когда динамические языки предназначались для тех, кто не понимал указатели, у нас была процедура getopt_long для разбора параметров командной строки. Код, необходимый для фактического использования этой функции, обычно представлял собой большой оператор switch который после написания должен был быть забыт. Пока, конечно, не настал день, когда нужно было добавить еще один вариант. Посмотрите пример:

 while (1) { static struct option long_options[] = { /* These options set a flag. */ {"verbose", no_argument, &verbose_flag, 1}, {"brief", no_argument, &verbose_flag, 0}, /* These options don't set a flag. We distinguish them by their indices. */ {"add", no_argument, 0, 'a'}, {"append", no_argument, 0, 'b'}, {"delete", required_argument, 0, 'd'}, {"create", required_argument, 0, 'c'}, {"file", required_argument, 0, 'f'}, {0, 0, 0, 0} }; /* getopt_long stores the option index here. */ int option_index = 0; c = getopt_long (argc, argv, "abc:d:f:", long_options, &option_index); /* Detect the end of the options. */ if (c == -1) break; switch (c) { case 0: /* If this option set a flag, do nothing else now. */ if (long_options[option_index].flag != 0) break; printf ("option %s", long_options[option_index].name); if (optarg) printf (" with arg %s", optarg); printf ("\n"); break; case 'a': puts ("option -a\n"); break; case 'b': puts ("option -b\n"); break; case 'c': printf ("option -c with value `%s'\n", optarg); break; case 'd': printf ("option -d with value `%s'\n", optarg); break; case 'f': printf ("option -f with value `%s'\n", optarg); break; case '?': /* getopt_long already printed an error message. */ break; default: abort (); } } 

Даже если вы никогда не смотрели на C-код в своей жизни, это не выглядит как приятный опыт. Итак, они придумали библиотеки, чтобы сделать эту операцию немного проще. В сообществе Perl «достижение совершеннолетия» включает в себя написание собственного анализатора командной строки. Как оказалось, интенсивное использование Perl в утилитах командной строки выросло из его завидного модуля GetOpts :: Long. Посмотрите на этот пример из Perldoc (система документации Perl):

 use Getopt::Long; my $data = "file.dat"; my $length = 24; my $verbose; GetOptions("length=i" => \$length, "file=s" => \$data, "verbose" => \$verbose) or die("Error in command line arguments"); 

По сути, этот код позволяет вам указать длину, данные и подробные параметры, а также числовые, строковые и логические значения соответственно. Но это все еще не кажется правильным. Он не очень чистый или объектно-ориентированный, потому что мы просто складываем вещи в переменные.

Thor — это «Рубиновый способ» анализа параметров командной строки. Это делает все удивительно объектно-ориентированным и абстрагирует детали. Это позволяет вам сконцентрироваться на своей великой идее, вместо того, чтобы без необходимости возиться с опциями и разбором строк.

Давайте посмотрим на конкретный пример того, как Тор творит свою магию.

Первые шаги с Тором

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

 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 

Имея всего несколько строк кода, у нас есть полное описание команды, которую мы создали! Конечно, если вы запустите сценарий с аргументами «Привет, Дхаиват», вы должны получить ответ «Привет, Дхаиват!». Давайте разберем код.

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

Что именно я подразумеваю под получением? Как только мы передадим аргумент, Тор выясняет, к какому формату он подходит, и вызывает этот метод из нашего класса «SayHi». В этом случае он вызывает метод hi с аргументом name.

Наконец, у нас есть метод 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», он выведет содержимое файла (который обрабатывается с помощью метода «output» в «FileOp»). учебный класс).

Копать немного глубже

Одна из самых удивительных функций Тора — автоматическое создание справки. Метод «desc», использованный ранее, имеет второй аргумент, который фактически описывает, для чего предназначена каждая команда. Итак, с нашей утилитой file-op, если запустить:

 ruby file-op.rb help output 

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

 Usage: file_op_v1.rb output FILE_NAME print out the contents of FILE_NAME 

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

Очевидно, что немногие важные приложения командной строки будут удовлетворены такой базовой структурой опций. К счастью, у Тора тоже есть спина. Разве не было бы неплохо, если бы мы могли добавить флаг в нашу команду «output» для вывода файла в 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) 

Мы добавили несколько строк, но наиболее важной является option :stderr . Эта строка сообщает Thor, что любая команда, которую мы только что определили (например, «output» в этом случае), может иметь флаг, переданный как логическое значение. Другими словами, оно либо передается, либо не передается; к нему не прикреплено другое значение, например, --times 15 . Итак, мы можем запустить:

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

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

Но вся эта «опция» делает Тора немного странным. Как он узнает, к какой команде мы добавляем опцию? Каждый раз, когда вы используете option , она ссылается на вызов desc непосредственно перед ним. Давайте добавим еще одну команду в нашу утилиту file-op которая просто создает пустой файл. Это эквивалентно touch в * nix системах, поэтому мы будем называть команду «touch»:

 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 desc 'touch FILE_NAME', 'creates an empty file named FILE_NAME' option :chmod, :type => :numeric def touch(file_name) f = File.new(file_name, "w") f.chmod(options[:chmod]) if options[:chmod] end end FileOp.start(ARGV) 

Реализация довольно проста. Все, что я сделал, закончил с output кодом, а затем поместил вызов desc для «touch».

В качестве дополнительного бонуса я даже включил опцию --chmod которая позволяет вам устанавливать права доступа к файлам, к которым вы «прикасаетесь» (например, передача в 000 по существу создает заблокированный файл). Что касается личных предпочтений, мне не нравится, как Тор поддерживает связь между option и методом, на который он влияет, хотя я не уверен в альтернативном решении, которое было бы так же приятно использовать.

Параметры класса

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

 some_utility.rb -v 

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

 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 . Чтобы сообщить Тору, что log не связан с командой, мы должны поместить ее в этот блок. В методе log мы выводим данную строку, если Thor получает --verbose . Затем мы используем этот метод в выходных и сенсорных командах.

Завершение

Thor — невероятно универсальная библиотека, которая делает анализ командной строки простым и интуитивно понятным. Однако есть несколько nocommands таких как блок nocommands о которых нужно знать, чтобы избежать потенциальных ошибок.

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

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