execv system call replaces the current process with the executed image.
To keep the parent process you need to fork your process and then call execv in the child process.
I am very happy because, so far, I have been able to translate my code generator (curse) written in “C” or “Nim” without having to use another language and while keeping it very simple. At the same time, it allows me to explore the ins and outs of the “Zig” language.
For a higher-level (and more portable) API you can also use std.ChildProcess.spawn() instead of calling fork() and execv() yourself.
By the way, you should reconsider using catch unreachable to ignore errors. unreachable is intended to inform the compiler that a particular branch is, in fact, unreachable (which is not something the compiler can always infer on its own).
When the branch is in fact reachable, as is the case here, then it leads to undefined behavior in release mode. For example, it’s quite likely that if fork() succeeds but execv() fails then you will have two copies of your original process running, which probably isn’t good.
An easy way to handle errors is to propagate them up with try, but if you don’t want to do that and just terminate the program if an error occurs, then the reliable way to do that is with catch @panic("execv() failed"), which works the same in debug and release modes.