diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1ccab0e..d004411 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,7 +7,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - gleam: ["0.30.0-rc3"] + erlang: ["25.3.2.3", "26.0.2"] + gleam: ["0.30.3"] steps: - uses: actions/checkout@v2 - uses: ./.github/actions/test diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d83ac1d..ee6f8d3 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: "0.30.0-rc3" + gleam-version: "0.30.3" - name: publish to hex env: HEXPM_USER: ${{ secrets.HEXPM_USER }} diff --git a/.tool-versions b/.tool-versions index 50d8620..d968c6a 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -gleam 0.30.0-rc4 +gleam 0.30.3 diff --git a/README.md b/README.md index 51509cf..72f658b 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,90 @@ gleam add glint ## Usage -You can import `glint` as a dependency and use it as follows: +### Glint's Core -... +`glint` is conceptually quite small, your general flow will be: + +1. create a new glint instance with `glint.new` +1. configure it with `glint.with_pretty_help` and other configuration functions +1. add commands with `glint.add` + 1. create a new command with `glint.cmd` + 1. assign that command any flags required + 1. assign the command a custom description +1. run your cli with `glnt.run`, run with a function to handle command output with `glint.run_and_handle` + +### Mini Example + +You can import `glint` as a dependency and use it to build simple command-line applications like the following simplified version of the [the hello world example](./examples/hello/README.md) + +```gleam +// stdlib imports +import gleam/io +import gleam/list +import gleam/result +import gleam/string.{uppercase} +// external dep imports +import snag +// glint imports +import glint +import glint/flag +// erlang-specific imports + +@target(erlang) +import gleam/erlang.{start_arguments} + +/// the key for the caps flag +const caps = "caps" + +/// a boolean flag with default False to control message capitalization. +/// +fn caps_flag() -> flag.FlagBuilder(Bool) { + flag.new(flag.B) + |> flag.default(False) + |> flag.description("Capitalize the provided name") +} + +/// the command function that will be executed +/// +fn hello(input: glint.CommandInput) -> Nil { + let assert Ok(caps) = flag.get_bool(from: input.flags, for: caps) + + let name = + case input.args { + [] -> "Joe" + [name,..] -> name + } + + let msg = "Hello, " <> name <> "!" + + + case caps { + True -> uppercase(msg) + False -> msg + } + |> io.println +} + +pub fn main() { + // create a new glint instance + glint.new() + // with an app name of "hello", this is used when printing help text + |> glint.with_name("hello") + // with pretty help enabled, using the built-in colours + |> glint.with_pretty_help(glint.default_pretty_help()) + // with a root command that executes the `hello` function + |> glint.add( + // add the command to the root + at: [], + // create the command, add any flags + do: glint.command(hello) + // with flag `caps` + |> glint.flag(caps, caps_flag()) + // with flag `repeat` + |> glint.flag(repeat, repeat_flag()) + // with a short description + |> glint.description("Prints Hello, !"), + ) + |> glint.run(start_arguments()) +} +``` diff --git a/examples/hello/.github/workflows/test.yml b/examples/hello/.github/workflows/test.yml index ec75407..a13562c 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: "0.30.0-rc3" + gleam-version: "0.30.3" rebar3-version: "3" # elixir-version: "1.14.2" - run: gleam format --check src test diff --git a/examples/hello/.tool-versions b/examples/hello/.tool-versions deleted file mode 100644 index 8295f32..0000000 --- a/examples/hello/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -gleam 0.30.0-rc3 diff --git a/examples/hello/README.md b/examples/hello/README.md index 4a921ba..2d57891 100644 --- a/examples/hello/README.md +++ b/examples/hello/README.md @@ -1,24 +1,41 @@ # hello -[![Package Version](https://img.shields.io/hexpm/v/hello)](https://hex.pm/packages/hello) -[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/hello/) +This is an example project that demonstrates a simple workflow using `glint`. +It contains only one command, the root command. +Feel free to browse `src/hello.gleam` to get a sense of how a small cli application is written. -A Gleam project +## Usage -## Quick start +### Running the application -```sh -gleam run # Run the project -gleam test # Run the tests -gleam shell # Run an Erlang shell -``` +You can run this example from the `examples/hello` directory by calling `gleam run` which prints `Hello, !` -## Installation +The `hello` application accepts any number of arguments, being the names of people to say hello to. -If available on Hex this package can be added to your Gleam project: +- No input: `gleam run` -> prints "Hello, Joe!" +- One input: `gleam run Rob` -> prints "Hello, Rob!" +- Two inputs: `gleam run Rob Louis` -> prints "Hello, Rob and Louis!" +- \>2 inputs: `gleam run Rob Louis Hayleigh` -> prints "Hello, Rob, Louis and Hayleigh!" -```sh -gleam add hello -``` +### Flags + +The root command accepts two flags: + +- `--caps`: capitalizes the output, so if output would be "Hello, Joe!" it prints "HELLO, JOE!" +- `--repeat=N`: repeats the output N times separated , so with N=2 if output would be "Hello, Joe!" it prints "Hello, Joe!\nHello, Joe!" + +## Help Output -and its documentation can be found at . +Generated help output for the root command is as follows + +```txt +Prints Hello, ! + +USAGE: + hello [ ARGS ] [ --caps= --repeat= ] + +FLAGS: + --caps= Capitalize the provided name + --help Print help information + --repeat= Repeat the message n-times +``` diff --git a/examples/hello/src/hello.gleam b/examples/hello/src/hello.gleam index cf615ce..4291ea4 100644 --- a/examples/hello/src/hello.gleam +++ b/examples/hello/src/hello.gleam @@ -1,9 +1,7 @@ // stdlib imports import gleam/io import gleam/list -import gleam/result -import gleam/string.{join, uppercase} -import gleam/function.{compose} +import gleam/string.{uppercase} // external dep imports import snag // glint imports @@ -13,24 +11,66 @@ import glint/flag @target(erlang) import gleam/erlang.{start_arguments} +// ----- APPLICATION LOGIC ----- + +/// a helper function to join a list of names +fn join_names(names: List(String)) -> String { + case names { + [] -> "Joe" + [name] -> name + [name, ..rest] -> do_join_names(rest, name) + } +} + +// tail-recursive implementation of join_naemes +fn do_join_names(names: List(String), acc: String) { + case names { + [] -> acc + [a] -> acc <> " and " <> a + [a, ..b] -> do_join_names(b, acc <> ", " <> a) + } +} + +pub fn message(names: List(String)) { + "Hello, " <> join_names(names) <> "!" +} + +pub fn capitalize(msg, caps) -> String { + case caps { + True -> uppercase(msg) + False -> msg + } +} + +/// hello is a function that +pub fn hello(names: List(String), caps: Bool, repeat: Int) -> String { + names + |> message + |> capitalize(caps) + |> list.repeat(repeat) + |> string.join("\n") +} + +// ----- CLI SETUP ----- + /// the key for the caps flag -const caps = "caps" +pub const caps = "caps" /// a boolean flag with default False to control message capitalization. /// -fn caps_flag() -> flag.FlagBuilder(Bool) { +pub fn caps_flag() -> flag.FlagBuilder(Bool) { flag.new(flag.B) |> flag.default(False) |> flag.description("Capitalize the provided name") } /// the key for the repeat flag -const repeat = "repeat" +pub const repeat = "repeat" /// an int flag with default 1 to control how many times to repeat the message. /// this flag has the `gtz` constraint applied to it. /// -fn repeat_flag() -> flag.FlagBuilder(Int) { +pub fn repeat_flag() -> flag.FlagBuilder(Int) { flag.new(flag.I) |> flag.default(1) |> flag.constraint(gtz) @@ -46,58 +86,44 @@ fn gtz(n: Int) -> snag.Result(Nil) { } } -/// the command function that will be executed +/// the command function that will be executed as the root command /// -fn hello(input: glint.CommandInput) -> snag.Result(String) { - let assert Ok(caps) = flag.get_bool(from: input.flags, for: caps) - let assert Ok(repeat) = flag.get_int(from: input.flags, for: repeat) - - use name <- result.map(case input.args { - [] -> snag.error("no arguments provided") - _ -> Ok(input.args) - }) - - ["Hello,", ..name] - |> case caps { - True -> compose(join(_, " "), uppercase) - False -> join(_, " ") +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 + hello(input.args, caps, repeat) } - |> string.append("!") - |> list.repeat(repeat) - |> string.join("\n") + // with flag `caps` + |> glint.flag(caps, caps_flag()) + // with flag `repeat` + |> glint.flag(repeat, repeat_flag()) + // with flag `repeat` + |> glint.description("Prints Hello, !") } -@target(erlang) -pub fn main() { - use res <- glint.run_and_handle( - // create a new glint instance - glint.new() - // with an app name of "hello", this is used when printing help text - |> glint.with_name("hello") - // with pretty help enabled, using the built-in colours - |> glint.with_pretty_help(glint.default_pretty_help()) - // with a root command that executes the `hello` function - |> glint.add( - // add the command to the root - at: [], - // create the command, add any flags - do: glint.command(hello) - // with flag `caps` - |> glint.flag(caps, caps_flag()) - // with flag `repeat` - |> glint.flag(repeat, repeat_flag()) - |> glint.description("Prints Hello, !"), - ), - start_arguments(), +// the function that describes our cli structure +pub fn app() { + // create a new glint instance + glint.new() + // with an app name of "hello", this is used when printing help text + |> glint.with_name("hello") + // with pretty help enabled, using the built-in colours + |> glint.with_pretty_help(glint.default_pretty_help()) + // with a root command that executes the `hello` function + |> glint.add( + // add the hello command to the root + at: [], + do: hello_cmd(), ) +} - // run with a handler that converts the command output to a string and prints it - case res { - Ok(out) -> out - Error(err) -> - err - |> snag.layer("failed to execute command") - |> snag.pretty_print() - } - |> io.println +@target(erlang) +pub fn main() { + // run with a handler that prints the command output + glint.run_and_handle(app(), start_arguments(), io.println) } diff --git a/examples/hello/test/hello_test.gleam b/examples/hello/test/hello_test.gleam index 7e95d32..0bbc0e8 100644 --- a/examples/hello/test/hello_test.gleam +++ b/examples/hello/test/hello_test.gleam @@ -1,7 +1,38 @@ import gleeunit - -// import gleeunit/should +import gleeunit/should +import hello +import gleam/list pub fn main() { gleeunit.main() } + +type TestCase { + TestCase(input: List(String), caps: Bool, repeat: Int, expected: String) +} + +pub fn hello_test() { + use tc <- list.each([ + TestCase([], False, 1, "Hello, Joe!"), + TestCase(["Rob"], False, 1, "Hello, Rob!"), + TestCase([], True, 1, "HELLO, JOE!"), + TestCase([], True, 2, "HELLO, JOE!\nHELLO, JOE!"), + TestCase(["Rob"], True, 1, "HELLO, ROB!"), + TestCase(["Tony", "Maria"], True, 1, "HELLO, TONY AND MARIA!"), + TestCase( + ["Tony", "Maria", "Nadia"], + True, + 1, + "HELLO, TONY, MARIA AND NADIA!", + ), + TestCase(["Tony", "Maria"], False, 1, "Hello, Tony and Maria!"), + TestCase( + ["Tony", "Maria", "Nadia"], + False, + 1, + "Hello, Tony, Maria and Nadia!", + ), + ]) + hello.hello(tc.input, tc.caps, tc.repeat) + |> should.equal(tc.expected) +} diff --git a/gleam.toml b/gleam.toml index fc92fd7..d2a50cb 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "glint" -version = "0.12.0-rc5" +version = "0.12.0" # Fill out these fields if you intend to generate HTML documentation or publish # your project to the Hex package manager. @@ -11,7 +11,7 @@ links = [ { title = "Hex", href = "https://hex.pm/packages/glint" }, { title = "Docs", href = "https://hexdocs.pm/glint/" }, ] - +gleam = ">= 0.30.3" [dependencies] gleam_stdlib = "~> 0.19" snag = "~> 0.2" diff --git a/src/glint/flag.gleam b/src/glint/flag.gleam index 86afc3f..cd05f90 100644 --- a/src/glint/flag.gleam +++ b/src/glint/flag.gleam @@ -335,6 +335,8 @@ pub fn flags_help(flags: Map) -> List(String) { |> list.map(flag_help) } +// -- FLAG ACCESS FUNCTIONS -- + /// Access the contents for the associated flag /// fn access(flags: Map, name: String) -> Result(Flag) {