Командная строка: аргументы, окружение, потоки и терминал
Библиотекой пользуются другие программы. Утилитой командной строки пользуется человек за терминалом, и ей нужно правильно обойтись с четырьмя вещами: с тем, что человек набрал, с тем, что он задал в своей оболочке, с тем, куда идут байты, чтобы их мог принять конвейер, и с тем, как вести себя за терминалом, когда ничего не перенаправлено. В kola каждая из них — капабилити на sys: Args, Env, Stdout и Stdin (плюс sys.stderr) и Tty. Утилита, которая запускает другие программы, добавляет к ним Proc. На этой странице показано, как пользоваться каждой из них, где каждая отказывает и как пакет cli превращает сырые байты аргументов в типизированный разбор без замыканий.
sys.args: вызов в виде байтов
Запуская программу, оболочка передаёт ей слова командной строки списком строк, который по традиции называется argv (argument vector, вектор аргументов). sys.args.all() возвращает этот список как &heap [&heap [u8]] — срез байтовых срезов, живущих всё время работы программы (heap — регион, который никогда не освобождается). Как и в C, argv[0] — имя программы, а аргументы вызывающего начинаются с индекса 1. Каждый элемент — байтовый срез, а не str, потому что ничто не гарантирует, что аргументы — корректный UTF-8. Когда нужен текст, преобразуйте его через utf8::to_str, который на некорректный ввод отвечает None:
use std::stdio;
use std::text::utf8;
fn main(sys: Sys) -> u8 !{io} {
let argv = sys.args.all();
match utf8::to_str(argv[0]) {
Some(name) => {
let _ = stdio::write_all(&sys.stdout, &b"program: ");
let _ = stdio::write_all(&sys.stdout, name.bytes);
let _ = stdio::write_all(&sys.stdout, &b"\n");
}
None => {}
}
for i in 1..argv.len {
let _ = stdio::write_all(&sys.stdout, &b"arg: ");
let _ = stdio::write_all(&sys.stdout, argv[i]);
let _ = stdio::write_all(&sys.stdout, &b"\n");
}
0
}kola run перестаёт разбирать собственные флаги на --, так что всё, что идёт после него, доходит до программы без изменений, в том числе слова, похожие на флаги:
$ kola run args-demo.kl -- deploy --env=prod --replicas 3
program: args-demo.kl
arg: deploy
arg: --env=prod
arg: --replicas
arg: 3Чтение аргументов имеет эффект !{io} по той же причине, что и чтение stdin: это внешний ввод, и ряд эффектов сообщает об этом рецензенту ещё до того, как он прочтёт тело функции. Нативная сборка читает настоящий argv процесса, и её argv[1..] побайтно совпадает с тем, что видит интерпретатор. argv[0] — то, что передала операционная система.
У теста (test) вызова нет. sim.args.all() всегда пуст, поэтому код, поведение которого зависит от аргументов, проверяют через kola run FILE -- ... или передают срез аргументов в функцию, которую можно протестировать напрямую.
sys.env: одна переменная, по имени
Каждый процесс к тому же наследует от родителя набор переменных окружения — пар «имя — значение» вроде HOME=/home/ana или PORT=8080, через которые оболочки и менеджеры служб передают настройки. sys.env.get(b"HOME") отвечает Option[&heap [u8]] и имеет эффект !{io}. Рассуждение то же, что и для Args (байты, внешний ввод, фиксируется при старте процесса), но с одним дополнительным ограничением: возможен только поиск. Спросить одну переменную по имени можно, перечислить окружение нельзя. Капабилити, отдающая весь блок целиком, раскрывала бы неограниченный объём сведений, и рецензенту было бы не на какое имя посмотреть. Запрос по одному имени проверяется так же легко, как любой другой поиск, и именно он нужен для приоритета настроек: явный флаг важнее переменной окружения, а она важнее встроенного значения по умолчанию.
// Флаг важнее окружения, окружение важнее значения по умолчанию.
use std::stdio;
use std::text::num;
// Ищет `--port <value>` в сырых аргументах. Настоящая программа
// взяла бы пакет `cli`, а не писала разбор вручную.
fn flag_port(argv: &[&heap [u8]]) -> Option[i64] !{} {
let mut i: usize = 1;
while i < argv.len {
if argv[i] == &b"--port" && i + 1 < argv.len {
return num::parse_i64(argv[i + 1]);
}
i = i + 1;
}
None
}
fn resolve_port(sys: &Sys) -> i64 !{io} {
match flag_port(sys.args.all()) {
Some(p) => p,
None => match sys.env.get(&b"PORT") {
Some(v) => match num::parse_i64(v) {
Some(p) => p,
None => 8080i64, // задана, но не число: берём значение по умолчанию
}
None => 8080i64,
}
}
}
fn main(sys: Sys) -> u8 !{io, alloc(heap)} {
let port = resolve_port(&sys);
let mut buf: Vec[u8] = Vec::new();
num::push_i64(&mut buf, port);
let _ = stdio::write_all(&sys.stdout, &b"port: ");
let _ = stdio::write_all(&sys.stdout, &buf[0..buf.len]);
let _ = stdio::write_all(&sys.stdout, &b"\n");
0
}$ kola run env-precedence.kl
port: 8080
$ PORT=9090 kola run env-precedence.kl
port: 9090
$ PORT=9090 kola run env-precedence.kl -- --port 7000
port: 7000Под kola test окружение пусто, даже для переменных, которые есть у самого исполнителя тестов, например PATH. Тест, видящий окружение того, кто его запустил, проходил бы или падал в зависимости от того, кто это был. У freestanding-сборки нет процесса, а значит, и окружения, поэтому kola build --native --freestanding отвергает sys.env.get по имени и предлагает читать настройку из выданного каталога конфигурации (sys.fs.root(b"cfg")), принимать её параметром или собирать hosted-вариант. Для настроек, которые на самом деле являются файлами, выданный корень в любом случае подходит лучше: манифест точно называет каталог, который программа может читать.
Вывод, диагностика и журнал
Каждый процесс Unix начинает работу с тремя открытыми потоками: стандартным вводом, стандартным выводом и стандартным потоком ошибок, у которых файловые дескрипторы 0, 1 и 2 (дескриптор — небольшое целое число, которым процесс обозначает открытый файл или поток). По умолчанию оболочка подключает их к терминалу, а операторы | и > переключают на другую программу или в файл. В ls | grep x вывод ls становится вводом grep, так что всё лишнее, что ls напечатал бы в stdout, оказалось бы во вводе grep. kola различает три вида того, что выдаёт программа, и утилита командной строки сталкивается с этим различием каждый день:
Вывод идёт в
sys.stdout: это байты, которые будет разбирать другая программа. Случайная отладочная строка здесь не просто захламляет вывод, она портит его для того, кто читает конвейер.Диагностика для вызывающего идёт в
sys.stderr, файловый дескриптор 2, в собственном формате программы (например, строки компилятора видаfile:line: message). Тип у него тот же,Stdout, так чтоstdio::write_all(&sys.stderr, ...)работает.Телеметрия для оператора идёт в
log, которому капабилити не нужна. Приёмник (--log stderr,KOLA_LOG) и уровень выбирает оператор.
Stdout и Stdin — разные капабилити, потому что «может ли этот код читать с консоли» и «может ли он писать на неё» при аудите — разные вопросы.
Две практические детали. Во-первых, write может записать не всё: канал с заполненным буфером принимает только часть предложенного, и это нормально. stdio::write_all повторяет запись, пока не уйдут все байты или не случится настоящая ошибка; пользуйтесь им, а не голым write. Во-вторых, sys.stdin.read_line(&mut buf) дописывает в buf одну строку без завершающего перевода строки и отвечает Ok(true); Ok(false) означает конец ввода, и это обычный ответ, а не ошибка. Ошибки этих операций — пары (IoErr, OsErr). Чтение продвигает поток, поэтому читающему нужен изменяемый Stdin, и main объявлен с mut sys:
use std::stdio;
use std::log;
fn main(mut sys: Sys) -> u8 !{io, block, log} {
let mut buf: Vec[u8] = Vec::new();
let mut n: u64 = 0;
loop {
buf.clear();
match sys.stdin.read_line(&mut buf) {
Ok(true) => {
n = n + 1;
// Это уходит в приёмник журнала и никогда не попадёт
// в повторённые строки на stdout.
log::info("echoing one line");
let _ = stdio::write_all(&sys.stdout, &buf[0..buf.len]);
let _ = stdio::write_all(&sys.stdout, &b"\n");
}
Ok(false) => { break; } // конец ввода
Err(_) => { break; }
}
}
log::info(f"done: {n} line(s)");
0
}Если перехватить stdout и stderr по отдельности, разделение видно:
$ printf 'alpha\nbeta\ngamma\n' | kola run stdio-echo.kl --log stderr >out.txt 2>err.txt
$ cat out.txt
alpha
beta
gamma
$ cat err.txt
[info] echoing one line
[info] echoing one line
[info] echoing one line
[info] done: 3 line(s)Посмотрите на два вызова log. Обычный строковый литерал никогда не интерполируется, так что первый — фиксированный текст. Второй с помощью f"..." подставляет в сообщение n. Запись log::info("done: {n} line(s)") компилятор отвергнет и предложит добавить f.
Под kola test поток sim.stdin сразу находится в конце ввода, а sim.stdout перехватывается. Этого хватает, чтобы доказать, что цикл вроде показанного выше завершается на пустом вводе, но не чтобы проверить, что он отвечает на настоящие строки. Построчный протокол поверх stdin тестируют, пропуская настоящие байты через kola run и kola run --native и сравнивая выводы.
Терминал: sys.tty, RawMode и is_terminal
Обычно терминал работает в каноническом режиме (cooked mode): собирает строку целиком, даёт пользователю править её клавишей Backspace, отображает набранные символы и только потом отдаёт строку программе. Редакторам и запросам пароля нужен сырой режим (raw mode), в котором каждая клавиша доходит до программы в момент нажатия и ничего не отображается. Программа, которая переключилась в сырой режим и завершилась, не переключившись обратно, оставляет оболочку пользователя непригодной для работы, пока тот не наберёт reset.
sys.tty — управляющий терминал (/dev/tty), тот, за которым сидит пользователь, даже если стандартные потоки программы перенаправлены куда-то ещё. Любая операция с ним работает независимо от того, как перенаправлены stdin, stdout и stderr:
tty.size()отвечает(rows, cols)типа(u16, u16). Ошибкой он завершиться не может; без настоящего терминала сообщает 24 на 80.tty.write(&bytes)выводит приглашение для человека, напримерPassword:. Он не для диагностики (её место вlog) и не для вывода (его место в stdout). Утилите, у которой в stdout идут машиночитаемые данные, больше некуда вывести приглашение, а stderr — ненадёжная замена:2>logотправит приглашение в файл, и программа будет ждать ответа, о котором никого не спросили. Полноэкранный TUI по-прежнему пишет свои управляющие последовательности вsys.stdout.tty.raw()отвечаетResult[RawMode, (IoErr, OsErr)]. Он переключает терминал в сырой режим (без буферизации строк и без отображения), чтобы программа читала клавиши по мере нажатия.
RawMode — линейный ресурс ровно с одной расходующей операцией, raw.restore(), которая отвечает (). Поэтому оставить оболочку в сыром режиме — ошибка компиляции. Пока сеанс жив, чтение идёт через него, raw.read(&mut buf, timeout_ms), и по истечении тайм-аута ответ — Ok(None) (конца файла у терминала нет). Запись тоже идёт через него, raw.write(&bytes); tty.write при живом сеансе отвергается, потому что сырой режим отключил обработку вывода, и \n пришёл бы голым переводом строки без возврата каретки. tty.raw() отбрасывает весь ввод, набранный до переключения, так что нажатие клавиши или вставка, пришедшие раньше вашего приглашения, не могут стать ответом на него.
Поскольку sys.tty не обращает внимания на перенаправление, на вопрос «печатает ли мне человек» он ответить не может. Это вопрос о собственном stdin программы, и отвечает на него sys.stdin.is_terminal(): если он говорит false, пароль читают из канала, если true — с терминала.
use std::log;
use std::stdio;
fn main(sys: Sys) -> u8 !{io, block, log} {
let sz = sys.tty.size();
log::info(f"size: {sz.0}x{sz.1}");
let piped = !sys.stdin.is_terminal();
log::info(f"stdin is a pipe: {piped}");
let _ = sys.tty.write(&b"Password: ");
match sys.tty.raw() {
Ok(raw) => {
let mut raw = raw;
let mut buf: Vec[u8] = Vec::new();
match raw.read(&mut buf, 200u64) {
Ok(Some(n)) => { log::info(f"read {n} byte(s)"); }
Ok(None) => { log::info("no key within 200 ms"); }
Err(_) => { log::info("read failed"); }
}
let _ = raw.write(&b"\r\n");
raw.restore();
}
Err(_) => { log::info("no controlling terminal, raw mode refused"); }
}
0
}В первом запуске ниже управляющего терминала нет вовсе, как в задании CI или в демоне. Каждая операция с терминалом завершается ошибкой, а не пишет туда, где её никто не увидит. Второй запуск идёт под псевдотерминалом (программным терминалом, который здесь создаёт команда script), а stdin перенаправлен из /dev/null: приглашение доходит до терминала, и сырой режим включается, хотя stdin — не терминал, потому что сеанс открыт на /dev/tty.
$ kola run tty-demo.kl --log stderr </dev/null
[info] size: 24x80
[info] stdin is a pipe: true
[info] no controlling terminal, raw mode refused
$ script -qec "kola run tty-demo.kl --log stderr </dev/null" /dev/null
[info] size: 24x80
[info] stdin is a pipe: true
Password: [info] no key within 200 msТеперь уберите restore:
use std::log;
use std::stdio;
fn main(sys: Sys) -> u8 !{io, block, log} {
match sys.tty.raw() {
Ok(raw) => {
let mut raw = raw;
let mut buf: Vec[u8] = Vec::new();
let _ = raw.read(&mut buf, 200u64);
// raw.restore() нет: терминал остаётся в сыром режиме
}
Err(_) => {}
}
0
}error[card §2]: resource `raw` is not consumed on this path
--> tty-forgot-restore.kl:7:13
|
7 | let mut raw = raw;
| ^^^
= help: no implicit destructors: move it on, return it, destructure it, or pass it to a consuming fnЛинейность охватывает только обычные пути. Если процесс в сыром режиме убит сигналом SIGINT или SIGTERM, среда выполнения восстанавливает терминал из сохранённой копии. Внутри блока domain пользоваться RawMode нельзя: держите сеанс снаружи, а внутрь помещайте только вычисление, чтобы после трапа терминал можно было восстановить.
Под kola test терминал заменён детерминированной подделкой: size() равен (24, 80), raw() успешно возвращает сеанс, который ничего не делает, read() сразу истекает по тайм-ауту, а is_terminal() равен false. Этого хватает, чтобы проверить весь сценарий, включая обязанность восстановить терминал.
use std::stdio;
test sim_has_no_invocation (mut sim: Sim) {
assert sim.args.all().len == 0usize;
assert sim.env.get(b"PATH") == None;
assert !sim.stdin.is_terminal();
let mut buf: Vec[u8] = Vec::new();
let r = sim.stdin.read_line(&mut buf);
assert r == Ok(false);
let sz = sim.tty.size();
assert sz.0 == 24 && sz.1 == 80;
}Пакет cli: флаги без замыканий
Для одного флага ручной разбор вполне годится. Он перестаёт справляться, когда у утилиты появляются подкоманды, --help, обязательные или повторяемые флаги, и для этого есть пакет cli. Его устройство следует из того, что в kola нет замыканий. Фреймворк, который привязывал бы к каждой команде функцию run, навязал бы всему дереву команд один набор капабилити и эффектов, а подкоманде list не нужны те полномочия, что нужны подкоманде import. Поэтому cli только разбирает. Чистая функция без капабилити читает байты аргументов и возвращает, какая команда совпала, и типизированные значения флагов. match пишет само приложение и в каждой ветке тратит только те капабилити, которые нужны этой ветке. В терминах clap вы получаете ArgMatches, а диспетчеризацию пишете сами.
Флаг, объявленный как multi, собирает все вхождения (--peer a --peer b превращается в список из двух элементов, который читают через matches::get_list), и при этом может быть required. Эта утилита использует один обязательный повторяемый флаг. Она оформлена как пакет: её kola.pack перечисляет cli в [deps] и объявляет [caps] allowed = ["Sys", "Args", "Stdout", "log"].
use std::stdio;
use std::log;
use std::text::utf8;
use cli::spec;
use cli::parse;
use cli::parse::Outcome;
use cli::matches;
fn build[r]() -> spec::Command[r] !{alloc(r)} {
let mut root = spec::command("deploy", "deploy to a set of peers", "1.0.0");
spec::flag_str(&mut root, "peer", 112u8, true, "a peer address (repeatable)");
spec::multi(&mut root, "peer");
root
}
fn main(sys: Sys) -> u8 !{io, log, alloc(heap)} {
let mut code = 0u8;
region r {
let root = build();
let argv = sys.args.all();
let out: &r mut Vec[u8] = alloc in r mut Vec::new();
match parse::parse(&root, &argv[1..argv.len], out) {
Ok(o) => match o {
Outcome::Run(m) => {
let peers: &r mut Vec[&heap [u8]] = alloc in r mut Vec::new();
matches::get_list(&m, "peer", peers);
log::info(f"{peers.len} peer(s) declared");
for i in 0..peers.len {
let _ = stdio::write_all(&sys.stdout, &b"peer: ");
let _ = stdio::write_all(&sys.stdout, peers[i]);
let _ = stdio::write_all(&sys.stdout, &b"\n");
}
}
Outcome::Help => { let _ = stdio::write_all(&sys.stdout, &out[0..out.len]); }
Outcome::Version => { let _ = stdio::write_all(&sys.stdout, &out[0..out.len]); }
}
Err(e) => {
// Ошибка разбора — это диагностика, а не вывод.
let rendered: &r mut Vec[u8] = alloc in r mut Vec::new();
parse::render_error(&e, rendered);
match utf8::to_str(&rendered[0..rendered.len]) {
Some(s) => { log::error(s); }
None => { log::error("parse error"); }
}
code = 2u8;
}
}
}
code
}$ kola run src/main.kl --log stderr -- --peer 10.0.0.1:9000 --peer 10.0.0.2:9000
[info] 2 peer(s) declared
peer: 10.0.0.1:9000
peer: 10.0.0.2:9000
$ kola run src/main.kl --log stderr
[error] error: missing required peer
$ echo $?
2
$ kola run src/main.kl -- --help
deploy to a set of peers
USAGE:
deploy [OPTIONS]
OPTIONS:
-p, --peer <PEER> a peer address (repeatable) [repeatable]
-h, --help print helpСам cli никаких капабилити не объявляет, но программа, которая им пользуется, обращается к Args и Stdout в собственном main, поэтому её манифест обязан их назвать; это проверяет kola audit. В итоге манифест утилиты командной строки перечисляет капабилити, о которых рассказано на этой странице, и ничего больше. Сам механизм описан в главе Пакеты.
sys.proc: запуск другой программы по выданному имени
Утилита, которая запускает git, ffmpeg или ssh, пользуется Proc, и устроено это так же, как выданный корень файловой системы: программа никогда не пишет путь. Оператор привязывает имя к исполняемому файлу через kola run --proc-bin ffmpeg=/usr/bin/ffmpeg или через переменную окружения KOLA_PROC_BIN_ffmpeg, которую развёрнутый бинарник читает и без kola. Строка [caps] proc_bins = ["ffmpeg"] в манифесте точно говорит аудитору, какие программы пакет может запускать.
sys.proc.spawn(name, argv, env, stdio, slots) отвечает Result[Child, (ProcErr, OsErr)]. Несколько деталей угадать нельзя:
argv— список аргументов без имени программы, его подставляет среда выполнения. Если переслать свойsys.args.all()целиком, путь к вашей программе станет первым аргументом дочерней.envзадаётся явно и может быть пустым. Ничего не наследуется: ни окружение, ни состояние сигналов. Если дочерней программе нужна переменная, она получает её потому, что родитель прочитал её через собственныйEnvи передал дальше. Ключ окружения с префиксом выдачи (KOLA_FS_ROOT_*,KOLA_PROC_BIN_*и подобные) вызывает трап, потому что выдачи приходят от оператора, а не от родителя.Элементы
argvиenvмогут жить в любом регионе, посколькуspawnкопирует их до возврата.У расположения stdio (
Stdio::Inherit,Stdio::PipedилиStdio::Null) нет значения по умолчанию, потому что очевидное умолчание приводит к взаимной блокировке: канал, который никто не читает, блокирует дочерний процесс, как только заполнится его буфер, иwaitродителя никогда не вернётся.slots— массив значенийSpawnSlot(Closed,File,Dev,Listen,Netlink); если дочерней программе ничего не передаётся, пишут[].slots[i]попадает в дескриптор дочернего процесса3 + i, а записьClosedоставляет этот дескриптор закрытым, не сдвигая следующие. Каждый ресурс, переданный в слоте, расходуется, даже если запуск не удался. Никакие другие дескрипторы родителя до дочернего процесса не доходят.
use std::log;
use std::proc;
use std::stdio;
// Исполняемый файл — выданное имя: `kola run --proc-bin echo=/bin/echo`.
fn main(sys: Sys) -> u8 !{io, block, log} {
let name = "world".bytes;
match sys.proc.spawn(b"echo", &[&b"hello", name], &[], Stdio::Piped, []) {
Ok(child) => {
let mut child = child;
let mut out = child.take_stdout().unwrap();
let mut buf = [0u8; 64];
let n = match out.read(&mut buf) { Ok(n) => n, Err(_) => 0usize };
let _c = out.close();
match child.wait() {
ExitStatus::Code(c) => { log::info(f"child wrote {n} bytes, exited {c}"); c }
ExitStatus::Signaled(_) => { log::info("child was signalled"); 1 }
}
}
Err((ProcErr::NotGranted, _)) => { log::error("no `echo` grant"); 2 }
Err(_) => { log::error("echo did not start"); 3 }
}
}$ kola run child.kl --proc-bin echo=/bin/echo --log stderr
[info] child wrote 12 bytes, exited 0
$ kola run child.kl --log stderr
[error] no `echo` grant
$ echo $?
2use std::stdio; нужен, хотя программа нигде не называет IoErr: операции с каналами дочернего процесса отвечают (IoErr, OsErr), и компилятор требует модуль, в котором определён этот тип.
В Unix завершившийся дочерний процесс остаётся в таблице процессов зомби, пока родитель не заберёт его код завершения; это называется «пожать» процесс (reaping). Child — линейный дескриптор. wait() и kill() расходуют его и пожинают дочерний процесс, так что непожатый процесс — ошибка компиляции, а не зомби. try_wait(&mut Child) и signal(&mut Child, Signal) его не расходуют, и именно благодаря этому можно написать последовательность остановки (послать Term, подождать отведённое время, затем kill). С Stdio::Piped каждый из вызовов take_stdin(), take_stdout() и take_stderr() отвечает Some ровно один раз. Каждая половина канала линейна и должна быть закрыта. Закрыть пишущую половину важно: дочерняя программа, читающая stdin до конца файла, не увидит этого конца, пока открыта ваша копия. Чтение 0 байтов из ChildOut означает конец потока.
Сигнал — небольшое асинхронное уведомление, которое операционная система доставляет процессу: SIGTERM просит его остановиться, SIGKILL останавливает безусловно, SIGINT посылается по Ctrl-C. ExitStatus — это Code(u8) или Signaled(Signal). Signal — имя (Hup, Int, Quit, Term, Kill, Usr1, Usr2) плюс Other(i32) для полученного сигнала без имени, но никогда не сырой номер. Номера сигналов на разных системах разные (10 — это SIGBUS в macOS и SIGUSR1 в Linux x86-64), так что номер в журнале супервизора может молча указывать не на тот исход.
Выдача может закрепить содержимое исполняемого файла: --proc-bin name=/path:sha256=<hex>; если файл перестал совпадать, ответом будет PinMismatch, и ничего не запустится. Симулятора для Proc нет: под kola test выданный запуск выполняется по-настоящему. Программа, которая хочет сама обрабатывать SIGINT или SIGTERM, вызывает sys.proc.interrupt_on(Signal::Int): он устанавливает обработчик и возвращает линейный InterruptGuard, который нужно восстановить; подробности — в разделе Processes в STD-CARD.
Есть у этого и сторона дочернего процесса. sys.inherited.take(i) отвечает SpawnSlot, который родитель положил под индексом i, или None. Повторный take того же индекса даёт None. Так передаётся уже существующий дескриптор; новой выдачи этот вызов никогда не создаёт.
Коды завершения и трапы
Каждый процесс заканчивается кодом завершения — небольшим числом, которое может прочитать родитель: по соглашению 0 означает успех, а всё остальное описывает неудачу (оболочка показывает его как $?). В kola нет ни вызова exit(), ни глобального статуса завершения. Код завершения процесса — это u8, который возвращает main. Ширина у него восемь бит, потому что больше родитель увидеть не может: main возвращает u8 или ничего, -> i32 отвергается, а вычисленный код сужают явно, через narrow[u8](n), так что что делать со значением больше 255, решает ваш исходный код.
use std::stdio;
fn main(sys: Sys) -> u8 !{io} {
let argv = sys.args.all();
if argv.len < 2usize {
let _ = stdio::write_all(&sys.stderr, &b"no target given\n");
return 1;
}
let _ = stdio::write_all(&sys.stdout, &b"deploying: ");
let _ = stdio::write_all(&sys.stdout, argv[1]);
let _ = stdio::write_all(&sys.stdout, &b"\n");
0
}$ kola run exit-code.kl
no target given
$ echo $?
1
$ kola run exit-code.kl -- prod-east
deploying: prod-east
$ echo $?
0Возвращённый код программа сообщает намеренно. Трап (выход индекса за границы, невыполненный req, явный trap("msg")) — это необработанная остановка, о которой рассказано в главе Контракты, трапы и тесты. В обоих исполнителях трап завершает процесс со статусом 101, так что скрипт оболочки или супервизор может отличить «программа отвергла свой ввод» от «в программе ошибка», не разбирая вывод:
use std::stdio;
// argv[5] вызывает трап, если вызывающий передал меньше пяти аргументов.
fn main(sys: Sys) -> u8 !{io} {
let argv = sys.args.all();
let _ = stdio::write_all(&sys.stdout, argv[5]);
0
}$ kola run exit-trap.kl
trap: error[card §8]: index 5 out of bounds (len 1)
--> exit-trap.kl:6:43
|
6 | let _ = stdio::write_all(&sys.stdout, argv[5]);
| ^^^^^^^
= help: indexing carries a builtin req; an upstream `if i < xs.len` guard discharges it statically
$ echo $?
101
$ kola run --native exit-trap.kl
trap: error[card §8]: index 5 out of bounds (len 1)
--> exit-trap.kl:6
$ echo $?
101Оба исполнителя выдают одно и то же сообщение, раздел карты и file:line. В нативном бинарнике нет только номера столбца, фрагмента исходного текста и подсказки.