Introduzione

Le utility da riga di comando sono raramente pronte all'uso senza ulteriore configurazione. Sulla maggior parte delle piattaforme, le utility della riga di comando accettano flag per personalizzare l'esecuzione del comando. I flag sono stringhe delimitate dal valore-chiave aggiunte dopo il nome del comando. Go consente di creare utilità da riga di comando che accettano flag utilizzando il pacchetto flag dalla libreria standard.

In questo tutorial esplorerai vari modi per utilizzare il pacchetto flag per creare diversi tipi di utility da riga di comando. Utilizzerai un flag per controllare l'output del programma, introdurre argomenti posizionali in cui mescoli flag e altri dati e quindi implementi i comandi secondari.

Usare una flag per cambiare il comportamento di un programma

L'utilizzo del pacchetto flag prevede tre passaggi: in primo luogo, definire le variabili per acquisire i valori dei flag, quindi definire i flag che verranno utilizzati dall'applicazione Go e infine analizzare i flag forniti all'applicazione al momento dell'esecuzione. La maggior parte delle funzioni all'interno del pacchetto flag riguardano la definizione di flag e l'associazione a variabili definite dall'utente. La fase di analisi è gestita dalla funzione Parse().

Creerai un programma che definisce un flag booleano che modifica il messaggio che verrà stampato sullo standard output. Se è presente una flag -color, il programma stamperà un messaggio in blu. Se non viene fornita alcun flag, il messaggio verrà stampato senza alcun colore.

Crea un nuovo file chiamato boolean.go:

nano boolean.go

Aggiungi il seguente codice al file per creare il programma:

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, Noviello!")
        return
    }
    fmt.Println("Hello, Noviello!")
}
boolean.go

In questo esempio vengono utilizzate le sequenze di escape ANSI per indicare al terminale di visualizzare un output colorato. Queste sono sequenze specializzate di caratteri, quindi ha senso definire un nuovo type per loro. In questo esempio, abbiamo chiamato quel tipo (type) Color e definito il tipo (type) come string. Definiamo quindi una tavola di colori (palette) da utilizzare nel blocco const che segue. La funzione colorize definita dopo il blocco const accetta una di queste costanti Color e una variabile string per la colorazione del messaggio. Indica quindi al terminale di cambiare colore stampando prima la sequenza di escape per il colore richiesto, quindi stampa il messaggio e infine richiede che il terminale ripristini il suo colore stampando la speciale sequenza di ripristino del colore.

All'interno main, usiamo la funzione flag.Bool per definire un flag booleano chiamato color. Il secondo parametro di questa funzione, false, imposta il valore predefinito per questo flag quando non viene fornito. Contrariamente alle aspettative che potresti avere, impostando questo su true non si inverte il comportamento in modo tale che fornire un flag lo farà diventare falso. Di conseguenza, il valore di questo parametro è quasi sempre false con flag booleani.

Il parametro finale è una stringa di documentazione che può essere stampata come messaggio di utilizzo. Il valore restituito da questa funzione è un puntatore bool. La funzione flag.Parse sulla riga successiva utilizza questo puntatore per impostare la variabile di base bool ai flag passati dall'utente. Siamo quindi in grado di controllare il valore di questo puntatore bool dereferenziando il puntatore. Utilizzando questo valore booleano, possiamo quindi chiamare colorize quando il flag -color è impostato e chiamare la variabile fmt.Println quando il flag è assente.

Salvare il file ed eseguire il programma senza flag:

go run boolean.go

Vedrai il seguente output:

Hello, Noviello!

Ora esegui di nuovo questo programma con il flag -color:

go run boolean.go -color

L'output sarà lo stesso testo, ma questa volta nel colore blu.

I flag non sono gli unici valori passati ai comandi. È inoltre possibile inviare nomi di file o altri dati.

Lavorare con argomenti posizionali

In genere i comandi accettano una serie di argomenti che fungono da oggetto del focus del comando. Ad esempio, il comando head, che stampa le prime righe di un file, viene spesso invocato come head example.txt. Il file example.txt è un argomento posizionale nell'invocazione del comando head.

La funzione Parse() continuerà ad analizzare i flag che incontra fino a quando non rileva un argomento non flag. Il pacchetto flag li rende disponibili tramite le funzioni Args() e Arg().

Per illustrare questo, costruirai una reimplementazione semplificata del comando head, che visualizza le prime diverse righe di un determinato file:

Crea un nuovo file chiamato head.go e aggiungi il seguente codice:

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)
    }
}
head.go

Innanzitutto, definiamo una variabile count per contenere il numero di righe che il programma dovrebbe leggere dal file. Definiamo quindi il flag  -n usando flag.IntVar, rispecchiando il comportamento del programma originale head. Questa funzione ci consente di passare il nostro puntatore a una variabile in contrasto con le funzioni flag che non hanno il suffisso Var. A parte questa differenza, il resto dei parametri seguirà flag.IntVar la sua controparte flag.Int: il nome del flag, un valore predefinito e una descrizione. Come nell'esempio precedente, chiamiamo quindi flag.Parse() per elaborare l'input dell'utente.

La sezione successiva legge il file. Definiamo innanzitutto una variabile io.Reader che verrà impostata sul file richiesto dall'utente o input standard passato al programma. All'interno dell'istruzione if, utilizziamo la funzione flag.Arg per accedere al primo argomento posizionale dopo tutti i flag. Se l'utente ha fornito un nome file, questo verrà impostato. Altrimenti, sarà la stringa vuota (""). Quando è presente un nome file, utilizziamo la funzione os.Open per aprire quel file e impostare il file io.Reader precedentemente definito su quel file. Altrimenti, usiamo os.Stdin per leggere dallo standard input.

La sezione finale utilizza un *bufio.Scanner creato con bufio.NewScanner per leggere le righe io.Reader dalla variabile in. Esaminiamo fino al valore count utilizzando un ciclo for, chiamando break se la scansione della linea buf.Scan produce un valore false, indicando che il numero di linee è inferiore al numero richiesto dall'utente.

Esegui questo programma e visualizza il contenuto del file che hai appena scritto usando head.go come argomento del file:

go run head.go -- head.go

Il separatore -- è un flag speciale riconosciuto dal pacchetto flag che indica che non seguono più argomenti flag. Quando si esegue questo comando, si riceve il seguente output:

package main

import (
        "bufio"
        "flag"

Usa il flag -n che hai definito per regolare la quantità di output:

go run head.go -n 1 head.go

Questo produce solo l'istruzione del pacchetto:

package main

Infine, quando il programma rileva che non sono stati forniti argomenti posizionali, legge l'input dall'input standard, proprio come head. Prova a eseguire questo comando:

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

Vedrai l'output:

fish
lobsters
sharks

Il comportamento delle funzioni flag che hai visto finora è stato limitato all'esame dell'intero richiamo del comando. Non sempre si desidera questo comportamento, soprattutto se si sta scrivendo uno strumento da riga di comando che supporta i comandi secondari.

Utilizzare il FlagSet per implementare i comandi secondari

Le moderne applicazioni da riga di comando spesso implementano "sotto-comandi" (sub-commands) per raggruppare una suite di strumenti in un unico comando. Lo strumento più noto che utilizza questo modello è git. Quando si esamina un comando come git init, git è il comando ed init è il sottocomando di git. Una caratteristica notevole dei sotto-comandi è che ogni sotto-comando può avere la propria collezione di flag.

Le applicazioni Go possono supportare i comandi secondari con il proprio set di flag utilizzando il type flag.(*FlagSet). Per illustrare ciò, creare un programma che implementa un comando usando due sotto-comandi con flag diversi.

Crea un nuovo file chiamato subcommand.go e aggiungi il seguente contenuto al file:

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)
    }
}
subcommand.go

Questo programma è diviso in diverse parti: la funzione main, la funzione root e le singole funzioni per implementare il comando secondario. La funzione main gestisce gli errori restituiti dai comandi. Se una funzione restituisce un errore, l'istruzione if la rileva, stampa l'errore e il programma esce con un codice di stato di 1, indicando che si è verificato un errore nel resto del sistema operativo. All'interno di main, passiamo tutti gli argomenti con cui è stato invocato il programma root. Rimuoviamo il primo argomento, che è il nome del programma (negli esempi precedenti ./subcommand) tagliando per primo os.Args.

La funzione root definisce []Runner dove verranno definiti tutti i sotto-comandi. Runner è un'interfaccia per i comandi secondari che consente al root di recuperare il nome del comando secondario utilizzando Name() e confrontarlo con il contenuto  della variabile subcommand . Una volta individuato il comando secondario corretto dopo aver ripetuto la variabile cmds, inizializziamo il comando secondario con il resto degli argomenti e invochiamo il metodo Run() di quel comando.

Definiamo solo un sotto-comando, sebbene questo framework ci consenta facilmente di crearne altri. Con GreetCommand viene creata un'istanza utilizzando NewGreetCommand in cui creiamo un nuovo *flag.FlagSet utilizzando flag.NewFlagSet. flag.NewFlagSet accetta due argomenti: un nome per il set di flag e una strategia per la segnalazione degli errori di analisi. Il nome *flag.FlagSet è accessibile usando il metodo flag.(*FlagSet).Name. Usiamo questo nel metodo (*GreetCommand).Name() in modo che il nome del comando secondario corrisponda al nome che abbiamo dato a *flag.FlagSet. NewGreetCommand definisce anche un flag -name in modo analogo agli esempi precedenti, ma richiede invece ciò come metodo fuori dal campo *flag.FlagSet di *GreetCommand, gc.fs. Quando root chiama il metodo Init() di *GreetCommand, passiamo gli argomenti forniti al metodo Parse del campo *flag.FlagSet.

Sarà più semplice visualizzare i sotto-comandi se si crea questo programma e quindi lo si esegue. Costruisci il programma:

go build subcommand.go

Ora esegui il programma senza argomenti:

./subcommand

Vedrai questo output:

You must pass a sub-command

Ora esegui il programma con il comando secondario greet:

./subcommand greet

Questo produce il seguente output:

Hello World !

Ora usa il flag -name con greet per specificare un nome:

./subcommand greet -name Noviello

Vedrai questo output dal programma:

Hello Noviello !

Questo esempio illustra alcuni principi alla base di come le più grandi applicazioni della riga di comando potrebbero essere strutturate in Go. I FlagSet sono progettati per fornire agli sviluppatori un maggiore controllo su dove e come i flag vengono elaborati dalla logica di analisi dei flag.

Conclusione

I flag rendono le tue applicazioni più utili in più contesti perché offrono ai tuoi utenti il ​​controllo su come vengono eseguiti i programmi. È importante fornire agli utenti utili impostazioni predefinite, ma dovresti dare loro l'opportunità di ignorare le impostazioni che non funzionano per la loro situazione. Hai visto che il pacchetto flag offre opzioni flessibili per presentare le opzioni di configurazione ai tuoi utenti. Puoi scegliere alcuni semplici flag o creare una suite estensibile di sotto-comandi.