⚡ Zig Guide LiveUnofficialbut fully verified
✓ Zig 0.18.0-dev.738+ae9a8acc7What's newOn an older Zig?Blograylib-zig

The Environment

Updated · Code verified against Zig 0.18.0-dev.738+ae9a8acc7

The environment is a list of KEY=value strings. The parent process selected the list, and the kernel copied the list into your address space at startup. The environment belongs only to your process. After startup, a change that you make to the environment does not get to any other process that runs.

const std = @import("std");

pub fn main(init: std.process.Init) !void {
    var buf: [1024]u8 = undefined;
    var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &buf);
    const out = &stdout_writer.interface;

    // The real environment. The startup code parsed it into a map before
    // main. This program runs in a wasm sandbox that has no parent process.
    // Thus the count is the real value, not a placeholder.
    const env = init.environ_map;
    try out.print("inherited variables: {d}\n", .{env.count()});
    try out.print("PATH: {?s}\n\n", .{env.get("PATH")});

    // A put into the map does not change any other process. The map is the
    // copy that belongs to this program. A child gets a copy at spawn time.
    try env.put("ZIG_GUIDE", "live");
    try env.put("EDITOR", "vim");
    try out.print("after two puts: {d}\n", .{env.count()});
    try out.print("ZIG_GUIDE: {?s}\n", .{env.get("ZIG_GUIDE")});
    try out.print("contains EDITOR: {}\n", .{env.contains("EDITOR")});

    _ = env.swapRemove("EDITOR");
    try out.print("after removing it: {?s}\n\n", .{env.get("EDITOR")});

    // A name must not be empty and must not contain '='. You cannot escape
    // the separator. Thus you cannot read back a key that contains '='.
    try out.print("\"HOME\" valid: {}\n", .{std.process.Environ.Map.validateKeyForPut("HOME")});
    try out.print("\"A=B\" valid: {}\n", .{std.process.Environ.Map.validateKeyForPut("A=B")});

    try out.flush();
}

The map is ready at startup

pub fn main(init: std.process.Init) !void {
    const env = init.environ_map;
    const path = env.get("PATH");
}

init.environ_map is a *std.process.Environ.Map. The startup code filled the map from the raw block before the call to main. The startup code allocated the map with the same gpa that is on init. You do not free the map, and you do not build the map.

In the playground above, the count is 0. The count is the real result, not a stub. A wasm module in a browser tab has no parent process, so the module inherits no environment. Thus the environment is really empty.

In the playground, env.get("PATH") returns null. A program must be ready for this result on all platforms. If you did not set a variable yourself, the variable can be absent.

Your copy belongs to you

try env.put("ZIG_GUIDE", "live");

This line changes the map of this program and nothing else. It does not change the shell that started your program. Alone, it does not change the environment of a child process either.

A child process inherits a copy of a block. The block is the one that existed when you spawned the child. When you spawn the child, you select the block. If you give null, the child gets the environment of the parent.

Many people expect the other direction to work, but it does not work. No call lets a program change the environment of its parent. Thus a tool that must change your shell prints text, and the shell evaluates that text. The kernel copied the environment at fork time, and the copies have no connection.

Two rules for keys

A name must not be empty. A name must not contain = or a NUL byte. The map can tell you if a key is valid:

std.process.Environ.Map.validateKeyForPut("A=B") // false

You cannot escape the separator. A block is a flat list of KEY=VALUE strings, and the split occurs at the first =. Thus you can write a key that contains =, but you cannot read the key back as you intended.

All bytes after the first = are the value. The value can contain newlines, spaces and more = signs. For this reason, PATH works, and a value never needs quotes at this level. The quotes that you write in a shell belong to the shell. The shell removes the quotes before the string gets to your program.

Case, and the platform that is different

On POSIX, names are case-sensitive. On Windows, names are case-insensitive, and the block is UTF-16. Thus on Windows, Path and PATH are the same variable. Environ.Map follows the target platform of the build.

Code that reads one spelling and writes a different spelling works on Windows. On all other platforms, the code is wrong, and no error tells you. Thus select the usual upper-case spelling, and use the same spelling in both places.