Skip to content

Commit

Permalink
Print help text for command execution failure (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
TanklesXL authored Feb 14, 2024
1 parent 5e61dc8 commit 7be5fbe
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 47 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Changelog

## [Unreleased](https://github.com/TanklesXL/glint/compare/v0.14.0...HEAD)

## [Unreleased](https://github.com/TanklesXL/glint/compare/v0.15.0...HEAD)
- `glint.CommandResult(a)` is now a `Result(Out(a), String)` instead of a `Result(Out(a),Snag)`
- command exectution failures due to things like invalid flags or too few args now print help text for the current command

## [0.15.0](https://github.com/TanklesXL/glint/compare/v0.14.0...v0.15.0)

Expand Down
12 changes: 6 additions & 6 deletions examples/hello/manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@

packages = [
{ name = "argv", version = "1.0.1", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "A6E9009E50BBE863EB37D963E4315398D41A3D87D0075480FC244125808F964A" },
{ name = "filepath", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "534E8161A0DE192A9A105EFEC34369E9FD5834BB58ED449B5ACAEE8704358588" },
{ name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_community_colour"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
{ name = "filepath", version = "0.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "FC1B1B29438A5BA6C990F8047A011430BEC0C5BA638BFAA62718C4EAEFE00435" },
{ name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
{ name = "gleam_community_colour", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "A49A5E3AE8B637A5ACBA80ECB9B1AFE89FD3D5351FF6410A42B84F666D40D7D5" },
{ name = "gleam_erlang", version = "0.24.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "26BDB52E61889F56A291CB34167315780EE4AA20961917314446542C90D1C1A0" },
{ name = "gleam_stdlib", version = "0.34.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1FB8454D2991E9B4C0C804544D8A9AD0F6184725E20D63C3155F0AEB4230B016" },
{ name = "gleescript", version = "1.0.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib", "simplifile", "snag", "tom", "gleam_erlang"], otp_app = "gleescript", source = "hex", outer_checksum = "F7C152E206167000420F90983E4D4A076703292AAC4335A9248BA46D380841AC" },
{ name = "gleescript", version = "1.0.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_erlang", "gleam_stdlib", "simplifile", "snag", "tom"], otp_app = "gleescript", source = "hex", outer_checksum = "F7C152E206167000420F90983E4D4A076703292AAC4335A9248BA46D380841AC" },
{ name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" },
{ name = "glint", version = "0.14.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], source = "local", path = "../.." },
{ name = "simplifile", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "359CD7006E2F69255025C858CCC6407C11A876EC179E6ED1E46809E8DC6B1AAD" },
{ name = "glint", version = "0.15.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], source = "local", path = "../.." },
{ name = "simplifile", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "AAFCF154F69B237D269FF2764890F61ABC4A7EF2A592D44D67627B99694539D9" },
{ name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
{ name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" },
]

[requirements]
argv = { version = "~> 1.0" }
gleam_stdlib = { version = "~> 0.29" }
gleam_stdlib = { version = "~> 0.34 or ~> 1.0" }
gleescript = { version = "~> 1.0" }
gleeunit = { version = "~> 1.0" }
glint = { path = "../.." }
Expand Down
2 changes: 1 addition & 1 deletion manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ packages = [
[requirements]
gleam_community_ansi = { version = "~> 1.0" }
gleam_community_colour = { version = "~> 1.0" }
gleam_stdlib = { version = "~> 0.19" }
gleam_stdlib = { version = "~> 0.34 or ~> 1.0" }
gleeunit = { version = "~> 1.0" }
snag = { version = "~> 0.2" }
90 changes: 52 additions & 38 deletions src/glint.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import gleam_community/ansi
import gleam_community/colour.{type Colour}
import gleam/result
import gleam/function
import gleam

// --- CONFIGURATION ---

Expand Down Expand Up @@ -94,10 +95,10 @@ pub opaque type Glint(a) {
///
pub type ArgsCount {
/// Specifies that a command must accept a specific number of arguments
///
///
EqArgs(Int)
/// Specifies that a command must accept a minimum number of arguments
///
///
MinArgs(Int)
}

Expand All @@ -118,10 +119,10 @@ pub opaque type Command(a) {
/// Arguments passed to `glint` are provided as the `args` field.
///
/// Flags passed to `glint` are provided as the `flags` field.
///
///
/// If named arguments are specified at command creation, they will be accessible via the `named_args` field.
/// IMPORTANT: Arguments matched by `named_args` will not be present in the `args` field.
///
///
pub type CommandInput {
CommandInput(
args: List(String),
Expand Down Expand Up @@ -156,7 +157,7 @@ pub type Out(a) {
/// Result type for command execution
///
pub type CmdResult(a) =
Result(Out(a))
gleam.Result(Out(a), String)

// -- CORE: BUILDER FUNCTIONS --

Expand Down Expand Up @@ -252,7 +253,7 @@ pub fn count_args(cmd: Command(a), count: ArgsCount) -> Command(a) {
/// These named arguments will be matched with the first N arguments passed to the command
/// All named arguments must match for a command to succeed, this is considered an implicit MinArgs(N)
/// This works in combination with CommandInput.named_args which will contain the matched args in a Dict(String,String)
/// IMPORTANT: Matched named arguments will not be present in CommandInput.args
/// IMPORTANT: Matched named arguments will not be present in CommandInput.args
///
pub fn named_args(cmd: Command(a), args: List(String)) -> Command(a) {
Command(..cmd, named_args: args)
Expand Down Expand Up @@ -376,7 +377,7 @@ fn do_execute(

// when there are no more available arguments
// run the current command
[] -> execute_root(cmd, global_flags, [], flags)
[] -> execute_root(command_path, config, cmd, global_flags, [], flags)

// when there are arguments remaining
// check if the next one is a subcommand of the current command
Expand All @@ -397,7 +398,7 @@ fn do_execute(
|> Ok
// subcommand not found, but help flag has not been passed
// execute the current command
_ -> execute_root(cmd, global_flags, args, flags)
_ -> execute_root(command_path, config, cmd, global_flags, args, flags)
}
}
}
Expand All @@ -419,40 +420,57 @@ fn args_compare(expected: ArgsCount, actual: Int) -> Result(Nil) {
/// Executes the current root command.
///
fn execute_root(
path: List(String),
config: Config,
cmd: CommandNode(a),
global_flags: FlagMap,
args: List(String),
flag_inputs: List(String),
) -> CmdResult(a) {
{
use contents <- option.map(cmd.contents)
use new_flags <- result.try(list.try_fold(
over: flag_inputs,
from: dict.merge(global_flags, contents.flags),
with: flag.update_flags,
))

use _ <- result.try(case contents.count_args {
Some(count) ->
args_compare(count, list.length(args))
|> snag.context("invalid number of arguments provided")
None -> Ok(Nil)
})
let res =
{
use contents <- option.map(cmd.contents)
use new_flags <- result.try(list.try_fold(
over: flag_inputs,
from: dict.merge(global_flags, contents.flags),
with: flag.update_flags,
))

use _ <- result.try(case contents.count_args {
Some(count) ->
args_compare(count, list.length(args))
|> snag.context("invalid number of arguments provided")
None -> Ok(Nil)
})

let #(named_args, rest) = list.split(args, list.length(contents.named_args))
let #(named_args, rest) =
list.split(args, list.length(contents.named_args))

use named_args_dict <- result.map(
contents.named_args
|> list.strict_zip(named_args)
|> result.replace_error(snag.new("not enough arguments")),
)
use named_args_dict <- result.map(
contents.named_args
|> list.strict_zip(named_args)
|> result.replace_error(snag.new("not enough arguments")),
)

CommandInput(rest, new_flags, dict.from_list(named_args_dict))
|> contents.do
|> Out
}
|> option.unwrap(snag.error("command not found"))
|> snag.context("failed to run command")
|> result.map_error(fn(err) {
#(err, cmd_help(path, cmd, config, global_flags))
})

CommandInput(rest, new_flags, dict.from_list(named_args_dict))
|> contents.do
|> Out
case res {
Ok(out) -> Ok(out)
Error(#(snag, help)) ->
Error(
snag.pretty_print(snag)
<> "\nSee the following help text, available via the '--help' flag.\n\n"
<> help,
)
}
|> option.unwrap(snag.error("command not found"))
|> snag.context("failed to run command")
}

/// A wrapper for `execute` that prints any errors enountered or the help text if requested.
Expand All @@ -472,11 +490,7 @@ pub fn run_and_handle(
with handle: fn(a) -> _,
) -> Nil {
case execute(glint, args) {
Error(err) ->
err
|> snag.pretty_print
|> io.println
Ok(Help(help)) -> io.println(help)
Error(s) | Ok(Help(s)) -> io.println(s)
Ok(Out(out)) -> {
handle(out)
Nil
Expand Down
18 changes: 18 additions & 0 deletions test/glint_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,24 @@ pub fn help_test() {
glint.execute(cli, ["a", "b"])
|> should.equal(Ok(Out(Nil)))

glint.execute(cli, ["a"])
|> should.be_error()

glint.execute(cli, [])
|> should.be_error()

glint.execute(cli, ["cmd2"])
|> should.be_error()

glint.execute(cli, ["cmd2", "1"])
|> should.be_error()

glint.execute(cli, ["cmd2", "1", "2"])
|> should.equal(Ok(Out(Nil)))

glint.execute(cli, ["cmd2", "1", "2", "3"])
|> should.equal(Ok(Out(Nil)))

// help message for root command
glint.execute(cli, [glint.help_flag()])
|> should.equal(
Expand Down

0 comments on commit 7be5fbe

Please sign in to comment.