import gleam/map.{Map} import gleam/option.{None, Option, Some} import gleam/list import gleam/io import gleam/int import gleam/result import gleam/string import gleam/bool import gleam/function import snag.{Result} import glint/flag.{Flag, FlagMap} /// Input type for `Runner`. /// pub type CommandInput { CommandInput(args: List(String), flags: FlagMap) } /// Function type to be run by `glint`. /// pub type Runner(a) = fn(CommandInput) -> a /// Command tree representation. /// pub opaque type Command(a) { Command( do: Option(Runner(a)), subcommands: Map(String, Command(a)), flags: FlagMap, ) } /// Creates a new command tree. /// pub fn new() -> Command(a) { Command(do: None, subcommands: map.new(), flags: map.new()) } /// Trim each path element and remove any resulting empty strings. /// fn sanitize_path(path: List(String)) -> List(String) { path |> list.map(string.trim) |> list.filter(function.compose(string.is_empty, bool.negate)) } /// Adds a new command to be run at the specified path. /// /// If the path is [], the root command is set with the provided function and /// flags. /// pub fn add_command( to root: Command(a), at path: List(String), do f: Runner(a), with flags: List(Flag), ) -> Command(a) { path |> sanitize_path |> do_add_command(to: root, do: f, with: flags) } fn do_add_command( to root: Command(a), at path: List(String), do f: Runner(a), with flags: List(Flag), ) -> Command(a) { case path { [] -> Command(..root, do: Some(f), flags: flag.build_map(flags)) [x, ..xs] -> { let update_subcommand = fn(node) { node |> option.lazy_unwrap(new) |> do_add_command(xs, f, flags) } Command( ..root, subcommands: map.update(root.subcommands, x, update_subcommand), ) } } } /// Executes the current root command. /// fn execute_root( cmd: Command(a), args: List(String), flags: List(String), ) -> Result(a) { case cmd.do { Some(f) -> { try new_flags = flags |> list.try_fold(from: cmd.flags, with: flag.update_flags) |> snag.context("failed to run command") args |> CommandInput(new_flags) |> f |> Ok } None -> snag.new("command not found") |> snag.layer("failed to run command") |> Error } } /// Determines which command to run and executes it. /// /// Sets any provided flags if necessary. /// /// Each value prefixed with `--` is parsed as a flag. /// pub fn execute(cmd: Command(a), args: List(String)) -> Result(a) { let #(flags, args) = list.partition(args, string.starts_with(_, flag.prefix)) do_execute(cmd, args, flags) } fn do_execute( cmd: Command(a), args: List(String), flags: List(String), ) -> Result(a) { case args { [] -> execute_root(cmd, [], flags) [arg, ..rest] -> case map.get(cmd.subcommands, arg) { Ok(cmd) -> do_execute(cmd, rest, flags) _ -> execute_root(cmd, args, flags) } } } /// A wrapper for `execute` that discards output and prints any errors /// encountered. /// pub fn run(cmd: Command(a), args: List(String)) -> Nil { case execute(cmd, args) { Error(err) -> err |> snag.pretty_print |> io.println _ -> Nil } }