Argparse.Program

A convenience wrapper that turns a Argparse.Parser.App into a ready-to-run Node program, handling the boilerplate most command-line tools want:

  • parse errors (unknown command, bad flags, bad arguments) are printed to stderr and the program exits with code 1
  • --help, --version, and the bare-invocation help screen are printed to stdout (exit 0)
  • a successfully parsed command is handed to your onCommand function

Your handler never touches exit codes. It returns a Task Failure {}, and this module turns that into the process exit status for you:

  • the task succeeds with {} → exit 0
  • the task fails with ExitFailure → exit 1, printing nothing extra (you've already written whatever report you wanted)
  • the task fails with ExitMessage → that string is printed to stderr and the program exits 1
  • the task fails with ExitValue → exits with the given code, printing nothing extra
  • the task fails with ExitMessageValue → that string is printed to stderr and the program exits with the given code

So Task.succeed {} is the only success path — exit 0. Task.fail is how you signal any non-zero exit, with the Failure constructor choosing whether to print a message and which code to use.

This is opinionated on purpose. Programs that need a full model/update loop should skip this module entirely: call Argparse.Parser.run inside your own Node.defineSimpleProgram, match on each CommandParseResult constructor by hand, and call Node.setExitCode yourself wherever you need it. See examples/manual/ for a complete working example that exits 2 on usage errors.

If all you need is access to subsystem permissions (filesystem, terminal, child processes, …), you can use runWithContext, which lets you run your own initialization before the command handler while keeping all of the boilerplate above.

type Failure
= ExitFailure
| ExitMessage String
| ExitValue Int
| ExitMessageValue ({ message : String, value : Int })

How a command handler signals failure. A handler returns Task Failure {}; Task.succeed {} exits 0, and Task.fail with one of these constructors decides the non-zero behavior.

  • ExitFailure — exit 1, print nothing (you've already written your report)
  • ExitMessage msg — print msg to stderr, then exit 1
  • ExitValue n — exit n, print nothing
  • ExitMessageValue { message, value } — print message to stderr, then exit value

Here is an example of using ExitFalure after having already printed the error message to stderr.

check : Node.Environment -> Cmd -> Task Argparse.Program.Failure {}
check env cmd =
    when cmd is
        Lint { path } ->
            lint path
                |> Task.andThen
                    (\problems ->
                        if Array.length problems == 0 then
                            Stream.Log.line env.stdout "All good."

                        else
                            reportProblems env.stderr problems
                                |> Task.andThen (\_ -> Task.fail Argparse.Program.ExitFailure)
                    )
run :
{ parser : App cmd
, onCommand : Environment -> cmd -> Task Failure {}
}
-> SimpleProgram a

Build a Node program from a parser and a command handler.

main : Node.SimpleProgram a
main =
    Argparse.Program.run
        { parser = MyArgparse.parser
        , onCommand =
            \env command ->
                when command is
                    MyArgparse.Greet { name } ->
                        Stream.Log.line env.stdout ("Hello, " ++ name)
        }

onCommand receives the Node.Environment (stdout, stderr, args, …) and your parsed command, and returns a Task Failure {}. Succeed with {} to exit 0; fail the task with a Failure constructor to choose a non-zero exit and whether to print a message — the same treatment a parse error gets.

runWithContext :
{ parser : App cmd
, init : Environment -> (context -> Task ( Cmd a))
-> Task (Cmd a)
, onCommand : Environment -> context -> cmd -> Task Failure {}
}
-> SimpleProgram a

Like run, but with a chance to initialize subsystems and acquire permissions before any command runs. Use this when your command handler needs a FileSystem.Permission, a Terminal configuration, child-process access, and so on — these can only be obtained during a program's initialization phase, which run doesn't expose.

init is given the Node.Environment and a continuation (toProgram). Acquire whatever you need with Init.await, then call toProgram with a context value of your choosing; that same value is later handed to every onCommand invocation.

import FileSystem
import Init

type alias Context =
    { fs : FileSystem.Permission }

main : Node.SimpleProgram a
main =
    Argparse.Program.runWithContext
        { parser = MyArgparse.parser
        , init =
            \_env toProgram ->
                Init.await FileSystem.initialize <| \fsPermission ->
                    toProgram { fs = fsPermission }
        , onCommand =
            \env context command ->
                when command is
                    MyArgparse.Build { path } ->
                        FileSystem.readFile context.fs path
                            |> Task.mapError FileSystem.errorToString
                            |> Task.andThen (\contents -> Stream.Log.line env.stdout contents)
                            |> Task.map (\_ -> Argparse.Program.Succeeded)
        }

Parse errors, --help/--version, and the Failure→exit-code mapping are handled exactly as in run. The context is acquired up front.

runRoot :
{ name : String
, version : String
, command : Command args flags cmd
, onCommand : Environment -> cmd -> Task Failure {}
}
-> SimpleProgram a

Build a Node program from a single command, with no sub-command word — for tools that are just flags and arguments, like mytool --loud World. This is the Argparse.Parser.runCommand counterpart to run, and handles the same boilerplate (parse errors → stderr + exit 1, --help/--version → stdout, success → your handler).

main : Node.SimpleProgram a
main =
    Argparse.Program.runRoot
        { name = "greet"
        , version = "1.0.0"
        , command = MyArgparse.greetCommand
        , onCommand =
            \env (MyArgparse.Greet { name, loud }) ->
                Stream.Log.line env.stdout (greeting name loud)
        }

name and version drive the --help usage line and --version output. The command's own word is ignored (the help line reads name <args>), so you can leave it empty. The Failure→exit-code mapping works as in run.

If the command needs subsystem permissions, use runRootWithContext — this is just that with an empty context, the same way run relates to runWithContext.

runRootWithContext :
{ name : String
, version : String
, command : Command args flags cmd
, init : Environment -> (context -> Task ( Cmd a))
-> Task (Cmd a)
, onCommand : Environment -> context -> cmd -> Task Failure {}
}
-> SimpleProgram a

Like runRoot, but with a chance to initialize subsystems and acquire permissions before the command runs — the rootless counterpart to runWithContext. Use it for a no-sub-command tool (mytool --loud World) whose handler still needs a FileSystem.Permission, a Terminal configuration, child-process access, and so on.

init is given the Node.Environment and a continuation (toProgram). Acquire whatever you need with Init.await, then call toProgram with a context value of your choosing; that same value is later handed to onCommand.

import FileSystem
import Init

type alias Context =
    { fs : FileSystem.Permission }

main : Node.SimpleProgram a
main =
    Argparse.Program.runRootWithContext
        { name = "wc"
        , version = "1.0.0"
        , command = MyArgparse.countCommand
        , init =
            \_env toProgram ->
                Init.await FileSystem.initialize <| \fsPermission ->
                    toProgram { fs = fsPermission }
        , onCommand =
            \env context (MyArgparse.Count { path }) ->
                FileSystem.readFile context.fs path
                    |> Task.mapError (\e -> ExitMessage (FileSystem.errorToString e))
                    |> Task.andThen (\contents -> Stream.Log.line env.stdout contents)
        }

Parse errors, --help/--version, and the Failure→exit-code mapping are handled exactly as in runRoot. The context is acquired up front, so it is available even on the help/error paths; that's harmless, since acquiring a permission performs no I/O.