Skip to content

Process Management

A process is a running program.

A process is a running program.

When you run:

zig build-exe main.zig
./main

the operating system creates a process for ./main.

A process has memory, open files, environment variables, command-line arguments, and an exit code. It may also create other processes.

Command-Line Arguments

Command-line arguments are text values passed to a program when it starts.

Example:

./hello Alice

Here, Alice is an argument.

A simple Zig program can read arguments through std.process.

const std = @import("std");

pub fn main() !void {
    var args = try std.process.argsWithAllocator(std.heap.page_allocator);
    defer args.deinit();

    _ = args.next();

    const name = args.next() orelse {
        std.debug.print("usage: hello NAME\n", .{});
        return;
    };

    std.debug.print("Hello, {s}!\n", .{name});
}

The first argument is usually the program name, so this line skips it:

_ = args.next();

Then the program reads the next argument:

const name = args.next() orelse {
    std.debug.print("usage: hello NAME\n", .{});
    return;
};

Exit Codes

A process ends with an exit code.

By convention:

0 means success
nonzero means failure

A command-line tool should return success when it did what the user asked. It should return failure when the command was invalid, a file could not be read, a network request failed, or another real error occurred.

In simple Zig programs, returning an error from main usually causes the program to fail.

pub fn main() !void {
    return error.SomethingWentWrong;
}

For user-facing tools, you often catch errors and print a clearer message.

Spawning a Child Process

A process can start another process. The new process is called a child process.

For example, a build tool might run a compiler. A test runner might run test executables. A shell runs commands as child processes.

The standard library provides process APIs for this through std.process.

The exact child process APIs may change across Zig versions, so check your local docs with:

zig std

The conceptual shape is:

var child = std.process.Child.init(&.{ "echo", "hello" }, allocator);
try child.spawn();
const result = try child.wait();

This starts the command:

echo hello

and waits for it to finish.

Waiting for a Process

Starting a child process is only half the job.

You usually also need to wait for it.

Waiting tells you how the child process ended.

It may have exited successfully.

It may have exited with a nonzero code.

It may have been terminated by the operating system.

A command runner should inspect the result rather than assuming success.

Capturing Output

Sometimes you want the child process output.

For example:

git rev-parse HEAD

prints the current Git commit hash.

A Zig program may want to capture that output into memory.

The conceptual pattern is:

const result = try std.process.Child.run(.{
    .allocator = allocator,
    .argv = &.{ "git", "rev-parse", "HEAD" },
});
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);

Then you can read:

result.stdout
result.stderr

Standard output is normal output.

Standard error is diagnostic output.

Keep them separate when possible.

Environment for Child Processes

A child process normally inherits environment variables from its parent.

Sometimes you want to pass a modified environment.

Examples:

PATH
HOME
APP_ENV
DATABASE_URL

Build systems, package managers, and test runners often control child environments carefully.

The general idea is:

child.env_map = custom_environment;

or use a run API that accepts environment configuration.

The details depend on the exact Zig version.

Working Directory for Child Processes

A child process also has a current working directory.

You might want to run a command inside a project directory:

cd my-project
zig build

Programmatically, that means setting the child process working directory.

Conceptually:

child.cwd = "my-project";

Then the command runs as though it started inside that directory.

This is important for tools that operate on projects, repositories, or generated files.

Do Not Build Shell Commands as Strings

This is dangerous:

const command = try std.fmt.allocPrint(
    allocator,
    "rm -rf {s}",
    .{path},
);

If path contains unexpected characters, the shell may interpret them.

Prefer passing arguments as separate values:

&.{ "rm", "-rf", path }

This avoids shell parsing.

The operating system receives the command and arguments directly.

This is safer and clearer.

A Small Argument Parser

Here is a small program that expects a file name:

const std = @import("std");

pub fn main() !void {
    var args = try std.process.argsWithAllocator(std.heap.page_allocator);
    defer args.deinit();

    _ = args.next();

    const path = args.next() orelse {
        std.debug.print("usage: show PATH\n", .{});
        return;
    };

    std.debug.print("path = {s}\n", .{path});
}

Run it:

./show hello.txt

Output:

path = hello.txt

Run it without an argument:

./show

Output:

usage: show PATH

A Small Command Runner

This example shows the intended structure, though exact APIs may need adjustment for your installed Zig version:

const std = @import("std");

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    const result = try std.process.Child.run(.{
        .allocator = allocator,
        .argv = &.{ "zig", "version" },
    });
    defer allocator.free(result.stdout);
    defer allocator.free(result.stderr);

    std.debug.print("stdout:\n{s}\n", .{result.stdout});

    if (result.stderr.len != 0) {
        std.debug.print("stderr:\n{s}\n", .{result.stderr});
    }
}

This runs:

zig version

and captures its output.

Common Mistakes

Do not assume arguments exist.

Do not treat command-line arguments as trusted input.

Do not join shell commands into one string unless you intentionally need a shell.

Do not ignore child process exit status.

Do not mix stdout and stderr without reason.

Do not forget that environment variables and working directories affect child processes.

The Core Pattern

For arguments:

var args = try std.process.argsWithAllocator(allocator);
defer args.deinit();

_ = args.next();
const value = args.next() orelse return error.MissingArgument;

For a child process:

var child = std.process.Child.init(&.{ "program", "arg" }, allocator);
try child.spawn();
const result = try child.wait();

For captured output:

const result = try std.process.Child.run(.{
    .allocator = allocator,
    .argv = &.{ "program", "arg" },
});
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);

What You Should Remember

A process is a running program.

Command-line arguments are text passed at startup.

Exit code 0 usually means success.

Nonzero exit codes usually mean failure.

A process can start child processes.

Child processes have arguments, environment variables, working directories, stdout, stderr, and exit status.

Pass command arguments as separate values, not as one shell string.

Process management is the foundation for command-line tools, build systems, test runners, package managers, and automation programs.