An environment variable is a named value provided to a program by the operating system.
An environment variable is a named value provided to a program by the operating system.
Programs use environment variables for configuration.
Examples:
HOME
PATH
TMPDIR
USER
EDITORA program may also define its own variables:
APP_PORT
DATABASE_URL
LOG_LEVELEnvironment variables are text.
If you need a number, boolean, path, or URL, you must parse the text yourself.
Viewing Environment Variables
On Linux and macOS:
echo $HOMEOn Windows Command Prompt:
echo %USERNAME%Programs inherit environment variables from the process that started them.
For example:
APP_PORT=8080 ./my_programThe program can then read:
APP_PORTfrom its environment.
Reading an Environment Variable
The standard library provides access to environment variables through std.process.
A common pattern is:
const std = @import("std");
pub fn main() !void {
const allocator = std.heap.page_allocator;
const home = std.process.getEnvVarOwned(allocator, "HOME") catch {
std.debug.print("HOME not set\n", .{});
return;
};
defer allocator.free(home);
std.debug.print("HOME = {s}\n", .{home});
}This asks for the value of:
HOMEIf the variable exists, the function returns an allocated string.
If the variable does not exist, the function returns an error.
Why an Allocator Is Needed
Environment variable lengths are not known at compile time.
The returned text must be stored somewhere.
That is why this function needs an allocator:
std.process.getEnvVarOwned(allocator, "HOME")The returned memory belongs to the caller.
That is why this line matters:
defer allocator.free(home);Without freeing, the memory would leak.
This follows Zig’s normal ownership rules:
The function allocates memory.
The caller later frees it.
Handling Missing Variables
Environment variables are optional.
A program should not assume a variable exists unless it controls the environment itself.
This pattern handles missing variables:
const value = std.process.getEnvVarOwned(allocator, "APP_PORT") catch {
std.debug.print("APP_PORT not set\n", .{});
return;
};You can also provide a default value.
const port_text = std.process.getEnvVarOwned(allocator, "APP_PORT") catch {
const default = "8080";
std.debug.print("using default port {s}\n", .{default});
return;
};But remember the distinction:
A missing variable may be acceptable.
A malformed variable is usually an error.
Do not silently accept broken input.
Parsing Environment Variables
Environment variables are strings.
Suppose:
APP_PORT=8080You still need to parse the number.
const std = @import("std");
pub fn main() !void {
const allocator = std.heap.page_allocator;
const port_text = std.process.getEnvVarOwned(
allocator,
"APP_PORT",
) catch {
std.debug.print("APP_PORT not set\n", .{});
return;
};
defer allocator.free(port_text);
const port = try std.fmt.parseInt(u16, port_text, 10);
std.debug.print("port = {}\n", .{port});
}This turns:
"8080"into:
8080of type u16.
Boolean Environment Variables
Suppose:
DEBUG=trueYou can parse it manually.
const std = @import("std");
fn parseBool(text: []const u8) !bool {
if (std.mem.eql(u8, text, "true")) return true;
if (std.mem.eql(u8, text, "false")) return false;
return error.InvalidBoolean;
}
pub fn main() !void {
const allocator = std.heap.page_allocator;
const debug_text = std.process.getEnvVarOwned(
allocator,
"DEBUG",
) catch {
std.debug.print("DEBUG not set\n", .{});
return;
};
defer allocator.free(debug_text);
const debug = try parseBool(debug_text);
std.debug.print("debug = {}\n", .{debug});
}This accepts only:
true
falseThat strictness is useful.
Empty Variables
An environment variable can exist but contain an empty string.
Example:
APP_PORT=The variable exists, but its value is:
an empty string.
This is different from the variable being missing entirely.
So these are separate cases:
| Situation | Meaning |
|---|---|
| Variable missing | No value provided |
| Variable exists but empty | Value provided, but empty |
Your program should decide how to handle both.
Listing Environment Variables
Some APIs allow iterating through the whole environment.
The general structure is:
var env_map = try std.process.getEnvMap(allocator);
defer env_map.deinit();
var it = env_map.iterator();
while (it.next()) |entry| {
std.debug.print("{s} = {s}\n", .{
entry.key_ptr.*,
entry.value_ptr.*,
});
}This creates a map of environment variables.
Then it iterates through them.
The exact API details can vary across Zig versions, but the idea is stable:
Environment variables are key-value string pairs.
Common Environment Variables
Some variables appear often on Unix-like systems.
| Variable | Meaning |
|---|---|
HOME | User home directory |
PATH | Executable search paths |
USER | Username |
PWD | Current working directory |
SHELL | User shell |
TMPDIR | Temporary directory |
On Windows, names differ somewhat.
Examples:
| Variable | Meaning |
|---|---|
USERNAME | Username |
TEMP | Temporary directory |
APPDATA | Application data directory |
Portable programs should not assume every variable exists on every platform.
The PATH Variable
PATH is especially important.
It contains directories where the operating system searches for executables.
Example on Linux:
/usr/local/bin:/usr/bin:/binThe separator is usually:
:On Windows, it is usually:
;Programs that work with executable lookup often need platform-aware path splitting.
Security and Environment Variables
Environment variables come from outside the program.
Treat them as untrusted input.
Bad:
const level = try std.fmt.parseInt(u8, value, 10);without checking the allowed range.
Bad:
const path = value;without validating whether the path is acceptable.
An attacker may control the environment.
This matters especially for:
paths
temporary directories
shell commands
library loading
configuration values
Environment variables are convenient, but they are still external input.
Avoid Global Hidden State
One reason Zig keeps environment access explicit is that environment variables are global process state.
Hidden global state makes programs harder to reason about.
This style is clearer:
const port = try loadPortFromEnvironment();
runServer(port);than code that reads environment variables from many unrelated places.
Read configuration once near startup when possible.
Then pass typed values through the program.
A Small Configuration Loader
This example loads a small config from the environment.
const std = @import("std");
const Config = struct {
port: u16,
debug: bool,
};
fn parseBool(text: []const u8) !bool {
if (std.mem.eql(u8, text, "true")) return true;
if (std.mem.eql(u8, text, "false")) return false;
return error.InvalidBoolean;
}
fn loadConfig(allocator: std.mem.Allocator) !Config {
const port_text = std.process.getEnvVarOwned(
allocator,
"APP_PORT",
) catch return error.MissingPort;
defer allocator.free(port_text);
const debug_text = std.process.getEnvVarOwned(
allocator,
"DEBUG",
) catch return error.MissingDebug;
defer allocator.free(debug_text);
return Config{
.port = try std.fmt.parseInt(u16, port_text, 10),
.debug = try parseBool(debug_text),
};
}
pub fn main() !void {
const allocator = std.heap.page_allocator;
const config = try loadConfig(allocator);
std.debug.print("port = {}\n", .{config.port});
std.debug.print("debug = {}\n", .{config.debug});
}Run it like this:
APP_PORT=8080 DEBUG=true ./my_programOutput:
port = 8080
debug = trueDefault Values
Some programs want defaults.
fn loadPort(allocator: std.mem.Allocator) !u16 {
const text = std.process.getEnvVarOwned(
allocator,
"APP_PORT",
) catch return 8080;
defer allocator.free(text);
return std.fmt.parseInt(u16, text, 10);
}This says:
If the variable is missing, use 8080.
But if the variable exists and is invalid:
APP_PORT=helloparsing still fails.
That is usually the correct behavior.
Missing configuration and malformed configuration are different problems.
Environment Variables Are Strings
This is one of the most important ideas.
Everything starts as text.
The operating system does not know:
this variable is a port number
this variable is a boolean
this variable is JSON
this variable is a path
Your program defines the meaning.
That means your program also defines:
which values are valid
how parsing works
what errors should say
what defaults exist
Common Mistakes
Do not assume environment variables exist.
Do not assume they contain valid values.
Do not silently ignore malformed configuration.
Do not forget to free allocated environment strings.
Do not hard-code platform-specific variable names unless your program is platform-specific.
Do not scatter environment access across the whole program.
The Core Pattern
Read the variable:
const value = try std.process.getEnvVarOwned(
allocator,
"NAME",
);
defer allocator.free(value);Parse the text:
const port = try std.fmt.parseInt(u16, value, 10);Handle missing variables:
const value = std.process.getEnvVarOwned(
allocator,
"NAME",
) catch {
return default_value;
};Validate strictly:
if (!std.mem.eql(u8, value, "true")) {
return error.InvalidValue;
}What You Should Remember
Environment variables are named text values provided by the operating system.
Programs use them for configuration.
Environment values are strings and must often be parsed.
Missing variables and malformed variables are different cases.
Many environment APIs allocate memory, so free the returned strings.
Treat environment variables as external input.
Load configuration early, validate it clearly, and convert it into typed values before using it throughout the program.