Трета задача - Четене на командни аргументи

Предадени решения

Краен срок:
09.11.2016 17:00
Точки:
6

Срокът за предаване на решения е отминал

Четене на командни аргументи

Хайде да напишем микро-библиотека за четене на аргументи, която да ни помага, когато създаваме собствени конзолни програми.

Конзолен интерфейс!?

Голяма част от софтуера, който се използва, има конзолен интерфейс. Конзолният интерфейс на дадено приложение предоставя конзолна команда, с която да се взаимодейства със съответното приложение. Често командата може да приема аргументи и опции.

Например, RSpec - Ruby библиотеката, с която си пускате тестовете.

След като ви дадем задача, ние ви даваме и примерни тестове, които може да изпълните така (допускайки, че се намирате в директорията на задачата):

rspec --require=./solution.rb sample_spec.rb

Това, което виждате е част от конзолния интерфейс на RSpec:

  • rspec е конзолната команда, която ни позволява да работим с RSpec
  • --require=./solution.rb е опция с аргумент, която указва път до файл, който RSpec трябва да зареди преди да изпълни тестовете
  • sample_spec.rb е самостоятелен, позиционен аргумент, указващ път до файл с тестове

Ето още няколко примера за взаимодействие с RSpec (+ обяснения):

  • rspec spec.rb - пуска тестовете от spec.rb
    • rspec - команда
    • spec.rb - аргумент
  • rspec --version - извежда версията на RSpec
    • rspec - команда
    • --version - опция
  • rspec -v - кратък вариант на горната опция
    • rspec - команда
    • -v - опция (кратък вариант)
  • rspec -r./solution.rb spec.rb
    • rspec - команда
    • -r./solution.rb - опция (кратка версия) с аргумент
    • spec.rb - аргумент
  • rspec - какво прави извикването на командата без аргументи и опции разберете сами (:

Изводи и изисквания към задачата: * Конзолните команди могат да приемат аргументи и/или опции * Опциите могат да имат и кратки версии * Опциите могат да приемат аргументи * Кратките версии на опциите започват с - и са еднобуквени * Аргументи на дадена опция подаваме като след името на опцията слагаме = и стойността на аргумента. Аргументи можем да подаваме и с краткия вариант, но обърнете внимание, че няма празно място между името на опцията и стойността ѝ (-r./solution.rb)

Конзолен интерфейс и Ruby

В Ruby стандартният начин за разчитане на командни аргументи и опции идва от C. Когато се изпълни един Ruby скрипт, те ще стоят в масива ARGV.

Примери:

# inspect_argv.rb

puts ARGV.inspect
> ruby inspect_argv.rb 1 2 3
["1", "2", "3"]

> ruby inspect_argv.rb --foo=bar baz
["--foo=bar", "baz"]

> ruby inspect_argv.rb -l
["-l"]

Това е сравнително удобно, но от този масив не се разбира лесно кое е опция, аргумент или опция с аргумент.

И сега... Задачата

Дефинирайте клас CommandParser с конструктор приемащ един аргумент command_name.

Този клас трябва да има методи #argument, #option и #option_with_parameter. На тези три метода трябва да може да се подава име на опция/аргумент и блок. Този блок трябва да бъде извикан, когато бъде прочетена съответната опция или аргумент. Повече детайли - по-надолу в условието.

CommandParser трябва да има и метод parse, който върши реалното четене на аргументи и опции. parse трябва да приема следните аргументи:

  • command_runner - произволен Ruby обект
  • argv - масив от стрингове наподобяващ ARGV

Четене на aргументи

parser = CommandParser.new('rspec')

parser.argument('FILE') do |runner, value|
  runner[:file] = value
end

command_runner = {}

parser.parse(command_runner, ['spec.rb'])

command_runner #=> {file: 'spec.rb'}

В горния пример виждаме как трябва да работи #parse.

Блокът, подаден на #argument трябва да се изпълни, когато #parse бъде извикан и в argv се съдържа аргумент на съответната позиция. В този пример, FILE е името на първия позиционен аргумент в argv. Тоест, FILE има стойност spec.rb.

В блока трябва да се подадат две стойности:

  • Произволният Ruby обект, който сме дали на parse. Идеята на този обект е да представлява "състоянието" на програмата, което зависи от параметрите ѝ. Обикновено тук се предава хеш, който се пълни с опциите, но това не е задължително (може да е наш собствен клас). Вашата задача е единствено да предадете този обект на блока - не се интересувате какъв е той.
  • Стойността на аргумента от argv

Забележка: В горния пример, FILE е името на аргумента, но ключът в хеша (:file) не произлиза от него. Можем да направим и следното:

parser.argument('FILE') do |runner, value|
  runner[:larodi] = value
end

command_runner = {}

parser.parse(command_runner, ['spec.rb'])

command_runner #=> {larodi: 'spec.rb'}

CommandParser може да дефинира повече от един аргумент:

parser = CommandParser.new('rspec')

parser.argument('FIRST') do |runner, value|
  runner[:first] = value
end

parser.argument('SECOND') do |runner, value|
  runner[:second] = value
end

command_runner = {}

parser.parse(command_runner, ['foo', 'larodi'])

command_runner #=> {first: 'foo', second: 'larodi'}

Когато има повече от един аргумент, стойностите подадени в argv трябва да се обработват последователно и да се подават като value на блоковете. Това значи, че редът на дефиниране на стойностите е от значение.

Забележка: За по-просто, няма да тестваме с различен брой позиционни аргументи в argv от броя на дефинираните в нашия парсер. За сметка на това, може да има опции (нещата, започващи с тире), които да направят argv с различна дължина от броя на извикванията на argument.

Опции

Освен позиционни аргументи, CommandParser трябва да може да чете и произволен брой опции. Разпознаваме една опция по това, че започва с - или --. Няма да подаваме позиционни аргументи, започващи с тирета. Ако опция започва с --, значи е повече от една буква. Ако започва само с едно тире - значи е еднобуквена.

За четене на опции, дефинирайте метод option, подобен на argument:

parser = CommandParser.new('rspec')

parser.option('v', 'version', 'show version number') do |runner, value|
  runner[:version] = value
end

command_runner = {}
parser.parse(command_runner, ['--version'])

command_runner #=> {version: true}

command_runner = {}
parser.parse(command_runner, ['-v'])

command_runner #=> {version: true}

option трябва да приема три стринга:

  • Кратко име на опцията (например 'v')
  • Пълно (дълго) име на опцията (например 'version')
  • Помощен текст - описание на опцията. Това ще го използваме по-натам, за да генерираме инструкции за използване на нашата програма
  • Блок, който трябва да се извика само ако при извикването на parse, в argv има стринг, отговаряш на опцията (в примера - --version или -v)

Още:

  • Вторият аргумент на блока - value - винаги трябва да има стойност true
  • Игнорирайте всички опции от argv, които не са дефинирани в кода
  • Опциите може да са "разпръснати" измежду аргументите. ['arg_one', '--list', 'arg_two', '-a'] е валиден argv масив.

Опции с параметри

Дефинирайте и метод option_with_parameter, който е силно подобен на option, но дефинира опция със стойност.

Пример:

parser = CommandParser.new('rspec')

parser.option_with_parameter('r', 'require', 'require FILE in spec', 'FILE') do |runner, value|
  runner[:require] = value
end

command_runner = {}
parser.parse(command_runner, ['--require=solution.rb'])
command_runner #=> {require: 'solution.rb'}

command_runner = {}
parser.parse(command_runner, ['-rsolution.rb'])
command_runner #=> {require: 'solution.rb'}

Първите три аргумента се държат като аргументите на option. Последният аргумент е placeholder за стойността. Това отново се използва по-надолу при генерирането на документация.

Както при #option, подаденият блок се извиква само ако опцията съществува в argv.

Забележка: Няма да подаваме в argv неща които са опции с параметри без параметрите им (в горният пример няма да подаваме за argv ['--require']).

Помощ

Последният метод който трябва да дефинира CommandParser е #help. Той трябва да връща стринг с кратък текст документиращ програмата, която пишем.

На първия си ред, това съобщение съдържа стринга Usage:, името на командата и имената на позиционните аргументи (оградени в квадратни скоби):

parser = CommandParser.new('rspec')
parser.argument('FIRST') { |_, _| }
parser.argument('SECOND') { |_, _| }

parser.help #=> 'Usage: rspec [FIRST] [SECOND]'

Ако имаме опции (с или без параметри) трябва да изведем по един нов ред за всяка, в следния формат:

parser = CommandParser.new('rspec')
parser.argument('SPEC FILE') { |_, _| }
parser.option('v', 'verbose', 'Verbose mode') { |_, _| }
parser.option_with_parameter('r', 'require', 'require FILE in spec', 'FILE') { |_, _| }

parser.help #=>
'Usage: rspec [SPEC FILE]
    -v, --verbose Verbose mode
    -r, --require=FILE require FILE in spec'

Всяка опция има едноредово описание. Реда започва с 4 празни места. Ако имаме опция с параметър - след знака за равенство на дългата опция сложете последния аргумент от #option_with_parameter.

Забележка Редът в който извеждате опциите няма значение. Важното е всички да са там. За аргументите редът има значение, тъй като по това се разпознават от програмата.

Други неща

  • Задачата по никакъв начин не е обвързана конкретно с RSpec. RSpec е само един пример за програма с конзолен интерфейс. Практически всички конзолни програми използват тези понятия под една или друга форма.
  • Помага да прочетете условието няколко пъти :)
  • Ако не разбирате нещо от условието или причината за определено поведение - пишете в темата за задачата във форума - ще помагаме!
  • Можете да ни пишете и ако имате проблеми с определена част от имплементацията - ще ви дадем насоки.
  • Не забравяйте да си пуснете примерните тестове преди да предадете решение.

Успех!

Ограничения

Тази задача има следните ограничения:

  • Най-много 80 символа на ред
  • Най-много 2 нива на влагане
  • Най-много 10 реда на метод
  • Най-много 60 реда на клас
  • Най-много 50 реда на модул
  • Най-много 7 аргумента на метод

Ако искате да проверите дали задачата ви спазва ограниченията, следвайте инструкциите в описанието на хранилището за домашните.

Няма да приемаме решения, които не спазват ограниченията. Изпълнявайте rubocop редовно, докато пишете кода. Ако смятате, че rubocop греши по някакъв начин, пишете ни на fmi@ruby.bg, заедно с прикачен код или линк към такъв като private gist. Ако пуснете кода си публично (например във форумите), ще смятаме това за преписване.