2010-09-04 7 views
10

¿Hay alguna forma de iniciar OptionParser varias veces en un programa de Ruby, cada uno con diferentes conjuntos de opciones?¿Puede OptionParser omitir las opciones desconocidas para procesarlas más adelante en un programa de Ruby?

Por ejemplo:

$ myscript.rb --subsys1opt a --subsys2opt b 

Aquí, myscript.rb usaría subsys1 y subsys2, delegando sus opciones de manejo de la lógica para ellos, posiblemente en una secuencia en la que 'a' es procesado primero, seguido por 'b' en un objeto OptionParser separado; cada vez que seleccionas opciones solo relevantes para ese contexto. Una fase final podría verificar que no quede nada desconocido después de que cada parte haya procesado la suya.

Los casos de uso son:

  1. En un programa anticipado de acoplamiento flexible, donde varios componentes tienen diferentes argumentos, no quiero 'principal' que saber de todo, sólo para delegar conjuntos de argumentos/opciones para cada parte.

  2. Incrustar un sistema más grande como RSpec en mi aplicación, y simplemente pasaría una línea de comandos a través de sus opciones sin que mi envoltorio las conozca.

Estaría bien con alguna opción de delimitación también, como -- o --vmargs en algunas aplicaciones Java.

Hay muchos ejemplos del mundo real para cosas similares en el mundo de Unix (startx/X, git fontanería y porcelana), donde una capa maneja algunas opciones pero propaga el resto a la capa inferior.

Fuera de la caja, esto no parece funcionar. Cada llamada OptionParse.parse! hará un procesamiento exhaustivo, fallando en cualquier cosa que desconozca. Supongo que me encantaría omitir las opciones desconocidas.

Cualquier sugerencia, tal vez enfoques alternativos son bienvenidos.

+0

En el ejemplo anterior, miscript .rb recibirá todas las opciones como ARGV. Si te entiendo, dices que algunas de esas opciones deben pasarse a "subcapas". ¿Myscript.rb llama a esas subcapas? En caso afirmativo, su pregunta se convierte simplemente en cómo recuperar algunos elementos de la matriz ARGV, pasando el resto a otro programa. Si myscript.rb no llama a las capas secundarias, ¿qué hace? – Alkaline

+0

Sí, myscript.rb utiliza esas subcapas (actualizó la descripción para que quede más claro). Así que su pregunta reformulada es casi correcta "cómo recuperar algunos elementos de la matriz ARGV, pasando el resto a otro programa", excepto que no es necesario otro programa (es por eso que utilicé el término de subsistema/componente más genérico), y específicamente preguntó sobre 'optparse'. De ahí que "¿puede optparse omitir las opciones desconocidas, para ser procesadas más adelante en un programa ruby?" – inger

Respuesta

4

Suponiendo que el orden en que se ejecutarán los analizadores está bien definido, puede simplemente almacenar las opciones adicionales en una variable global temporal y ejecutar OptionParser#parse! en cada conjunto de opciones.

La manera más fácil de hacer esto es usar un delimitador como el que usted mencionó. Supongamos que el segundo conjunto de argumentos está separado del primero por el delimitador --. Entonces esto va a hacer lo que quiere:

opts = OptionParser.new do |opts| 
    # set up one OptionParser here 
end 

both_args = $*.join(" ").split(" -- ") 
$extra_args = both_args[1].split(/\s+/) 
opts.parse!(both_args[0].split(/\s+/)) 

Luego, en el segundo código/contexto, se podría hacer:

other_opts = OptionParser.new do |opts| 
    # set up the other OptionParser here 
end 

other_opts.parse!($extra_args) 

Por otra parte, y esto es probablemente la forma "más adecuada" para hacer esto , simplemente podría usar OptionParser#parse, sin el signo de exclamación, que no eliminará los modificadores de la línea de comandos de la matriz $*, y asegúrese de que no haya opciones definidas de la misma manera en ambos conjuntos. Aconsejo no modificar manualmente la matriz $*, ya que hace que el código sea más difícil de entender si solo está mirando la segunda parte, pero podría hacer eso.Usted tendría que hacer caso omiso de las opciones no válidas en este caso:

begin 
    opts.parse 
rescue OptionParser::InvalidOption 
    puts "Warning: Invalid option" 
end 

El segundo método en realidad no funciona, como se señaló en un comentario. Sin embargo, si tiene que modificar la matriz $* todos modos, se puede hacer esto en su lugar:

tmp = Array.new 

while($*.size > 0) 
    begin 
     opts.parse! 
    rescue OptionParser::InvalidOption => e 
     tmp.push(e.to_s.sub(/invalid option:\s+/,'')) 
    end 
end 

tmp.each { |a| $*.push(a) } 

Es más que un poco truco-Y, pero debe hacer lo que quiera.

+0

Sí, el enfoque de delimitador parece una forma viable, solo esperaba que fuera más bonito que ajustar matrices, con join y split; quizás directamente soportado por OptionParser. El problema con su solución alternativa es que, cuando se produce esa excepción, se interrumpe el procesamiento completo, por lo que también se omiten los buenos argumentos subsiguientes. Comprueba esto: 'ruby -roptparse -e 'begin OptionParser.new {| o | o.on (" - ok ") {pone" OK "}}. Parse * ARGV; rescata OptionParser :: InvalidOption; warn" BAD " ; end '- --bad --ok' # esto dice MALO, aunque debería decir OK también. – inger

+0

Además, uno de mis usos anteriores es envolver frameworks/sistemas existentes como RSpec con un mínimo esfuerzo, algo así como llamar a Spec :: Runner.run_examples, que hace el optparsing internamente. Por lo tanto, desafortunadamente esto significa que sí reescribí ARGV (aunque es constante y de acuerdo con usted para evitarlo si es posible) – inger

+0

Parece que nadie encontró una mejor solución, por lo que quizás se requiera un hack de hecho :(De todos modos, aceptando esta respuesta por ahora :). Gracias – inger

2

Tengo el mismo problema, y ​​encontré la siguiente solución:

 
options = ARGV.dup 
remaining = [] 
while !options.empty? 
    begin 
    head = options.shift 
    remaining.concat(parser.parse([head])) 
    rescue OptionParser::InvalidOption 
    remaining << head 
    retry 
    end 
end 

+0

otro truco agradable, gracias :) ¿cómo maneja esto las opciones parametrizadas? parece que está mirando un argumento a la vez, pero debería ser el conocimiento de OptParser para saber cuáles necesitan cuántos parámetros? – inger

+0

Ese es definitivamente un buen punto :) No funcionará con opciones que tienen argumentos a menos que el usuario use la sintaxis --option = value. –

2

Another solution which relies on parse! teniendo un efecto secundario en la lista de argumentos incluso si se produce un error.

Vamos a definir un método que intenta digitalizar determinada lista de argumentos utilizando un programa de análisis definido por el usuario y llama a sí mismo de forma recursiva cuando se produce un error InvalidOption, el ahorro de la opción no es válida para más adelante con los parámetros posibles:

def parse_known_to(parser, initial_args=ARGV.dup) 
    other_args = []           # this contains the unknown options 
    rec_parse = Proc.new { |arg_list|      # in_method defined proc 
     begin 
      parser.parse! arg_list       # try to parse the arg list 
     rescue OptionParser::InvalidOption => e 
      other_args += e.args       # save the unknown arg 
      while arg_list[0] && arg_list[0][0] != "-"  # certainly not perfect but 
       other_args << arg_list.shift    # quick hack to save any parameters 
      end 
      rec_parse.call arg_list       # call itself recursively 
     end 
    } 
    rec_parse.call initial_args        # start the rec call 
    other_args            # return the invalid arguments 
end 

my_parser = OptionParser.new do 
    ... 
end 

other_options = parse_known_to my_parser 
2

Para la posteridad , se puede hacer esto con el método order!:

option_parser.order!(args) do |unrecognized_option| 
    args.unshift(unrecognized_option) 
end 

en este punto, se ha modificado args - todas las opciones conocidas se consumieron y manejados por option_parser - y se pueden pasar a una opción diferente analizador:

some_other_option_parser.order!(args) do |unrecognized_option| 
    args.unshift(unrecognized_option) 
end 

Obviamente, esta solución depende del orden, pero lo que está tratando de hacer es algo compleja e inusual.

Una cosa que podría ser un buen compromiso es simplemente usar -- en la línea de comandos para detener el procesamiento. Hacer eso dejaría args con lo que sea que siguió a --, ya sean más opciones o solo argumentos regulares.

0

Me encontré con un problema similar cuando escribía un guión que envolvía una gema de rubí, que necesitaba sus propias opciones con argumentos pasados ​​a ella.

Se me ocurrió la siguiente solución en la que admite opciones con argumentos para la herramienta envuelta. Funciona analizándolo a través del primer optparser, y separa lo que no puede usar en una matriz separada (que puede volver a analizarse con otra optparse).

optparse = OptionParser.new do |opts| 
    # OptionParser settings here 
end 

arguments = ARGV.dup 
secondary_arguments = [] 

first_run = true 
errors = false 
while errors || first_run 
    errors = false 
    first_run = false 
    begin 
    optparse.order!(arguments) do |unrecognized_option| 
     secondary_arguments.push(unrecognized_option) 
    end 
    rescue OptionParser::InvalidOption => e 
    errors = true 
    e.args.each { |arg| secondary_arguments.push(arg) } 
    arguments.delete(e.args) 
    end 
end 

primary_arguments = ARGV.dup 
secondary_arguments.each do |cuke_arg| 
    primary_arguments.delete(cuke_arg) 
end 

puts "Primary Args: #{primary_arguments}" 
puts "Secondary Args: #{secondary_args}" 

optparse.parse(primary_arguments) 
# Can parse the second list here, if needed 
# optparse_2.parse(secondary_args) 

Probablemente no sea la manera más grande o más eficiente de hacerlo, pero funcionó para mí.

0

Acabo de mudarme de Python. Python's ArgumentParser tiene un gran método parse_known_args(). Sin embargo, todavía no acepta segundo argumento, como por ejemplo:

$ your-app -x 0 -x 1 

Primera -x 0 es el argumento de su aplicación. El segundo -x 1 puede pertenecer a la aplicación de destino a la que debe reenviar. ArgumentParser provocará un error en este caso.

Ahora regrese a Ruby, puede usar #order. Afortunadamente acepta argumentos duplicados ilimitados. Por ejemplo, necesita -a y -b.Su aplicación de destino necesita otro -ay un argumento obligatorio some (tenga en cuenta que no hay prefijo -/--). Normalmente #parse ignorará los argumentos obligatorios. Pero con #order, obtendrá el resto, genial. Nota que debe pasar los argumentos de su propia aplicación primero, luego los argumentos de la aplicación de destino.

$ your-app -a 0 -b 1 -a 2 some 

Y el código debe ser:

require 'optparse' 
require 'ostruct' 

# Build default arguments 
options = OpenStruct.new 
options.a = -1 
options.b = -1 

# Now parse arguments 
target_app_argv = OptionParser.new do |opts| 
    # Handle your own arguments here 
    # ... 
end.order 

puts ' > Options   = %s' % [options] 
puts ' > Target app argv = %s' % [target_app_argv] 

Tada :-)

+0

esto todavía arroja OptionParser :: InvalidOption si encuentra un indicador desconocido --foo – ScottJ

1

también necesita el mismo ... me tomó un tiempo, pero de una manera relativamente simple ha trabajado bien en el fin.

options = { 
    :input_file => 'input.txt', # default input file 
} 

opts = OptionParser.new do |opt| 
    opt.on('-i', '--input FILE', String, 
     'Input file name', 
     'Default is %s' % options[:input_file]) do |input_file| 
    options[:input_file] = input_file 
    end 

    opt.on_tail('-h', '--help', 'Show this message') do 
    puts opt 
    exit 
    end 
end 

extra_opts = Array.new 
orig_args = ARGV.dup 

begin 
    opts.parse!(ARGV) 
rescue OptionParser::InvalidOption => e 
    extra_opts << e.args 
    retry 
end 

args = orig_args & (ARGV | extra_opts.flatten) 

"args" contendrán todos los argumentos de la línea de comandos sin los ya analizados en la tabla hash "opciones". Estoy pasando este "args" a un programa externo para ser llamado desde este script de ruby.

0

Mi intento:

def first_parse 
    left = [] 
    begin 
    @options.order!(ARGV) do |opt| 
     left << opt 
    end 
    rescue OptionParser::InvalidOption => e 
    e.recover(args) 
    left << args.shift 
    retry 
    end 
    left 
end 

En mi caso, quiero explorar las opciones y recoger todas las opciones predefinidas que pueden establecer los niveles de depuración, los archivos de salida, etc. A continuación, voy a cargar procesadores personalizados los cuales puede agregar a las opciones. Después de que se hayan cargado todos los procesadores personalizados, llamo al @options.parse!(left) para procesar las opciones sobrantes. Tenga en cuenta que --help está integrado en las opciones, por lo que si no quiere reconocer la ayuda por primera vez, debe hacer 'OptionParser :: Officious.delete (' help ')' antes de crear OptParser y luego agregar su Opción de ayuda propia

3

Necesitaba una solución que no arrojara OptionParser::InvalidOption alguna vez, y no pude encontrar una solución elegante entre las respuestas actuales. Este parche de mono se basa en uno de los other answers pero lo limpia y lo hace funcionar más como la semántica actual order!. Pero consulte a continuación un problema no resuelto inherente al análisis de opciones de pase múltiple.

class OptionParser 
    # Like order!, but leave any unrecognized --switches alone 
    def order_recognized!(args) 
    extra_opts = [] 
    begin 
     order!(args) { |a| extra_opts << a } 
    rescue OptionParser::InvalidOption => e 
     extra_opts << e.args[0] 
     retry 
    end 
    args[0, 0] = extra_opts 
    end 
end 

funciona igual que order! excepto que en lugar de tirar InvalidOption, deja el interruptor no reconocida en ARGV.

pruebas RSpec:

describe OptionParser do 
    before(:each) do 
    @parser = OptionParser.new do |opts| 
     opts.on('--foo=BAR', OptionParser::DecimalInteger) { |f| @found << f } 
    end 
    @found = [] 
    end 

    describe 'order_recognized!' do 
    it 'finds good switches using equals (--foo=3)' do 
     argv = %w(one two --foo=3 three) 
     @parser.order_recognized!(argv) 
     expect(@found).to eq([3]) 
     expect(argv).to eq(%w(one two three)) 
    end 

    it 'leaves unknown switches alone' do 
     argv = %w(one --bar=2 two three) 
     @parser.order_recognized!(argv) 
     expect(@found).to eq([]) 
     expect(argv).to eq(%w(one --bar=2 two three)) 
    end 

    it 'leaves unknown single-dash switches alone' do 
     argv = %w(one -bar=2 two three) 
     @parser.order_recognized!(argv) 
     expect(@found).to eq([]) 
     expect(argv).to eq(%w(one -bar=2 two three)) 
    end 

    it 'finds good switches using space (--foo 3)' do 
     argv = %w(one --bar=2 two --foo 3 three) 
     @parser.order_recognized!(argv) 
     expect(@found).to eq([3]) 
     expect(argv).to eq(%w(one --bar=2 two three)) 
    end 

    it 'finds repeated args' do 
     argv = %w(one --foo=1 two --foo=3 three) 
     @parser.order_recognized!(argv) 
     expect(@found).to eq([1, 3]) 
     expect(argv).to eq(%w(one two three)) 
    end 

    it 'maintains repeated non-switches' do 
     argv = %w(one --foo=1 one --foo=3 three) 
     @parser.order_recognized!(argv) 
     expect(@found).to eq([1, 3]) 
     expect(argv).to eq(%w(one one three)) 
    end 

    it 'maintains repeated unrecognized switches' do 
     argv = %w(one --bar=1 one --bar=3 three) 
     @parser.order_recognized!(argv) 
     expect(@found).to eq([]) 
     expect(argv).to eq(%w(one --bar=1 one --bar=3 three)) 
    end 

    it 'still raises InvalidArgument' do 
     argv = %w(one --foo=bar) 
     expect { @parser.order_recognized!(argv) }.to raise_error(OptionParser::InvalidArgument) 
    end 

    it 'still raises MissingArgument' do 
     argv = %w(one --foo) 
     expect { @parser.order_recognized!(argv) }.to raise_error(OptionParser::MissingArgument) 
    end 
    end 
end 

Problema: opciones normalmente OptionParser permite abreviada, siempre que existan suficientes caracteres para identificar de forma única la opción deseada. Las opciones de análisis en múltiples etapas rompen esto, porque OptionParser no ve todos los argumentos posibles en el primer pase. Por ejemplo:

describe OptionParser do 
    context 'one parser with similar prefixed options' do 
    before(:each) do 
     @parser1 = OptionParser.new do |opts| 
     opts.on('--foobar=BAR', OptionParser::DecimalInteger) { |f| @found_foobar << f } 
     opts.on('--foo=BAR', OptionParser::DecimalInteger) { |f| @found_foo << f } 
     end 
     @found_foobar = [] 
     @found_foo = [] 
    end 

    it 'distinguishes similar prefixed switches' do 
     argv = %w(--foo=3 --foobar=4) 
     @parser1.order_recognized!(argv) 
     expect(@found_foobar).to eq([4]) 
     expect(@found_foo).to eq([3]) 
    end 
    end 

    context 'two parsers in separate passes' do 
    before(:each) do 
     @parser1 = OptionParser.new do |opts| 
     opts.on('--foobar=BAR', OptionParser::DecimalInteger) { |f| @found_foobar << f } 
     end 
     @parser2 = OptionParser.new do |opts| 
     opts.on('--foo=BAR', OptionParser::DecimalInteger) { |f| @found_foo << f } 
     end 
     @found_foobar = [] 
     @found_foo = [] 
    end 

    it 'confuses similar prefixed switches' do 
     # This is not generally desirable behavior 
     argv = %w(--foo=3 --foobar=4) 
     @parser1.order_recognized!(argv) 
     @parser2.order_recognized!(argv) 
     expect(@found_foobar).to eq([3, 4]) 
     expect(@found_foo).to eq([]) 
    end 
    end 
end 
+0

¡Buen trabajo, @ScottJ! Esto funciona bien para mi. Comencé este camino yo mismo, y me encontré tratando de sobrescribir 'order' con' safe_order' así como 'order'' con' safe_order! 'Etc. Se salió un poco de las manos, ¡pero el tuyo funciona bien! – Volte

+0

¡GUAU, no puedo creer que 'OptionParser' no pueda manejar esto de forma nativa! Acabo de pasar casi una hora buscando cómo hacerlo antes de encontrar esto. Gran solución; por favor considere enviar esto a Ruby para ser incluido en 'OptionParser'! –

0

opciones Parse hasta la primera opción desconocida ... el bloque podría ser llamado varias veces, así que asegúrese de que es seguro ...

options = { 
    :input_file => 'input.txt', # default input file 
} 

opts = OptionParser.new do |opt| 
    opt.on('-i', '--input FILE', String, 
    'Input file name', 
    'Default is %s' % options[:input_file]) do |input_file| 
    options[:input_file] = input_file 
    end 

    opt.on_tail('-h', '--help', 'Show this message') do 
    puts opt 
    exit 
    end 
end 

original = ARGV.dup 
leftover = [] 

loop do 
    begin 
    opts.parse(original) 
    rescue OptionParser::InvalidOption 
    leftover.unshift(original.pop) 
    else 
    break 
    end 
end 

puts "GOT #{leftover} -- #{original}" 
Cuestiones relacionadas