Vai al contenuto
Ridgun
Italiano

24 Il Libro di Ridgun

La lib cli — Flag, Opzioni, Barriera --, Sottocomandi e Variabili d'Ambiente

24.1. Perché esiste la lib cli? (Il problema reale e la filosofia Ridgun)

«Perché ho bisogno di una lib cli scritta da zero in Ridgun puro invece di collegare una libreria C tradizionale?»

Questa è la prima domanda che si pone qualsiasi ingegnere di sistema arrivando a cli. E la risposta riassume l'intera filosofia di ridgun-libs:

Siamo sinceri: analizzare argv a mano inizia sempre con un ciclo innocente e finisce per rompersi quando qualcuno passa -la raggruppato, --port=9090 con il segno di uguale o -- seguito da un file che inizia con un trattino -file.rg.

La lib cli gestisce flag lunghi (--verbose), corti (-v), raggruppati (-la), opzioni con valore (--port=8080, --port 8080, -p 8080), il separatore POSIX --, argomenti posizionali, sottocomandi e lettura tipizzata delle variabili d'ambiente (env get int or, env get bool or).

In un ecosistema tradizionale, risolvere i compiti del dominio CLI comporta trascinare dipendenze esterne in C, gestire puntatori grezzi, conversioni opache di tipo, eccezioni non verificate o pause imprevedibili del garbage collector. In Ridgun, la lib cli (src/main/ridgun/lib/cli.rg) affronta il problema con quattro principi non negoziabili:

  1. Dichiara import cli (o import cli as alias) all'inizio del file: ogni lib definisce il proprio spazio dei nomi module :: cli e in modalità --strict le funzioni multi-parola si invocano sempre con spazi naturali.
  2. Concatena le tue pipeline da sinistra a destra con ->: scrivi input passo uno -> passo due -> risultato, usa le maschere (+ _ _ +) per destrutturare le sequenze e ometti _ = nelle istruzioni sequenziali (o scarta il valore con -> ()).
  3. Ricorsione di coda con TCO (Tail-Call Optimization): tutte le scansioni interne di cli vengono compilate in cicli nativi in spazio di stack $O(1)$.
  4. Memoria deterministica (RMM1 / RMM2): ogni struttura intermedia viene liberata nell'istante esatto in cui termina la sua ultima lettura, senza Garbage Collector.

24.2. Artefatti nativi, importazione e catalogo completo dell'API di cli

In ridgun-libs, cli è una lib di prima classe situata in src/main/ridgun/lib/cli.rg (dominio CLI). Compilando con stable bundle, viene impacchettata in tre artefatti nativi:

ArtefattoPercorsoScopo
Bundle Ridguntarget/bundles/cli.rglibDistribuzione e importazione diretta con import cli
File compilatotarget/cli.rgcArtefatto intermedio verificato da ridgunc
Oggetto condiviso ELF64target/libcli.soLibreria dinamica nativa senza dipendenze C né libc

Ogni lib dichiara il proprio spazio dei nomi con module :: cli: basta scrivere import cli (o import cli as alias) nell'intestazione e invocare le sue funzioni con spazi naturali (mia funzione x) e pipeline -> (x mia funzione -> risultato).

24.2.1. Riepilogo delle famiglie funzionali di cli

Firma / Funzione / TipoCosa fa e quando usarla
cli has flag : [String] -> String -> String -> BoolRileva --long, -s o flag raggruppati -xyz prima di --
cli get opt / cli get int optEstrae un'opzione testuale o intera (--k=v, --k v, -k v) con valore predefinito
cli positionals / cli subcommandRaccoglie gli argomenti posizionali (incluso tutto ciò che segue --)
env get or / env get int or / env get bool orLegge le variabili d'ambiente con conversione tipizzata e valori di fallback

24.2.2. Catalogo esaustivo dei tipi e delle firme in src/main/ridgun/lib/cli.rg

Di seguito trovi l'inventario completo dei tipi algebrici (type), degli operatori (op ::) e delle firme di funzione (fn ::) definiti in src/main/ridgun/lib/cli.rg (0 tipi, 0 operatori e 26 funzioni):

#Funzione / Firma (fn ::)Tipo di Ritorno
1fn :: cl str contains char String Int Int IntBool
2fn :: cl is bundled short String StringBool
3fn :: cl list contains str [String] StringBool
4fn :: cl is digits loop String Int IntBool
5fn :: cl is signed int len String IntBool
6fn :: cl is signed int StringBool
7fn :: cl is ws IntBool
8fn :: cl lower char String Int IntString
9fn :: cl trim lower loop String Int IntString
10fn :: cli has flag [String] String StringBool
11fn :: cl match eq pfx String StringString
12fn :: cl match eq prefix String StringString
13fn :: cl has eq prefix String StringBool
14fn :: cli get opt [String] String String StringString
15fn :: cl parse int opt String IntInt
16fn :: cli get int opt [String] String String IntInt
17fn :: cl is flag tok StringBool
18fn :: cli positionals with opts [String] [String][String]
19fn :: cli positionals [String][String]
20fn :: cl first str [String]String
21fn :: cli subcommand [String]String
22fn :: cl env val or String StringString
23fn :: env get or String StringString
24fn :: env get int or String IntInt
25fn :: cl parse bool val String BoolBool
26fn :: env get bool or String BoolBool

24.3. Esplorare cli a fondo: Esempi passo dopo passo di ogni funzionalità

Non basta consultare la tabella delle firme: esploreremo ciascuna delle funzionalità della lib cli tramite esempi progressivi ed eseguibili in Ridgun. Inizieremo con due ricette tabulari di uso quotidiano e poi percorreremo nel dettaglio tutti gli scenari funzionali, le trasformazioni e i casi limite verificati in src/test/ridgun/test cli.rg.

24.3.1. Esempio 1: Flusso principale in stile tabulare con cli

Osserva come in Ridgun i segni = e i passaggi di trasformazione con cli risultino allineati come una tabella pulita, senza variabili mutabili superflue né effetti collaterali nascosti:

import cli

fn :: analizar comando [String] => String Bool Int [String]
      analizar comando args =
        args cli subcommand -> sub
        cli has flag args "--all" "-a" -> all_f
        cli get int opt args "--port" "-p" 8080 -> port
        cli positionals with opts args ["--port", "-p"] -> arch
        (sub, all_f, port, arch)

24.3.2. Esempio 2: Composizione sicura e gestione dei casi limite

Ogni funzione della lib cli è progettata per comporsi con le guardie booleane | e con il resto delle 34 lib di ridgun-libs:

import cli

fn :: leer config entorno => Int Bool
      leer config entorno =
        env get int or "SOLSTICE_WORKERS" 4 -> workers
        env get bool or "SOLSTICE_DEBUG" False -> debug
        (workers, debug)

24.3.3. Esempio 3: Flag booleani (cli has flag) e opzioni con valore (--key=val e --key val, cli opt int, cli opt bool)

In questo esempio pratico esploriamo in modo esaustivo come si comporta cli in questo blocco funzionale, inclusi input standard, combinazioni di parametri e valori di frontiera:

fn :: test cli flags and options => ()
      test cli flags and options =
        args = ["serve", "-la", "--verbose", "--port=9090", "-h", "127.0.0.1", "--workers", "-4", "--", "--ignored-flag", "-z", "file.rg"]
        assert true (cli has flag args "--verbose" "-v") "AC1: long flag --verbose"
        assert true (cli has flag args "--all" "-a") "AC1: bundled short flag -a in -la"
        assert true (cli has flag args "--list" "-l") "AC1: bundled short flag -l in -la"
        assert false (cli has flag args "--quiet" "-q") "AC1: missing flag returns False"
        assert false (cli has flag args "--ignored-flag" "") "AC1: long flag after -- is ignored"
        assert false (cli has flag args "" "-z") "AC1: short flag after -- is ignored"
        assert false (cli has flag [] "--verbose" "-v") "AC1: empty args returns False"

        -- Option extraction: --key=val, --key val, -k=val, -k val, empty value --key=
        assert str eq (cli get opt args "--port" "-p" "8080") "9090" "AC2: --port=9090 inline equals"
        assert str eq (cli get opt args "--host" "-h" "0.0.0.0") "127.0.0.1" "AC2: -h 127.0.0.1 separate token"
        assert str eq (cli get opt args "--mode" "-m" "prod") "prod" "AC2: missing option returns fallback"
        assert int equals (cli get int opt args "--port" "-p" 8080) 9090 "AC2: cli_get_int_opt positive int"
        assert int equals (cli get int opt args "--workers" "-w" 1) (0 - 4) "AC2: cli_get_int_opt negative int"
        assert int equals (cli get int opt args "--host" "-h" 8080) 8080 "AC2: non-numeric option falls back to default int"

        short_eq_args = ["-p=3000", "--empty="]
        assert int equals (cli get int opt short_eq_args "--port" "-p" 80) 3000 "AC2: short option -p=3000"
        assert str eq (cli get opt short_eq_args "--empty" "-e" "def") "" "AC2: explicit empty --empty= returns empty string"
        ()

24.3.4. Esempio 4: Estrazione dei posizionali nel rispetto di -- (cli positionals), sottocomandi (cli subcommand) e variabili d'ambiente tipizzate (env str/int/bool or)

In questo esempio pratico esploriamo in modo esaustivo come si comporta cli in questo blocco funzionale, inclusi input standard, combinazioni di parametri e valori di frontiera:

fn :: test cli positionals subcommand and env => ()
      test cli positionals subcommand and env =
        args = ["build", "-v", "--output", "target/app", "-j", "8", "--", "-dashfile.rg", "--another-literal", "main.rg"]
        assert str eq (cli subcommand args) "build" "AC3: cli_subcommand first positional"
        assert str eq (cli subcommand ["-v", "--release", "test"]) "test" "AC3: cli_subcommand skips leading flags"
        assert str eq (cli subcommand ["--help", "-v"]) "" "AC3: cli_subcommand empty when only flags"
        assert str eq (cli subcommand []) "" "AC3: cli_subcommand empty on []"

        pos = cli positionals with opts args ["--output", "-j"]
        assert eq pos ["build", "-dashfile.rg", "--another-literal", "main.rg"]

        -- Single "-" (stdin/stdout convention) is treated as a positional argument, not a flag!
        stdin_args = ["cat", "-n", "-"]
        assert eq (cli positionals stdin_args) ["cat", "-"]

        -- Environment variable readers
        assert true ((str len (env get or "PATH" "")) > 0) "AC4: env_get_or PATH non-empty"
        assert str eq (env get or "RIDGUN_NONEXISTENT_ENV_999" "fallback_val") "fallback_val" "AC4: env_get_or fallback"
        assert int equals (env get int or "RIDGUN_NONEXISTENT_ENV_999" 42) 42 "AC4: env_get_int_or fallback"
        assert true (env get bool or "RIDGUN_NONEXISTENT_ENV_999" True) "AC4: env_get_bool_or True fallback"
        assert false (env get bool or "RIDGUN_NONEXISTENT_ENV_999" False) "AC4: env_get_bool_or False fallback"
        ()

24.3.5. Esempio 5: Dispatcher completo per strumento CLI che combina sottocomandi, flag, opzioni tipizzate e fallback su variabili d'ambiente

In questo esempio pratico esploriamo in modo esaustivo come si comporta cli in questo blocco funzionale, inclusi input standard, combinazioni di parametri e valori di frontiera:

import cli

fn :: despachar comando (argv : [String]) => (String, String, Int, Bool, [String])
      despachar comando argv =
        cmd     = cli subcommand argv
        host    = cli opt or argv "--host" "-H" (env str or "RG_HOST" "127.0.0.1")
        puerto  = cli opt int or argv "--port" "-p" (env int or "RG_PORT" 8080)
        verbose = (cli has flag argv "--verbose" "-v") or (env bool or "RG_VERBOSE" False)
        archivos = cli positionals argv
        (cmd, host, puerto, verbose, archivos)

24.4. Sotto il cofano: come è costruita cli in Ridgun puro (src/main/ridgun/lib/cli.rg)

Uno dei maggiori vantaggi di ridgun-libs è che non ci sono scatole nere in C. Puoi aprire src/main/ridgun/lib/cli.rg e leggere esattamente come è implementata ogni operazione usando pattern matching, alberi di guardie | / |. e ricorsione di coda (TCO). Vediamo i pezzi chiave della sua implementazione reale:

24.4.1. Implementazione interna: Internal String & List Helpers

Nota come cli.rg strutturi prima le firme fn :: ... => ... e risolva ogni caso tramite equazioni allineate e chiamate di coda:

module :: cli

fn :: cl str contains char String Int Int Int => Bool
      cl str contains char s ch idx len =
        | idx >= len                = False
        | (str char at s idx) == ch = True
        | otherwise                 = cl str contains char s ch (idx + 1) len

fn :: cl is bundled short String String => Bool
      cl is bundled short arg short_flag =
        | (str len short_flag) != 2        = False
        | (str char at short_flag 0) != 45 = False
        | (str len arg) < 2                = False
        | (str char at arg 0) != 45        = False
        | (str char at arg 1) == 45        = False
        | otherwise                        = cl str contains char arg (str char at short_flag 1) 1 (str len arg)

fn :: cl list contains str [String] String => Bool
      cl list contains str [] _ = False
      cl list contains str (x:rest) tgt =
        | x == tgt  = True
        | otherwise = cl list contains str rest tgt

fn :: cl is digits loop String Int Int => Bool
      cl is digits loop s idx len =
        | idx >= len                                           = True
        | (str char at s idx) < 48 or (str char at s idx) > 57 = False
        | otherwise                                            = cl is digits loop s (idx + 1) len

fn :: cl is signed int len String Int => Bool
      cl is signed int len s n =
        | n == 0                  = False
        | (str char at s 0) == 45 =
          | n <= 1    = False
          | otherwise = cl is digits loop s 1 n |.
        | otherwise               = cl is digits loop s 0 n

fn :: cl is signed int String => Bool
      cl is signed int s      =  cl is signed int len s (str len s)

fn :: cl is ws Int => Bool
      cl is ws c   =  c == 32 or c == 9 or c == 10 or c == 13

fn :: cl lower char String Int Int => String
      cl lower char s idx c =
        | c >= 65 and c <= 90 = char from code (c + 32)
        | otherwise           = str slice s idx (idx + 1)

fn :: cl trim lower loop String Int Int => String
      cl trim lower loop s idx len =
        | idx >= len                   = ""
        | cl is ws (str char at s idx) = cl trim lower loop s (idx + 1) len
        | otherwise                    = (cl lower char s idx (str char at s idx)) + (cl trim lower loop s (idx + 1) len)

24.4.2. Implementazione interna: Positional Arguments & Subcommand Extraction

Nota come cli.rg strutturi prima le firme fn :: ... => ... e risolva ogni caso tramite equazioni allineate e chiamate di coda:

fn :: cl is flag tok String => Bool
      cl is flag tok a      =
        | (str len a) <= 1        = False
        | (str char at a 0) == 45 = True
        | otherwise               = False

fn :: cli positionals with opts [String] [String] => [String]
      cli positionals with opts [] _ = []
      cli positionals with opts (a:[]) _ =
        | a == "--"        = []
        | cl is flag tok a = []
        | otherwise        = [a]
      cli positionals with opts (a:b:rst) val_opts =
        | a == "--"                       = [b] :: rst
        | cl list contains str val_opts a = cli positionals with opts rst val_opts
        | cl is flag tok a                = cli positionals with opts ([b] :: rst) val_opts
        | otherwise                       = [a] :: (cli positionals with opts ([b] :: rst) val_opts)

fn :: cli positionals [String] => [String]
      cli positionals args     =  cli positionals with opts args []

fn :: cl first str [String] => String
      cl first str []       =  ""
      cl first str (x:_)    =  x

fn :: cli subcommand [String] => String
      cli subcommand args     =  args cli positionals -> cl first str

24.4.3. Implementazione interna: Typed Environment Variable Readers

Nota come cli.rg strutturi prima le firme fn :: ... => ... e risolva ogni caso tramite equazioni allineate e chiamate di coda:

fn :: cl env val or String String => String
      cl env val or val fallback =
        | (str len val) == 0 = fallback
        | otherwise          = val

fn :: env get or String String   => String
      env get or key    fallback =  cl env val or (sys getenv key) fallback

fn :: env get int or String Int => Int
      env get int or key fallback = cl parse int opt (sys getenv key) fallback

fn :: cl parse bool val String Bool => Bool
      cl parse bool val val fallback =
        | val == "1" or val == "true" or val == "yes" or val == "on"  = True
        | val == "0" or val == "false" or val == "no" or val == "off" = False
        | otherwise                                                   = fallback

fn :: env get bool or String Bool => Bool
      env get bool or key fallback =
        key sys getenv -> raw
        cl trim lower loop raw 0 (str len raw) -> norm
        cl parse bool val norm fallback

24.5. Garanzie algoritmiche, memoria RMM1/RMM2 e le 5 Regole d'Oro di cli

  • Garanzia algoritmica e di sicurezza: Rispetto rigoroso del separatore POSIX --: nessun argomento posto a destra di -- viene mai interpretato come flag.
  • Complessità dello stack $O(1)$ con TCO: Tutti i cicli ricorsivi in src/main/ridgun/lib/cli.rg sono in posizione di coda, perciò ridgunc li compila in salti nativi senza crescita dello stack.
  • Memoria deterministica (RMM1/RMM2): Zero pause del garbage collector; tutte le strutture intermedie vengono liberate in modo deterministico all'uscita dallo scope.
  • Verifica continua: Testata in modo esaustivo con stable check e la suite automatizzata src/test/ridgun/test cli.rg.

Le 5 Regole d'Oro quando programmi con cli

  1. Dichiara import cli all'inizio del file e usa le funzioni multi-parola con spazi naturali affinché il tuo codice si legga come prosa.
  2. Allinea le tue pipeline di dati con = e ->: concatena le chiamate a cli in passaggi con nomi chiari e lascia che stable format allinei le colonne.
  3. Sfrutta le funzioni totali di cli nelle tue guardie |: controlla i valori di ritorno o destruttura gli ADT con pattern matching invece di dare per scontato che l'input esterno sia sempre valido.
  4. Evita di copiare grandi strutture se devi solo consultare un campo: ricorda che in RMM2 gli argomenti di input sono prestati e non incrementano né decrementano i contatori (+0 / -0).
  5. Combina cli con test (import test): scrivi sempre asserzioni con assert equals o assert true per fissare gli invarianti del tuo dominio.

Nel Capitolo 25 passeremo a esplorare in profondità la lib path (La lib path — Normalizzazione POSIX (path clean) e Confinamento contro Path Traversal).