A file lives inside a directory.
A directory is a container for file names and other directories. Many systems call directories “folders,” but in programming, “directory” is the more common term.
A path is a name that tells the operating system where something is.
For example:
hello.txtis a path.
src/main.zigis also a path.
/home/alice/project/src/main.zigis another path.
When your program reads or writes files, it almost always works with paths and directories.
Current Working Directory
Every running program has a current working directory.
This is the directory that relative paths are resolved from.
If your program uses this path:
hello.txtthe operating system interprets it relative to the current working directory.
So if your program is running inside:
/home/alice/projectthen:
hello.txtmeans:
/home/alice/project/hello.txtIn Zig 0.16 style, you can refer to the current working directory through std.Io.Dir.cwd() when using the newer I/O APIs. Zig 0.16.0 is the current latest official release, and its release notes describe the standard library’s newer std.Io work as one of the major changes.
A typical file operation starts like this:
const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const file = try std.Io.Dir.cwd().openFile(io, "hello.txt", .{});
defer file.close(io);
// use file here
}This means: open hello.txt from the current working directory.
Relative Paths
A relative path is interpreted from some starting directory.
These are relative paths:
hello.txt
src/main.zig
../notes.txt
./data/input.txtThe path:
src/main.zigmeans: go into the src directory, then find main.zig.
The path:
../notes.txtmeans: go to the parent directory, then find notes.txt.
The path:
./data/input.txtmeans: start here, go into data, then find input.txt.
The dot . means the current directory.
The double dot .. means the parent directory.
Relative paths are useful for project files, test data, configuration files, and command-line tools.
Absolute Paths
An absolute path starts from a system root.
On Linux and macOS, an absolute path usually starts with /:
/home/alice/project/hello.txtOn Windows, an absolute path may look like this:
C:\Users\Alice\project\hello.txtAbsolute paths are useful when you need an exact location.
But many programs should avoid hard-coding absolute paths. They make programs less portable.
This is fragile:
/home/alice/project/data.txtIt works only on Alice’s machine, in that exact directory.
This is usually better:
data.txtor:
data/input.txtThe program can then run from the project directory on different machines.
Path Separators
Different operating systems write paths differently.
Unix-like systems use /:
src/main.zigWindows commonly uses \:
src\main.zigZig’s standard library has path utilities to help with platform differences. For simple examples, you will often see / used in paths because Zig can handle many common cases, but real cross-platform programs should avoid manually stitching paths with string concatenation.
This is suspicious:
const path = "data/" ++ filename;It assumes /.
A safer design is to use standard library path helpers when building paths dynamically.
Opening a Directory
A directory can be opened much like a file.
You open a directory when you want to work relative to that directory, list entries inside it, or create files inside it.
The current working directory is already available:
const cwd = std.Io.Dir.cwd();Then you can open files relative to it:
const file = try cwd.openFile(io, "hello.txt", .{});
defer file.close(io);This style is useful because the directory becomes the base for later operations.
Instead of thinking in raw strings, think in two parts:
the directory you start from
the relative path inside that directory
That is often safer and clearer.
Creating a Directory
Many programs need to create directories.
For example, a command-line tool might create an output directory:
build-outputThe shape of the code is:
try std.Io.Dir.cwd().makeDir(io, "build-output");Directory creation can fail.
The directory might already exist.
The parent path might not exist.
The program might not have permission.
The disk might be read-only.
So directory creation uses try.
A practical program often handles “already exists” separately:
std.Io.Dir.cwd().makeDir(io, "build-output") catch |err| switch (err) {
error.PathAlreadyExists => {},
else => return err,
};This says: if the directory already exists, that is fine. For every other error, return the error.
This is a common Zig pattern.
Creating a File Inside a Directory
Once you have a directory, you can create a file inside it.
const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const cwd = std.Io.Dir.cwd();
cwd.makeDir(io, "out") catch |err| switch (err) {
error.PathAlreadyExists => {},
else => return err,
};
const file = try cwd.createFile(io, "out/result.txt", .{});
defer file.close(io);
try file.writeAll(io, "created inside out\n");
}This program creates a directory named out, then creates:
out/result.txtThe important point is that out/result.txt is still a relative path. It is relative to cwd.
Listing Directory Entries
A directory contains entries.
An entry can be a file, a directory, a symbolic link, or something else depending on the operating system.
The exact iterator API can vary across Zig versions, so the safest source for your installed compiler is:
zig stdConceptually, directory listing looks like this:
const dir = try std.Io.Dir.cwd().openDir(io, "src", .{
.iterate = true,
});
defer dir.close(io);
var iterator = dir.iterate(io);
while (try iterator.next()) |entry| {
std.debug.print("{s}\n", .{entry.name});
}The structure is more important than the exact syntax.
Open the directory.
Ask for iteration support.
Create an iterator.
Call next until it returns no more entries.
Use each entry name.
Directory iteration can fail because the file system can change while you are reading it. A directory can be removed. Permissions can change. A disk can fail. Network file systems can disconnect.
That is why next may need try.
Entry Names Are Not Full Paths
When you list a directory, each entry usually gives you a name, not a full path.
If you are listing:
srcand the entry name is:
main.zigthe full relative path is:
src/main.zigDo not confuse the two.
Entry name:
main.zigPath from project root:
src/main.zigAbsolute path:
/home/alice/project/src/main.zigThese are different pieces of information.
Joining Paths
Suppose you have a directory name and a file name:
const dir_name = "src";
const file_name = "main.zig";You want:
src/main.zigFor quick examples, you might write the path directly. For real code, path joining should use standard library helpers because path rules vary by operating system.
The conceptual operation is:
const path = join(dir_name, file_name);The result is a valid path for the target platform.
When path construction needs memory, the function may require an allocator. This follows Zig’s rule: allocation should be visible.
A common shape is:
const path = try std.fs.path.join(allocator, &.{ "src", "main.zig" });
defer allocator.free(path);This creates a joined path and later frees it.
The exact namespace and APIs may differ depending on the Zig version and whether you are using older std.fs APIs or newer std.Io APIs. The principle stays the same: do not build nontrivial paths by careless string concatenation.
Checking File Metadata
Sometimes you need information about a file or directory.
That information is called metadata.
Metadata may include:
file size
file kind
permissions
modification time
A common operation is stat.
For a file:
const stat = try file.stat(io);
std.debug.print("size = {}\n", .{stat.size});For a path, a directory API may also provide ways to stat an entry.
This is useful when you need to know whether something is a file or directory before processing it.
But be careful: file systems can change between checking and using. A file might exist when you check it and be gone one moment later.
So code still needs error handling at the point of use.
Deleting Files
Deleting a file removes a directory entry.
The conceptual operation is:
try std.Io.Dir.cwd().deleteFile(io, "old.txt");This can fail.
The file might not exist.
The path might be a directory.
The program might not have permission.
The file might be locked by another process.
For command-line tools, you may want to handle missing files gracefully:
std.Io.Dir.cwd().deleteFile(io, "old.txt") catch |err| switch (err) {
error.FileNotFound => {},
else => return err,
};This says: if the file is already gone, that is acceptable.
Removing Directories
Removing a directory is different from deleting a file.
A directory may need to be empty before it can be removed.
The conceptual operation is:
try std.Io.Dir.cwd().deleteDir(io, "empty-dir");If the directory contains files, this may fail.
Recursive deletion is more dangerous because it removes a whole tree. Use it carefully. Bugs in recursive deletion can destroy data quickly.
For beginner code, prefer simple deletion first.
Paths Are Data from Outside Your Program
Paths often come from users.
For example:
mytool input.txtHere, input.txt is user input.
Treat paths carefully.
A user might pass:
../../important-fileor an absolute path:
/etc/passwdor a path with unusual characters.
This matters especially for servers, archive extractors, package managers, build tools, and programs that write files.
Do not blindly join untrusted paths and write to them.
A safe program should decide which directories it is allowed to access, then validate or normalize paths before using them.
A Small Directory Listing Program
Here is a complete beginner-style example:
const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const dir = try std.Io.Dir.cwd().openDir(io, ".", .{
.iterate = true,
});
defer dir.close(io);
var iterator = dir.iterate(io);
while (try iterator.next()) |entry| {
std.debug.print("{s}\n", .{entry.name});
}
}This lists entries in the current directory.
The path "." means “this directory.”
The option .iterate = true means we want to iterate through entries.
The loop keeps asking for the next entry until there are no more.
A Small “Create Output File” Program
This example creates an output directory, then writes a file inside it:
const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const cwd = std.Io.Dir.cwd();
cwd.makeDir(io, "out") catch |err| switch (err) {
error.PathAlreadyExists => {},
else => return err,
};
const file = try cwd.createFile(io, "out/message.txt", .{
.truncate = true,
});
defer file.close(io);
try file.writeAll(io, "hello from Zig\n");
}After running it, the project directory contains:
out/message.txtand the file contains:
hello from ZigDirectory APIs Teach a Larger Zig Habit
Directory and path code teaches a larger habit in Zig: be explicit about the base of an operation.
Instead of passing loose path strings everywhere, think about which directory owns the operation.
For example:
const cwd = std.Io.Dir.cwd();Then operations are relative to that directory:
try cwd.createFile(io, "out/message.txt", .{});This helps you reason about file system effects.
Where can this program read?
Where can this program write?
Which paths are relative?
Which paths are absolute?
Which operation may fail?
Zig does not remove these questions. It makes them visible.
What You Should Remember
A directory contains files and other directories.
A path names a file system location.
A relative path depends on a starting directory.
An absolute path starts from the system root.
The current working directory is the default base for many relative paths.
Use directory APIs to open, create, list, and delete entries.
Use defer to close opened directories and files.
Use path helpers instead of careless string concatenation.
Treat user-provided paths carefully.
File systems can change while your program runs, so always handle errors at the point where you open, read, write, create, or delete.