Skip to content

std.os.process arguments

std.os.process exposes the arguments typed after the program or script name. A compiled ./tool alpha beta sees two arguments, alpha and beta. The program path itself is never counted.

Status: merged to unstable at c70c0099 (PR #41, STD-GAP-064) on 2026-10-11. Not yet part of a tagged release. The examples on this page were compiled and run on Linux x86_64 with janus build.

use std.os.process

An argument is returned byte-for-byte or not at all.

A shortened argument is not a harmless cosmetic defect. Take a requested path such as ./././…/config.kdlx that is cut at a buffer boundary: it becomes ./././…/config.kdl, which may be a different file that exists. The program then reads, overwrites or deletes a file nobody asked for. Before the STD-GAP-064 repair the compiled backend did exactly that, silently.

The compiled Linux backend reads /proc/self/cmdline without allocating, so it has two fixed limits:

LimitValueChecked error
Whole command line, including the program path and the NUL after each argumentat most 8192 bytesCliArgError.CommandLineTooLong
One returned argumentat most 4095 bytesCliArgError.ArgumentTooLong

Both limits are inclusive: a 4095-byte argument is returned whole. The program path counts toward the 8192 bytes, so a binary started from a deep directory has less room for its arguments.

The development interpreter (janus run --jit) holds complete argument strings and reports only IndexOutOfRange.

Use the checked functions whenever an argument selects data: an input or output path, a key, a host, anything the program acts on.

pub error CliArgError {
ReadFailed,
CommandLineTooLong,
ArgumentTooLong,
IndexOutOfRange,
}
pub func args_count_checked() -> CliArgError!i64 !{IO, Alloc}
pub func arg_at_checked(i: i64) -> CliArgError![*:0]u8 !{IO, Alloc}
FunctionReturnsErrors
args_count_checked()number of arguments after the program pathReadFailed, CommandLineTooLong
arg_at_checked(i)argument i as a NUL-terminated [*:0]u8ReadFailed, CommandLineTooLong, ArgumentTooLong, IndexOutOfRange
ErrorMeaning
ReadFailedThe command line could not be read, or it was empty.
CommandLineTooLongThe whole command line exceeds 8192 bytes. No argument is returned, not even a short one.
ArgumentTooLongArgument i exceeds 4095 bytes.
IndexOutOfRangei is negative or past the last argument.

Each call reads the command line once, and the error describes that same read. You never pair a status with a different snapshot.

use std.os.process
func main() -> i32 do
const count = process.args_count_checked() catch |err| do
println("command line too long or unreadable")
return 2
end
if count < 1 do
println("usage: tool <input>")
return 2
end
const path = process.arg_at_checked(0) catch |err| do
if err == process.CliArgError.ArgumentTooLong do
println("input path is too long")
end
return 2
end
println(path)
return 0
end
InvocationOutputExit
./tool config.kdlconfig.kdl0
./toolusage: tool <input>2
./tool <4096-byte path>input path is too long2

This program is examples/std_cli_args_checked.jan in the compiler repository. The command-line arguments tutorial builds a complete selector step by step.

arg_at_checked and arg_at return a pointer into one process-wide buffer. The pointer is valid until the next arg_at or arg_at_checked call. Consume or copy an argument before you read the next one. Holding two argument pointers at once gives you two views of the same, latest argument.

The buffer and the error status are process-global. Read arguments from a single thread, typically at startup.

pub func args_count() -> i64 !{IO, Alloc}
pub func arg_at(i: i64) -> [*:0]u8 !{IO, Alloc}

The original functions keep their signatures. When an argument or the command line exceeds a limit they cannot say why, so they fail closed:

Conditionargs_count()arg_at(i)
Command line over 8192 bytes0empty string
Argument i over 4095 bytesreal countempty string
i out of rangereal countempty string

Neither function ever returns a prefix. Earlier versions silently truncated; the STD-GAP-064 repair changed this behavior.

Failing closed is safe but mute. An empty string from arg_at looks the same as an empty argument passed on purpose, and 0 from args_count looks the same as no arguments. Use the legacy functions only where that ambiguity is harmless, such as echoing arguments in diagnostics.

Legacy callReplacement
process.args_count()process.args_count_checked() catch …
process.arg_at(i)process.arg_at_checked(i) catch …

Handle at least ArgumentTooLong and CommandLineTooLong with an explicit refusal and a non-zero exit code.

Each function has a _sovereign twin that takes a caps.ProcCap first: args_count_sovereign, arg_at_sovereign, args_count_checked_sovereign and arg_at_checked_sovereign. They return exactly what the plain functions return.

The std.os.process facade currently binds the Linux backend (std/sys/linux/cli_args.zig). Builds for other targets do not get an argument backend from this module yet. The limits above describe that Linux backend.

The native regression suite builds a probe and asserts either the exact argument or a named error for ordinary, empty, absent, at-limit and over-limit arguments, an over-long command line, and both truncation reproductions:

Terminal window
./scripts/zb test-std-cli-args-native

Sources: std/os/process.jan, std/sys/linux/cli_args.zig, std/os/tests/test_cli_args_native.py.

Related: std.cli parses flags once you hold complete arguments. Practice the contract in the Monastery mission 04-honest-boundary/01-whole-or-nothing.