Merge branch 'flow/sx-foundation/F3.3' into dist-foundation
This commit is contained in:
69
examples/0718-modules-cli-exit-json.sx
Normal file
69
examples/0718-modules-cli-exit-json.sx
Normal file
@@ -0,0 +1,69 @@
|
||||
// std.cli EXIT-CODE + `--json` contract (F3.3 — FOUNDATION MILESTONE CLOSE).
|
||||
//
|
||||
// The minimal contract `dist` (and any sx CLI front-end) relies on:
|
||||
//
|
||||
// 1. NAMED EXIT CODES — `EX_OK` (0) and `EX_USAGE` (64, the sysexits.h
|
||||
// usage-error code) are public constants in `std.cli`.
|
||||
// 2. TERMINATORS — `exit_ok()` / `exit_usage()` end the process with the
|
||||
// matching code, routing through the canonical `process.exit(code: u8)`.
|
||||
// 3. `--json` MODE — the reserved global `--json` flag surfaces as
|
||||
// `parsed.json`: TRUE when `--json` is in the argv, FALSE when it is not.
|
||||
// In json mode stdout carries ONLY the machine result; human text goes
|
||||
// to stderr (here via `log.err`, which writes `ERROR: …` to fd 2).
|
||||
//
|
||||
// The run DELIBERATELY ends on the usage path: after the assertions it
|
||||
// triggers an `UnknownCommand`, writes the human diagnostic to STDERR, and
|
||||
// terminates via `exit_usage()` — so the process exits 64 (EX_USAGE),
|
||||
// captured in expected/0718-….exit. (sx `print`/`out` are unbuffered, so the
|
||||
// stdout assertion lines still appear despite the `_exit`.)
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/std/cli.sx";
|
||||
log :: #import "modules/log.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> ! {
|
||||
publish_flags : []FlagSpec = .[
|
||||
FlagSpec.{ name = "out", takes_value = true, required = true },
|
||||
];
|
||||
cmds : []Command = .[
|
||||
Command.{ group = "ci", command = "publish", flags = publish_flags },
|
||||
];
|
||||
|
||||
// ── 1. Named exit-code constants (the dist contract) ──────────────
|
||||
report("ex-ok-is-0", EX_OK == 0);
|
||||
report("ex-usage-is-64", EX_USAGE == 64);
|
||||
|
||||
// ── 2. `--json` detection — true with the flag, false without ─────
|
||||
d : Diag = .{};
|
||||
with_json : []string = .["ci", "publish", "--out", "dist", "--json"];
|
||||
without_json : []string = .["ci", "publish", "--out", "dist"];
|
||||
pj := try parse(with_json, cmds, @d);
|
||||
pn := try parse(without_json, cmds, @d);
|
||||
report("json-set-true", pj.json);
|
||||
report("json-set-false", !pn.json);
|
||||
|
||||
// ── 3. json mode keeps stdout machine-pure ────────────────────────
|
||||
// When `parsed.json`, the front-end emits ONLY the machine result on
|
||||
// stdout. `out` writes the bytes verbatim (no `{}` interpolation), so
|
||||
// the JSON braces are literal.
|
||||
if pj.json {
|
||||
out("{\"out\":\"");
|
||||
out(pj.value_of("out"));
|
||||
out("\"}\n");
|
||||
}
|
||||
|
||||
// ── 4. Usage error → human text to stderr → exit_usage() (= 64) ───
|
||||
// A bad command raises a `CliError`. The front-end maps every usage
|
||||
// error to `EX_USAGE`: it writes the human diagnostic to STDERR (stdout
|
||||
// stays machine-clean) and terminates with the usage code via the
|
||||
// canonical `process.exit`.
|
||||
bad : []string = .["ci", "deploy", "--out", "x"]; // unknown command
|
||||
_, e := parse(bad, cmds, @d);
|
||||
report("usage-error-raised", e == error.UnknownCommand);
|
||||
log.err("unknown command '{}' (argv index {})", d.token, d.index);
|
||||
exit_usage(); // -> _exit(EX_USAGE = 64)
|
||||
}
|
||||
1
examples/expected/0718-modules-cli-exit-json.exit
Normal file
1
examples/expected/0718-modules-cli-exit-json.exit
Normal file
@@ -0,0 +1 @@
|
||||
64
|
||||
1
examples/expected/0718-modules-cli-exit-json.stderr
Normal file
1
examples/expected/0718-modules-cli-exit-json.stderr
Normal file
@@ -0,0 +1 @@
|
||||
ERROR: unknown command 'deploy' (argv index 1)
|
||||
6
examples/expected/0718-modules-cli-exit-json.stdout
Normal file
6
examples/expected/0718-modules-cli-exit-json.stdout
Normal file
@@ -0,0 +1,6 @@
|
||||
ex-ok-is-0: ok
|
||||
ex-usage-is-64: ok
|
||||
json-set-true: ok
|
||||
json-set-false: ok
|
||||
{"out":"dist"}
|
||||
usage-error-raised: ok
|
||||
@@ -25,11 +25,13 @@
|
||||
//
|
||||
// Platform: macOS only for now, via the C runtime's `_NSGetArgv()`
|
||||
// (char***) and `_NSGetArgc()` (int*). On any other OS the accessors bail
|
||||
// loudly (message + non-zero exit) rather than returning a silent empty.
|
||||
// loudly (message + `EX_UNAVAILABLE` exit via `process.exit`) rather than
|
||||
// returning a silent empty.
|
||||
// =====================================================================
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/compiler.sx";
|
||||
proc :: #import "modules/process.sx";
|
||||
|
||||
libc :: #library "c";
|
||||
|
||||
@@ -42,9 +44,39 @@ libc :: #library "c";
|
||||
ns_get_argv :: () -> *s64 #foreign libc "_NSGetArgv";
|
||||
ns_get_argc :: () -> *s32 #foreign libc "_NSGetArgc";
|
||||
|
||||
// Bound to POSIX `_exit(2)`. Used only on the unsupported-platform path to
|
||||
// terminate loudly instead of handing back a misleading empty slice.
|
||||
cli_bail_exit :: (code: s32) -> noreturn #foreign libc "_exit";
|
||||
// =====================================================================
|
||||
// EXIT-CODE & `--json` CONTRACT (F3.3) — the minimal surface `dist` (and
|
||||
// any sx CLI front-end) relies on. Two pieces:
|
||||
//
|
||||
// 1. NAMED EXIT CODES (sysexits.h subset). A front-end ends with the
|
||||
// code that matches the outcome: `EX_OK` on success, `EX_USAGE`
|
||||
// after a `parse` failure (a `CliError` — bad command / flag /
|
||||
// value), `EX_UNAVAILABLE` when the platform is unsupported.
|
||||
// 2. TERMINATORS. `exit_ok()` / `exit_usage()` end the process with the
|
||||
// matching code. They route through the canonical
|
||||
// `process.exit(code: u8)` (modules/process.sx) — there is NO second
|
||||
// hand-rolled `_exit` binding in this module; the unsupported-platform
|
||||
// path below goes through `proc.exit(EX_UNAVAILABLE)` too.
|
||||
//
|
||||
// `--json` MODE needs no new code here: the parser already surfaces it as
|
||||
// `parsed.json` (true iff `--json` appears in the argv — see `Parsed.json`).
|
||||
// The convention a front-end follows: in json mode stdout carries ONLY the
|
||||
// machine result, and human diagnostics go to stderr (e.g. via
|
||||
// `modules/log.sx`'s `log.err`). Detect json mode by reading `parsed.json`.
|
||||
// =====================================================================
|
||||
|
||||
EX_OK :u8: 0; // success
|
||||
EX_USAGE :u8: 64; // command-line usage error (sysexits.h EX_USAGE)
|
||||
EX_UNAVAILABLE :u8: 70; // service / platform unavailable (sysexits.h)
|
||||
|
||||
// End the process successfully (`EX_OK` = 0). Thin wrapper over the
|
||||
// canonical `process.exit` terminator — immediate `_exit(2)`, no unwinding.
|
||||
exit_ok :: () -> noreturn { proc.exit(EX_OK); }
|
||||
|
||||
// End the process with the usage-error code (`EX_USAGE` = 64). A CLI
|
||||
// front-end calls this after `parse` raises a `CliError` and the human
|
||||
// diagnostic has been written to stderr.
|
||||
exit_usage :: () -> noreturn { proc.exit(EX_USAGE); }
|
||||
|
||||
// Number of process arguments (argc). >= 1 for any normally-launched
|
||||
// process, since argv[0] is the executable path.
|
||||
@@ -53,7 +85,7 @@ os_argc :: () -> s64 {
|
||||
case .macos: { return cast(s64) ns_get_argc().*; }
|
||||
else: {
|
||||
out("std.cli: unsupported platform — only macOS is implemented (needs _NSGetArgv/_NSGetArgc).\n");
|
||||
cli_bail_exit(70);
|
||||
proc.exit(EX_UNAVAILABLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,7 +120,7 @@ os_args :: (buf: []string) -> []string {
|
||||
}
|
||||
else: {
|
||||
out("std.cli: unsupported platform — only macOS is implemented (needs _NSGetArgv/_NSGetArgc).\n");
|
||||
cli_bail_exit(70);
|
||||
proc.exit(EX_UNAVAILABLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,7 +226,7 @@ Parsed :: struct {
|
||||
group: string;
|
||||
command: string;
|
||||
cmd_index: s64;
|
||||
json: bool;
|
||||
json: bool; // `--json` mode: true iff `--json` is in argv
|
||||
rest: []string;
|
||||
spec: []FlagSpec; // view of the matched command's flag specs
|
||||
values: [16]FlagValue; // fixed inline storage, indexed by spec position
|
||||
|
||||
27
readme.md
27
readme.md
@@ -425,6 +425,33 @@ The standard library (`modules/std.sx`) provides:
|
||||
- **Math**: `sqrt`, `sin`, `cos`
|
||||
- **Introspection**: `type_of`, `type_name`, `field_count`, `field_name`, `field_value`, `size_of`
|
||||
|
||||
### Command-line interface (`modules/std/cli.sx`)
|
||||
|
||||
`std.cli` builds command-line front-ends over an explicit logical argv
|
||||
(`[]string`): `os_args(buf)` reads the real process argv, and
|
||||
`parse(args, commands, diag) -> !Parsed` does subcommand dispatch + `--flag`
|
||||
parsing. On top of that it defines the small **exit-code / `--json` contract**
|
||||
a CLI program (e.g. `dist`) relies on:
|
||||
|
||||
```sx
|
||||
#import "modules/std/cli.sx";
|
||||
|
||||
p, e := parse(args, cmds, @diag); // (Parsed, !CliError)
|
||||
if e == error.UnknownCommand {
|
||||
log.err("unknown command '{}'", diag.token); // human text -> stderr
|
||||
exit_usage(); // usage error -> exit 64
|
||||
}
|
||||
if p.json { /* emit ONLY machine output on stdout */ }
|
||||
```
|
||||
|
||||
- **Named exit codes** — `EX_OK` (0), `EX_USAGE` (64, the sysexits.h
|
||||
command-line-usage code), `EX_UNAVAILABLE` (70, unsupported platform).
|
||||
- **Terminators** — `exit_ok()` / `exit_usage()` end the process with the
|
||||
matching code; both route through the canonical `process.exit(code: u8)`.
|
||||
- **`--json` mode** — the reserved global `--json` flag surfaces as
|
||||
`parsed.json` (true iff `--json` is in the argv). Convention: in json mode
|
||||
stdout carries only the machine result; human diagnostics go to stderr.
|
||||
|
||||
## Cross-Compilation
|
||||
|
||||
```sh
|
||||
|
||||
Reference in New Issue
Block a user