polar

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 an Option<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.