From 7be5fbe14f2528693c4e45e6973860e14dedd888 Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Tue, 13 Feb 2024 20:58:05 -0500 Subject: [PATCH] Print help text for command execution failure (#26) --- CHANGELOG.md | 5 +- examples/hello/manifest.toml | 12 ++--- manifest.toml | 2 +- src/glint.gleam | 90 +++++++++++++++++++++--------------- test/glint_test.gleam | 18 ++++++++ 5 files changed, 80 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db3f8a4..bb2c92f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/examples/hello/manifest.toml b/examples/hello/manifest.toml index e8c72f6..1d37081 100644 --- a/examples/hello/manifest.toml +++ b/examples/hello/manifest.toml @@ -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 = "../.." } diff --git a/manifest.toml b/manifest.toml index bbbeb8f..8f77e50 100644 --- a/manifest.toml +++ b/manifest.toml @@ -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" } diff --git a/src/glint.gleam b/src/glint.gleam index 9e1e780..acbce39 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -12,6 +12,7 @@ import gleam_community/ansi import gleam_community/colour.{type Colour} import gleam/result import gleam/function +import gleam // --- CONFIGURATION --- @@ -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) } @@ -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), @@ -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 -- @@ -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) @@ -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 @@ -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) } } } @@ -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. @@ -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 diff --git a/test/glint_test.gleam b/test/glint_test.gleam index cda9c01..d692693 100644 --- a/test/glint_test.gleam +++ b/test/glint_test.gleam @@ -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(