Tabela de Conteúdos
:: Topo :: doCoding :: Linguagens de Programação :: Ruby :: FORPC101 :: CourseProject ::
Milestone 1
Referências
Objectivos
- o programa tem 3 modos de funcionamento:
- opções na linha de comando - por default, o programa parte do princípio que receberá os valores obrigatórios através de short-options - -s, -u, -p - ou long-options - –server, –username, –password - e dois argumentos finais com “posição fixa”: source_files_or_folder e target_files_or_folder.
- interactivo - despoletado pela opção - exclusiva - de linha de comandos -i/–interactive, para aceitar e tratar input do utilizador até ter todos os valores obrigatórios, sendo que podem existir múltiplas ocorrências de source_files_or_folder e target_files_or_folder, até ao input final “bye”.
- scriptable - despoletado pela opção - exclusiva - de linha de comandos -b/–batchjob, para ler e tratar um ficheiro com os valores obrigatórios por linha, sendo que podem existir múltiplas ocorrências de source_files_or_folder e target_files_or_folder.
- o programa, nesta milestone, termina de 2 formas:
- com sucesso, quando conseguiu realizar o parse de 1 dos 3 modos de funcionamento com sucesso.
- sem sucesso, quando ocorrer ou detectar um valor inválido para um dos valores obrigatórios.
- codificar testes unitários, seguindo a metodologia TDD.
- determinar qual a library que simplifica o código necessário - mantendo o conceito DRY.
Resolução
Decidi seguir a metodologia TDD para me conduzir na codificação das classes que irão efectuar o parsing dos argumentos, para cada library.
Análise
- GetoptLong - seguir o exemplo que vem com a documentação.
- OptionParser - seguir o exemplo que vem com a documentação.
- Getopt::Declare
- Choice
- cmdparse
- User Choices
- Optiflag
- CommandLine - supostamente é a mesma library que a OptionParser.
- OptConfig - está em Japonês, não cheguei a analisar.
Desenho
test_milestone-01.rb
=begin Milestone 1 is for evaluating several libraries for Command-Line Argument Parsers. =end require 'stringio' require 'test/unit' # require 'milestone-01-GetoptLong' require 'milestone-01-OptionParser' class TestMilestone01 < Test::Unit::TestCase def setup @batch_job_script = 'batch_job_script' @server_name = 'server.nowhere.net' @username = 'guest' @password = @username + '@' + @server_name @input_arguments_help = [ [ '-h' ], [ '--help' ] ] @input_arguments_empty = [ ] @input_arguments_unknown = [ [ '-x' ], [ '--xpto' ] ] @input_arguments_interactive = [ [ '-i' ], [ '--interactive' ] ] @input_arguments_batchjob_required = [ [ '-b' ], [ '--batchjob' ] ] @input_arguments_batchjob = [ [ '-b', @batch_job_script ], [ '--batchjob', @batch_job_script ] ] @input_arguments_server = [ [ '-s', @server_name ], [ '--server' , @server_name ] ] @input_arguments_username = [ [ '-u', @username ], [ '--username', @username ] ] @input_arguments_password = [ [ '-p', @password ], [ '--password', @password ] ] end def teardown end # def test_getoptlong # end def test_optionparser parser = Milestone01OptionParser.new assert_kind_of OptionParser, parser, 'Parser is a descendant of OptionParser.' message = UsageMessage.new captured_output = '' @input_arguments_help.each do | argument | $stdout = StringIO.new(captured_output) options_parsed = parser.options_parsed(argument) $stdout = STDOUT assert_equal message.usage_message, captured_output, "UsageMessage Output." end options_parsed = parser.options_parsed(@input_arguments_empty) assert options_parsed.interactive, 'Parsing empty arguments triggers InteractiveMode.' assert_raise OptionParser::InvalidOption, "InvalidOption Exception Raised." do @input_arguments_unknown.each do | argument | options_parsed = parser.options_parsed(argument) end end @input_arguments_interactive.each do | argument | options_parsed = parser.options_parsed(argument) assert options_parsed.interactive, "Parsing ArgumentOption: '#{argument}'." end assert_raise OptionParser::MissingArgument, "ArgumentError Exception Raised." do @input_arguments_batchjob_required.each do | argument | options_parsed = parser.options_parsed(argument) end end @input_arguments_batchjob.each do | argument | options_parsed = parser.options_parsed(argument) assert_equal @batch_job_script, options_parsed.batchjob, "Parsing ArgumentOption: '#{argument}'." end @input_arguments_server.each do | argument | options_parsed = parser.options_parsed(argument) assert_equal @server_name, options_parsed.server, "Parsing ArgumentOption: '#{argument}'." end @input_arguments_username.each do | argument | options_parsed = parser.options_parsed(argument) assert_equal @username, options_parsed.username, "Parsing ArgumentOption: '#{argument}'." end @input_arguments_password.each do | argument | options_parsed = parser.options_parsed(argument) assert_equal @password, options_parsed.password, "Parsing ArgumentOption: '#{argument}'." end end end
Implementação
Achei melhor ter uma classe para manter a mensagem a apresentar como usage:
milestone-01-Usage.rb
class ExceptionUsage < ArgumentError end class UsageMessage attr_reader :usage_message def initialize @usage_message = <<-EOS ruby milestone-01.rb [ options «source» «target» ] -h, --help : show help -i, --interactive: to start the program in interactive mode. -b, --batchjob : to start the program in "scriptable" mode, using a filename. -s, --server : to pass the server IP Name or IP Number. -u, --username : to pass the identification of the account. -p, --password : to pass the password for the account. «source»: filename, filename mask or folder to use as source for the transfer. «target»: filename or folder to use as target of the transfer. EOS end end
Para ser um MixIn, deveria ser um module em vez de uma class.
GetoptLong
GetoptLong
Alterar a variável para apontar para uma instância da classe OpenStruct, em vez da actual Hash.
OptionParser
milestone-01-OptionParser.rb
require 'ostruct' require 'optparse' require 'milestone-01-Usage' class Milestone01OptionParser < OptionParser attr_reader :options_parsed def options_parsed(arguments) @options_parsed = OpenStruct.new @options_parsed.interactive = true on('-h', '--help') do | value | @options_parsed.interactive = false raise ExceptionUsage end on('-i', '--interactive') do | value | nil end on('-b FILENAME', '--batchjob FILENAME') do | filename | @options_parsed.interactive = false @options_parsed.batchjob = filename end on('-s SERVER_NAME', '--server SERVER_NAME') do | server_name | @options_parsed.server = server_name end on('-u USERNAME', '--username USERNAME') do | username | @options_parsed.username = username end on('-p PASSWORD', '--password PASSWORD') do | password | @options_parsed.password = password end parse!(arguments) @options_parsed rescue ExceptionUsage => e message = UsageMessage.new puts message.usage_message end end
Getopt::Declare
Choice
cmdparse
User Choices
Optiflag
Considerações
GetoptLong
Pros
- Vem por omissão com a Standard Library, não sendo portanto necessário instalar nenhuma gem.
- Reconhece tanto a long-option quanto a sua forma abreviada - suponho que desde que não haja ambiguidade.
Cons
- Não permite o carregamento - via load() ou require() - para UnitTests, por causa da classe RDoc::usage.
- Os argumentos não podem facilmente ser parsed numa classe.
- Precisa de levantar uma excepção para o código apresentar a mensagem de Usage, de forma mais prática.
- Precisa do módulo RDoc::usage para ser mais DRY.
- Necessita de “duplicados” das opções:
- na construção da instância;
- no parsing das opções reconhecidas;
- devido à minha escolha de manter uma hash com as opções: nas chaves da hash.
- Não lida bem com opções não reconhecidas. Ver exemplo mais abaixo.
Exemplo de Invalid Option
milestone-01.rb: invalid option -- ?
Ending...OptionParser
Pros
- Vem por omissão com a Standard Library, não sendo portanto necessário instalar nenhuma gem.
- Realiza o output de um sumário dos argumentos, sem ser necessário manter a descrição de cada um em separado. Não precisa do RDoc::usage.
- Permite a declaração de argumentos mandatórios e opcionais, de forma elegante.
- A declaração dos argumentos e o código para os tratar está toda num só lugar.
- Os argumentos podem ser restringidos a um dado conjunto.
- Os argumentos podem facilmente ser parsed por uma classe.
- Permite o carregamento - via load() ou require() - para UnitTests, pois não necessita da classe RDoc::usage.
- Ignora os argumentos não reconhecidos.
Cons
- Necessita de “duplicados” dos argumentos:
- no parsing das opções reconhecidas;
- nos atributos da class OpenStruct.
- Não é tão “condensado” quanto desejável, tendo mais linhas de código do que a versão padrão - GetoptLong.
Não obriga a que existe um valor para os argumentos definidos como not-boolean.Na realidade obriga, desde que a definição da opção tenha um valor em maiúsculas, sem estar envolvido em parêntesis rectos.
Getopt::Declare
Pros
Cons
- Precisa de ser instalada, pois não faz parte da Standard Library.
Install Library
sudo gem install -b getoptdeclare