I took a few computer science electives during my Mechatronics Engineering major, but unfortunately I didn’t attend operating system lectures, although I had all the pre-requisites. I’ve decided to study the subject and I’ve found a free book which is quite good. On Chapter 5 about the Process API, there are details about how the Unix Process API works, detailing some system calls. I’ve found the trick for file redirection particularly interesting:
echo "hello" > file.txt
When a shell parses the above, we first need a fork
system call, which clones the current process, creating a child. After that, we use exec
system call to replace the child process by the new one, which is echo "hello"
in our case. However, we need to set up the redirection before calling exec
.
To understand how redirection works, we can focus on just the redirection mechanism without fork
. The key insight is that when we close stdout (file descriptor 1) and then open a new file, that file gets assigned the lowest available file descriptor number, which will be 1 (stdout). In Zig, we could do that with:
const std = @import("std");
const print = std.debug.print; // writes to stderr (fd 2), not redirected stdout
const STDOUT_FILENO = std.posix.STDOUT_FILENO;
pub fn main() !void {
// Close stdout (fd 1) to make it available
std.posix.close(STDOUT_FILENO);
// Open file - gets assigned fd 1 (lowest available)
const fd = try std.posix.open("./file.txt", .{
.ACCMODE = .WRONLY,
.CREAT = true,
.TRUNC = true,
}, 0o700);
// This prints to stderr, confirming our redirection worked
print("redirection fd is equal to stdout? {}\n", .{fd == STDOUT_FILENO});
const file = "echo";
const argv = [_:null]?[*:0]const u8{
"echo",
"hello world",
null,
};
// Replace current process with echo - stdout now goes to file.txt
return std.posix.execvpeZ(file, &argv, &.{null});
}
and when running it, we can see the echo was written to the file:
$ zig run redirect.zig
redirection fd is equal to stdout? true
$ cat file.txt
hello world