Einführung
Befehlszeilendienstprogramme sind ohne zusätzliche Konfiguration nur selten von Haus aus nützlich. Gute Standardeinstellungen sind wichtig, aber nützliche Dienstprogramme müssen die Konfiguration von Benutzern akzeptieren. Auf den meisten Plattformen akzeptieren Befehlszeilendienstprogramme Flags, um die Ausführung des Befehls anzupassen. Flags sind durch Schlüsselwerte getrennte Zeichenfolgen, die nach dem Namen des Befehls eingefügt werden. Mit Go können Sie Befehlszeilenprogramme erstellen, die Flags akzeptieren, indem Sie das Paketflag
aus der Standardbibliothek verwenden.
In diesem Tutorial erfahren Sie, wie Sie mit dem Paketflag
verschiedene Arten von Befehlszeilenprogrammen erstellen können. Sie verwenden ein Flag, um die Programmausgabe zu steuern, Positionsargumente einzufügen, in denen Sie Flags und andere Daten mischen, und dann Unterbefehle zu implementieren.
Verwenden eines Flags zum Ändern des Verhaltens eines Programms
Die Verwendung des Paketsflag
umfasst drei Schritte: Zuerstdefine variables, um Flag-Werte zu erfassen, dann die Flags zu definieren, die Ihre Go-Anwendung verwendet, und schließlich die Flags zu analysieren, die der Anwendung bei der Ausführung bereitgestellt werden. Die meisten Funktionen im Paketflag
befassen sich mit der Definition von Flags und deren Bindung an von Ihnen definierte Variablen. Die Analysephase wird von derParse()
-Funktion übernommen.
Zur Veranschaulichung erstellen Sie ein Programm, das einBoolean-Flag definiert, das die Nachricht ändert, die in die Standardausgabe gedruckt wird. Wenn ein-color
-Flag angegeben ist, druckt das Programm eine Nachricht in Blau. Wenn keine Flagge angegeben ist, wird die Nachricht ohne Farbe gedruckt.
Erstellen Sie eine neue Datei mit dem Namenboolean.go
:
nano boolean.go
Fügen Sie der Datei den folgenden Code hinzu, um das Programm zu erstellen:
boolean.go
package main
import (
"flag"
"fmt"
)
type Color string
const (
ColorBlack Color = "\u001b[30m"
ColorRed = "\u001b[31m"
ColorGreen = "\u001b[32m"
ColorYellow = "\u001b[33m"
ColorBlue = "\u001b[34m"
ColorReset = "\u001b[0m"
)
func colorize(color Color, message string) {
fmt.Println(string(color), message, string(ColorReset))
}
func main() {
useColor := flag.Bool("color", false, "display colorized output")
flag.Parse()
if *useColor {
colorize(ColorBlue, "Hello, DigitalOcean!")
return
}
fmt.Println("Hello, DigitalOcean!")
}
In diesem Beispiel wirdANSI Escape Sequences verwendet, um das Terminal anzuweisen, eine farbige Ausgabe anzuzeigen. Da es sich um spezialisierte Zeichenfolgen handelt, ist es sinnvoll, für sie einen neuen Typ zu definieren. In diesem Beispiel haben wir diesen TypColor
genannt und den Typ alsstring
definiert. Anschließend definieren wir eine Farbpalette, die im folgenden Blockconst
verwendet werden soll. Die nach demconst
-Block definiertecolorize
-Funktion akzeptiert eine dieserColor
-Konstanten und einestring
-Variable, damit die Nachricht koloriert werden kann. Anschließend wird das Terminal angewiesen, die Farbe zu ändern, indem zuerst die Escape-Sequenz für die angeforderte Farbe gedruckt wird, dann die Nachricht gedruckt wird und schließlich das Terminal aufgefordert wird, seine Farbe durch Drucken der speziellen Farbrücksetzsequenz zurückzusetzen.
Innerhalb vonmain
verwenden wir die Funktionflag.Bool
, um ein Boolesches Flag namenscolor
zu definieren. Der zweite Parameter dieser Funktion,false
, legt den Standardwert für dieses Flag fest, wenn es nicht bereitgestellt wird. Entgegen den Erwartungen, die Sie möglicherweise haben, wird durch das Setzen auftrue
das Verhalten nicht umgekehrt, sodass das Bereitstellen eines Flags dazu führt, dass es falsch wird. Folglich beträgt der Wert dieses Parameters bei Booleschen Flags fast immerfalse
.
Der letzte Parameter ist eine Dokumentationszeichenfolge, die als Verwendungsmeldung gedruckt werden kann. Der von dieser Funktion zurückgegebene Wert ist ein Zeiger aufbool
. Die Funktionflag.Parse
in der nächsten Zeile verwendet diesen Zeiger, um die Variablebool
basierend auf den vom Benutzer übergebenen Flags festzulegen. Wir können dann den Wert diesesbool
-Zeigers überprüfen, indem wir den Zeiger dereferenzieren. Weitere Informationen zu Zeigervariablen finden Sie intutorial on pointers. Mit diesem Booleschen Wert können wir danncolorize
aufrufen, wenn das Flag-color
gesetzt ist, und die Variablefmt.Println
aufrufen, wenn das Flag fehlt.
Speichern Sie die Datei und führen Sie das Programm ohne Flags aus:
go run boolean.go
Sie sehen die folgende Ausgabe:
OutputHello, DigitalOcean!
Führen Sie dieses Programm nun erneut mit dem Flag-color
aus:
go run boolean.go -color
Die Ausgabe ist der gleiche Text, diesmal jedoch in der Farbe Blau.
Flags sind nicht die einzigen Werte, die an Befehle übergeben werden. Sie können auch Dateinamen oder andere Daten senden.
Arbeiten mit Positionsargumenten
In der Regel verwenden Befehle eine Reihe von Argumenten, die als Gegenstand des Befehlsfokus dienen. Beispielsweise wird der Befehlhead
, der die ersten Zeilen einer Datei druckt, häufig alshead example.txt
aufgerufen. Die Dateiexample.txt
ist ein Positionsargument beim Aufruf des Befehlshead
.
Die FunktionParse()
analysiert weiterhin Flags, auf die sie stößt, bis sie ein Nicht-Flag-Argument erkennt. Das Paketflag
stellt diese über die FunktionenArgs()
undArg()
zur Verfügung.
Um dies zu veranschaulichen, erstellen Sie eine vereinfachte Neuimplementierung des Befehlshead
, in der die ersten Zeilen einer bestimmten Datei angezeigt werden:
Erstellen Sie eine neue Datei mit dem Namenhead.go
und fügen Sie den folgenden Code hinzu:
head.go
package main
import (
"bufio"
"flag"
"fmt"
"io"
"os"
)
func main() {
var count int
flag.IntVar(&count, "n", 5, "number of lines to read from the file")
flag.Parse()
var in io.Reader
if filename := flag.Arg(0); filename != "" {
f, err := os.Open(filename)
if err != nil {
fmt.Println("error opening file: err:", err)
os.Exit(1)
}
defer f.Close()
in = f
} else {
in = os.Stdin
}
buf := bufio.NewScanner(in)
for i := 0; i < count; i++ {
if !buf.Scan() {
break
}
fmt.Println(buf.Text())
}
if err := buf.Err(); err != nil {
fmt.Fprintln(os.Stderr, "error reading: err:", err)
}
}
Zuerst definieren wir einecount
-Variable, die die Anzahl der Zeilen enthält, die das Programm aus der Datei lesen soll. Anschließend definieren wir das-n
-Flag mitflag.IntVar
und spiegeln das Verhalten des ursprünglichenhead
-Programms wider. Diese Funktion ermöglicht es uns, unsere eigenenpointer an eine Variable zu übergeben, im Gegensatz zu denflag
-Funktionen, die nicht das SuffixVar
haben. Abgesehen von diesem Unterschied folgen die restlichen Parameter fürflag.IntVar
dem Gegenstück vonflag.Int
: dem Flag-Namen, einem Standardwert und einer Beschreibung. Wie im vorherigen Beispiel rufen wir dannflag.Parse()
auf, um die Benutzereingaben zu verarbeiten.
Der nächste Abschnitt liest die Datei. Wir definieren zunächst eineio.Reader
-Variable, die entweder auf die vom Benutzer angeforderte Datei oder auf die an das Programm übergebene Standardeingabe gesetzt wird. In der Anweisungif
verwenden wir die Funktionflag.Arg
, um nach allen Flags auf das erste Positionsargument zuzugreifen. Wenn der Benutzer einen Dateinamen angegeben hat, wird dieser festgelegt. Andernfalls ist es die leere Zeichenfolge (""
). Wenn ein Dateiname vorhanden ist, verwenden wir die Funktionos.Open
, um diese Datei zu öffnen und die zuvor definiertenio.Reader
auf diese Datei zu setzen. Andernfalls verwenden wiros.Stdin
, um von der Standardeingabe zu lesen.
Der letzte Abschnitt verwendet ein*bufio.Scanner
, das mitbufio.NewScanner
erstellt wurde, um Zeilen aus derio.Reader
-Variablenin
zu lesen. Wir iterieren bis zum Wert voncount
mit einemfor
loop und rufenbreak
auf, wenn das Scannen der Zeile mitbuf.Scan
einen Wert vonfalse
ergibt, der die Anzahl von angibt Zeilen ist kleiner als die vom Benutzer angeforderte Nummer.
Führen Sie dieses Programm aus und zeigen Sie den Inhalt der gerade geschriebenen Datei an, indem Siehead.go
als Dateiargument verwenden:
go run head.go -- head.go
Das Trennzeichen--
ist ein spezielles Flag, das vom Paketflag
erkannt wird und angibt, dass keine Flag-Argumente mehr folgen. Wenn Sie diesen Befehl ausführen, erhalten Sie die folgende Ausgabe:
Outputpackage main
import (
"bufio"
"flag"
Verwenden Sie das von Ihnen definierte-n
-Flag, um die Ausgabemenge anzupassen:
go run head.go -n 1 head.go
Dies gibt nur die Paketanweisung aus:
Outputpackage main
Wenn das Programm schließlich feststellt, dass keine Positionsargumente angegeben wurden, liest es die Eingabe von der Standardeingabe, genau wiehead
. Versuchen Sie diesen Befehl auszuführen:
echo "fish\nlobsters\nsharks\nminnows" | go run head.go -n 3
Sie sehen die Ausgabe:
Outputfish
lobsters
sharks
Das Verhalten der bisher gesehenenflag
-Funktionen beschränkte sich auf die Untersuchung des gesamten Befehlsaufrufs. Sie möchten dieses Verhalten nicht immer, insbesondere wenn Sie ein Befehlszeilentool schreiben, das Unterbefehle unterstützt.
Verwenden von FlagSet zum Implementieren von Unterbefehlen
Moderne Befehlszeilenanwendungen implementieren häufig „Unterbefehle“, um eine Reihe von Tools unter einem einzigen Befehl zu bündeln. Das bekannteste Werkzeug, das dieses Muster verwendet, istgit
. Bei der Untersuchung eines Befehls wiegit init
istgit
der Befehl undinit
der Unterbefehl vongit
. Eine bemerkenswerte Eigenschaft von Unterbefehlen ist, dass jeder Unterbefehl eine eigene Sammlung von Flags haben kann.
Go-Anwendungen können Unterbefehle mit eigenen Flags unter Verwendung des Typsflag.(*FlagSet)
unterstützen. Erstellen Sie zur Veranschaulichung ein Programm, das einen Befehl mit zwei Unterbefehlen mit unterschiedlichen Flags implementiert.
Erstellen Sie eine neue Datei mit dem Namensubcommand.go
und fügen Sie der Datei den folgenden Inhalt hinzu:
package main
import (
"errors"
"flag"
"fmt"
"os"
)
func NewGreetCommand() *GreetCommand {
gc := &GreetCommand{
fs: flag.NewFlagSet("greet", flag.ContinueOnError),
}
gc.fs.StringVar(&gc.name, "name", "World", "name of the person to be greeted")
return gc
}
type GreetCommand struct {
fs *flag.FlagSet
name string
}
func (g *GreetCommand) Name() string {
return g.fs.Name()
}
func (g *GreetCommand) Init(args []string) error {
return g.fs.Parse(args)
}
func (g *GreetCommand) Run() error {
fmt.Println("Hello", g.name, "!")
return nil
}
type Runner interface {
Init([]string) error
Run() error
Name() string
}
func root(args []string) error {
if len(args) < 1 {
return errors.New("You must pass a sub-command")
}
cmds := []Runner{
NewGreetCommand(),
}
subcommand := os.Args[1]
for _, cmd := range cmds {
if cmd.Name() == subcommand {
cmd.Init(os.Args[2:])
return cmd.Run()
}
}
return fmt.Errorf("Unknown subcommand: %s", subcommand)
}
func main() {
if err := root(os.Args[1:]); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
Dieses Programm ist in einige Teile unterteilt: die Funktionmain
, die Funktionroot
und die einzelnen Funktionen zur Implementierung des Unterbefehls. Die Funktionmain
behandelt Fehler, die von Befehlen zurückgegeben werden. Wenn eine Funktionerror zurückgibt, wird sie von der Anweisungif
abgefangen, der Fehler ausgegeben und das Programm mit einem Statuscode von1
beendet, was darauf hinweist, dass im Rest ein Fehler aufgetreten ist des Betriebssystems. Innerhalb vonmain
übergeben wir alle Argumente, mit denen das Programm aufgerufen wurde, anroot
. Wir entfernen das erste Argument, das der Name des Programms ist (in den vorherigen Beispielen./subcommand
), indem wir zuerstos.Args
aufteilen.
Die Funktionroot
definiert[]Runner
, wobei alle Unterbefehle definiert würden. Runner
ist eininterface für Unterbefehle, mit demroot
den Namen des Unterbefehls mitName()
abrufen und mit der Inhaltsvariablensubcommand
vergleichen kann . Sobald der richtige Unterbefehl gefunden wurde, nachdem die Variablecmds
durchlaufen wurde, initialisieren wir den Unterbefehl mit den restlichen Argumenten und rufen dieRun()
-Methode dieses Befehls auf.
Wir definieren nur einen Unterbefehl, obwohl dieses Framework es uns leicht erlauben würde, andere zu erstellen. DasGreetCommand
wird mitNewGreetCommand
instanziiert, wobei wir mitflag.NewFlagSet
ein neues*flag.FlagSet
erstellen. flag.NewFlagSet
akzeptiert zwei Argumente: einen Namen für das gesetzte Flag und eine Strategie zum Melden von Analysefehlern. Auf den Namen von*flag.FlagSet
kann mit der Methodeflag.(*FlagSet).Name
zugegriffen werden. Wir verwenden dies in der Methode(*GreetCommand).Name()
, sodass der Name des Unterbefehls mit dem Namen übereinstimmt, den wir*flag.FlagSet
gegeben haben. NewGreetCommand
definiert auch ein-name
-Flag auf ähnliche Weise wie in den vorherigen Beispielen, ruft dies jedoch als Methode außerhalb des*flag.FlagSet
-Felds von*GreetCommand
,gc.fs
auf. s. Wennroot
dieInit()
-Methode von*GreetCommand
aufruft, übergeben wir die angegebenen Argumente an dieParse
-Methode des*flag.FlagSet
-Felds.
Es ist einfacher, Unterbefehle zu sehen, wenn Sie dieses Programm erstellen und dann ausführen. Erstellen Sie das Programm:
go build subcommand.go
Führen Sie nun das Programm ohne Argumente aus:
./subcommand
Sie sehen diese Ausgabe:
OutputYou must pass a sub-command
Führen Sie nun das Programm mit dem Unterbefehlgreet
aus:
./subcommand greet
Dies erzeugt die folgende Ausgabe:
OutputHello World !
Verwenden Sie nun das Flag-name
mitgreet
, um einen Namen anzugeben:
./subcommand greet -name Sammy
Sie sehen diese Ausgabe des Programms:
OutputHello Sammy !
Dieses Beispiel zeigt einige Prinzipien, wie größere Befehlszeilenanwendungen in Go strukturiert werden können. `FlagSet`s sollen Entwicklern mehr Kontrolle darüber geben, wo und wie Flags von der Flag-Parsing-Logik verarbeitet werden.
Fazit
Flags machen Ihre Anwendungen in mehr Kontexten nützlicher, da sie Ihren Benutzern die Kontrolle über die Ausführung der Programme geben. Es ist wichtig, Benutzern nützliche Standardeinstellungen zu geben. Sie sollten ihnen jedoch die Möglichkeit geben, Einstellungen zu überschreiben, die für ihre Situation nicht geeignet sind. Sie haben gesehen, dass das Paketflag
flexible Auswahlmöglichkeiten bietet, um Ihren Benutzern Konfigurationsoptionen zu präsentieren. Sie können einige einfache Flags auswählen oder eine erweiterbare Reihe von Unterbefehlen erstellen. In beiden Fällen können Sie mit dem Paketflag
Dienstprogramme im Stil der langen Geschichte flexibler und skriptfähiger Befehlszeilentools erstellen.
Weitere Informationen zur Programmiersprache Go finden Sie in unseren vollständigenHow To Code in Go series.