Code Monkey home page Code Monkey logo

glint's Introduction

glint

Warning

This README is being updated in preparation for glint v1.0.0. For documentation on the latest released glint versions please see hexdocs.

Hex Package Hex.pm Hex Docs GitHub Workflow Status

Gleam command-line argument parsing with flags and automatic help text.

Installation

To install from hex:

gleam add glint

Usage

Glint has 3 main concepts (see below for more details): glint itself, commands and flags.

The general workflow involves

  1. creating a new glint instance with glint.new
  2. configuring it
  3. creating commands with glint.command
    • attach flags with glint.flag
    • set named args with glint.named_arg
    • set unnamed args with glint.unnamed_args
  4. attach the commands to glint with glint.add
  5. run your glint app with glint.run or glint.run_and_handle

Help text

Note:Help text is generated and printed whenever a glint command is called with the built-in flag --help. It is also printed after the error text when any errors are encountered due to invalid flags or arguments.

Help text descriptions can be attached to all of glint's components:

  • attach global help text with glint.global_help
  • attach comand help text with glint.command_help
  • attach flag help text with glint.flag_help
  • attach help text to a non-initialized command with glint.path_help

Mini Example

You can import glint as a dependency and use it to build command-line applications like the following simplified version of the the hello world example.

// stdlib imports
import gleam/io
import gleam/list
import gleam/result
import gleam/string.{uppercase}
// external dep imports
import snag
import argv
// glint imports
import glint

// this function returns the builder for the caps flag
fn caps_flag() -> glint.Flag(Bool) {
  // create a new boolean flag with key "caps"
  // this flag will be called as --caps=true (or simply --caps as glint handles boolean flags in a bit of a special manner) from the command line
  glint.bool_flag("caps")
  // set the flag default value to False
  |> glint.flag_default(False)
  //  set the flag help text
  |> glint.flag_help("Capitalize the hello message")
}

/// the glint command that will be executed
///
fn hello() -> glint.Command(Nil) {
  // set the help text for the hello command
  use <- glint.command_help("Prints Hello, <NAME>!")
  // register the caps flag with the command
  // the `caps` variable there is a type-safe getter for the flag value
  use caps <- glint.flag(caps_flag())
  // start the body of the command
  // this is what will be executed when the command is called
  use _, args, flags <- glint.command()
  // we can assert here because the caps flag has a default
  // and will therefore always have a value assigned to it
  let assert Ok(caps) = caps(flags)
  // this is where the business logic of our command starts
  let name = case 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(at: [], do: hello)
  // execute given arguments from stdin
  |> glint.run(argv.load().arguments)
}

Glint at-a-glance

Glint core: glint.Glint(a)

glint is conceptually quite small, your general flow will be:

  • create a new glint instance with glint.new.
  • configure glint with functions like glint.with_pretty_help.
  • add commands with glint.add.
  • run your cli with glint.run, run with a function to handle command output with glint.run_and_handle.

Glint commands: glint.Command(a)

Note: Glint commands are most easily set up by chaining functions with use. (See the above example)

  • Create a new command with glint.command.
  • Set the command description with glint.command_help.
  • Add a flag to a command with glint.flag.
  • Create a named argumend with glint.named_arg.
  • Set the expectation for unnamed args with glint.unnamed_args.

Glint flags: glint.Flag(a)

Glint flags are a type-safe way to provide options to your commands.

  • Create a new flag with a typed flag constructor function:

    • glint.int_flag: glint.Flag(Int)
    • glint.ints_flag: glint.Flag(List(Int))
    • glint.float_flag: glint.Flag(Float)
    • glint.floats_flag: glint.Flag(List(Floats))
    • glint.string_flag: glint.Flag(String)
    • glint.strings_flag: glint.Flag(List(String))
    • glint.bool_flag: glint.Flag(Bool)
  • Set the flag description with glint.flag_help

  • Set the flag default value with glint.flag_default, note: it is safe to use let assert when fetching values for flags with default values.

  • Add a flag to a command with glint.flag.

  • Add a constraint.Constraint(a) to a glint.Flag(a) with glint.flag_constraint

Glint flag constraints: constraint.Constraint(a)

Constraints are functions of shape fn(a) -> Result(a, snag.Snag) that are executed after a flag value has been successfully parsed, all constraints applied to a flag must succeed for that flag to be successfully processed.

Constraints can be any function so long as it satisfies the required type signature, and are useful for ensuring that data is correctly shaped before your glint commands are executed. This reduces unnecessary checks polluting the business logic of your commands.

Here is an example of a constraint that guarantees a processed integer flag will be a positive number.

Note that constraints can both nicely be set up via pipes (|>) or with use.

import glint
import snag
// ...
// with pipes
glint.int_flag("my_int")
|> glint.flag_default(0)
|> glint.constraint(fn(i){
  case i < 0 {
    True -> snag.error("cannot be negative")
    False -> Ok(i)
  }
})
// or
// with use
use i <- glint.flag_constraint(
  glint.int_flag("my_int")
  |> glint.flag_default(0)
)
case i < 0 {
  True -> snag.error("cannot be negative")
  False -> Ok(i)
}

The glint/constraint module provides a few helpful utilities for applying constraints, namely

  • constraint.one_of: ensures that a value is one of some list of allowed values.
  • constraint.none_of: ensures that a value is not one of some list of disallowed values.
  • constraint.each: applies a constraint on individual items in a list of values (useful for applying constraints like one_of and none_of to lists.

The following example demonstrates how to constrain a glint.Flag(List(Int)) to only allow the values 1, 2, 3 or 4 by combining constraint.each with constraint.one_of

import glint
import glint/constraint
import snag
// ...
glint.ints_flag("my_ints")
|> glint.flag_default([])
|> glint.flag_constraint(
  [1, 2, 3, 4]
  |> constraint.one_of
  |> constraint.each
)

✨ Complementary packages

Glint works amazingly with these other packages:

  • argv, use this for cross-platform argument fetching
  • gleescript, use this to generate erlang escripts for your applications

glint's People

Contributors

czepluch avatar tanklesxl avatar tynanbe avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

glint's Issues

Consider adding enum-flag

I did some very quick proof on concept on how to add an enum-flag. Maybe this can be a starting-point for the design.

import gleam/io
import gleam/map

pub type Enum {
  Foo
  Bar
}

pub fn main() {
  let type_map = map.from_list([#("foo", Foo), #("bar", Bar)])

  assert Ok(enum) =
    type_map
    |> map.get("bar")

  use_enum(enum)
}

fn use_enum(enum: Enum) {
  enum
  |> io.debug()
}

The idea is that the user provides a type_map with the config which can be used to parse and construct enum-values based on a string argument. I think the mapping needs to come from userland. I had an other idea where I could use atoms to construct the enum values on erlang-side, but could not find a way to that on js.

✨ Add an option to show `gleam run -m` in the "Usage" section of the default help text.

Lustre's cli tooling will never be distributed outside of the Gleam package and so any of the commands will always be invoked by Gleam. I'd like the ability to indicate this in the usage text so users don't get confused thinking I might have added something to their PATH etc.

  % gleam run -m lustre/try -- --help          
    Compiling lustre
     Compiled in 0.18s
      Running lustre/try.main
  USAGE:
-         lustre/try [ ARGS ] [ --host=<STRING> --include-styles=<BOOL> --port=<INT> ]
+         gleam run -m lustre/try [ ARGS ] -- [ --host=<STRING> --include-styles=<BOOL> --port=<INT> ]

  FLAGS:
          --help                  Print help information
          --host=<STRING>         The host to run the server on
          --include-styles=<BOOL>         Include lustre_ui's sty

It would be extra great if the help text also showed the required (or at least, sometimes required depending on how clap decides to work that day) -- before including flags as I suspect that will be a frequent cause of confusion for folks.

✨ Add the ability to hide `[ ARGS ]` from default help text.

I'm experimenting with using glint to configure lustre's preview web server. The default help text prints like this:

% gleam run -m lustre/try -- --help
  Compiling lustre
   Compiled in 0.18s
    Running lustre/try.main
USAGE:
        lustre/try [ ARGS ] [ --host=<INT> --include-styles=<BOOL> --port=<STRING> ]

FLAGS:
        --help                  Print help information
        --host=<INT>            The port to run the server on
        --include-styles=<BOOL>         Include lustre_ui's stylesheet in your app.
        --port=<STRING>         The host to run the server on

This is really great except for the fact that this command will never have anything that could be a valid arg and showing [ ARGS ] makes it sound possible. I'd rather not write my own help text from scratch, so it'd be helpful if glint had a way for me to say "this thing doesn't take arguments, don't document them" somewhere.

Add Support for Shell Completion

In order to support shell completion, we'll need something like a subcommand:

$ glint_cli __complete list of arguments h
completion1  description1
completion2  description2

Then we can use a version of cobras shell completion logic:
https://github.com/spf13/cobra/blob/main/bash_completionsV2.go

I'm happy to do the PR that ports the completion logic for each shell from cobra,
but glint will need to support the hidden subcommand to make that work.

Where does `flag` come from in the example?

In the minimal example in the readme there is the line:

fn caps_flag() -> flag.FlagBuilder(Bool) {

I don't understand where flag is coming from. Tried replacing it with snag, but that also doesn't work.

Subcommand and flag descriptions are inconsistently formatted.

When displaying the description for a flag, the description is presented inline:

FLAGS:
    --help          Print help information
    --host=<STRING>     The host to run the server on.
    --no-styles=<BOOL>      When false, lustre/ui's styles will not be included.
    --port=<INT>        The port to run the server on.

Contrast this to the description of a subcommand:

SUBCOMMANDS:
    app
Build and bundle an entire Lustre application. The generated JavaScript module
calls your app's `main` function on page load and can be included in any Web
page without Gleam or Lustre being present.
    
    component
Build a Lustre component as a portable Web Component. The generated JavaScript
module can be included in any Web page and used without Gleam or Lustre being
present.

I think these should be formatted consistently, and in a way that handles the line-breaks sensibly. Perhaps something like:

SUBCOMMANDS:
    app        Build and bundle an entire Lustre application. The generated JavaScript module
               calls your app's `main` function on page load and can be included in any Web
               page without Gleam or Lustre being present.
    
    component  Build a Lustre component as a portable Web Component. The generated JavaScript
               module can be included in any Web page and used without Gleam or Lustre being
               present.

Customizable Help Text

I want to create a CLI app for my Globe Backend and i would use deno to compile it to a single executable ( until we get native) and i want to be able to customize more the help text. Now the help text looks like that:

Prints Hello, <NAME>!

USAGE:
        'gleam run <NAME>' or 'gleam run <NAME> --caps'

FLAGS:
        --help                             Print help information
        --caps=<CAPS>           Capitalize the provided name

but in the USAGE: section I want to be able to change it to:

USAGE:
        'globe <NAME>' or 'globe <NAME> --caps'

EDIT: Also it would be nice to be able to add a header above description :-P

✨ Add a `with_description` builder for the overall program.

For some hand-formatted cli help text, I've found it useful to include a brief description of the program at the top (see below). I think glint could benefit from similar functionality.

Build and bundle Lustre applications and components for deployment and use
outside of Gleam.

Usage:
  gleam run -m lustre/build <command>

Options:
  -- --help   Show this help message.
  --minify    Minify the output.

Commands:
  app         Build a complete Lustre app for deployment.
  component   Bundle a Lustre component for use in other Web apps.

Generate help text with the Glam package

Help text is currently done with custom code, this leads to potential issues like incorrect spacing for flags when one has a long name, use the glam package to do this printing instead

New `notes:` help output feels superfluous.

With the addition of named args as well as the ability to require a specific number of args, there is a new notes: section in the help output.

% gleam run -m lustre build component --help
   Compiled in 0.02s
    Running lustre.main
build component

Build a Lustre component as a portable Web Component. The generated JavaScript
module can be included in any Web page and used without Gleam or Lustre being
present.

Hint: Use the `--minify` flag to produce and smaller bundle for production.

USAGE:
    gleam run -m lustre build component <module_path> [ --minify=<BOOL> ]
notes:
* this command accepts 1 argument
* this command has named arguments: "module_path"

FLAGS:
    --help          Print help information
    --minify=<BOOL>     Minify the output

I think this has a few oddities:

  • It should have a preceding newline to separate it from USAGE
  • It should be in all-caps to match USAGE and FLAGS
  • It repeats information the user can already see from the usage:
    • "named arguments" are a developer-facing implementation detail
    • the number of arguments accepted is already documented in the usage above

Add the ability to provide a description for parent commands.

I have two commands add esbuild and add tailwind. If I run --help on the overall program I get:

% gleam run -m lustre/dev -- --help
  Compiling lustre_dev_tools
   Compiled in 0.17s
    Running lustre/dev.main
USAGE:
	gleam run -m lustre/dev [ ARGS ]

SUBCOMMANDS:
	add

I'd like to add a description for what the add family of commands do, without writing a proper command for it.

Example program "caps" flag capitalizes the whole message and not just the name

The caps flag description is "Capitalized the provided name", however the uppercase function is applied to msg, not name. This results in: HELLO, JOE! and not Hello, JOE!.

The correct code could be something like this:

let name = 
  case input.args {
    ...
  }
let name = case caps {
  True -> uppercase(name)
  False -> name
}

There appears to be a similar bug in the hello example.

🐛 Incorrect indentation in help text for long flags.

By the looks of it, glint my have an off-by-one error when calculating how to indent the descriptions for flags that is causing certain flags to be indented a level further than they should:

% gleam run -m lustre/try -- --help
  Compiling lustre
   Compiled in 0.18s
    Running lustre/try.main
USAGE:
        lustre/try [ ARGS ] [ --host=<INT> --include-styles=<BOOL> --port=<STRING> ]

FLAGS:
        --help                  Print help information
        --host=<INT>            The port to run the server on
        --include-styles=<BOOL>         Include lustre_ui's stylesheet in your app.
        --port=<STRING>         The host to run the server on

The default flag value possible improvements

Thanks for the library! It's good! The nice QoL improvement would be either parametrization of flag output type, or make it at least Option(a). Providing default "a" value for every flag, and then making additional clause which handles default value does not feel very good. Best regards!

Improve global flags

Add a way to have group flags available at each level for each subtree of commands

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.