Capítulo 27 14 min de leitura

CLI: argparse & spinner

Dois módulos pequenos para construir ferramentas de linha de comando. stdlib::argparse é um parser de argumentos alocado no heap que suporta flags longas/curtas de três tipos de valor (string, int, bool), captura de posicionais, o marcador -- de fim de flags e texto --help gerado automaticamente. stdlib::spinner é um spinner de progresso em braille minimalista que anima no stderr e degrada com elegância em não-TTYs.

Import

import stdlib::argparse::*;   // ArgParser, StrFlag, IntFlag, BoolFlag
import stdlib::spinner::*;    // spinner_start / spinner_set / spinner_finish

Superfície pública em resumo

Módulo Item público Tipo
argparse StrFlag, IntFlag, BoolFlag structs (valores de flag em caixa)
argparse ArgParser struct
argparse new, free ciclo de vida do ArgParser
argparse add_string, add_int, add_bool declarar flags
argparse parse_argv, parse_tokens parsear (-> !i32)
argparse get_string, get_int, get_bool, positional ler resultados
argparse help_requested, format_help, print_help ajuda
spinner spinner_start, spinner_set, spinner_finish funções livres

argparse — visão geral

Crie um parser com ArgParser::new, declare cada flag com add_string / add_int / add_bool, depois chame parse_argv() (ou parse_tokens() para uma lista fornecida). Após um parse bem-sucedido, leia os valores com get_string / get_int / get_bool e os argumentos posicionais com positional().

import stdlib::argparse::*;
import stdlib::io::*;

fn main() -> !i32 {
    let p: *ArgParser = ArgParser::new("greet", "Say hello to someone");
    defer p.free();

    p.add_string("name", "n", "world", "who to greet");
    p.add_int("count", "c", 1, "number of greetings");
    p.add_bool("verbose", "v", false, "extra output");

    let r: !i32 = p.parse_argv();
    if !r.ok { eprintln("error: ".concat(r.err)); return ok(2); }
    if p.help_requested() { p.print_help(); return ok(0); }

    let who: string = p.get_string("name");
    let n: i32 = p.get_int("count");
    for let i: i32 = 0; i < n; i++ { println!("hello,", who); }
    return ok(0);
}

Formas de token reconhecidas

Token Significado
--name=value flag longa com valor inline
--name value flag longa; o valor é o próximo token
-n value flag curta; o valor é o próximo token
--name / -n flag bool sem valor (inverte o padrão)
--help / -h ativa help_requested(); não é impresso automaticamente
-- marcador de fim de flags; tudo depois é posicional
qualquer outra coisa argumento posicional (incluindo negativos no estilo -3)

Structs de valor de flag

Os três contêineres de valor em caixa são públicos. Raramente você os constrói diretamente — add_* os aloca e armazena — mas estão visíveis para inspeção.

Struct Campos Usado por
StrFlag pub default_value: string, pub value: string add_string / get_string
IntFlag pub default_value: i32, pub value: i32 add_int / get_int
BoolFlag pub default_value: bool, pub value: bool add_bool / get_bool
pub struct StrFlag  { pub default_value: string, pub value: string }
pub struct IntFlag  { pub default_value: i32,    pub value: i32 }
pub struct BoolFlag { pub default_value: bool,   pub value: bool }
// Os structs de valor de flag são públicos e podem ser construídos diretamente.
let f: IntFlag  = IntFlag{ default_value: 5, value: 5 };
let sf: StrFlag = StrFlag{ default_value: "x", value: "x" };
let bf: BoolFlag = BoolFlag{ default_value: false, value: true };

ArgParser

pub struct ArgParser { /* prog, desc, mapas de flags, ordem, posicionais ... (privado) */ }

Todos os campos são privados; interaja através dos métodos abaixo. Internamente ele mantém três HashMaps de structs de flag em caixa (um por tipo), vários mapas stringstring para alias curto/longo, tags de tipo e texto de ajuda, um Vector<string> de nomes de flags na ordem de declaração, e o Vector<string> de posicionais.

Construção

ArgParser::new

pub fn new(prog: string, desc: string) -> *ArgParser

Aloca um parser vazio no heap. prog e desc aparecem na saída do --help. O ponteiro retornado possui vários hash maps e vetores internos; libere-o com free() (combine com defer).

let p: *ArgParser = ArgParser::new("greet", "Say hello");
defer p.free();

free

pub fn free(self: *ArgParser)

Libera cada struct de flag, hash map e vetor de posse, depois o parser em si. O ponteiro fica inválido depois disso — chame apenas uma vez, de preferência via defer.

Declarando flags

Método Assinatura
add_string pub fn add_string(self: *ArgParser, long: string, short: string, default_value: string, help: string)
add_int pub fn add_int(self: *ArgParser, long: string, short: string, default_value: i32, help: string)
add_bool pub fn add_bool(self: *ArgParser, long: string, short: string, default_value: bool, help: string)

Cada um declara uma flag. long é o nome --long; short é o alias de um caractere -s, ou "" para nenhum. default_value inicializa o valor antes do parse, e help é a descrição por flag em --help.

p.add_string("name", "n", "world", "who to greet");  // --name=alice  --name alice  -n alice
p.add_int("count", "c", 1, "number of greetings");   // --count=5     --count 5     -c 5
p.add_bool("verbose", "v", false, "extra output");   // --verbose / -v sets it true

Parseando

parse_argv

pub fn parse_argv(self: *ArgParser) -> !i32

Parseia o argv do processo a partir do índice 1 (o índice 0, o caminho do programa, é ignorado). Retorna ok(0) em caso de sucesso ou err(message) no primeiro token malformado (flag desconhecida, valor ausente, int/bool inválido). Leia a mensagem via .err.

let r: !i32 = p.parse_argv();
if !r.ok { eprintln("error: ".concat(r.err)); return 2; }

parse_tokens

pub fn parse_tokens(self: *ArgParser, tokens: *Vector<string>) -> !i32

A mesma lógica de parse sobre uma lista de tokens fornecida pelo chamador — útil para testes e para construir subcomandos manualmente. parse_argv é implementado em termos deste.

let toks: *Vector<string> = Vector::new();
toks.push_all!("--name=alice", "--count", "3", "-v", "file.txt");
p.parse_tokens(toks)?;

Mensagens de erro que você pode checar (todas acessadas via .err):

Condição Mensagem
flag --long desconhecida unknown flag: --<name>
--int-flag sem valor seguinte flag --<name> requires a value
curta -i sem valor seguinte flag -<short> requires a value
não-inteiro para uma flag int flag --<name> expects an integer, got: <val>
não-bool para uma flag bool (via =) flag --<name> expects a bool, got: <val>

Lendo valores

Método Assinatura Retorna
get_string pub fn get_string(self: *ArgParser, long: string) -> string flag string parseada
get_int pub fn get_int(self: *ArgParser, long: string) -> i32 flag int parseada
get_bool pub fn get_bool(self: *ArgParser, long: string) -> bool flag bool parseada
positional pub fn positional(self: *ArgParser) -> *Vector<string> args posicionais, em ordem
let who: string = p.get_string("name");
let n: i32 = p.get_int("count");
if p.get_bool("verbose") { eprintln("verbose mode on"); }

let files: *Vector<string> = p.positional();
for let i: i32 = 0; i < files.len(); i++ { println!("file:", files.get(i)); }

Posicionais, -- e números negativos

positional() coleta, em ordem: qualquer token que não seja flag, qualquer token curto desconhecido -x (assim -3 sobrevive), e tudo após um separador -- (mesmo que pareça uma flag).

import stdlib::argparse::*;
import stdlib::io::*;

fn main() -> i32 {
    let p: *ArgParser = ArgParser::new("calc", "");
    defer p.free();

    p.add_bool("verbose", "v", false, "noisy");
    p.add_int("scale", "s", 1, "scale factor");

    let toks: *Vector<string> = Vector::new();
    // -3 não é uma flag curta conhecida, então sobrevive como posicional.
    // Tudo após `--` é posicional mesmo que pareça uma flag.
    toks.push_all!("-v", "--scale=2", "-3", "--", "--scale", "literal");

    let r: !i32 = p.parse_tokens(toks);
    if !r.ok { eprintln(r.err); return 2; }

    println!("verbose:", format!("{}", p.get_bool("verbose")));   // true
    println!("scale:", format!("{}", p.get_int("scale")));        // 2

    let pos: *Vector<string> = p.positional();
    for let i: i32 = 0; i < pos.len(); i++ {
        println!("positional:", pos.get(i));   // -3, --scale, literal
    }
    return 0;
}

Saída de ajuda

Método Assinatura
help_requested pub fn help_requested(self: *ArgParser) -> bool
format_help pub fn format_help(self: *ArgParser) -> string
print_help pub fn print_help(self: *ArgParser)

help_requested() informa se --help / -h foi visto durante o parse — o parser nunca imprime a ajuda sozinho, então o chamador decide o que fazer. format_help() monta a string de uso (nome do programa, descrição, uma linha por flag na ordem de declaração, com tipo + padrão + ajuda); print_help() imprime no stdout via print!.

if p.help_requested() { p.print_help(); return ok(0); }

let txt: string = p.format_help();   // captura em vez de imprimir

format_help produz uma saída com este formato:

output
usage: greet [flags] [positional...]

Say hello to someone

flags:
  --name (-n)  [string, default="world"]
      who to greet
  --count (-c)  [int, default=1]
      number of greetings
  --verbose (-v)  [bool, default=false]
      extra output
  --help (-h)  show this message

Um desc vazio (passado como "") omite o bloco de descrição; uma flag declarada com help vazio omite sua segunda linha (indentada).


Exemplo completo: um CLI real com spinner

Uma ferramenta completa no estilo build: várias flags, tratamento de --help, exibição de erros e trabalho real envolto em um spinner.

import stdlib::argparse::*;
import stdlib::spinner::*;
import stdlib::io::*;

fn main() -> !i32 {
    let p: *ArgParser = ArgParser::new("build", "Compile and link a target");
    defer p.free();

    p.add_string("target", "t", "app", "target name to build");
    p.add_int("jobs", "j", 1, "number of parallel jobs");
    p.add_bool("release", "r", false, "optimized release build");
    p.add_bool("color", "", true, "colorized output (--color turns it off)");

    let r: !i32 = p.parse_argv();
    if !r.ok { eprintln("error: ".concat(r.err)); return ok(2); }
    if p.help_requested() { p.print_help(); return ok(0); }

    let target: string = p.get_string("target");
    let jobs: i32 = p.get_int("jobs");
    let release: bool = p.get_bool("release");

    let mode: string = "debug";
    if release { mode = "release"; }
    eprintln(format!("building {} ({}) with {} jobs", target, mode, jobs));

    // Spinner em torno do trabalho. Em um TTY anima; em pipe, degrada
    // para linhas de status simples.
    spinner_start("compiling ".concat(target));
    // ... o trabalho demorado aconteceria aqui ...
    spinner_set("linking ".concat(target));
    // ... mais trabalho ...
    spinner_finish(true, "built ".concat(target));

    // Posicionais restantes (após as flags, ou após um `--`).
    let extra: *Vector<string> = p.positional();
    for let i: i32 = 0; i < extra.len(); i++ {
        println!("extra:", extra.get(i));
    }
    return ok(0);
}

Invoque assim:

shell
build -t mylib -j 4 --release -- a.o b.o

Padrão: subcomandos sobre parse_tokens

Não existe um tipo de subcomando embutido. Construa um removendo o primeiro token posicional e despachando para um parser por comando alimentado via parse_tokens.

import stdlib::argparse::*;
import stdlib::io::*;

fn run_add(rest: *Vector<string>) -> !i32 {
    let p: *ArgParser = ArgParser::new("git add", "stage files");
    defer p.free();
    p.add_bool("all", "a", false, "stage everything");
    p.parse_tokens(rest)?;
    println!("add all =", format!("{}", p.get_bool("all")));
    let files: *Vector<string> = p.positional();
    for let i: i32 = 0; i < files.len(); i++ { println!("stage:", files.get(i)); }
    return ok(0);
}

fn main() -> !i32 {
    let argv: *Vector<string> = Vector::new();
    argv.push_all!("add", "-a", "main.glide", "lib.glide");   // seria o argv real

    if argv.len() == 0 { eprintln("missing subcommand"); return ok(2); }
    let cmd: string = argv.get(0);

    let rest: *Vector<string> = Vector::new();
    for let i: i32 = 1; i < argv.len(); i++ { rest.push(argv.get(i)); }

    if cmd.eq("add") { run_add(rest)?; return ok(0); }
    eprintln("unknown subcommand: ".concat(cmd));
    return ok(2);
}

spinner

Um único spinner global. Uma pthread em segundo plano redesenha um frame de braille a cada 80 ms no stderr. Iniciar um novo spinner enquanto um está ativo o substitui silenciosamente (une o antigo primeiro). Quando o stderr não é um TTY (saída em pipe, CI sem PTY) a animação é suprimida: spinner_start / spinner_set imprimem sua mensagem como uma linha simples, e spinner_finish ainda emite sua linha final, mantendo os logs limpos.

Função Assinatura Descrição
spinner_start pub fn spinner_start(msg: string) Inicia (ou substitui) o spinner com msg como status inicial.
spinner_set pub fn spinner_set(msg: string) Atualiza o texto de status do spinner ativo. Não faz nada se nenhum estiver ativo (emite uma nova linha em não-TTY).
spinner_finish pub fn spinner_finish(ok: bool, msg: string) Para e une o animador, limpa a linha e imprime ✓ msg (quando ok) ou ✗ msg. Um msg vazio suprime a linha final.
import stdlib::spinner::*;

fn main() -> i32 {
    spinner_start("compiling...");
    // ... trabalho demorado ...
    spinner_set("linking...");
    // ... mais trabalho ...
    spinner_finish(true, "built target/my_app");

    spinner_start("deploying...");
    spinner_finish(false, "deploy failed");   // imprime  ✗ deploy failed

    spinner_start("warming up");
    spinner_finish(true, "");                 // sem linha final
    return 0;
}

Os frames ciclam pelos dez glifos de braille ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏. Ao finalizar a linha é apagada (\r\x1b[K) antes de o resumo / ser impresso.