Comment utiliser le package de drapeaux dans Go

introduction

Les utilitaires de ligne de commande sont rarement utiles immédiatement sans configuration supplémentaire. De bonnes valeurs par défaut sont importantes, mais des utilitaires utiles doivent accepter la configuration des utilisateurs. Sur la plupart des plates-formes, les utilitaires de ligne de commande acceptent des indicateurs pour personnaliser l’exécution de la commande. Les drapeaux sont des chaînes délimitées par des valeurs clés ajoutées après le nom de la commande. Go vous permet de créer des utilitaires de ligne de commande qui acceptent les indicateurs en utilisant le packageflag de la bibliothèque standard.

Dans ce didacticiel, vous explorerez différentes manières d'utiliser le packageflag pour créer différents types d'utilitaires de ligne de commande. Vous allez utiliser un indicateur pour contrôler la sortie du programme, introduire des arguments de position dans lesquels vous mélangez des indicateurs et d’autres données, puis implémenter des sous-commandes.

Utilisation d’un indicateur pour modifier le comportement d’un programme

L'utilisation du packageflag implique trois étapes: Premièrement,define variables pour capturer les valeurs d'indicateur, puis définir les indicateurs que votre application Go utilisera, et enfin, analyser les indicateurs fournis à l'application lors de l'exécution. La plupart des fonctions du packageflag concernent la définition des indicateurs et leur liaison aux variables que vous avez définies. La phase d'analyse est gérée par la fonctionParse().

Pour illustrer, vous allez créer un programme qui définit un indicateurBoolean qui change le message qui sera imprimé en sortie standard. Si un indicateur-color est fourni, le programme imprimera un message en bleu. Si aucun indicateur n'est fourni, le message sera imprimé sans aucune couleur.

Créez un nouveau fichier appeléboolean.go:

nano boolean.go

Ajoutez le code suivant au fichier pour créer le programme:

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

Cet exemple utiliseANSI Escape Sequences pour indiquer au terminal d'afficher une sortie colorisée. Ce sont des séquences de caractères spécialisées, il est donc logique de définir un nouveau type pour elles. Dans cet exemple, nous avons appelé ce typeColor et défini le type comme unstring. Nous définissons ensuite une palette de couleurs à utiliser dans le blocconst qui suit. La fonctioncolorize définie après le blocconst accepte une de ces constantesColor et une variablestring pour le message à coloriser. Il ordonne ensuite au terminal de changer de couleur en imprimant d'abord la séquence d'échappement correspondant à la couleur demandée, puis imprime le message et demande enfin au terminal de réinitialiser sa couleur en imprimant la séquence de réinitialisation de couleur spéciale.

Dansmain, nous utilisons la fonctionflag.Bool pour définir un drapeau booléen appelécolor. Le deuxième paramètre de cette fonction,false, définit la valeur par défaut de cet indicateur lorsqu'il n'est pas fourni. Contrairement aux attentes que vous pourriez avoir, définir ce paramètre surtrue n'inverse pas le comportement de telle sorte que la fourniture d'un indicateur le rendra faux. Par conséquent, la valeur de ce paramètre est presque toujoursfalse avec des indicateurs booléens.

Le dernier paramètre est une chaîne de documentation pouvant être imprimée sous forme de message d'utilisation. La valeur renvoyée par cette fonction est un pointeur vers unbool. La fonctionflag.Parse sur la ligne suivante utilise ce pointeur pour définir la variablebool en fonction des indicateurs transmis par l'utilisateur. On peut alors vérifier la valeur de ce pointeurbool en déréférençant le pointeur. Vous trouverez plus d'informations sur les variables de pointeur dans lestutorial on pointers. En utilisant cette valeur booléenne, nous pouvons alors appelercolorize lorsque l'indicateur-color est défini, et appeler la variablefmt.Println lorsque l'indicateur est absent.

Enregistrez le fichier et exécutez le programme sans aucun indicateur:

go run boolean.go

Vous verrez le résultat suivant:

OutputHello, DigitalOcean!

Maintenant, exécutez à nouveau ce programme avec l'indicateur-color:

go run boolean.go -color

La sortie sera le même texte, mais cette fois dans la couleur bleue.

Les drapeaux ne sont pas les seules valeurs transmises aux commandes. Vous pouvez également envoyer des noms de fichiers ou d’autres données.

Travailler avec des arguments positionnels

En général, les commandes prennent un certain nombre d’arguments qui font l’objet de la focalisation de la commande. Par exemple, la commandehead, qui imprime les premières lignes d'un fichier, est souvent appelée en tant quehead example.txt. Le fichierexample.txt est un argument de position lors de l'invocation de la commandehead.

La fonctionParse() continuera à analyser les indicateurs qu'elle rencontre jusqu'à ce qu'elle détecte un argument sans indicateur. Le packageflag les rend disponibles via les fonctionsArgs() etArg().

Pour illustrer cela, vous allez créer une réimplémentation simplifiée de la commandehead, qui affiche les premières lignes d'un fichier donné:

Créez un nouveau fichier appeléhead.go et ajoutez le code suivant:

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

Tout d'abord, nous définissons une variablecount pour contenir le nombre de lignes que le programme doit lire dans le fichier. Nous définissons ensuite l'indicateur-n en utilisantflag.IntVar, reflétant le comportement du programme originalhead. Cette fonction nous permet de passer nos proprespointer à une variable contrairement aux fonctionsflag qui n'ont pas le suffixeVar. Hormis cette différence, le reste des paramètres deflag.IntVar suivent son homologueflag.Int: le nom du drapeau, une valeur par défaut et une description. Comme dans l'exemple précédent, nous appelons ensuiteflag.Parse() pour traiter l'entrée de l'utilisateur.

La section suivante lit le fichier. Nous définissons d'abord une variableio.Reader qui sera soit définie sur le fichier demandé par l'utilisateur, soit sur une entrée standard passée au programme. Dans l'instructionif, nous utilisons la fonctionflag.Arg pour accéder au premier argument positionnel après tous les indicateurs. Si l'utilisateur a fourni un nom de fichier, celui-ci sera défini. Sinon, ce sera la chaîne vide (""). Lorsqu'un nom de fichier est présent, nous utilisons la fonctionos.Open pour ouvrir ce fichier et définir lesio.Reader que nous avons définis auparavant sur ce fichier. Sinon, nous utilisonsos.Stdin pour lire à partir de l'entrée standard.

La dernière section utilise un*bufio.Scanner créé avecbufio.NewScanner pour lire les lignes de la variableio.Readerin. Nous itérons jusqu'à la valeur decount en utilisant unfor loop, en appelantbreak si le balayage de la ligne avecbuf.Scan produit une valeurfalse, indiquant que le nombre de lignes est inférieur au nombre demandé par l'utilisateur.

Exécutez ce programme et affichez le contenu du fichier que vous venez d'écrire en utilisanthead.go comme argument de fichier:

go run head.go -- head.go

Le séparateur-- est un drapeau spécial reconnu par le packageflag qui indique qu'il n'y a plus d'arguments d'indicateur. Lorsque vous exécutez cette commande, vous recevez le résultat suivant:

Outputpackage main

import (
        "bufio"
        "flag"

Utilisez l'indicateur-n que vous avez défini pour ajuster la quantité de sortie:

go run head.go -n 1 head.go

Ceci affiche uniquement l'instruction de package:

Outputpackage main

Enfin, lorsque le programme détecte qu'aucun argument de position n'a été fourni, il lit l'entrée à partir de l'entrée standard, tout commehead. Essayez d'exécuter cette commande:

echo "fish\nlobsters\nsharks\nminnows" | go run head.go -n 3

Vous verrez la sortie:

Outputfish
lobsters
sharks

Le comportement des fonctionsflag que vous avez vues jusqu'à présent s'est limité à l'examen de l'ensemble de l'appel de commande. Vous ne voulez pas toujours ce comportement, surtout si vous écrivez un outil en ligne de commande qui prend en charge les sous-commandes.

Utiliser FlagSet pour implémenter des sous-commandes

Les applications modernes en ligne de commande implémentent souvent des «sous-commandes» pour regrouper une suite d'outils sous une seule commande. L'outil le plus connu qui utilise ce modèle estgit. Lors de l'examen d'une commande commegit init,git est la commande etinit est la sous-commande degit. Une caractéristique notable des sous-commandes est que chaque sous-commande peut avoir sa propre collection de drapeaux.

Les applications Go peuvent prendre en charge les sous-commandes avec leur propre ensemble d'indicateurs en utilisant le typeflag.(*FlagSet). Pour illustrer cela, créez un programme qui implémente une commande en utilisant deux sous-commandes avec des indicateurs différents.

Créez un nouveau fichier appelésubcommand.go et ajoutez le contenu suivant au fichier:

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

Ce programme est divisé en quelques parties: la fonctionmain, la fonctionroot et les fonctions individuelles pour implémenter la sous-commande. La fonctionmain gère les erreurs renvoyées par les commandes. Si une fonction renvoie unerror, l'instructionif l'attrapera, imprimera l'erreur, et le programme se terminera avec un code d'état de1, indiquant qu'une erreur s'est produite au reste du système d'exploitation. Dansmain, nous passons tous les arguments avec lesquels le programme a été appelé àroot. Nous supprimons le premier argument, qui est le nom du programme (dans les exemples précédents./subcommand) en découpant d'abordos.Args.

La fonctionroot définit[]Runner, où toutes les sous-commandes seraient définies. Runner est uninterface pour les sous-commandes qui permet àroot de récupérer le nom de la sous-commande en utilisantName() et de le comparer avec la variable de contenusubcommand . Une fois que la sous-commande correcte est localisée après avoir parcouru la variablecmds, nous initialisons la sous-commande avec le reste des arguments et invoquons la méthodeRun() de cette commande.

Nous ne définissons qu'une seule sous-commande, bien que ce cadre nous permette facilement de créer d'autres. LeGreetCommand est instancié en utilisantNewGreetCommand où nous créons un nouveau*flag.FlagSet en utilisantflag.NewFlagSet. flag.NewFlagSet prend deux arguments: un nom pour le jeu d'indicateurs et une stratégie pour signaler les erreurs d'analyse. Le nom du*flag.FlagSet est accessible en utilisant la méthodeflag.(*FlagSet).Name. Nous l'utilisons dans la méthode(*GreetCommand).Name() afin que le nom de la sous-commande corresponde au nom que nous avons donné aux*flag.FlagSet. NewGreetCommand définit également un indicateur-name de la même manière que les exemples précédents, mais il l'appelle à la place comme une méthode à partir du champ*flag.FlagSet des*GreetCommand,gc.fs. Lorsqueroot appelle la méthodeInit() des*GreetCommand, on passe les arguments fournis à la méthodeParse du champ*flag.FlagSet.

Il sera plus facile de voir les sous-commandes si vous générez ce programme puis l’exécutez. Construisez le programme:

go build subcommand.go

Maintenant, lancez le programme sans argument:

./subcommand

Vous verrez cette sortie:

OutputYou must pass a sub-command

Exécutez maintenant le programme avec la sous-commandegreet:

./subcommand greet

Cela produit la sortie suivante:

OutputHello World !

Utilisez maintenant l'indicateur-name avecgreet pour spécifier un nom:

./subcommand greet -name Sammy

Vous verrez cette sortie du programme:

OutputHello Sammy !

Cet exemple illustre certains principes sur lesquels des applications de ligne de commande plus volumineuses pourraient être structurées dans Go. Les «FlagSet» sont conçus pour donner aux développeurs plus de contrôle sur où et comment les indicateurs sont traités par la logique d'analyse des indicateurs.

Conclusion

Les indicateurs rendent vos applications plus utiles dans plus de contextes, car elles permettent aux utilisateurs de contrôler l'exécution des programmes. Il est important de donner aux utilisateurs des valeurs par défaut utiles, mais vous devez leur donner la possibilité de remplacer les paramètres qui ne fonctionnent pas dans leur situation. Vous avez vu que le packageflag offre des choix flexibles pour présenter les options de configuration à vos utilisateurs. Vous pouvez choisir quelques indicateurs simples ou créer une suite extensible de sous-commandes. Dans les deux cas, l'utilisation du packageflag vous aidera à créer des utilitaires dans le style de la longue histoire des outils de ligne de commande flexibles et scriptables.

Pour en savoir plus sur le langage de programmation Go, consultez nosHow To Code in Go series complets.