Ruby Optparse - Optionen parsen in Shell-Werkzeugen

Winfried Mueller, www.reintechnisch.de, Start: 06.04.05, Stand: 30.05.05

Einführung

Wer Werkzeuge für die Kommandozeile in Ruby schreibt, wird sehr schnell die Möglichkeit brauchen, Optionen zu übergeben. Schalter also, die das Verhalten des Programms steuern. Was unter Windows schon immer etwas halbherzig implementiert war, ist unter Linux recht ausgeklügelt gelöst. Kein Wunder, hat die Shell hier zentrale Bedeutung. Option-Parsing orientiert sich bei Ruby deshalb an der Linux-Syntax, was auch POSIX-Industriestandard ist.

Eine typische Option ist z.B. '-h'. Optionen beginnen also immer mit einem Minus, gefolgt von einem Buchstaben. Das ist praktisch, weil schnell eingetippt. Jedoch ist es auch etwas kryptisch. Deshalb wurden die Langoptionen erfunden, z.B. '--help'. Normal gibt es für jede Langoption auch die kurze Variante.

Optionen stehen meist vor weiteren Argumenten, die ein Programm übergeben bekommt. Viele Tools können aber auch damit umgehen, wenn sie irgendwo stehen. Ein 'ls *.txt -lh' ist eigentlich nicht korrekt und funktioniert trotzdem.

Als Tool-Entwickler hat man nun die Aufgabe, die übergebenen Optionen zu parsen und entsprechend darauf zu reagieren. Und man muss eine kleine Hilfe schreiben, damit jeder weiß, was das Werkzeug kann.

Diese Aufgabe kann man Quick & Dirty in vielleicht 50 Zeilen Quellcode lösen. Wirklich gute Lösungen, die alle Regeln der Kunst beachten, können richtig aufwändig sein.

Ruby bringt mehrere Module mit, die sowas können. Das ältere ist getoptlong. Es löst das Problem in etwa 500 Zeilen Quellcode. Verfügbar ist es auch auf Ruby 1.6er Systemen.

Auf etwa 1800 Zeilen bringt es das neuere Modul optparse. Verfügbar ist es ab Ruby 1.8, wird aber wahrscheinlich recht einfach in Ruby 1.6 integriert werden können. Der Vorteil für den Anwender: Die Schnittstelle ist etwas intelligenter und man muss weniger Code selber schreiben. Es entlastet also von nerviger Arbeit, die bei jedem Shell-Werkzeug gemacht werden muss.

Ein Grund, um sich das Teil mal näher anzuschauen...

Ein einfaches Beispiel

Zu Anfang schreiben wir ein kleines Programm namens strview, welches einfach einen übergebenen String auf der Shell ausgibt.

 
# Programm strview

require 'optparse'

repeat = 1

opts = OptionParser.new do |o|
  o.banner = "Usage: strview [options] String"

  o.on( "-r", "--repeat REPEAT", "Ausgabe REPEAT mal." ) do |r|
    repeat = r.to_i    
  end
  o.on("-h", "--help", "Diese Hilfe." ) do
    puts o
    exit
  end
end


begin
  opts.parse!( ARGV )

  str = ARGV[0] || raise( "String fehlt." )

  repeat.times do
    puts str
  end

rescue => exc
  STDERR.puts "E: #{exc.message}"
  STDERR.puts opts.to_s
  exit 1
end

Neben der Option '--help' haben wir auch noch die Option '--repeat' mit der man den String n-mal ausgeben lassen kann. Was können wir jetzt alles damit tun? Hier ein paar Beispiele:

 
$ strview Hello
$ strview -r10 Hello
$ strview --repeat=5 Hello
$ strview --help
$ strview -l
$ strview

Mit ca. 30 Zeilen haben wir also ein Programm, welches Optionen verarbeiten kann, eine Hilfe-Message ausgibt und auch mit Fehlern schon zurecht kommt, was strview -l zeigt. Diese Option gibt es nämlich nicht und damit wirft das Programm ab und wird mit exit 1 verlassen. Gleiches gilt für das letzte Beispiel, wo der String nicht übergeben wurde.

Hier sieht man schon, dass es oft sinnvoll ist, im Fehlerfall die Hilfe-Message auszugeben. So verhalten sich viele Linux-Werkzeuge. Zusätzlich geben wir noch einen Fehlerstring aus, die Exception Message. Weil wir nett zu unseren Nächsten sein wollen, packen wir ein "E:" vor den Fehlerstring, dass lässt sich dann gut parsen. So macht es z.B. auch apt-get.

Der OptionParser#new Methode wird einfach ein Codeblock übergeben, der den Code zur Initialisierung enthält.

Zuerst legt man typischerweise den Banner fest, also die Usage Zeile. Wenn man das nicht macht, wird automatisch eine erzeugt, die aber meist nicht passen wird. Optionparser weiß ja nichts darüber, welche Argumente das Programm neben den Optionen noch braucht.

Jede Option wird mit der Methode OptionParser#on definiert. Neben der Kurz- und Langform der Option, gibt man eine kurze Beschreibung an, die später in der Hilfe erscheint. Argumente der Option gibt man mit Großbuchstaben in der Langform an. Dieses Argument bekommt man dann im darauffolgenden Codeblock übergeben. Hier ist es bei -r die Anzahl Wiederholungen, die wir uns dann in repeat merken.

Standardmäßig wird das Argument als String behandelt, weshalb wir es mit String#to_i umwandeln.

Die Ausgabe der Hilfe erfolgt über die Ausgabe des Objekts selber, welches implizit to_s aufruft. Und das ist so definiert, dass die komplette Beschreibung ausgegeben wird. In der Fehlerbehandlung nutzen wird das nochmal ähnlich, dort geben wir die Hilfe aber auf dem STDERR Kanal aus. Dort mit opts.to_s, nur ein STDERR.puts opts hätte auch gereicht.

OptionParser#parse! parst die Optionen von ARGV. Und zwar in der Art, dass diese von ARGV entfernt werden. Braucht man aus irgendeinem Grund die Optionen in ARGV noch, benutzt man die nicht verändernde Methode OptionParser#parse.

Manche rufen OptioneParser#parse! schon innerhalb des Blocks auf, der new übergeben wird. Grundsätzlich ist das möglich, dann sollte aber das Exceptionhandling nicht vergessen werden, was dann ja außerhalb unseres Exception-Blocks wäre.

Soweit, so gut. Damit hätten wir das grundlegende Handwerkszeug erarbeitet.

Das Beispiel erweitern

Die meisten Tools geben unterhalb von Usage noch eine Zeile aus, die das Programm kurz beschreibt. Mit der Methode OptionParser#separator können wir das tun. Separator heißt sie, weil sie ursprünglich dafür gedacht war, Zwischenzeilen bei der Optionausgabe einzufügen, also die Optionen zu separieren. Nach der banner-Zeile fügen wir also diese ein:

 
o.separator( "Ausgabe des Strings auf der Konsole." )
o.separator( "" )

Der zweite Separator fügt eine Leerzeile ein. Wenn man sich das Format der Ausgabe von 'ls --help' mal anschaut, sieht man, dass wir jetzt ganz gut auf Kurs liegen. Dort schaut es von der Formatierung ganz ähnlich aus.

Als nächstes können wir festlegen, dass der Parameter bei Repeat vom Typ Integer ist. Das hat den Vorteil, dass dann der Optionparser schon checkt, ob der Wert vom Typ her korrekt ist. Und wir müssen mit String#to_i nicht mehr umwandeln.

 
o.on( "-r", "--repeat REPEAT", Integer, "Ausgabe REPEAT mal." ) do |r|
  repeat = r    
end

Folgende Datentypen funktionieren dabei:

  • Integer
  • String (default)
  • Float
  • Time
  • OptionParser::OctalInteger (--rs 0377)
  • Array (--list x,y,z)
  • Keywords ( [:binary, :ascii] => --type binary)

Bei Keywords gibt man ein Array von Symbolen an, die als Keyword auftreten können. Beispiel:

 
o.on( "-t", "--type TYPE", [:binary, :ascii], "Auswahl Type." ) do |t|
  type = t   
end

Kommen wir zu einer weiteren Möglichkeit der Formatierung: Der Optionparser unterscheidet mehrere Ausgabebereiche. Normal ist es so, dass '--help' und '--version' immer ganz unten in der Liste von Optionen steht. Das haben wir erreicht, in dem wir es auch bei der Definition ganz unten geschrieben haben. Man kann aber auch unabhängig davon bestimmte Optionen in den Tail-Bereich packen, der immer unter alle anderen Optionen gesetzt wird. Dafür gibt es die Methode OptionParser#on_tail(). Diese wird normal für Help auch benutzt. Die Zeile würde dann so lauten:

 
o.on_tail("-h", "--help", "Diese Hilfe." ) do
  puts o
  exit
end


Apropos '--version'. Unser Programm sollte eine Version ausgeben können. Hierzu brauchen wir nur die globale Konstante Version zu setzen. Alles andere ist schon in optparse implementiert.

 
Version = "1.1"

Mitunter geben einige Tools bei '--version' auch noch aus, wer das Tool geschrieben hat und wer das Copyright hat. Will man das auch, schreibt man zusätzlich eine on_tail Methode. Das ist sowieso sinnvoll, damit auch in der Beschreibung ausgegeben wird, dass es '--version' gibt. Das macht nämlich optparse nicht automatisch. (In neueren Versionen scheint es zu gehen.)

 
 o.on_tail("--version", "Show version") do
   puts o.ver
   puts "Written by Rub Yves"
   puts ""
   puts "Copyright (C) 2005 RubyHunter Foundation"
   puts "This is free software; see the source for copying conditions."
   puts "There is NO warranty; not even for MERCHANTABILITY or" 
   puts "FITNESS FOR A PARTICULAR PURPOSE."
   exit
 end 

Hier zeigt sich auch, dass wir nicht unbedingt Lang- und Kurzform angeben müssen. Die Option -v steht nämlich oft für Verbose und --version hat in der Regel keine Kurzform.

Hier gibt o.ver den standardisierten Versionsstring aus, also zuerst den Programmnamen und dann die Version, die in der globalen Konstanten ::Version abgelegt ist. Dann folgen ein paar eigene Zeilen.

Unser komplettes Programm sieht also jetzt so aus:

 
# Programm strview

require 'optparse'

Version = "1.1"
repeat = 1

opts = OptionParser.new do |o|
  o.banner = "Usage: strview [options] String"

  o.separator( "Ausgabe des Strings auf der Konsole." )
  o.separator( "" )

  o.on( "-r", "--repeat REPEAT", Integer, "Ausgabe REPEAT mal." ) do |r|
    repeat = r   
  end
  o.on_tail("-h", "--help", "Diese Hilfe." ) do
    puts o
    exit
  end
  o.on_tail("--version", "Show version") do
    puts o.ver
    puts "Written by Rub Yves"
    puts ""
    puts "Copyright (C) 2005 RubyHunter Foundation"
    puts "This is free software; see the source for copying conditions."
    puts "There is NO warranty; not even for MERCHANTABILITY or" 
    puts "FITNESS FOR A PARTICULAR PURPOSE."
    exit
  end
end


begin
  opts.parse!( ARGV )

  str = ARGV[0] || raise( "String fehlt." )

  repeat.times do
    puts str
  end

rescue => exc
  STDERR.puts "E: #{exc.message}"
  STDERR.puts opts
  exit 1
end

Weitere Möglichkeiten

Manchmal hat man Optionen, die wahlweise ein Argument mitbekommen. Dies ist möglich, in dem man den Parameter in eckige Klammern schreibt.

 
  o.on( "-c", "--color [TYPE]", 
        [:never, :always, :auto], 
        "Set color", 
        "(never, always, auto)" ) do |t|
    color = t   
  end

Hier würde also '--color' genauso akzeptiert, wie '--color=never'. Wenn kein Argument angegeben wird, ist t nil.

Wir haben hier von einer weiteren Möglichkeit Gebrauch gemacht - die Aufteilung der Beschreibung auf mehrere Zeilen. Wir wollen ja nicht über die 80 Zeichen hinauskommen und optparse kann nicht sauber formatiert umbrechen.

Eine Spezialität sind Boolean-Schalter, die etwas ein- oder ausschalten. Es ist Standard, das man mit der Vorsilbe 'no-' einen Schalter umkehrt, also z.B. '--verbose' und '--no-verbose'. Wenn Werkzeuge z.B. über Umgebungsvariablen bestimmte Schalter schon einschalten, braucht man ja eine Möglichkeit, die wieder auszuschalten. Mit OptionParser definiert man das so:

 
o.on("-v", "--[no-]verbose", "Run verbosely") do |v|
  options.verbose = v
end

Im Falle von '--no-verbose' wird hier false zurückgegeben, ansonsten true.

Hier zeigt sich auch, was ein typischer Weg ist, um Optionen für späteren Gebrauch zu konservieren. Man schreibt sie in ein OpenStruct Objekt namens options.

 
require 'ostruct'
#...

options = OpenStruct.new
opts = OptionParser.new do |o|
  #...
  o.on("-v", "--[no-]verbose", "Run verbosely") do |v|
    options.verbose = v
  end
end

#...
puts options.verbose


Auch die interne Klasse Struct lässt sich dafür gut gebrauchen:

 
options = Struct.new( :repeat, :verbose ).new

# Vorbelegung
options.repeat  = 1
options.verbose = false

opts = OptionParser.new do |o|
  #...
  o.on("-v", "--[no-]verbose", "Run verbosely") do |v|
    options.verbose = v
  end
end


#...
puts options.verbose 

Und nun noch eine Vereinfachung, die wohl erst später entstand, weil viele noch die herkömmliche Form verwenden. Anstatt OptionParser.new kann man nämlich auch dies verwenden:

 
ARGV.options do |o|
  o.banner = "Usage: strview [options] String"
  o.separator( "Ausgabe des Strings auf der Konsole." )
  o.separator( "" )

  o.on( "-r", "--repeat REPEAT", Integer, "Ausgabe REPEAT mal." ) do |r|
    repeat = r   
  end
  #...
end 

ARGV wurde also um ein paar Methoden erweitert. Die wichtigsten wären:

  • options
    Neuanlage oder Zugriff auf das OptionParser-Objekt
  • parse!
    ARGV.parse!, um die Optionen zu parsen.

Wenn wir unser Programm von oben damit umschreiben, würde dieser einfachere Code bei rauskommen:

 
# Programm strview

require 'optparse'

Version = "1.2"
repeat = 1

ARGV.options do |o|
  o.banner = "Usage: strview [options] String"
  o.separator( "Ausgabe des Strings auf der Konsole." )
  o.separator( "" )

  o.on( "-r", "--repeat REPEAT", Integer, "Ausgabe REPEAT mal." ) do |r|
    repeat = r   
  end
  o.on_tail("-h", "--help", "Diese Hilfe." ) do
    puts o
    exit
  end
  o.on_tail("--version", "Show version") do
    puts o.ver
    puts "Written by Rub Yves"
    puts ""
    puts "Copyright (C) 2005 RubyHunter Foundation"
    puts "This is free software; see the source for copying conditions."
    puts "There is NO warranty; not even for MERCHANTABILITY or" 
    puts "FITNESS FOR A PARTICULAR PURPOSE."
    exit
  end
end


begin
  ARGV.parse!

  str = ARGV[0] || raise( "String fehlt." )

  repeat.times do
    puts str
  end

rescue => exc
  STDERR.puts "E: #{exc.message}"
  STDERR.puts ARGV.options.to_s
  exit 1
end


Das Werk ist vollbracht. Wir haben einen schönen Code und das Wissen im Gepäck, wie wir Optionen parsen. Für die meisten Anwendungsfälle sollte das Wissen reichen.

Wer noch mehr will, kann sich über den Quellcode von optparse hermachen. Oder aber nach vielen Einsatzbeispielen in einer Code-Suchmaschine suchen, z.B. über http://www.koders.com.

Wem noch was einfällt, was dieser Artikel berücksichtigen sollte, schreibe mir.

Changelog

  • 30.05.2005: Dank an Murphy (rubyforen) für einige Korrekturen und Anregungen, z.B. Einsatz von Struct anstatt OpenStruct.

Copyright

Copyright (c) 2005 Winfried Mueller, www.reintechnisch.de

Es wird die Erlaubnis gegeben dieses Dokument zu kopieren, zu verteilen und/oder zu verändern unter den Bedingungen der GNU Free Documentation License, Version 1.1 oder einer späteren, von der Free Software Foundation veröffentlichten Version; mit keinen unveränderlichen Abschnitten, mit keinen Vorderseitentexten, und keinen Rückseitentexten.

Eine Kopie dieser Lizenz finden Sie unter GNU Free Documentation License.

Eine inoffizielle Übersetzung finden Sie unter GNU Free Documention License, deutsch.

In diesem Artikel werden evtl. eingetragene Warenzeichen, Handelsnamen und Gebrauchsnamen verwendet. Auch wenn diese nicht als solche gekennzeichnet sind, gelten die entsprechenden Schutzbestimmungen.

Alle Informationen in diesem Artikel wurden mit Sorgfalt erarbeitet. Trotzdem können Inhalte fehlerhaft oder unvollständig sein. Ich übernehme keinerlei Haftung für eventuelle Schäden oder sonstige Nachteile, die sich durch die Nutzung der hier dargebotenen Informationen ergeben.

Sollten Teile dieser Hinweise der geltenden Rechtslage nicht entsprechen, bleiben die übrigen Teile davon unberührt.