From f3d7d78edf8297197fdbad749bf31b28355f8442 Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Mon, 19 Feb 2024 15:59:16 -0500 Subject: [PATCH] Fix help text (#29) * fix help text formatting and notes * Clean up help text generation for named and unnamed args --- .github/workflows/main.yml | 2 +- .github/workflows/release.yaml | 2 +- CHANGELOG.md | 4 + examples/hello/.github/workflows/test.yml | 2 +- examples/hello/src/hello.gleam | 46 ++++++-- src/glint.gleam | 131 +++++++++------------- test/glint_test.gleam | 43 ++++--- 7 files changed, 130 insertions(+), 100 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5856119..b698ac4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: erlang: ["26.2.2"] - gleam: ["1.0.0-rc1"] + gleam: ["1.0.0-rc2"] steps: - uses: actions/checkout@v2 - uses: ./.github/actions/test diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7c9f91d..7d3cb41 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v2 - uses: ./.github/actions/test with: - gleam-version: "1.0.0-rc1" + gleam-version: "1.0.0-rc2" - name: publish to hex env: HEXPM_USER: ${{ secrets.HEXPM_USER }} diff --git a/CHANGELOG.md b/CHANGELOG.md index bb2c92f..524e487 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## [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 +- fix help text formatting for commands that do not include arguments +- remove named args from help text usage +- change `glint.count_args` to `glint.unnamed_args`, behaviour changes for this function to explicitly only check the number of unnamed arguments +- remove notes section from usage text ## [0.15.0](https://github.com/TanklesXL/glint/compare/v0.14.0...v0.15.0) diff --git a/examples/hello/.github/workflows/test.yml b/examples/hello/.github/workflows/test.yml index a610f49..9727d7e 100644 --- a/examples/hello/.github/workflows/test.yml +++ b/examples/hello/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: - uses: erlef/setup-beam@v1.15.4 with: otp-version: "25.2" - gleam-version: "1.0.0-rc1" + gleam-version: "1.0.0-rc2" rebar3-version: "3" - run: gleam format --check src test - run: gleam deps download diff --git a/examples/hello/src/hello.gleam b/examples/hello/src/hello.gleam index 8885b73..53fbd18 100644 --- a/examples/hello/src/hello.gleam +++ b/examples/hello/src/hello.gleam @@ -36,7 +36,7 @@ pub fn capitalize(msg, caps) -> String { } } -/// hello is a function that +/// hello is a function that says hello pub fn hello( primary: String, rest: List(String), @@ -87,6 +87,33 @@ fn gtz(n: Int) -> snag.Result(Nil) { /// the command function that will be executed as the root command /// pub fn hello_cmd() -> glint.Command(String) { + { + use input <- glint.command() + + // the caps flag has a default value, so we can be sure it will always be present + let assert Ok(caps) = flag.get_bool(from: input.flags, for: caps) + + // the repeat flag has a default value, so we can be sure it will always be present + let assert Ok(repeat) = flag.get_int(from: input.flags, for: repeat) + + // call the hello function with all necessary inputs + // we can assert here because we have told glint that this command expects at least one argument + let assert [name, ..rest] = input.args + hello(name, rest, caps, repeat) + } + // with flag `caps` + |> glint.flag(caps, caps_flag()) + // with flag `repeat` + |> glint.flag(repeat, repeat_flag()) + // with flag `repeat` + |> glint.description("Prints Hello, !") + // with at least 1 unnamed argument + |> glint.unnamed_args(glint.MinArgs(1)) +} + +/// the command function that will be executed as the "single" command +/// +pub fn hello_single_cmd() -> glint.Command(String) { { use input <- glint.command() @@ -100,18 +127,18 @@ pub fn hello_cmd() -> glint.Command(String) { let assert Ok(name) = dict.get(input.named_args, "name") // call the hello function with all necessary inputs - hello(name, input.args, caps, repeat) + hello(name, [], caps, repeat) } // with flag `caps` |> glint.flag(caps, caps_flag()) // with flag `repeat` |> glint.flag(repeat, repeat_flag()) // with flag `repeat` - |> glint.description("Prints Hello, !") - // with a first arg called name - |> glint.named_args(["name"]) - // requiring at least 1 argument - |> glint.count_args(glint.MinArgs(1)) + |> glint.description("Prints Hello, !") + // with a named arg called 'name' + |> glint.named_args(["name", "nom"]) + // with at least 1 unnamed argument + |> glint.unnamed_args(glint.EqArgs(0)) } // the function that describes our cli structure @@ -130,6 +157,11 @@ pub fn app() { at: [], do: hello_cmd(), ) + |> glint.add( + // add the hello single command + at: ["single"], + do: hello_single_cmd(), + ) } pub fn main() { diff --git a/src/glint.gleam b/src/glint.gleam index acbce39..43e5adc 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -109,7 +109,7 @@ pub opaque type Command(a) { do: Runner(a), flags: FlagMap, description: String, - count_args: Option(ArgsCount), + unnamed_args: Option(ArgsCount), named_args: List(String), ) } @@ -232,7 +232,7 @@ pub fn command(do runner: Runner(a)) -> Command(a) { do: runner, flags: dict.new(), description: "", - count_args: None, + unnamed_args: None, named_args: [], ) } @@ -243,15 +243,15 @@ pub fn description(cmd: Command(a), description: String) -> Command(a) { Command(..cmd, description: description) } -/// Specify a specific number of args that a given command expects +/// Specify a specific number of unnamed args that a given command expects /// -pub fn count_args(cmd: Command(a), count: ArgsCount) -> Command(a) { - Command(..cmd, count_args: Some(count)) +pub fn unnamed_args(cmd: Command(a), count: ArgsCount) -> Command(a) { + Command(..cmd, unnamed_args: Some(count)) } /// Add a list of named arguments to a Command /// 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) +/// All named arguments must match for a command to succeed /// 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 /// @@ -369,7 +369,8 @@ fn do_execute( case args { // when there are no more available arguments // and help flag has been passed, generate help message - [] if help -> + [] if help + -> command_path |> cmd_help(cmd, config, global_flags) |> Help @@ -436,23 +437,34 @@ fn execute_root( with: flag.update_flags, )) - use _ <- result.try(case contents.count_args { + use named_args <- result.try({ + let named = list.zip(contents.named_args, args) + case list.length(named) == list.length(contents.named_args) { + True -> Ok(dict.from_list(named)) + False -> + snag.error( + "unmatched named arguments: " + <> { + contents.named_args + |> list.drop(list.length(named)) + |> list.map(fn(s) { "'" <> s <> "'" }) + |> string.join(", ") + }, + ) + } + }) + + let args = list.drop(args, dict.size(named_args)) + + use _ <- result.map(case contents.unnamed_args { Some(count) -> - args_compare(count, list.length(args)) + count + |> args_compare(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)) - - 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)) + CommandInput(args, new_flags, named_args) |> contents.do |> Out } @@ -594,8 +606,8 @@ type CommandHelp { flags: List(FlagHelp), // A command can have >= 0 subcommands associated with it subcommands: List(Metadata), - // A command cann have a set number of arguments - count_args: Option(ArgsCount), + // A command can have a set number of unnamed arguments + unnamed_args: Option(ArgsCount), // A command can specify named arguments named_args: List(String), ) @@ -610,12 +622,12 @@ fn build_command_help_metadata( node: CommandNode(_), global_flags: FlagMap, ) -> CommandHelp { - let #(description, flags, count_args, named_args) = case node.contents { + let #(description, flags, unnamed_args, named_args) = case node.contents { None -> #("", [], None, []) Some(cmd) -> #( cmd.description, build_flags_help(dict.merge(global_flags, cmd.flags)), - cmd.count_args, + cmd.unnamed_args, cmd.named_args, ) } @@ -624,7 +636,7 @@ fn build_command_help_metadata( meta: Metadata(name: name, description: description), flags: flags, subcommands: build_subcommands_help(node.subcommands), - count_args: count_args, + unnamed_args: unnamed_args, named_args: named_args, ) } @@ -730,61 +742,26 @@ fn args_count_to_usage_string(count: ArgsCount) -> String { } } -fn args_count_to_notes_string(count: Option(ArgsCount)) -> String { - { - use count <- option.map(count) - "this command accepts " - <> case count { - EqArgs(0) -> "no arguments" - EqArgs(1) -> "1 argument" - EqArgs(n) -> int.to_string(n) <> " arguments" - MinArgs(n) -> int.to_string(n) <> " or more arguments" - } - } - |> option.unwrap("") -} - -fn named_args_to_notes_string(named: List(String)) -> String { - named - |> list.map(fn(name) { "\"" <> name <> "\"" }) - |> string.join(", ") - |> string_map(fn(s) { "this command has named arguments: " <> s }) -} - -fn args_to_usage_string(count: Option(ArgsCount), named: List(String)) -> String { - case +fn args_to_usage_string( + unnamed: Option(ArgsCount), + named: List(String), +) -> String { + let named_args = named |> list.map(fn(s) { "<" <> s <> ">" }) |> string.join(" ") - { - "" -> - count - |> option.map(args_count_to_usage_string) - |> option.unwrap("[ ARGS ]") - named_args -> - count - |> option.map(fn(count) { - case count { - EqArgs(_) -> named_args - MinArgs(_) -> named_args <> "..." - } - }) - |> option.unwrap(named_args) + let unnamed_args = + option.map(unnamed, args_count_to_usage_string) + |> option.unwrap("[ ARGS ]") + + case named_args, unnamed_args { + "", "" -> "" + "", _ -> unnamed_args + _, "" -> named_args + _, _ -> named_args <> " " <> unnamed_args } } -fn usage_notes(count: Option(ArgsCount), named: List(String)) -> String { - [args_count_to_notes_string(count), named_args_to_notes_string(named)] - |> list.filter_map(fn(elem) { - case elem { - "" -> Error(Nil) - s -> Ok(string.append("\n* ", s)) - } - }) - |> string.concat - |> string_map(fn(s) { "\nnotes:" <> s }) -} - /// convert a CommandHelp to a styled usage block /// fn command_help_to_usage_string(help: CommandHelp, config: Config) -> String { @@ -796,7 +773,7 @@ fn command_help_to_usage_string(help: CommandHelp, config: Config) -> String { let flags = flags_help_to_usage_string(help.flags) - let args = args_to_usage_string(help.count_args, help.named_args) + let args = args_to_usage_string(help.unnamed_args, help.named_args) case config.pretty_help { None -> usage_heading @@ -805,9 +782,11 @@ fn command_help_to_usage_string(help: CommandHelp, config: Config) -> String { <> "\n\t" <> app_name <> string_map(help.meta.name, string.append(" ", _)) - <> string_map(args, fn(s) { " " <> s <> " " }) + <> case args { + "" -> " " + _ -> " " <> args <> " " + } <> flags - <> usage_notes(help.count_args, help.named_args) } // -- HELP - FUNCTIONS - STRINGIFIERS - FLAGS -- diff --git a/test/glint_test.gleam b/test/glint_test.gleam index d692693..d46d030 100644 --- a/test/glint_test.gleam +++ b/test/glint_test.gleam @@ -154,20 +154,23 @@ pub fn help_test() { at: ["cmd1", "cmd3"], do: glint.command(nil) |> glint.flag(flag_3.0, flag_3.1) - |> glint.description("This is cmd3"), + |> glint.description("This is cmd3") + |> glint.unnamed_args(glint.MinArgs(2)) + |> glint.named_args(["woo"]), ) |> glint.add( at: ["cmd1", "cmd4"], do: glint.command(nil) |> glint.flag(flag_4.0, flag_4.1) - |> glint.description("This is cmd4"), + |> glint.description("This is cmd4") + |> glint.unnamed_args(glint.EqArgs(0)), ) |> glint.add( at: ["cmd2"], do: glint.command(nil) |> glint.named_args(["arg1", "arg2"]) - |> glint.count_args(glint.MinArgs(2)) - |> glint.description("This is cmd2"), + |> glint.description("This is cmd2") + |> glint.unnamed_args(glint.EqArgs(0)), ) |> glint.add( at: ["cmd5", "cmd6"], @@ -195,7 +198,7 @@ pub fn help_test() { |> should.equal(Ok(Out(Nil))) glint.execute(cli, ["cmd2", "1", "2", "3"]) - |> should.equal(Ok(Out(Nil))) + |> should.be_error() // help message for root command glint.execute(cli, [glint.help_flag()]) @@ -204,9 +207,7 @@ pub fn help_test() { "This is the root command USAGE: -\tgleam run -m test [ --flag1= --global= ] -notes: -* this command has named arguments: \"arg1\", \"arg2\" +\tgleam run -m test [ ARGS ] [ --flag1= --global= ] FLAGS: \t--flag1=\t\tThis is flag1 @@ -250,7 +251,7 @@ SUBCOMMANDS: This is cmd4 USAGE: -\tgleam run -m test cmd1 cmd4 [ ARGS ] [ --flag4= --global= ] +\tgleam run -m test cmd1 cmd4 [ --flag4= --global= ] FLAGS: \t--flag4=\t\tThis is flag4 @@ -266,12 +267,26 @@ FLAGS: This is cmd2 USAGE: -\tgleam run -m test cmd2 ... [ --global= ] -notes: -* this command accepts 2 or more arguments -* this command has named arguments: \"arg1\", \"arg2\" +\tgleam run -m test cmd2 [ --global= ] + +FLAGS: +\t--global=\t\tThis is a global flag +\t--help\t\t\tPrint help information", + )), + ) + + // help message for command with no additional flags + glint.execute(cli, ["cmd1", "cmd3", glint.help_flag()]) + |> should.equal( + Ok(Help( + "cmd1 cmd3 +This is cmd3 + +USAGE: +\tgleam run -m test cmd1 cmd3 [ 2 or more arguments ] [ --flag3= --global= ] FLAGS: +\t--flag3=\t\tThis is flag3 \t--global=\t\tThis is a global flag \t--help\t\t\tPrint help information", )), @@ -308,7 +323,7 @@ pub fn global_flags_test() { ), ) - // root command keeps the global flag as an int + // root command keeps the global flag as an int cli |> glint.execute(["--f=2"]) |> should.be_ok