Трета задача - Четене на командни аргументи
- Краен срок:
- 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. Ако пуснете кода си публично (например във форумите), ще смятаме
това за преписване.