So verwenden Sie das Flag-Paket in Go

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 Paketflagaus der Standardbibliothek verwenden.

In diesem Tutorial erfahren Sie, wie Sie mit dem Paketflagverschiedene 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 Paketflagbefassen 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 Blockconstverwendet 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 Paketflagerkannt 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 Variablecmdsdurchlaufen 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).Namezugegriffen 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.fsauf. 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 Paketflagflexible 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 PaketflagDienstprogramme 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.