Introducción

Los servicios de línea de comandos rara vez son útiles cuando vienen listos para usar sin configuración adicional. Es importante disponer de buenos valores predeterminados, pero las utilidades prácticas deben aceptar la configuración de parte de los usuarios. En la mayoría de las plataformas, los servicios de línea de comandos aceptan indicadores para personalizar la ejecución del comando. Los parámetros son cadenas delimitadas por el valor de clave agregadas después del nombre del comando. Go le permite crear servicios de línea de comandos que acepten indicadores usando el paquete flag de la biblioteca estándar.

En este tutorial, explorará varias formas de usar el paquete flag para crear diferentes tipos de utilidades de línea de comandos. Utilizará un indicador para controlar el resultado del programa, introducir argumentos en posición en los que intercambie indicadores y otros datos, y luego implementará subcomandos.

Cómo usar un indicador para cambiar el comportamiento de un programa

Al usar el paquete flag se deben seguir tres pasos: primero, definir variables para capturar valores de indicadores, definir los parámetros que utilizará su aplicación de Go y, por último, analizar los parámetros proporcionados a la aplicación al ejecutarse. La mayor parte de las funciones del paquete flag se ocupan de definir los indicadores y vincularlos a las variables que definió. La función Parse() lleva a cabo la fase de análisis.

Para ilustrar esto, creará un programa que defina un indicador booleano que cambie el mensaje que se imprimirá para un resultado estándar. Si se proporciona un indicador -color, el programa imprimirá un mensaje en azul. Si no se proporciona un indicador, el mensaje se imprimirá sin color.

Cree un nuevo archivo llamado boolean.go:

  • nano boolean.go

Añada el siguiente código al archivo para crear el programa:

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

En este ejemplo, se utilizan secuencias de escape ANSI para dar instrucciones al terminal a fin de que muestre un resultado colorido. Estas son secuencias especializadas de caracteres, por lo que tiene sentido definir un nuevo tipo para ellos. En este ejemplo, asignamos el nombre Color al tipo y lo definimos como una string. Luego, definimos una paleta de colores para usar en el bloque const que sigue. La función colorize definida después del bloque const acepta una de estas constantes Color y una variable string para que el mensaje sea de color. Luego, indica al terminal que cambie de color imprimiendo primero la secuencia de escape para el color solicitado, luego imprime el mensaje y finalmente solicita que el terminal restablezca su color imprimiendo la secuencia especial de restablecimiento de color.

En main, usamos la función flag.Bool paradefinirun indicador booleano llamado color. El segundo parámetro de esta función, false, establece el valor predeterminado para este indicador cuando no se proporciona. En contra de las expectativas que pueda tener, fijar esto en true no invierte el comportamiento de modo tal que la incorporación de un indicador haga que se convierta en falso. Por lo tanto, el valor de este parámetro es casi siempre false con indicadores de booleano.

El parámetro final es una cadena de documentación que puede imprimirse como mensaje de uso. El valor devuelto de esta función es un puntero de un bool. La función flag.Parse en la siguiente línea utiliza este puntero para configurar la variable bool en función de los indicadores pasados por el usuario. Luego, podemos verificar el valor de este puntero bool eliminado su referencia. Se puede encontrar más información sobre las variables de punteros en el tutorial sobre punteros. Mediante este valor booleano, podemos invocar colorize cuando esté configurado el puntero -color e invocar la variable fmt.Println cuando no se encuentre el indicador.

Guarde el archivo y ejecute el programa sin indicadores:

  • go run boolean.go

Verá el siguiente resultado:

Output
Hello, DigitalOcean!

Ahora, ejecute este programa de nuevo con el indicador color:

  • go run boolean.go -color

El resultado será el mismo texto, pero esta vez en color azul.

Los indicadores no son los únicos valores pasados a comandos. Es posible que también envíe nombres de archivo u otros datos.

Trabajar con argumentos posicionales

Normalmente, los comandos toman una serie de argumentos que actúan como sujeto del foco del comando. Por ejemplo, el comando head, que imprime las primeras líneas de un archivo, a menudo se invoca como head example.txt. El archivo example.txt es un argumento posicional en la invocación del comando head.

La función Parse() continuará analizando los indicadores que encuentre hasta detectar un argumento sin indicador. El paquete flag los pone a disposición a través de las funciones Args () y Arg ().

Para ilustrar esto, compilará una nueva implementación simplificada del comando head, que muestra las primeras líneas de un archivo dado:

Cree un nuevo archivo llamado head.go y agregue el siguiente código:

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

Primero, definimos una variable count para contener el número de líneas que el programa debería leer del archivo. Luego definimos el indicador -n usando flag.IntVar, para reflejar el comportamiento del programa head original. Esta función nos permite pasar nuestro propio puntero a una variable en contraste con las funciones flag que no tienen el sufijo Var. Además de esta diferencia, el resto de los parámetros de flag.IntVar siguen a su homólogo flag.Int: el nombre del indicador, un valor predeterminado y una descripción. Como en el ejemplo anterior, invocamos a flag.Parse () para procesar la entrada del usuario.

La siguiente sección lee el archivo. Primero definimos una variable io.Reader que se establecerá en el archivo solicitado por el usuario o la entrada estándar pasada al programa. Dentro de la instrucción if, usamos la función flag.Arg para acceder al primer argumento posicional después de todos los indicadores. Si el usuario proporcionó un nombre de archivo, se establecerá. De lo contrario, será la cadena vacía (""). Cuando hay un nombre de archivo presente, usamos la función os.Open para abrir ese archivo y configurar el io.Reader que definimos antes para él. De lo contrario, usamos os.Stdin para la lectura desde entrada estándares.

En la sección final se utiliza un * bufio.Scanner creado con bufio.NewScanner para leer líneas de la variable in de io.Reader. Realizamos iteraciones hasta el valor de count utilizando un bucle for, invocando break si del análisis de la línea con buf.Scan surge un valor false, lo cual indica que el número de líneas es menor que el número solicitado por el usuario.

Ejecute este programa y muestre el contenido del archivo que acaba de escribir usando head.go como argumento del archivo:

  • go run head.go -- head.go

El separador -- es un indicador especial reconocido por el paquete flag que señala que no hay más argumentos de indicador a continuación. Cuando ejecute este comando, verá el siguiente resultado:

Output
package main import ( "bufio" "flag"

Utilice el indicador -n que definió para ajustar la cantidad de resultado:

  • go run head.go -n 1 head.go

Como resultado solo se muestra la instrucción del paquete:

Output
package main

Por último, cuando el programa detecta que no se proporcionaron argumentos posicionales, lee la entrada estándar como en el caso de head. Intente ejecutar este comando:

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

Verá el resultado:

Output
fish lobsters sharks

El comportamiento de las funciones flag que vio hasta ahora se limitó a examinar toda la invocación del comando. No siempre le convendrá este comportamiento, sobre todo si escribe una herramienta de línea de comandos que admite subcomandos.

Usar FlagSet para implementar subcomandos

Las aplicaciones de línea de comandos modernas a menudo implementan “subcomandos” para empaquetar una serie de herramientas bajo un único comando. La herramienta más conocida que utiliza este patrón es git. Si se examina un comando como git init, git es el comando e init es el subcomando de git. Una característica notable de los subcomandos es que cada uno puede tener su propio conjunto de indicadores.

Las aplicaciones de Go pueden admitir subcomandos con su propio conjunto de indicadores usando el tipo flag.( *FlagSet). Para ilustrar esto, cree un programa que ejecute un comando usando dos subcomandos con diferentes indicadores.

Cree un nuevo archivo llamado subcommand.go y agregue a este el siguiente contenido:

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

Este programa se divide en algunas partes: la función main, la función root y las funciones individuales para implementar el subcomando. La función main maneja los errores que muestran los comandos. Si alguna función muestra un error, la instrucción if lo detectará y lo imprimirá, y el programa se cerrará con un código de estado 1, lo cual indica que se produjo un error en el resto del sistema operativo. Dentro de main, pasamos todos los argumentos con los que se invocó el programa a root. Eliminamos el primer argumento, que es el nombre del programa (en los ejemplos anteriores, ./subcommand) cortando os.Args primero.

La función root define [] Runner, donde se definirían todos los subcomandos. Runner es una interfaz para subcomandos que permite a root recuperar el nombre del subcomando usando Name () y compararlo con la variable subcommand de contenido. Una vez que se encuentre el subcomando correcto después de la iteración de la variable cmds, inicializamos el subcomando con el resto de los argumentos e invocamos el método Run() de ese comando.

Solo definimos un subcomando, aunque este marco nos permitiría crear fácilmente otros. Se crean instancias de GreetCommand usando NewGreetCommand donde creamos un nuevo *flag.FlagSet usando flag.NewFlagSet. flag.NewFlagSet toma dos argumentos: un nombre para el conjunto de indicadores y una estrategia para informar errores de análisis. Es posible acceder al nombre de *flag.FlagSet con el método flag.( *FlagSet. Name. Lo usamos en (*GreetCommand). El método Name() para que el nombre del subcomando coincida con el nombre que asignamos a *flag.FlagSet. NewGreetCommand también define un indicador -name de una manera similar a la de los ejemplos anteriores, pero como alternativa lo invoca como un método fuera del campo *flag.FlagSet del *GreetCommand, gc.fs. Cuando root invoca al método Init () del * GreetCommand, pasamos los argumentos proporcionados al método Parse del campo *flag.FlagSet.

Será más sencillo ver subcomandos si compila este programa y lo ejecuta. Compile el programa:

  • go build subcommand.go

Ahora, ejecute el programa sin argumentos:

  • ./subcommand

Verá este resultado:

Output
You must pass a sub-command

Ahora, ejecute el programa con el subcomando greet:

  • ./subcommand greet

Esto produce el siguiente resultado:

Output
Hello World !

Ahora, utilice el indicador -name con greet para especificar un nombre:

  • ./subcommand greet -name Sammy

Verá este resultado del programa:

Output
Hello Sammy !

Este ejemplo permite ver algunos principios relacionados con la estructura que podrían tener las aplicaciones de línea de comandos más grandes en Go. Los FlagSets están diseñados para dar a los desarrolladores más control respecto de dónde y cómo lógica de análisis de indicadores los procesa.

Conclusión

Los indicadores hacen que sus aplicaciones sean más útiles en más contextos porque dan a sus usuarios control respecto de cómo se ejecutan los programas. Es importante proporcionar a los usuarios valores predeterminados útiles, pero debería darles la oportunidad de anular ajustes que no funcionen en sus casos. Pudo observar que el paquete flag ofrece alternativas flexibles para presentar opciones de configuración a sus usuarios. Puede elegir algunos indicadores sencillos o crear una serie de subcomandos extensible. En cualquiera de los casos, usar el paquete flag le permitirá crear utilidades siguiendo el estilo de la larga historia de herramientas de línea de comandos flexibles y programables.

Para obtener más información sobre el lenguaje de programación de Go, consulte nuestra serie Cómo realizar codificaciones en Go.

0 Comments

Creative Commons License