Command-Line Programs
Std.Process gives Node programs what a command-line
tool needs: arguments, environment variables, exit codes and child
processes.
Set up
Import the module and target the Node host. Functions
that use it list the Process effect:
module Tool
uses
Std.List
Std.Option
Std.Process
hosts
Node
functions
main() -> {} / {Process} {
let mode = Option.with_default(Process.env("APP_MODE"), "development")
let result = Process.output("echo", ["hello from echo"], Process.inherit())
Log.info("args: #{List.length(Process.args())}")
Log.info("mode: #{mode}")
Log.info("echo said: #{String.trim(result.stdout)} (exit #{result.code})")
Log.info("missing: #{Process.run("no-such-command", [], Process.inherit())}")
}
exports
main
$ polar run tool.px -- x y
args: 2
mode: development
echo said: hello from echo (exit 0)
error: cannot run `no-such-command`: command not found
missing: 127
Arguments
Process.args() returns the arguments after
--, as a List<String>. It works the
same whichever way the program is started:
$ polar run tool.px -- a b
$ polar start -- a b
$ node dist/start.mjs -- a b
All three pass ["a", "b"]. List patterns make argument
handling pleasant:
match Process.args() {
["build", ..rest] -> {
let code = Process.run("polar", ["build", ..rest], Process.inherit())
Process.set_exit_code(code)
}
_ -> Process.set_exit_code(2),
}
Environment
-
Process.env(name)returns anOption<String>, since the variable might not be set. Process.cwd()returns the current directory.
Exit codes
Process.set_exit_code(code) sets the code the process
exits with once main returns. It doesn't stop the
program. An uncaught error exits with code 1.
Running other programs
| Function | Does |
|---|---|
Process.run(cmd, args, options) |
Runs a program, streaming its output to yours, and returns its exit code |
Process.output(cmd, args, options) |
Runs a program and captures
{ code, stdout, stderr }
|
A command that can't be started returns 127, the same code a shell uses. Options say where and with what environment to run:
Process.inherit() |> Process.in_dir("sub") |> Process.with_env("NAME", "value")
Process.inherit() starts from the current directory and
environment. in_dir and with_env each
return new options, so they chain with the pipe.
Files and paths
Std.Fs reads and writes text files, and
Std.Path takes paths apart. Operations that can fail
return a Result whose error is an FsError,
so they chain with Result.and_then:
uses
Std.Fs
Std.Path
Std.Result
hosts
Node
functions
copy(from: String, to: String) -> Result<FsError, {}> / {Fs} {
Fs.read(from) |> Result.and_then(function(text) {
Fs.mkdir_all(Path.dirname(to)) |> Result.and_then(function(_) { Fs.write(to, text) })
})
}
Match on the error's code to handle a particular
failure, such as ENOENT for a missing file.
Fs.list and Fs.walk return sorted names,
so output built from them is the same on every run. See
the reference for every
operation.
Tip: Do the real work in pure functions
that take the arguments as a list, and keep
main thin. You can then test the logic without
starting a process.
Thinking in Polar builds a
whole tool this way.