runcmd #
Run External Commands
runcmd module implements a high-level interface for running external commands.
Why not vlib os?
The standard V os module already contains many tools for running external commands, but I don't like any of them. So let's overview.
-
os.execvp(),os.execve()— cross-platform versions of the C functions of the same name. -
os.execute(),os.execute_opt(),os.execute_or_exit(),os.execute_or_panic(),os.raw_execute()— starts and waits for a command to completed. Under the hood, they perform a dirty hack by calling shell with stream redirection'exec 2>&1;${cmd}'. -
util.execute_with_timeout()(fromos.util) — just anos.execute()wrapper. -
os.system()— also executes command in the shell, but does not redirect streams. This is fine for running commands that take a long time and write something to the terminal; it's convenient in build scripts. -
os.Processjust has an ugly interface with a lot of unnecessary methods. Actually, it's not bad; I copied parts of it. -
os.CommandcallsC.popen()under the hood and is not suitable for anything other than running a command in the shell (again) with stream processing of the mixed stdout and stderr.
The obvious downside of this module is that it only works on Linux and likely other POSIX-compliant operating systems. I'm not interested in working on MS Windows, but anyone interested can submit a PR on GitHub to support the worst operating system.
Usage
Basic usage:
import runcmd
mut cmd := runcmd.new('sh', '-c', 'echo Hello, World!')
cmd.run()! // Start and wait for process.
// Hello, World!
println(cmd.state) // exit status 0
If you don't want to wait for the child process to complete, call start() instead of run():
mut cmd := runcmd.new('sh', '-c', 'sleep 60')
pid := cmd.start()!
println(pid)
.state value is unavailable in this case because we didn't wait for the process to complete.
If you need to capture standard output and standard error, use the output() and combined_output(). See examples in its description.
To learn more about the runcmd's capabilities and usage, see the examples directory. Examples are very important.
Roadmap
- Basic implementation.
- Contexts support for creating cancelable commands, commands with timeouts, etc.
- Better error handling and more tests...
Send pull requests for additional features/bugfixes.
fn is_present #
fn is_present(cmd string) bool
is_present returns true if cmd is present on system. cmd may be a command name or filepath (relative or absolute). The result relies on look_path() output, see its docs for command search details.
fn look_path #
fn look_path(cmd string) !string
look_path returns the absolute path to executable file. cmd may be a command name or filepath (relative or absolute). The command is searched in PATH. If the name contains a slash, then the PATH search is not performed, instead the path will be resolved and the file existence will be checked. In both cases the file must have the execute permission bit enabled.
Note: To use executables located in the current working directory use './file' instead of just 'file'. Searching for executable files in the current directory is disabled for security reasons. See https://go.dev/blog/path-security.
fn new #
fn new(name string, arg ...string) &Command
new creates new command with given command name and arguments.
fn with_context #
fn with_context(ctx context.Context, name string, arg ...string) &Command
with_context creates new command with context, command name and arguments.
type CommandCancelFn #
type CommandCancelFn = fn () !
type ProcessHookFn #
type ProcessHookFn = fn (mut p Process) !
fn (ProcessState) pid #
fn (s ProcessState) pid() int
pid returns the child process identifier. If process is not launched yet -1 wil be returned.
fn (ProcessState) exited #
fn (s ProcessState) exited() bool
exited returns true if process is exited.
fn (ProcessState) exit_code #
fn (s ProcessState) exit_code() int
exit_code returns the process exit status code or -1 if process is not exited.
fn (ProcessState) success #
fn (s ProcessState) success() bool
success returns true if process if successfuly exited (0 exit status on POSIX).
fn (ProcessState) sys #
fn (s ProcessState) sys() voidptr
sys returns the system-specific process state object. For now its always WaitStatus.
fn (ProcessState) str #
fn (s ProcessState) str() string
str returns the text representation of process state. For non-started process it returns 'unknown' state.
fn (ReadFd) read #
fn (mut f ReadFd) read(mut buf []u8) !int
read reads the buf.len bytes from file descriptor and returns number of bytes read on success. This function implements the io.Reader interface.
fn (ReadFd) slurp #
fn (mut f ReadFd) slurp() ![]u8
slurp reads all data from file descriptor (until gets io.Eof) and returns result as byte array.
fn (ReadFd) close #
fn (mut f ReadFd) close() !
close closes the underlying file descriptor.
type WaitStatus #
type WaitStatus = u32
WaitStatus stores the result value of wait(2) syscall.
fn (WaitStatus) exited #
fn (w WaitStatus) exited() bool
exited returns true if process is exited.
fn (WaitStatus) exit_code #
fn (w WaitStatus) exit_code() int
exit_code returns the process exit status code or -1 if process is not exited.
fn (WaitStatus) signaled #
fn (w WaitStatus) signaled() bool
signaled returns true if the child process was terminated by a signal.
fn (WaitStatus) term_signal #
fn (w WaitStatus) term_signal() int
term_signal returns the number of the signal that caused the child process to terminate.
fn (WaitStatus) stopped #
fn (w WaitStatus) stopped() bool
stopped returns true if the child process was stopped by delivery of a signal.
fn (WaitStatus) stop_signal #
fn (w WaitStatus) stop_signal() int
stop_signal returns the number of the signal which caused the child to stop.
fn (WaitStatus) continued #
fn (w WaitStatus) continued() bool
continued returns true if the child process was resumed by delivery of SIGCONT.
fn (WaitStatus) coredump #
fn (w WaitStatus) coredump() bool
coredump returns true if the child produced a core dump. See core(5).
fn (WriteFd) write #
fn (mut f WriteFd) write(buf []u8) !int
write writes the buf.len bytes to the file descriptor and returns number of bytes written on success. This function implements the io.Writer interface.
fn (WriteFd) close #
fn (mut f WriteFd) close() !
close closes the underlying file descriptor.
struct Command #
struct Command {
pub mut:
// path may be a command name or absolute path to executable.
// If the specified path is not absolute, it will be obtained
// using the look_path() function before starting the process.
path string
// args holds command line arguments passed to the executable.
args []string
// env contains key-value pairs of environment variables that
// will be passed to the process. If not specified the current
// os.environ() will be used. If you want to update current
// environ instead of overriding it, use merge() function from
// `maps` module: `maps.merge(os.environ(), {'MYENV': 'value'})`
env map[string]string
// dir specifies the working directory for the child process.
// If not specified, the current working directory of parent
// will be used.
dir string
// If true create pipes for standart in/out/err streams and
// duplicate child streams to created file descriptors.
// redirect_stdio is required for work with child I/O.
redirect_stdio bool
// If the field is filled (any of stdin, stdout, stderr), then
// after the process is started, a coroutine will be launched
// in the background, which will copy the corresponding data
// stream between the child and parent processes.
//
// This fields must be set BEFORE calling the start() function.
//
// This is useful if we want to feed data to the child process
// via standard input or read stdout and/or stderr entirely.
// Since the coroutines copies the stream continuously in a loop,
// the data can only be processed once it has been completely
// read. To process data in chunks, do not set these fields, but
// use the stdin(), stdout() and stderr() functions to obtain
// the child process's file descriptors after calling start().
stdin ?io.Reader
stdout ?io.Writer
stderr ?io.Writer
// ctx holds a command context. It may be used to make a command
// cancelable or set timeout/deadline for it.
ctx ?context.Context
// cancel function is used to terminate the child process. Do
// not confuse with the context's cancel function.
//
// The default command cancel function created in with_context()
// terminates the command by sending SIGTERM signal to child. You
// can override it by setting your own command cancel function.
// If cancel is none the child process won't be terminated even
// if context is timed out or canceled.
cancel ?CommandCancelFn
// pre_exec_hooks will be called before starting the command in
// the child process. Hooks can be used to modify a child's envi-
// ronment, for example to perform a chroot.
pre_exec_hooks []ProcessHookFn
// process holds the underlying Process once started.
process ?&Process
// state holds an information about underlying process.
// This is set only if process if finished. Call `run()`
// or `wait()` to get actual state value.
// This value MUST NOT be changed by API user.
state ProcessState
mut:
// stdio holds a file descriptors for I/O processing.
// There is:
// * 0 — child process stdin, we must write into it.
// * 1 — child process stdout, we must read from it.
// * 2 — child process stderr, we must read from it.
stdio [3]int = [-1, -1, -1]!
// stdio_copy_fns is array of closures to copy data between
// parent and child processes. For standard I/O streams
// it does (if some of .stdin, .stdout and .stderr fields is set):
// * read from .stdin reader and write data into child stdin fd.
// * read from child stdout fd and write into .stdout writer.
// * read from child stderr fd and write into .stderr writer.
stdio_copy_fns []IOCopyFn
}
fn (Command) run #
fn (mut c Command) run() !
run starts a specified command and waits for it. After call see the .state value to get finished process identifier, exit status and other attributes. run() is shorthand for:
cmd.start()!
cmd.wait()!
fn (Command) output #
fn (mut c Command) output() !string
output runs the command and returns its stdout on success. If command exit status is non-zero ExitError error is returned.
Example
import runcmd
mut okcmd := runcmd.new('sh', '-c', 'echo Hello, World!')
ok_out := okcmd.output()!
println(ok_out)
// Hello, World!
mut badcmd := runcmd.new('sh', '-c', 'echo -n Error! >&2; false')
bad_out := badcmd.output() or {
if err is runcmd.ExitError {
eprintln(err)
exit(err.code())
} else {
// error starting process or handling I/O, see errno in err.code().
panic(err)
}
}
println(bad_out)
// &runcmd.ExitError{
// state: exit status 1
// stderr: 'Error!'
// }
fn (Command) combined_output #
fn (mut c Command) combined_output() !string
combined_output runs the command and returns its combined stdout and stderr. Unlike output(), this function does not return ExitError on command failure.
Note: The order of lines from stdout and stderr is not guaranteed, since reading from the corresponding file descriptors is done concurrently.
Example
import runcmd
mut cmd := runcmd.new('sh', '-c', 'echo Hello, STDOUT!; echo Hello, STDERR! >&2')
output := cmd.combined_output()!
println(output)
// Hello, STDOUT!
// Hello, STDERR!
fn (Command) start #
fn (mut c Command) start() !int
start starts a specified command and does not wait for it to complete. Call wait() after start() has successfully completed to wait for the command to complete and release associated resources.
Note: .state field is not set after start() call.
fn (Command) wait #
fn (mut c Command) wait() !
wait waits to previously started command is finished. After call see the .state field value to get finished process identifier, exit status and other attributes. wait() will return an error if the process has not been started or wait has already been called.
fn (Command) release #
fn (mut c Command) release() !
release releases all resources assocuated with process.
fn (Command) stdin #
fn (c Command) stdin() !WriteFd
stdin returns an open file descriptor associated with the standard input stream of the child process. This descriptor is write-only for the parent process.
fn (Command) stdout #
fn (c Command) stdout() !ReadFd
stdout returns an open file descriptor associated with the standard output stream of the child process. This descriptor is read-only for the parent process.
fn (Command) stderr #
fn (c Command) stderr() !ReadFd
stderr returns an open file descriptor associated with the standard error stream of the child process. This descriptor is read-only for the parent process.
struct ExitError #
struct ExitError {
pub:
state ProcessState
stderr string
}
fn (ExitError) code #
fn (e ExitError) code() int
code returns an exit status code of a failed process.
fn (ExitError) msg #
fn (e ExitError) msg() string
msg returns message about command failure.
struct Process #
struct Process {
pub:
// Absolute path to the executable.
path string
// Arguments that will be passed to the executable.
argv []string
// Environment variables that will be applied to the child process.
env map[string]string
// Working directory for the child process.
dir string
// The pre_* and post_* fields store the functions that will be executed, respectively:
// - before fork() call in the parent process;
// - after fork() call in the parent process;
// - after fork() call and before execve() call in the child process.
pre_fork []ProcessHookFn
post_fork []ProcessHookFn
pre_exec []ProcessHookFn
mut:
pid int = -1
}
fn (Process) start #
fn (mut p Process) start() !int
start starts new child process by performing fork(3p) and execve(3p) calls. Return value is the child process identifier.
fn (Process) pid #
fn (p &Process) pid() int
pid returns the child process identifier. -1 is returned if process is not started.
fn (Process) wait #
fn (p &Process) wait() !ProcessState
wait waits for process to change state and returns the ProcessState.
fn (Process) signal #
fn (p &Process) signal(sig os.Signal) !
signal sends the sig signal to the child process.
fn (Process) kill #
fn (p &Process) kill() !
kill sends SIGKILL to the child process.