:: Topo :: doCoding :: Linguagens de Programação :: Ruby :: FORPC101 :: CourseProject ::

Milestone 1

Referências

Objectivos

  1. 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.
  2. 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.
  3. codificar testes unitários, seguindo a metodologia TDD.
  4. 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:
    1. na construção da instância;
    2. no parsing das opções reconhecidas;
    3. 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:
    1. no parsing das opções reconhecidas;
    2. 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

Choice

Pros

Cons

cmdparse

Pros

Cons

User Choices

Pros

Cons

Optiflag

Pros

Cons

 
docoding/languages/ruby/forpc101/courseprojectms1.txt · Modificado em: 2008/09/25 10:14 por straider     Voltar ao topo