2010-09-04 38 views
10

Y a-t-il un moyen de lancer OptionParser plusieurs fois dans un programme Ruby, chacun avec différents ensembles d'options?Est-ce que OptionParser peut ignorer les options inconnues, qui seront traitées plus tard dans un programme Ruby?

Par exemple:

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

Ici, myscript.rb utiliseraient subsys1 et subsys2, déléguer leurs options de traitement pour les logiques, peut-être dans une séquence où 'a' est traitée en premier, suivi par 'b' dans un objet OptionParser séparé; chaque fois choisir des options uniquement pertinentes pour ce contexte. Une dernière phase pourrait vérifier qu'il n'y a plus rien d'inconnu après le traitement de chaque pièce.

Les cas d'utilisation sont:

  1. Dans un programme frontal à couplage lâche, où différents composants ont des arguments différents, je ne veux pas « principale » pour tout savoir, pour déléguer des ensembles de arguments/options pour chaque partie.

  2. Incorporation d'un système plus important comme RSpec dans mon application, et je passerais simplement une ligne de commande à travers leurs options sans que mon wrapper ne les connaisse.

je serais OK avec une option delimiter ainsi, comme -- ou --vmargs dans certaines applications Java. Il ya beaucoup d'exemples du monde réel pour des choses similaires dans le monde Unix (startx/X, plomberie git et porcelaine), où une couche gère certaines options mais propage le reste vers la couche inférieure. En dehors de la boîte, cela ne semble pas fonctionner. Chaque appel OptionParse.parse! fera un traitement exhaustif, échouant sur tout ce qu'il ne sait pas. Je suppose que je serais heureux d'ignorer les options inconnues.

Tous les indices, peut-être des approches alternatives sont les bienvenus.

+0

Dans votre exemple ci-dessus, MyScript .rb recevra toutes les options comme ARGV. Si je vous comprends, vous dites que certaines de ces options doivent être passées à des "sous-couches". Est-ce que myscript.rb appelle ces sous-couches? Si oui, votre question devient simplement comment récupérer certains éléments du tableau ARGV, en passant le reste à un autre programme. Si myscript.rb n'appelle pas les sous-couches, que fait-il? – Alkaline

+0

Oui, myscript.rb utilise ces sous-couches (mise à jour de la description pour rendre cela plus clair). Donc votre question reformulée est presque correcte "comment récupérer certains éléments de la matrice ARGV, en passant le reste à un autre programme", sauf qu'un autre programme n'est pas nécessaire (c'est pourquoi j'ai utilisé le terme générique de sous-système/composant) interrogé sur 'optparse'. Par conséquent "Can optparse ignore les options inconnues, à traiter plus tard dans un programme ruby?" – inger

Répondre

4

En supposant que l'ordre dans lequel les analyseurs s'exécuteront est bien défini, vous pouvez simplement stocker les options supplémentaires dans une variable globale temporaire et exécuter OptionParser#parse! sur chaque ensemble d'options.

La méthode la plus simple consiste à utiliser un délimiteur comme vous l'avez indiqué. Supposons que le second ensemble d'arguments soit séparé du premier par le délimiteur --. Ensuite, cela va faire ce que vous voulez:

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+/)) 

Ensuite, dans le second code/contexte, vous pouvez faire:

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

other_opts.parse!($extra_args) 

Alternativement, ce qui est probablement la façon « plus appropriée » pour faire , vous pouvez simplement utiliser OptionParser#parse, sans le point d'exclamation, qui ne supprime pas les commutateurs de ligne de commande du tableau $* et assurez-vous qu'il n'y a pas d'options définies identiques dans les deux ensembles. Je déconseille de modifier le tableau $* à la main, car il rend votre code plus difficile à comprendre si vous ne regardez que la deuxième partie, mais pourrait faire faire cela.Vous devez ignorer les options non valides dans ce cas:

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

La deuxième méthode ne fonctionne pas en fait, comme l'a souligné dans un commentaire. Toutefois, si vous devez modifier le tableau $* de toute façon, vous pouvez le faire à la place:

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) } 

Il est plus d'un hack y peu, mais il faut faire ce que vous voulez.

+0

Oui, l'approche du délimiteur semble être une méthode viable, j'espérais juste qu'elle soit plus agréable que de modifier les tableaux, avec join et split; peut-être directement pris en charge par OptionParser. Le problème avec votre solution alternative est que lorsque cette exception est levée, tout le traitement est abandonné, de sorte que les bons arguments suivants seront également ignorés. Vérifiez ceci: 'ruby -roptparse -e 'commence OptionParser.new {| o | o.on (" - ok ") {met" OK "}}. Parse * ARGV; sauvegarde OptionParser :: InvalidOption; warn" BAD " ; end '- --bad --ok' # ceci dit BAD bien que vous devriez dire OK aussi. – inger

+0

En outre, l'une de mes utilisations ci-dessus est d'emballer les frameworks/systèmes existants comme RSpec avec un minimum d'effort, quelque chose comme appeler Spec :: Runner.run_examples, qui opte pour l'interne. Donc, malheureusement, cela signifie que j'ai réécrire ARGV (même si c'est constant et d'accord avec vous pour l'éviter si possible) – inger

+0

Il semble que personne ne soit venu avec une meilleure solution - donc peut-être un hack est nécessaire :(En tout cas, accepter cette réponse pour l'instant :). Merci – inger

2

J'ai le même problème, et j'ai trouvé la solution suivante:

 
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

un autre hack sympa, merci :) comment ça gère les options paramétrées? vous semblez regarder un argument à la fois, mais ce devrait être la connaissance d'OptParser pour savoir lesquels ont besoin de combien de paramètres? – inger

+0

C'est certainement un bon point :) Ne fonctionnera pas avec les options qui ont des arguments à moins que l'utilisateur utilise la syntaxe --option = valeur. –

2

Another solution which relies on parse! avoir un effet secondaire sur la liste d'arguments même si une erreur est levée.

Définissons une méthode qui tente d'analyser une liste d'arguments à l'aide d'un analyseur défini par l'utilisateur et lui-même appelle récursive lorsqu'une erreur de InvalidOption est lancée, sauver l'option non valide pour plus tard avec des paramètres éventuels:

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

pour la postérité , vous pouvez le faire avec la méthode order!:

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

à ce stade, args a été modifié - toutes les options connues ont été consommés et manipulés par option_parser - et peuvent être transmis à un analyseur d'autre option:

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

De toute évidence, cette solution dépend de l'ordre, mais ce que vous essayez de faire est un peu complexe et inhabituelle.

Une chose qui pourrait être un bon compromis est d'utiliser simplement -- sur la ligne de commande pour arrêter le traitement. Faire cela laisserait args avec tout ce qui suivait --, que ce soit plus d'options ou simplement des arguments réguliers.

0

J'ai rencontré un problème similaire lorsque j'écrivais un script qui enveloppait une gemme ruby, qui avait besoin de ses propres options avec les arguments qui lui étaient passés.

Je suis venu avec la solution suivante dans laquelle prend en charge les options avec les arguments pour l'outil encapsulé. Il fonctionne en l'analysant à travers le premier optparser, et sépare ce qu'il ne peut pas utiliser dans un tableau séparé (qui peut être ré-analysé avec un autre optparse). Probablement pas le moyen le plus efficace ou le plus efficace, mais cela a fonctionné pour moi.

0

Je viens de passer de Python. Python ArgumentParser a une grande méthode parse_known_args(). Mais il n'accepte toujours pas deuxième argument, comme:

$ your-app -x 0 -x 1 

premier -x 0 est l'argument de votre application. Deuxième -x 1 peut appartenir à l'application cible que vous devez transférer. ArgumentParser provoquera une erreur dans ce cas.

Revenons maintenant à Ruby, vous pouvez utiliser #order. Heureusement, il accepte des arguments en double illimités. Par exemple, vous avez besoin de -a et -b.Votre application cible a besoin d'un autre -aet un argument obligatoire some (notez qu'il n'y a pas de préfixe -/--). Normalement, #parse ignore les arguments obligatoires. Mais avec #order, vous obtiendrez le reste - super. Notez que vous devez passer les arguments de votre propre application d'abord, puis les arguments de l'application cible.

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

Et le code doit être:

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

cela lance toujours OptionParser :: InvalidOption s'il trouve un drapeau non reconnu --foo – ScottJ

1

J'ai aussi besoin de la même ... il m'a fallu un certain temps, mais d'une manière relativement simple a bien fonctionné dans la 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 » contiendra tous les arguments de ligne de commande sans ceux qui sont déjà analysées dans le hachage « options ». Je passe ce "args" à un programme externe à appeler à partir de ce script ruby.

0

Ma tentative:

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 

Dans mon cas, je veux analyser les options et ramasser toutes les options prédéfinies qui peuvent définir les niveaux de débogage, les fichiers de sortie, etc. Ensuite, je vais charger des processeurs personnalisés qui peut ajouter aux options. Une fois tous les processeurs personnalisés chargés, j'appelle le @options.parse!(left) pour traiter les options restantes. Notez que --help est intégré aux options, donc si vous ne voulez pas reconnaître l'aide la première fois, vous devez faire 'OptionParser :: Officious.delete (' help ')' avant de créer le OptParser et ensuite ajouter dans votre propre option d'aide

3

J'avais besoin d'une solution qui ne lancerait jamais OptionParser::InvalidOption, et ne pouvait pas trouver une solution élégante parmi les réponses actuelles. Ce patch de singe est basé sur l'un des other answers mais le nettoie et le rend plus proche de la sémantique actuelle order!. Mais voir ci-dessous pour un problème non résolu inhérent à l'analyse des options à passages multiples.

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 

fonctionne comme order! sauf au lieu de jeter InvalidOption, il laisse le commutateur non reconnu dans ARGV.

essais 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 

Problème: normalement OptionParser permet des options, à la condition abrégée des caractères assez pour identifier l'option voulue. Options d'analyse dans plusieurs étapes casse cela, car OptionParser ne voit pas tous les arguments possibles dans la première passe. Par exemple:

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

Beau travail, @ScottJ! Cela fonctionne bien pour moi. J'ai moi-même commencé ce chemin, et je me suis retrouvé à essayer d'écraser 'order' avec un' safe_order' aussi bien que 'order!' Avec 'safe_order! ', Etc. C'est devenu un peu incontrôlable, mais le vôtre fonctionne bien! – Volte

+0

WOW, je ne peux pas croire que 'OptionParser' ne peut pas gérer cela nativement! Je viens de passer près d'une heure à chercher comment le faire avant de trouver cela. Excellente solution s'il vous plaît s'il vous plaît envisager de soumettre cela à Ruby lui-même pour être inclus dans 'OptionParser'! –

0

options Parse jusqu'à la première option inconnue ... le bloc peut être appelé plusieurs fois, alors assurez-vous qui est sûr ...

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}"