Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
TanklesXL committed Feb 19, 2024
1 parent 54f277d commit 6833bc0
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 79 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- 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
- change `glint.count_args` to `glint.unnamed_args`

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

Expand Down
44 changes: 38 additions & 6 deletions examples/hello/src/hello.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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, <names>!")
// 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()

Expand All @@ -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, <names>!")
// with a first arg called name
|> glint.description("Prints Hello, <name>!")
// with a named arg called 'name'
|> glint.named_args(["name"])
// requiring at least 1 argument
|> glint.count_args(glint.MinArgs(1))
// with at least 1 unnamed argument
|> glint.unnamed_args(glint.EqArgs(0))
}

// the function that describes our cli structure
Expand All @@ -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() {
Expand Down
109 changes: 47 additions & 62 deletions src/glint.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
}
Expand Down Expand Up @@ -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: [],
)
}
Expand All @@ -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
///
Expand Down Expand Up @@ -436,23 +436,33 @@ 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))
|> string.join(", ")
},
)
|> snag.context("invalid number of arguments provided")
}
})

let args = list.drop(args, dict.size(named_args))

use _ <- result.map(case contents.unnamed_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))

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
}
Expand Down Expand Up @@ -594,8 +604,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),
)
Expand All @@ -610,12 +620,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,
)
}
Expand All @@ -624,7 +634,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,
)
}
Expand Down Expand Up @@ -730,48 +740,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
Expand All @@ -785,7 +771,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
Expand All @@ -799,7 +785,6 @@ fn command_help_to_usage_string(help: CommandHelp, config: Config) -> String {
_ -> " " <> args <> " "
}
<> flags
<> usage_notes(help.count_args)
}

// -- HELP - FUNCTIONS - STRINGIFIERS - FLAGS --
Expand Down
37 changes: 26 additions & 11 deletions test/glint_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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()])
Expand All @@ -205,7 +207,7 @@ pub fn help_test() {
"This is the root command
USAGE:
\tgleam run -m test <arg1> <arg2> [ --flag1=<STRING> --global=<STRING> ]
\tgleam run -m test <arg1> <arg2> [ ARGS ] [ --flag1=<STRING> --global=<STRING> ]
FLAGS:
\t--flag1=<STRING>\t\tThis is flag1
Expand Down Expand Up @@ -250,8 +252,6 @@ This is cmd4
USAGE:
\tgleam run -m test cmd1 cmd4 [ --flag4=<FLOAT> --global=<STRING> ]
notes:
* this command accepts no arguments
FLAGS:
\t--flag4=<FLOAT>\t\tThis is flag4
Expand All @@ -267,11 +267,26 @@ FLAGS:
This is cmd2
USAGE:
\tgleam run -m test cmd2 <arg1> <arg2>... [ --global=<STRING> ]
notes:
* this command accepts 2 or more arguments
\tgleam run -m test cmd2 <arg1> <arg2> [ --global=<STRING> ]
FLAGS:
\t--global=<STRING>\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 <woo> [ 2 or more arguments ] [ --flag3=<BOOL> --global=<STRING> ]
FLAGS:
\t--flag3=<BOOL>\t\tThis is flag3
\t--global=<STRING>\t\tThis is a global flag
\t--help\t\t\tPrint help information",
)),
Expand Down

0 comments on commit 6833bc0

Please sign in to comment.