diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ba50d5..524e487 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ - `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 notes +- 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/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 db9c5d7..a8c9c90 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 /// @@ -436,23 +436,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 +605,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 +621,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 +635,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,48 +741,24 @@ 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 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) - } -} - -fn usage_notes(count: Option(ArgsCount)) -> String { - case args_count_to_notes_string(count) { - "" -> "" - s -> "\n* " <> s + 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 } - |> string_map(fn(s) { "\nnotes:" <> s }) } /// convert a CommandHelp to a styled usage block @@ -785,7 +772,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 @@ -799,7 +786,6 @@ fn command_help_to_usage_string(help: CommandHelp, config: Config) -> String { _ -> " " <> args <> " " } <> flags - <> usage_notes(help.count_args) } // -- HELP - FUNCTIONS - STRINGIFIERS - FLAGS -- diff --git a/test/glint_test.gleam b/test/glint_test.gleam index 27a7f36..ddcec5a 100644 --- a/test/glint_test.gleam +++ b/test/glint_test.gleam @@ -154,21 +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.count_args(glint.EqArgs(0)), + |> 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"], @@ -196,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()]) @@ -205,7 +207,7 @@ pub fn help_test() { "This is the root command USAGE: -\tgleam run -m test [ --flag1= --global= ] +\tgleam run -m test [ ARGS ] [ --flag1= --global= ] FLAGS: \t--flag1=\t\tThis is flag1 @@ -250,8 +252,6 @@ This is cmd4 USAGE: \tgleam run -m test cmd1 cmd4 [ --flag4= --global= ] -notes: -* this command accepts no arguments FLAGS: \t--flag4=\t\tThis is flag4 @@ -267,11 +267,26 @@ FLAGS: This is cmd2 USAGE: -\tgleam run -m test cmd2 ... [ --global= ] -notes: -* this command accepts 2 or more arguments +\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", )),