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
cliscritta 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:
- Dichiara
import cli(oimport cli as alias) all'inizio del file: ogni lib definisce il proprio spazio dei nomimodule :: clie in modalità--strictle funzioni multi-parola si invocano sempre con spazi naturali. - Concatena le tue pipeline da sinistra a destra con
->: scriviinput passo uno -> passo due -> risultato, usa le maschere(+ _ _ +)per destrutturare le sequenze e ometti_ =nelle istruzioni sequenziali (o scarta il valore con-> ()). - Ricorsione di coda con TCO (Tail-Call Optimization): tutte le scansioni interne di
clivengono compilate in cicli nativi in spazio di stack $O(1)$. - 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:
| Artefatto | Percorso | Scopo |
|---|---|---|
| Bundle Ridgun | target/bundles/cli.rglib | Distribuzione e importazione diretta con import cli |
| File compilato | target/cli.rgc | Artefatto intermedio verificato da ridgunc |
| Oggetto condiviso ELF64 | target/libcli.so | Libreria dinamica nativa senza dipendenze C né libc |
Ogni lib dichiara il proprio spazio dei nomi con
module :: cli: basta scrivereimport cli(oimport 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 / Tipo | Cosa fa e quando usarla |
|---|---|
cli has flag : [String] -> String -> String -> Bool | Rileva --long, -s o flag raggruppati -xyz prima di -- |
cli get opt / cli get int opt | Estrae un'opzione testuale o intera (--k=v, --k v, -k v) con valore predefinito |
cli positionals / cli subcommand | Raccoglie gli argomenti posizionali (incluso tutto ciò che segue --) |
env get or / env get int or / env get bool or | Legge 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 |
|---|---|---|
1 | fn :: cl str contains char String Int Int Int | Bool |
2 | fn :: cl is bundled short String String | Bool |
3 | fn :: cl list contains str [String] String | Bool |
4 | fn :: cl is digits loop String Int Int | Bool |
5 | fn :: cl is signed int len String Int | Bool |
6 | fn :: cl is signed int String | Bool |
7 | fn :: cl is ws Int | Bool |
8 | fn :: cl lower char String Int Int | String |
9 | fn :: cl trim lower loop String Int Int | String |
10 | fn :: cli has flag [String] String String | Bool |
11 | fn :: cl match eq pfx String String | String |
12 | fn :: cl match eq prefix String String | String |
13 | fn :: cl has eq prefix String String | Bool |
14 | fn :: cli get opt [String] String String String | String |
15 | fn :: cl parse int opt String Int | Int |
16 | fn :: cli get int opt [String] String String Int | Int |
17 | fn :: cl is flag tok String | Bool |
18 | fn :: cli positionals with opts [String] [String] | [String] |
19 | fn :: cli positionals [String] | [String] |
20 | fn :: cl first str [String] | String |
21 | fn :: cli subcommand [String] | String |
22 | fn :: cl env val or String String | String |
23 | fn :: env get or String String | String |
24 | fn :: env get int or String Int | Int |
25 | fn :: cl parse bool val String Bool | Bool |
26 | fn :: env get bool or String Bool | Bool |
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.rgsono in posizione di coda, perciòridguncli 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 checke la suite automatizzatasrc/test/ridgun/test cli.rg.
Le 5 Regole d'Oro quando programmi con cli
- Dichiara
import cliall'inizio del file e usa le funzioni multi-parola con spazi naturali affinché il tuo codice si legga come prosa. - Allinea le tue pipeline di dati con
=e->: concatena le chiamate acliin passaggi con nomi chiari e lascia chestable formatallinei le colonne. - Sfrutta le funzioni totali di
clinelle 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. - Evita di copiare grandi strutture se devi solo consultare un campo: ricorda che in
RMM2gli argomenti di input sono prestati e non incrementano né decrementano i contatori (+0/-0). - Combina
clicontest(import test): scrivi sempre asserzioni conassert equalsoassert trueper 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).