Skip to content

Commit

Permalink
Fix help text (#29)
Browse files Browse the repository at this point in the history
* fix help text formatting and notes

* Clean up help text generation for named and unnamed args
  • Loading branch information
TanklesXL authored Feb 19, 2024
1 parent 7be5fbe commit f3d7d78
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 100 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion examples/hello/.github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- uses: erlef/[email protected]
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
Expand Down
46 changes: 39 additions & 7 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.named_args(["name"])
// requiring at least 1 argument
|> glint.count_args(glint.MinArgs(1))
|> glint.description("Prints Hello, <name>!")
// 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
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
131 changes: 55 additions & 76 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 @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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),
)
Expand All @@ -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,
)
}
Expand All @@ -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,
)
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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 --
Expand Down
Loading

0 comments on commit f3d7d78

Please sign in to comment.