diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dd4cb22..5f71c9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,16 +1,16 @@ name: GH and Hex.pm Release on: - push: - tags: - - v*.*.* + push: + tags: + - v*.*.* jobs: - release: - uses: TanklesXL/gleam_actions/.github/workflows/release.yaml@main - secrets: inherit - with: - gleam_version: 1.9.1 - erlang_version: 27 - test_erlang: true - test_node: true + release: + uses: TanklesXL/gleam_actions/.github/workflows/release.yaml@main + secrets: inherit + with: + gleam_version: 1.12.0 + erlang_version: 27 + test_erlang: true + test_node: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5979cac..59caf7d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,16 +1,16 @@ name: Test on: - push: - branches: - - main - pull_request: - workflow_call: + push: + branches: + - main + pull_request: + workflow_call: jobs: - test: - uses: TanklesXL/gleam_actions/.github/workflows/test.yaml@main - with: - gleam_version: 1.9.1 - test_node: true - test_erlang: true + test: + uses: TanklesXL/gleam_actions/.github/workflows/test.yaml@main + with: + gleam_version: 1.12.0 + test_node: true + test_erlang: true diff --git a/README.md b/README.md index 9fcf396..ea236d6 100644 --- a/README.md +++ b/README.md @@ -46,11 +46,11 @@ import argv 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") + glint.default("caps") // set the flag default value to False - |> glint.flag_default(False) + |> glint.default(False) // set the flag help text - |> glint.flag_help("Capitalize the hello message") + |> glint.param_help("Capitalize the hello message") } /// the glint command that will be executed @@ -120,16 +120,16 @@ 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)` + - `glint.int`: `glint.Flag(Int)` + - `glint.ints`: `glint.Flag(List(Int))` + - `glint.float`: `glint.Flag(Float)` + - `glint.floats`: `glint.Flag(List(Floats))` + - `glint.string`: `glint.Flag(String)` + - `glint.strings`: `glint.Flag(List(String))` + - `glint.default`: `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. +- Set the flag description with `glint.param_help` +- Set the flag default value with `glint.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` @@ -148,8 +148,8 @@ import glint import snag // ... // with pipes -glint.int_flag("my_int") -|> glint.flag_default(0) +glint.int("my_int") +|> glint.default(0) |> glint.constraint(fn(i){ case i < 0 { True -> snag.error("cannot be negative") @@ -159,8 +159,8 @@ glint.int_flag("my_int") // or // with use use i <- glint.flag_constraint( - glint.int_flag("my_int") - |> glint.flag_default(0) + glint.int("my_int") + |> glint.default(0) ) case i < 0 { True -> snag.error("cannot be negative") @@ -181,8 +181,8 @@ import glint import glint/constraint import snag // ... -glint.ints_flag("my_ints") -|> glint.flag_default([]) +glint.ints("my_ints") +|> glint.default([]) |> glint.flag_constraint( [1, 2, 3, 4] |> constraint.one_of @@ -208,7 +208,7 @@ 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 flag help text with `glint.param_help` - attach help text to a non-initialized command with `glint.path_help` #### Help text formatting diff --git a/birdie_snapshots/cmd1_help.accepted b/birdie_snapshots/cmd1_help.accepted index 3033a1b..0d3d54b 100644 --- a/birdie_snapshots/cmd1_help.accepted +++ b/birdie_snapshots/cmd1_help.accepted @@ -1,32 +1,39 @@ --- -version: 1.1.5 +version: 1.3.1 title: cmd1 help file: ./test/glint_test.gleam test_name: cmd1_help_test --- Some awesome global help text! -Command: cmd1 - This is cmd1 USAGE: - gleam run -m test cmd1 ( cmd3 | cmd4 ) [ ARGS ] [ --flag2= - --global= --very-very-very-long-flag= ] + gleam run -m test cmd1 ( cmd3 | cmd4 ) [ args.. ] [ flags ] -FLAGS: - --flag2= This is flag2 +SUBCOMMANDS: + cmd3 This is cmd3 - --global= This is a global flag + cmd4 This is cmd4 which has a very very very very very very + very very long description - --help Print help information +ARGUMENTS: + args.. >= 0 args - --very-very-very-long-flag= This is a very long flag with a - very very very very very very long - description +FLAGS: + --flag2= This is flag2 -SUBCOMMANDS: - cmd3 This is cmd3 + --global= This is a global + flag - cmd4 This is cmd4 which has a very very very very very very - very very long description \ No newline at end of file + --help Print help + information + + --version Print the app + version + + --very-very-very-long-flag= This is a very long + flag with a very + very very very very + very long + description \ No newline at end of file diff --git a/birdie_snapshots/cmd2_help.accepted b/birdie_snapshots/cmd2_help.accepted index 168c626..37c6f94 100644 --- a/birdie_snapshots/cmd2_help.accepted +++ b/birdie_snapshots/cmd2_help.accepted @@ -1,18 +1,21 @@ --- -version: 1.1.5 +version: 1.2.6 title: cmd2 help file: ./test/glint_test.gleam -test_name: help_test +test_name: cmd2_help_test --- Some awesome global help text! -Command: cmd2 - This is cmd2 USAGE: - gleam run -m test cmd2 [ --global= ] + gleam run -m test cmd2 [ flags ] + +ARGUMENTS: + arg1: float + arg2: string list (comma-separated) FLAGS: - --global= This is a global flag - --help Print help information \ No newline at end of file + --global= This is a global flag + --help Print help information + --version Print the app version \ No newline at end of file diff --git a/birdie_snapshots/cmd3_help.accepted b/birdie_snapshots/cmd3_help.accepted index 3be6824..344ea6c 100644 --- a/birdie_snapshots/cmd3_help.accepted +++ b/birdie_snapshots/cmd3_help.accepted @@ -1,20 +1,22 @@ --- -version: 1.1.5 +version: 1.2.6 title: cmd3 help file: ./test/glint_test.gleam -test_name: help_test +test_name: cmd3_help_test --- Some awesome global help text! -Command: cmd1 cmd3 - This is cmd3 USAGE: - gleam run -m test cmd1 cmd3 [ 2 or more arguments ] [ --flag3= - --global= ] + gleam run -m test cmd1 cmd3 [ args.. ] [ flags ] + +ARGUMENTS: + args.. >= 2 args + woo: bool FLAGS: - --flag3= This is flag3 - --global= This is a global flag - --help Print help information \ No newline at end of file + --flag3= This is flag3 + --global= This is a global flag + --help Print help information + --version Print the app version \ No newline at end of file diff --git a/birdie_snapshots/cmd4_help.accepted b/birdie_snapshots/cmd4_help.accepted index d248e60..ab0b8d7 100644 --- a/birdie_snapshots/cmd4_help.accepted +++ b/birdie_snapshots/cmd4_help.accepted @@ -1,20 +1,19 @@ --- -version: 1.1.5 +version: 1.2.6 title: cmd4 help file: ./test/glint_test.gleam -test_name: help_test +test_name: cmd4_help_test --- Some awesome global help text! -Command: cmd1 cmd4 - This is cmd4 which has a very very very very very very very very long description USAGE: - gleam run -m test cmd1 cmd4 [ --flag4= --global= ] + gleam run -m test cmd1 cmd4 [ flags ] FLAGS: - --flag4= This is flag4 - --global= This is a global flag - --help Print help information \ No newline at end of file + --flag4= This is flag4 + --global= This is a global flag + --help Print help information + --version Print the app version \ No newline at end of file diff --git a/birdie_snapshots/cmd6_help.accepted b/birdie_snapshots/cmd6_help.accepted index 3bebc52..6b11233 100644 --- a/birdie_snapshots/cmd6_help.accepted +++ b/birdie_snapshots/cmd6_help.accepted @@ -1,21 +1,23 @@ --- -version: 1.1.5 +version: 1.3.1 title: cmd6 help file: ./test/glint_test.gleam -test_name: help_test +test_name: cmd6_help_test --- Some awesome global help text! -Command: cmd5 cmd6 - This is cmd6 USAGE: - gleam run -m test cmd5 cmd6 ( cmd7 ) [ ARGS ] [ --global= ] + gleam run -m test cmd5 cmd6 ( cmd7 ) [ args.. ] [ flags ] + +SUBCOMMANDS: + cmd7 This is cmd7 + +ARGUMENTS: + args.. >= 0 args FLAGS: - --global= This is a global flag + --global= This is a global flag --help Print help information - -SUBCOMMANDS: - cmd7 This is cmd7 \ No newline at end of file + --version Print the app version \ No newline at end of file diff --git a/birdie_snapshots/cmd6_help_with_residual_args.accepted b/birdie_snapshots/cmd6_help_with_residual_args.accepted index 25889a4..bdd1bf7 100644 --- a/birdie_snapshots/cmd6_help_with_residual_args.accepted +++ b/birdie_snapshots/cmd6_help_with_residual_args.accepted @@ -1,21 +1,23 @@ --- -version: 1.1.8 +version: 1.3.1 title: cmd6 help with residual args file: ./test/glint_test.gleam test_name: call_help_with_residual_args_test --- Some awesome global help text! -Command: cmd5 cmd6 - This is cmd6 USAGE: - gleam run -m test cmd5 cmd6 ( cmd7 ) [ ARGS ] [ --global= ] + gleam run -m test cmd5 cmd6 ( cmd7 ) [ args.. ] [ flags ] + +SUBCOMMANDS: + cmd7 This is cmd7 + +ARGUMENTS: + args.. >= 0 args FLAGS: - --global= This is a global flag + --global= This is a global flag --help Print help information - -SUBCOMMANDS: - cmd7 This is cmd7 \ No newline at end of file + --version Print the app version \ No newline at end of file diff --git a/birdie_snapshots/cmd7_help.accepted b/birdie_snapshots/cmd7_help.accepted index 33d5103..c5b6b4b 100644 --- a/birdie_snapshots/cmd7_help.accepted +++ b/birdie_snapshots/cmd7_help.accepted @@ -1,14 +1,15 @@ --- -version: 1.1.5 +version: 1.3.1 title: cmd7 help file: ./test/glint_test.gleam -test_name: help_test +test_name: cmd7_help_test --- Some awesome global help text! -Command: cmd5 cmd6 cmd7 - This is cmd7 USAGE: - gleam run -m test cmd5 cmd6 cmd7 [ ARGS ] \ No newline at end of file + gleam run -m test cmd5 cmd6 cmd7 [ args.. ] + +ARGUMENTS: + args.. >= 0 args \ No newline at end of file diff --git a/birdie_snapshots/cmd7_help_with_residual_args.accepted b/birdie_snapshots/cmd7_help_with_residual_args.accepted index 729e69f..15da330 100644 --- a/birdie_snapshots/cmd7_help_with_residual_args.accepted +++ b/birdie_snapshots/cmd7_help_with_residual_args.accepted @@ -1,14 +1,15 @@ --- -version: 1.1.8 +version: 1.3.1 title: cmd7 help with residual args file: ./test/glint_test.gleam test_name: call_leaf_help_with_residual_args_test --- Some awesome global help text! -Command: cmd5 cmd6 cmd7 - This is cmd7 USAGE: - gleam run -m test cmd5 cmd6 cmd7 [ ARGS ] \ No newline at end of file + gleam run -m test cmd5 cmd6 cmd7 [ args.. ] + +ARGUMENTS: + args.. >= 0 args \ No newline at end of file diff --git a/birdie_snapshots/root_help.accepted b/birdie_snapshots/root_help.accepted index 568d124..8056afc 100644 --- a/birdie_snapshots/root_help.accepted +++ b/birdie_snapshots/root_help.accepted @@ -1,5 +1,5 @@ --- -version: 1.1.5 +version: 1.3.1 title: root help file: ./test/glint_test.gleam test_name: root_help_test @@ -10,12 +10,7 @@ This is the root command USAGE: gleam run -m test ( cmd1 | cmd2 | cmd5 | cmd8-very-very-very-very-long ) - [ ARGS ] [ --flag1= --global= ] - -FLAGS: - --flag1= This is flag1 - --global= This is a global flag - --help Print help information + [ args.. ] [ flags ] SUBCOMMANDS: cmd1 This is cmd1 @@ -32,4 +27,15 @@ SUBCOMMANDS: New new line - New new new line. \ No newline at end of file + New new new line. + +ARGUMENTS: + arg1: string + arg2: int + args.. >= 0 args + +FLAGS: + --flag1= This is flag1 + --global= This is a global flag + --help Print help information + --version Print the app version \ No newline at end of file diff --git a/gleam.toml b/gleam.toml index e4fe7d8..889e966 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "glint" -version = "1.2.1" +version = "2.0.0-rc1" # Fill out these fields if you intend to generate HTML documentation or publishname = "glint" # your project to the Hex package manager. @@ -15,15 +15,18 @@ links = [ { title = "Docs", href = "https://hexdocs.pm/glint/" }, ] -gleam = ">= 1.4.0 and < 2.0.0" +gleam = ">= 1.11.0 and < 2.0.0" [dependencies] gleam_stdlib = ">= 0.43.0 and < 2.0.0" snag = ">= 1.0.0 and < 2.0.0" gleam_community_ansi = "~> 1.0" gleam_community_colour = "~> 1.0 or ~> 2.0" +lenient_parse = ">= 3.0.0 and < 4.0.0" +gleam_deque = ">= 1.0.0 and < 2.0.0" [dev-dependencies] gleeunit = "~> 1.0" argv = ">= 1.0.2 and < 2.0.0" birdie = ">= 1.1.8 and < 2.0.0" +asset = ">= 1.2.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index ff734d4..0cbff20 100644 --- a/manifest.toml +++ b/manifest.toml @@ -3,21 +3,24 @@ packages = [ { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, - { name = "birdie", version = "1.2.6", build_tools = ["gleam"], requirements = ["argv", "edit_distance", "filepath", "glance", "gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "justin", "rank", "simplifile", "term_size", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "1363F4C7E7433A4A8350CC682BCDDBA5BBC6F66C94EFC63BC43025F796C4F6D0" }, + { name = "asset", version = "1.2.0", build_tools = ["gleam"], requirements = ["argv", "filepath", "glance", "gleam_stdlib", "glexer", "simplifile"], otp_app = "asset", source = "hex", outer_checksum = "71B572581CE75CEA23F444EB56CD845DB64766FB5D52573DAE5F465D752526A5" }, + { name = "bigi", version = "3.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "bigi", source = "hex", outer_checksum = "5BE723D3F5C9BCD7C20C432FEDD9EF55AC8B357BD0592433E275483F3F14CB98" }, + { name = "birdie", version = "1.3.1", build_tools = ["gleam"], requirements = ["argv", "edit_distance", "filepath", "glance", "gleam_community_ansi", "gleam_stdlib", "justin", "rank", "simplifile", "term_size", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "F811C9EDAF920EF48597A26E788907AAF80D9239A5E8C8CCFBD0DD1BB10184D7" }, { name = "edit_distance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "A1E485C69A70210223E46E63985FA1008B8B2DDA9848B7897469171B29020C05" }, - { name = "filepath", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "67A6D15FB39EEB69DD31F8C145BB5A421790581BD6AA14B33D64D5A55DBD6587" }, - { name = "glance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "106111453AE9BA959184302B7DADF2E8CF322B27A7CB68EE78F3EE43FEACCE2C" }, + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, + { name = "glance", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "FAA3DAC74AF71D47C67D88EB32CE629075169F878D148BB1FF225439BE30070A" }, { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" }, - { name = "gleam_community_colour", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "FDD6AC62C6EC8506C005949A4FCEF032038191D5EAAEC3C9A203CD53AE956ACA" }, - { name = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" }, - { name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" }, - { name = "gleam_regexp", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "7F5E0C0BBEB3C58E57C9CB05FA9002F970C85AD4A63BA1E55CBCB35C15809179" }, - { name = "gleam_stdlib", version = "0.57.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86EFACDF6460B8681E82752C5490F9630EC0F138F88A037DDCB241799AA8811F" }, - { name = "gleeunit", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "0E6C83834BA65EDCAAF4FE4FB94AC697D9262D83E6F58A750D63C9F6C8A9D9FF" }, - { name = "glexer", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "F74FB4F78C3C1E158DF15A7226F33A662672F58EEF1DFE6593B7FCDA38B0A0EB" }, + { name = "gleam_community_colour", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "F0ACE69E3A47E913B03D3D0BB23A5563A91A4A7D20956916286068F4A9F817FE" }, + { name = "gleam_deque", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_deque", source = "hex", outer_checksum = "64D77068931338CF0D0CB5D37522C3E3CCA7CB7D6C5BACB41648B519CC0133C7" }, + { name = "gleam_json", version = "3.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "5BA154440B22D9800955B1AB854282FA37B97F30F409D76B0824D0A60C934188" }, + { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, + { name = "gleam_stdlib", version = "0.60.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "621D600BB134BC239CB2537630899817B1A42E60A1D46C5E9F3FAE39F88C800B" }, + { name = "gleeunit", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D33B7736CF0766ED3065F64A1EBB351E72B2E8DE39BAFC8ADA0E35E92A6A934F" }, + { name = "glexer", version = "2.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "5C235CBDF4DA5203AD5EAB1D6D8B456ED8162C5424FE2309CFFB7EF438B7C269" }, { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, + { name = "lenient_parse", version = "3.0.0", build_tools = ["gleam"], requirements = ["bigi", "gleam_deque", "gleam_stdlib"], otp_app = "lenient_parse", source = "hex", outer_checksum = "E6C654A75E6EB17B559418C5216583D0B9E7D82018E0608A7FF1AFF328D9F474" }, { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" }, - { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, + { name = "simplifile", version = "2.2.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C88E0EE2D509F6D86EB55161D631657675AA7684DAB83822F7E59EB93D9A60E3" }, { name = "snag", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "7E9F06390040EB5FAB392CE642771484136F2EC103A92AE11BA898C8167E6E17" }, { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, { name = "trie_again", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "5B19176F52B1BD98831B57FDC97BD1F88C8A403D6D8C63471407E78598E27184" }, @@ -25,9 +28,12 @@ packages = [ [requirements] argv = { version = ">= 1.0.2 and < 2.0.0" } +asset = { version = ">= 1.2.0 and < 2.0.0" } birdie = { version = ">= 1.1.8 and < 2.0.0" } gleam_community_ansi = { version = "~> 1.0" } gleam_community_colour = { version = "~> 1.0 or ~> 2.0" } +gleam_deque = { version = ">= 1.0.0 and < 2.0.0" } gleam_stdlib = { version = ">= 0.43.0 and < 2.0.0" } gleeunit = { version = "~> 1.0" } +lenient_parse = { version = ">= 3.0.0 and < 4.0.0" } snag = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..06a18f7 --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +gleam = "latest" diff --git a/src/glint.gleam b/src/glint.gleam index c7834d2..823d82f 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -1,15 +1,16 @@ -import gleam +import gleam/bool import gleam/dict -import gleam/float +import gleam/function import gleam/int -import gleam/io import gleam/list import gleam/option.{type Option, None, Some} import gleam/result import gleam/string -import gleam_community/colour.{type Colour} +import gleam_community/ansi import glint/constraint import glint/internal/help +import glint/internal/parameter +import glint/internal/parse import snag.{type Snag} // --- CONFIGURATION --- @@ -20,22 +21,25 @@ import snag.{type Snag} /// type Config { Config( - pretty_help: Option(PrettyHelp), + header_format: HeaderFormat, name: Option(String), as_module: Bool, description: Option(String), - exit: Bool, indent_width: Int, max_output_width: Int, min_first_column_width: Int, column_gap: Int, + version: Option(String), ) } -/// PrettyHelp defines the header colours to be used when styling help text -/// -pub type PrettyHelp { - PrettyHelp(usage: Colour, flags: Colour, subcommands: Colour) +type HeaderFormat { + HeaderFormat( + usage: fn(String) -> String, + named_args: fn(String) -> String, + flags: fn(String) -> String, + subcommands: fn(String) -> String, + ) } // -- CONFIGURATION: CONSTANTS -- @@ -43,25 +47,87 @@ pub type PrettyHelp { /// default config /// const default_config = Config( - pretty_help: None, + header_format: HeaderFormat( + function.identity, + function.identity, + function.identity, + function.identity, + ), name: None, as_module: False, description: None, - exit: True, indent_width: 4, max_output_width: 80, min_first_column_width: 20, column_gap: 2, + version: None, ) // -- CONFIGURATION: FUNCTIONS -- -/// Enable custom colours for help text headers. +/// Set the version to be printed when the --version flag is passed to glint +pub fn with_version(glint: Glint(a), version: String) -> Glint(a) { + Glint(..glint, config: Config(..glint.config, version: Some(version))) +} + +/// Set the style for each of the glint helptext headers (usage, flags, subcommands). +/// +/// This function will likely be used with colouring functions from [gleam_community/ansi](https://hexdocs.pm/gleam_community_ansi/). +/// +/// When setting your own header styles, you may want to leverage the default formatting as provided by [`glint.default_header_format`](#default_header_format) +/// which can be composed witth other functions from [gleam_community/ansi](https://hexdocs.pm/gleam_community_ansi/).. +/// +/// +/// Example: +/// +/// ```gleam +/// glint.with_header_style( +/// glint, +/// usage: fn(s) { s |> glint.default_header_format |> ansi.cyan }, +/// flags: fn(s) { s |> glint.default_header_format |> ansi.magenta }, +/// subcommands: fn(s) { s |> glint.default_header_format |> ansi.yellow }, +/// ) +/// ``` +pub fn with_header_style( + glint: Glint(a), + usage usage: fn(String) -> String, + named_args named_args: fn(String) -> String, + flags flags: fn(String) -> String, + subcommands subcommands: fn(String) -> String, +) -> Glint(a) { + let header_format = HeaderFormat(usage:, named_args:, flags:, subcommands:) + let config = Config(..glint.config, header_format:) + Glint(..glint, config:) +} + +/// This function sets the glint help text header styles using glint's defaults. +/// +/// Each help text header is formatted as bold, italic and underline (see [`glint.default_header_format`](#default_header_format)). /// -/// For a pre-made style, pass in [`glint.default_pretty_help`](#default_pretty_help) +/// The default colours are ANSI compatible as follows: +/// - usage: cyan +/// - flags: pink +/// - subcommands: yellow /// -pub fn pretty_help(glint: Glint(a), pretty: PrettyHelp) -> Glint(a) { - Glint(..glint, config: Config(..glint.config, pretty_help: Some(pretty))) +pub fn with_default_header_style(glint: Glint(a)) -> Glint(a) { + with_header_style( + glint, + usage: fn(s) { s |> default_header_format |> ansi.cyan }, + named_args: fn(s) { s |> default_header_format |> ansi.blue }, + flags: fn(s) { s |> default_header_format |> ansi.pink }, + subcommands: fn(s) { s |> default_header_format |> ansi.yellow }, + ) +} + +/// Style heading text as bold, underlined and italic. +/// +/// This function can be combined with other functions from [gleam_community/ansi](https://hexdocs.pm/gleam_community_ansi/) +/// +pub fn default_header_format(heading: String) -> String { + heading + |> ansi.bold + |> ansi.underline + |> ansi.italic } /// Give the current glint application a name. @@ -76,10 +142,6 @@ pub fn with_name(glint: Glint(a), name: String) -> Glint(a) { /// /// Calling this function disables that feature. /// -pub fn without_exit(glint: Glint(a)) -> Glint(a) { - Glint(..glint, config: Config(..glint.config, exit: False)) -} - /// Adjust the generated help text to reflect that the current glint app should be run as a gleam module. /// /// Use in conjunction with [`glint.with_name`](#with_name) to get usage text output like `gleam run -m ` @@ -139,68 +201,53 @@ pub opaque type Glint(a) { /// Specify the expected number of unnamed arguments with this type and the [`glint.unnamed_args`](#unnamed_args) function /// -pub type ArgsCount { +type ArgsCount { /// Specifies that a command must accept a specific number of unnamed arguments /// - EqArgs(Int) + EqArgs(name: String, help: String, count: Int) /// Specifies that a command must accept a minimum number of unnamed arguments /// - MinArgs(Int) + MinArgs(name: String, help: String, count: Int) + /// Specifies that a command accepts no arguments + NoArgs } /// The type representing a glint command. /// /// To create a new command, use the [`glint.command`](#command) function. /// -pub opaque type Command(a) { +pub opaque type Command(output) { Command( - do: Runner(a), + do: fn(NamedArgs, List(String), Flags) -> Out(output), flags: Flags, description: String, - unnamed_args: Option(ArgsCount), - named_args: List(String), - ) -} - -type InternalCommand(a) { - InternalCommand( - do: Runner(a), - flags: Flags, - unnamed_args: Option(ArgsCount), - named_args: List(String), + unnamed_args: ArgsCount, + named_args: List(Parameters(NamedArg)), ) } /// A container for named arguments available to commands at runtime. /// pub opaque type NamedArgs { - NamedArgs(internal: dict.Dict(String, String)) + NamedArgs(internal: dict.Dict(String, Parameters(NamedArg))) } -/// Functions that execute when glint commands are run. -/// -pub type Runner(a) = - fn(NamedArgs, List(String), Flags) -> a - /// CommandNode tree representation. /// type CommandNode(a) { CommandNode( - contents: Option(InternalCommand(a)), + contents: Option(Command(a)), subcommands: dict.Dict(String, CommandNode(a)), group_flags: Flags, description: String, ) } -/// Ok type for command execution -/// -@internal +/// This type defines the success cases for running a glint application. pub type Out(a) { - /// Container for the command return value - Out(a) - /// Container for the generated help string - Help(String) + Success(a) + Failure(Snag) + Info(String) } // -- CORE: BUILDER FUNCTIONS -- @@ -264,11 +311,12 @@ pub fn add( CommandNode( ..node, description: command.description, - contents: Some(InternalCommand( + contents: Some(Command( do: command.do, + description: command.description, flags: command.flags, - named_args: command.named_args, unnamed_args: command.unnamed_args, + named_args: command.named_args, )), ) } @@ -279,7 +327,7 @@ fn empty_command() -> CommandNode(a) { CommandNode( contents: None, subcommands: dict.new(), - group_flags: new_flags(), + group_flags: Flags(dict.new()), description: "", ) } @@ -305,12 +353,14 @@ fn sanitize_path(path: List(String)) -> List(String) { /// let my_arg = named_arg(named) /// ... /// ``` -pub fn command(do runner: Runner(a)) -> Command(a) { +pub fn command( + do runner: fn(NamedArgs, List(String), Flags) -> Out(a), +) -> Command(a) { Command( do: runner, - flags: new_flags(), + flags: Flags(dict.new()), description: "", - unnamed_args: None, + unnamed_args: MinArgs(name: "args", help: "", count: 0), named_args: [], ) } @@ -321,7 +371,9 @@ pub fn command(do runner: Runner(a)) -> Command(a) { /// pub fn map_command(command: Command(a), with fun: fn(a) -> b) -> Command(b) { Command( - do: fn(named_args, args, flags) { fun(command.do(named_args, args, flags)) }, + do: fn(named_args, args, flags) { + map_out(command.do(named_args, args, flags), fun) + }, description: command.description, flags: command.flags, named_args: command.named_args, @@ -329,6 +381,14 @@ pub fn map_command(command: Command(a), with fun: fn(a) -> b) -> Command(b) { ) } +fn map_out(out: Out(a), f: fn(a) -> b) -> Out(b) { + case out { + Success(a) -> Success(f(a)) + Failure(snag) -> Failure(snag) + Info(info) -> Info(info) + } +} + /// Attach a helptext description to a [`Command(a)`](#Command) /// /// This function allows for user-supplied newlines in long text strings. Individual newline characters are instead converted to spaces. @@ -341,89 +401,39 @@ pub fn command_help(of desc: String, with f: fn() -> Command(a)) -> Command(a) { Command(..f(), description: desc) } -/// Specify a specific number of unnamed args that a given command expects. +/// Require at least N arguments be provided for a command to execute. +/// This requirement does not apply to named arguments, which are matched before this constraint is applied. /// -/// Use in conjunction with [`glint.ArgsCount`](#ArgsCount) to specify either a minimum or a specific number of args. +/// The name and description given are used for help text generation. /// -/// ### Example: -/// -/// ```gleam -/// ... -/// // for a command that accets only 1 unnamed argument: -/// use <- glint.unnamed_args(glint.EqArgs(1)) -/// ... -/// named, unnamed, flags <- glint.command() -/// let assert Ok([arg]) = unnamed -/// ``` -/// -pub fn unnamed_args( - of count: ArgsCount, +pub fn min_args( + named name: String, + of count: Int, + described help: String, with f: fn() -> Command(b), ) -> Command(b) { - Command(..f(), unnamed_args: Some(count)) + Command(..f(), unnamed_args: MinArgs(name, help, count)) } -/// Add a list of named arguments to a [`Command(a)`](#Command). The value can be retrieved from the command's [`NamedArgs`](#NamedArgs) -/// -/// These named arguments will be matched with the first N arguments passed to the command. -/// -/// -/// **IMPORTANT**: -/// -/// - Matched named arguments will **not** be present in the commmand's unnamed args list +/// Require exactly N arguments be provided for a command to execute. +/// This requirement does not apply to named arguments, which are matched before this constraint is applied. /// -/// - All named arguments must match for a command to succeed. +/// The name and description given are used for help text generation. /// -/// ### Example: -/// -/// ```gleam -/// ... -/// use first_name <- glint.named_arg("first name") -/// ... -/// use named, unnamed, flags <- glint.command() -/// let first = first_name(named) -/// ``` -/// -pub fn named_arg( +pub fn eq_args( named name: String, - with f: fn(fn(NamedArgs) -> String) -> Command(a), -) -> Command(a) { - let cmd = { - use named_args <- f() - // we can assert here because the command runner will only execute if the named args match - let assert Ok(arg) = dict.get(named_args.internal, name) - arg - } - - Command(..cmd, named_args: [name, ..cmd.named_args]) + of count: Int, + described help: String, + with f: fn() -> Command(b), +) -> Command(b) { + Command(..f(), unnamed_args: EqArgs(name, help, count)) } -/// Add a [`Flag(a)`](#Flag) to a [`Command(a)`](#Command) -/// -/// The provided callback is provided a function to fetch the current flag fvalue from the command input [`Flags`](#Flags). -/// -/// This function is most ergonomic as part of a `use` chain when building commands. -/// -/// ### Example: -/// -/// ```gleam -/// ... -/// use repeat <- glint.flag( -/// glint.int_flag("repeat") -/// |> glint.flag_default(1) -/// |> glint.flag_help("Repeat the message n-times") -/// ) -/// ... -/// use named, unnamed, flags <- glint.command() -/// let repeat_value = repeat(flags) -/// ``` +/// Require that no argumenets be provided for a command to execute. +/// This requirement does not apply to named arguments, which are matched before this constraint is applied. /// -pub fn flag( - of flag: Flag(a), - with f: fn(fn(Flags) -> snag.Result(a)) -> Command(b), -) -> Command(b) { - let cmd = f(flag.getter(_, flag.name)) - Command(..cmd, flags: insert(cmd.flags, flag.name, build_flag(flag))) +pub fn no_args(with f: fn() -> Command(b)) -> Command(b) { + Command(..f(), unnamed_args: NoArgs) } /// Add a flag for a group of commands. @@ -432,30 +442,37 @@ pub fn flag( pub fn group_flag( in glint: Glint(a), at path: List(String), - of flag: Flag(_), + of flag: Parameter(_, Flag), ) -> Glint(a) { use node <- update_at(in: glint, at: path) CommandNode( ..node, - group_flags: insert(node.group_flags, flag.name, build_flag(flag)), + group_flags: Flags(dict.insert( + node.group_flags.internal, + flag.internal.name, + flag.constructor(flag), + )), ) } // -- CORE: EXECUTION FUNCTIONS -- -/// Determines which command to run and executes it. -/// -/// Sets any provided flags if necessary. +/// Run a glint app and return the result. /// -/// Each value prefixed with `--` is parsed as a flag. +/// This function will return `Error(String)` if glint itself encountered an error with the provided input. +/// This function will return `Ok(glint.Out(a))` if glint successfully ran with the provided input +/// . +/// If you would like to do handle the command output please see the `glintio` package. /// -/// This function does not print its output and is mainly intended for use within `glint` itself. -/// If you would like to print or handle the output of a command please see the `run_and_handle` function. -/// -@internal -pub fn execute(glint: Glint(a), args: List(String)) -> Result(Out(a), String) { - // create help flag to check for +pub fn run(glint: Glint(a), args: List(String)) -> Out(a) { + // create help and version flags to check for let help_flag = flag_prefix <> help.help_flag.meta.name + let version_flag = flag_prefix <> help.version_flag.meta.name + + use <- bool.guard( + when: list.contains(args, version_flag), + return: Info(glint.config.version |> option.unwrap("")), + ) // check if help flag is present let #(help, args) = case list.partition(args, fn(s) { s == help_flag }) { @@ -481,15 +498,15 @@ fn do_execute( flags: List(String), help: Bool, command_path: List(String), -) -> Result(Out(a), String) { +) -> Out(a) { case args { // when there are no more available arguments // and help flag has been passed, generate help message - [] if help -> Ok(Help(cmd_help(command_path, cmd, config))) + [] if help -> Info(cmd_help(command_path, cmd, config)) // when there are no more available arguments // run the current command - [] -> execute_root(command_path, config, cmd, [], flags) |> result.map(Out) + [] -> execute_root(command_path, config, cmd, [], flags) // when there are arguments remaining // check if the next one is a subcommand of the current command @@ -499,29 +516,32 @@ fn do_execute( Ok(sub_command) -> CommandNode( ..sub_command, - group_flags: merge(cmd.group_flags, sub_command.group_flags), + group_flags: Flags(dict.merge( + cmd.group_flags.internal, + sub_command.group_flags.internal, + )), ) |> do_execute(config, rest, flags, help, [arg, ..command_path]) // subcommand not found, but help flag has been passed // generate and return help message - _ if help -> Ok(Help(cmd_help(command_path, cmd, config))) + _ if help -> Info(cmd_help(command_path, cmd, config)) // subcommand not found, but help flag has not been passed // execute the current command - _ -> - execute_root(command_path, config, cmd, args, flags) - |> result.map(Out) + _ -> execute_root(command_path, config, cmd, args, flags) } } } fn args_compare(expected: ArgsCount, actual: Int) -> snag.Result(Nil) { use err <- result.map_error(case expected { - EqArgs(expected) if actual == expected -> Ok(Nil) - MinArgs(expected) if actual >= expected -> Ok(Nil) - EqArgs(expected) -> Error(int.to_string(expected)) - MinArgs(expected) -> Error("at least " <> int.to_string(expected)) + NoArgs if actual == 0 -> Ok(Nil) + NoArgs -> Error("no") + EqArgs(_, _, expected) if actual == expected -> Ok(Nil) + MinArgs(_, _, expected) if actual >= expected -> Ok(Nil) + EqArgs(_, _, expected) -> Error(int.to_string(expected)) + MinArgs(_, _, expected) -> Error("at least " <> int.to_string(expected)) }) snag.new( "expected: " <> err <> " argument(s), provided: " <> int.to_string(actual), @@ -536,7 +556,9 @@ fn execute_root( cmd: CommandNode(a), args: List(String), flag_inputs: List(String), -) -> Result(a, String) { +) -> Out(a) { + use <- flatten + { // check if the command can actually be executed use contents <- result.try(option.to_result( @@ -545,24 +567,36 @@ fn execute_root( )) // merge flags and parse flag inputs + // use new_flags <- result.try(list.try_fold( over: flag_inputs, - from: merge(cmd.group_flags, contents.flags), + from: dict.merge(cmd.group_flags.internal, contents.flags.internal), with: update_flags, )) // get named arguments + // 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)) + True -> { + use acc, #(param, input) <- list.try_fold(named, dict.new()) + use #(param, name) <- result.try( + parse_parameter(param, input) + |> snag.context( + "invalid named argument '" <> parameter_name(param) <> "'", + ), + ) + Ok(dict.insert(acc, name, param)) + } + False -> snag.error( "unmatched named arguments: " <> { contents.named_args |> list.drop(list.length(named)) - |> list.map(fn(s) { "'" <> s <> "'" }) + |> list.map(fn(s) { "'" <> parameter_name(s) <> "'" }) |> string.join(", ") }, ) @@ -573,84 +607,27 @@ fn execute_root( let args = list.drop(args, dict.size(named_args)) // validate unnamed argument quantity - use _ <- result.map(case contents.unnamed_args { - Some(count) -> - count - |> args_compare(list.length(args)) - |> snag.context("invalid number of arguments provided") - None -> Ok(Nil) - }) + use _ <- result.map( + contents.unnamed_args + |> args_compare(list.length(args)) + |> snag.context("invalid number of unnamed arguments provided"), + ) // execute the command - contents.do(NamedArgs(named_args), args, new_flags) + contents.do(NamedArgs(named_args), args, Flags(new_flags)) } |> result.map_error(fn(err) { - err + snag.Snag( + ..err, + cause: list.append(err.cause, [ + "See the following help text, available via the '--help' flag:\n\n" + <> cmd_help(path, cmd, config), + ]), + ) |> snag.layer("failed to run command") - |> snag.pretty_print - <> "\nSee the following help text, available via the '--help' flag.\n\n" - <> cmd_help(path, cmd, config) }) } -/// Run a glint app and print any errors enountered, or the help text if requested. -/// This function ignores any value returned by the command that was run. -/// If you would like to do handle the command output please see the [`glint.run_and_handle`](#run_and_handle) function. -/// -/// IMPORTANT: This function exits with code 1 if an error was encountered. -/// If this behaviour is not desired please disable it with [`glint.without_exit`](#without_exit) -/// -pub fn run(from glint: Glint(a), for args: List(String)) -> Nil { - run_and_handle(from: glint, for: args, with: fn(_) { Nil }) -} - -/// Run a glint app with a custom handler for command output. -/// This function prints any errors enountered or the help text if requested. -/// -/// IMPORTANT: This function exits with code 1 if an error was encountered. -/// If this behaviour is not desired please disable it with [`glint.without_exit`](#without_exit) -/// -pub fn run_and_handle( - from glint: Glint(a), - for args: List(String), - with handle: fn(a) -> _, -) -> Nil { - case execute(glint, args) { - Error(s) -> { - io.println(s) - case glint.config.exit { - True -> exit(1) - False -> Nil - } - } - Ok(Help(s)) -> io.println(s) - Ok(Out(out)) -> { - handle(out) - Nil - } - } -} - -/// Default colouring for help text. -/// -/// mint (r: 182, g: 255, b: 234) colour for usage -/// -/// pink (r: 255, g: 175, b: 243) colour for flags -/// -/// buttercup (r: 252, g: 226, b: 174) colour for subcommands -/// -pub fn default_pretty_help() -> PrettyHelp { - let assert Ok(usage_colour) = colour.from_rgb255(182, 255, 234) - let assert Ok(flags_colour) = colour.from_rgb255(255, 175, 243) - let assert Ok(subcommands_colour) = colour.from_rgb255(252, 226, 174) - - PrettyHelp( - usage: usage_colour, - flags: flags_colour, - subcommands: subcommands_colour, - ) -} - // -- HELP: FUNCTIONS -- /// generate the help text for a command @@ -668,9 +645,10 @@ fn cmd_help(path: List(String), cmd: CommandNode(a), config: Config) -> String { fn build_help_config(config: Config) -> help.Config { help.Config( name: config.name, - usage_colour: option.map(config.pretty_help, fn(p) { p.usage }), - flags_colour: option.map(config.pretty_help, fn(p) { p.flags }), - subcommands_colour: option.map(config.pretty_help, fn(p) { p.subcommands }), + usage_colour: config.header_format.usage, + named_args_colour: config.header_format.named_args, + flags_colour: config.header_format.flags, + subcommands_colour: config.header_format.subcommands, as_module: config.as_module, description: config.description, indent_width: config.indent_width, @@ -690,23 +668,24 @@ fn build_command_help(name: String, node: CommandNode(_)) -> help.Command { |> option.map(fn(cmd) { #( node.description, - build_flags_help(merge(node.group_flags, cmd.flags)), + build_flags_help(dict.merge( + node.group_flags.internal, + cmd.flags.internal, + )), cmd.unnamed_args, - cmd.named_args, + cmd.named_args |> build_named_args_help, ) }) - |> option.unwrap(#(node.description, [], None, [])) + |> option.unwrap(#(node.description, [], MinArgs("args", "", 0), [])) help.Command( meta: help.Metadata(name: name, description: description), flags: flags, subcommands: build_subcommands_help(node.subcommands), - unnamed_args: { - use args <- option.map(unnamed_args) - case args { - EqArgs(n) -> help.EqArgs(n) - MinArgs(n) -> help.MinArgs(n) - } + unnamed_args: case unnamed_args { + EqArgs(name, help, n) -> help.EqArgs(name, help, n) + MinArgs(name, help, n) -> help.MinArgs(name, help, n) + NoArgs -> help.NoArgs }, named_args: named_args, ) @@ -714,31 +693,48 @@ fn build_command_help(name: String, node: CommandNode(_)) -> help.Command { /// generate the string representation for the type of a flag /// -fn flag_type_info(flag: FlagEntry) { - case flag.value { - I(_) -> "INT" - B(_) -> "BOOL" - F(_) -> "FLOAT" - LF(_) -> "FLOAT_LIST" - LI(_) -> "INT_LIST" - LS(_) -> "STRING_LIST" - S(_) -> "STRING" +fn parameters_type_info(p: Parameters(a)) { + case p { + B(_) -> "bool" + I(_) -> "int" + F(_) -> "float" + S(_) -> "string" + LF(_) -> "float list (comma-separated)" + LI(_) -> "int list (comma-separated)" + LS(_) -> "string list (comma-separated)" } } /// build the help representation for a list of flags /// -fn build_flags_help(flags: Flags) -> List(help.Flag) { - use acc, name, flag <- fold(flags, []) +fn build_flags_help( + params: dict.Dict(String, Parameters(Flag)), +) -> List(help.Parameter) { + use acc, name, flag <- dict.fold(params, []) [ - help.Flag( - meta: help.Metadata(name: name, description: flag.description), - type_: flag_type_info(flag), + help.Parameter( + meta: help.Metadata(name: name, description: parameter_help(flag)), + type_: parameters_type_info(flag), ), ..acc ] } +/// build the help representation for a list of flags +/// +fn build_named_args_help( + params: List(Parameters(NamedArg)), +) -> List(help.Parameter) { + use param <- list.map(params) + help.Parameter( + meta: help.Metadata( + name: parameter_name(param), + description: parameter_help(param), + ), + type_: parameters_type_info(param), + ) +} + /// build the help representation for a list of subcommands /// fn build_subcommands_help( @@ -757,165 +753,177 @@ const flag_prefix = "--" /// The separation character for flag names and their values const flag_delimiter = "=" -/// Supported flag types. +/// Flags passed as input to a command. /// -type Value { - /// Boolean flags, to be passed in as `--flag=true` or `--flag=false`. - /// Can be toggled by omitting the desired value like `--flag`. - /// Toggling will negate the existing value. - /// - B(FlagInternals(Bool)) - - /// Int flags, to be passed in as `--flag=1` - /// - I(FlagInternals(Int)) - - /// List(Int) flags, to be passed in as `--flag=1,2,3` - /// - LI(FlagInternals(List(Int))) - - /// Float flags, to be passed in as `--flag=1.0` - /// - F(FlagInternals(Float)) - - /// List(Float) flags, to be passed in as `--flag=1.0,2.0` - /// - LF(FlagInternals(List(Float))) +pub opaque type Flags { + Flags(internal: dict.Dict(String, Parameters(Flag))) +} - /// String flags, to be passed in as `--flag=hello` - /// - S(FlagInternals(String)) +/// Updates a flag value, ensuring that the new value can satisfy the required type. +/// Assumes that all flag inputs passed in start with -- +/// This function is only intended to be used from glint.execute_root +/// +fn update_flags( + in flags: dict.Dict(String, Parameters(Flag)), + with flag_input: String, +) -> Result(dict.Dict(String, Parameters(Flag)), Snag) { + let flag_input = string.drop_start(flag_input, string.length(flag_prefix)) - /// List(String) flags, to be passed in as `--flag=hello,world` - /// - LS(FlagInternals(List(String))) + case string.split_once(flag_input, flag_delimiter) { + Ok(data) -> update_flag_value(flags, data) + Error(_) -> attempt_toggle_flag(flags, flag_input) + } } -/// Glint's typed flags. -/// -/// Flags can be created using any of: -/// - [`glint.int_flag`](#int_flag) -/// - [`glint.ints_flag`](#ints_flag) -/// - [`glint.float_flag`](#float_flag) -/// - [`glint.floats_flag`](#floats_flag) -/// - [`glint.string_flag`](#string_flag) -/// - [`glint.strings_flag`](#strings_flag) -/// - [`glint.bool_flag`](#bool_flag) -/// -pub opaque type Flag(a) { - Flag( - name: String, - desc: String, - parser: Parser(a, Snag), - value: fn(FlagInternals(a)) -> Value, - getter: fn(Flags, String) -> snag.Result(a), - default: Option(a), +fn update_flag_value( + in flags: dict.Dict(String, Parameters(Flag)), + with data: #(String, String), +) -> Result(dict.Dict(String, Parameters(Flag)), Snag) { + let #(key, input) = data + use contents <- result.try( + dict.get(flags, key) + |> result.replace_error(undefined_flag_err(key)), + ) + use #(value, name) <- result.map( + parse_parameter(contents, input) + |> snag.context("invalid value for flag '" <> key <> "'"), ) + dict.insert(flags, name, value) } -/// An internal representation of flag contents -/// -type FlagInternals(a) { - FlagInternals(value: Option(a), parser: Parser(a, Snag)) +fn attempt_toggle_flag( + in flags: dict.Dict(String, Parameters(_)), + at key: String, +) -> Result(dict.Dict(String, Parameters(_)), Snag) { + use contents <- result.try( + dict.get(flags, key) |> result.replace_error(undefined_flag_err(key)), + ) + case contents { + B( + Parameter(internal: parameter.Parameter(value: None, ..) as internal, ..) as param, + ) -> + Parameter( + ..param, + internal: parameter.Parameter(..internal, value: Some(True)), + ) + |> B + |> dict.insert(into: flags, for: key) + |> Ok + B( + Parameter( + internal: parameter.Parameter(value: Some(val), ..) as internal, + .., + ) as param, + ) -> + Parameter( + ..param, + internal: parameter.Parameter(..internal, value: Some(!val)), + ) + |> B + |> dict.insert(into: flags, for: key) + |> Ok + _ -> Error(cannot_toggle_flag_error(key)) + } } -// Flag initializers - -type Parser(a, b) = - fn(String) -> gleam.Result(a, b) - -/// Initialise an int flag. -/// -pub fn int_flag(named name: String) -> Flag(Int) { - use input <- new_builder(name, I, get_int_flag) - input - |> int.parse - |> result.replace_error(cannot_parse(input, "int")) +// Error creation and manipulation functions +fn layer_invalid_flag(err: Snag, flag: String) -> Snag { + snag.layer(err, "invalid flag '" <> flag <> "'") } -/// Initialise an int list flag. -/// -pub fn ints_flag(named name: String) -> Flag(List(Int)) { - use input <- new_builder(name, LI, get_ints_flag) - input - |> string.split(",") - |> list.try_map(int.parse) - |> result.replace_error(cannot_parse(input, "int list")) +fn cannot_toggle_flag_error(flag_input: String) -> Snag { + snag.new("flag '" <> flag_input <> "' cannot be toggled") + |> layer_invalid_flag(flag_input) } -///Initialise a float flag. -/// -pub fn float_flag(named name: String) -> Flag(Float) { - use input <- new_builder(name, F, get_floats) - input - |> float.parse - |> result.replace_error(cannot_parse(input, "float")) +fn undefined_flag_err(key: String) -> Snag { + snag.new("flag provided but not defined") + |> layer_invalid_flag(key) } -/// Initialise a float list flag. +// -- FLAG ACCESS FUNCTIONS -- + +/// Gets the value for the associated flag. +/// +/// This function is generally only used for fetching flags set at the group level. +/// In most cases you should use the getter functions provided when calling [`glint.flag`](#flag). /// -pub fn floats_flag(named name: String) -> Flag(List(Float)) { - use input <- new_builder(name, LF, get_floats_flag) - input - |> string.split(",") - |> list.try_map(float.parse) - |> result.replace_error(cannot_parse(input, "float list")) +pub fn get_flag( + from flags: Flags, + for flag: Parameter(a, Flag), +) -> Result(a, Snag) { + flags.internal + |> flag.getter + |> snag.context("failed to retrieve flag value") } -/// Initialise a string flag. +/// Gets the value for the associated named argument. /// -pub fn string_flag(named name: String) -> Flag(String) { - new_builder(name, S, get_string_flag, fn(s) { Ok(s) }) +/// In most cases you should use the getter functions provided when calling [`glint.named_arg`](#named_arg). +/// +pub fn get_named_arg( + from named_args: NamedArgs, + for named_arg: Parameter(a, NamedArg), +) -> a { + let assert Ok(a) = named_arg.getter(named_args.internal) + a } -/// Intitialise a string list flag. -/// -pub fn strings_flag(named name: String) -> Flag(List(String)) { - use input <- new_builder(name, LS, get_strings_flag) - input - |> string.split(",") - |> Ok +// traverses a Glint(a) tree for the provided path +// executes the provided function on the terminal node +// +fn update_at( + in glint: Glint(a), + at path: List(String), + do f: fn(CommandNode(a)) -> CommandNode(a), +) -> Glint(a) { + Glint( + ..glint, + cmd: do_update_at(through: glint.cmd, at: sanitize_path(path), do: f), + ) } -/// Initialise a boolean flag. -/// -pub fn bool_flag(named name: String) -> Flag(Bool) { - use input <- new_builder(name, B, get_bool_flag) - case string.lowercase(input) { - "true" | "t" -> Ok(True) - "false" | "f" -> Ok(False) - _ -> Error(cannot_parse(input, "bool")) +fn do_update_at( + through node: CommandNode(a), + at path: List(String), + do f: fn(CommandNode(a)) -> CommandNode(a), +) -> CommandNode(a) { + case path { + [] -> f(node) + [next, ..rest] -> { + CommandNode(..node, subcommands: { + use found <- dict.upsert(node.subcommands, next) + found + |> option.lazy_unwrap(empty_command) + |> do_update_at(rest, f) + }) + } } } -/// initialize custom builders using a Value constructor and a parsing function -/// -fn new_builder( +pub type NamedArg + +pub type Flag + +fn new_parameter( name: String, - valuer: fn(FlagInternals(a)) -> Value, - getter: fn(Flags, String) -> snag.Result(a), - p: Parser(a, Snag), -) -> Flag(a) { - Flag( - name: name, - desc: "", - parser: p, - value: valuer, - default: None, + constructor: fn(Parameter(a, b)) -> Parameters(b), + parse: fn(String) -> Result(a, Snag), + getter: fn(dict.Dict(String, Parameters(b))) -> Result(a, Snag), +) -> Parameter(a, b) { + Parameter( + internal: parameter.Parameter( + name: name, + description: "", + parser: parse, + value: option.None, + ), + constructor: constructor, getter: getter, ) } -/// convert a (Flag(a) into its corresponding FlagEntry representation -/// -fn build_flag(fb: Flag(a)) -> FlagEntry { - FlagEntry( - value: fb.value(FlagInternals(value: fb.default, parser: fb.parser)), - description: fb.desc, - ) -} - -/// Attach a constraint to a flag. +/// Attach a constraint to a parameter. /// /// As constraints are just functions, this works well as both part of a pipeline or with `use`. /// @@ -923,9 +931,9 @@ fn build_flag(fb: Flag(a)) -> FlagEntry { /// ### Pipe: /// /// ```gleam -/// glint.int_flag("my_flag") -/// |> glint.flag_help("An awesome flag") -/// |> glint.flag_constraint(fn(i) { +/// glint.int("my_flag") +/// |> glint.param_help("An awesome flag") +/// |> glint.constraint(fn(i) { /// case i < 0 { /// True -> snag.error("must be greater than 0") /// False -> Ok(i) @@ -935,9 +943,9 @@ fn build_flag(fb: Flag(a)) -> FlagEntry { /// ### Use: /// /// ```gleam -/// use i <- glint.flag_constraint( -/// glint.int_flag("my_flag") -/// |> glint.flag_help("An awesome flag") +/// use i <- glint.constraint( +/// glint.int("my_flag") +/// |> glint.param_help("An awesome flag") /// ) /// case i < 0 { /// True -> snag.error("must be greater than 0") @@ -945,34 +953,27 @@ fn build_flag(fb: Flag(a)) -> FlagEntry { /// } /// ``` /// -pub fn flag_constraint( - builder: Flag(a), - constraint: constraint.Constraint(a), -) -> Flag(a) { - Flag(..builder, parser: wrap_with_constraint(builder.parser, constraint)) +pub fn constraint( + p: Parameter(a, b), + c: constraint.Constraint(a), +) -> Parameter(a, b) { + Parameter(..p, internal: parameter.add_constraint(p.internal, c)) } -/// attach a Constraint(a) to a Parser(a,Snag) -/// this function should not be used directly unless -fn wrap_with_constraint( - p: Parser(a, Snag), - constraint: constraint.Constraint(a), -) -> Parser(a, Snag) { - fn(input: String) -> snag.Result(a) { attempt(p(input), constraint) } -} - -fn attempt( - val: gleam.Result(a, e), - f: fn(a) -> gleam.Result(_, e), -) -> gleam.Result(a, e) { - use a <- result.try(val) - result.replace(f(a), a) -} - -/// FlagEntry data and descriptions. +/// Set the default value for a parameter. +/// +/// ### Example: /// -type FlagEntry { - FlagEntry(value: Value, description: String) +/// ```gleam +/// glint.int("awesome_flag") +/// |> glint.default(1) +/// ``` +/// +pub fn default(p: Parameter(a, Flag), d: a) { + Parameter( + ..p, + internal: parameter.Parameter(..p.internal, value: option.Some(d)), + ) } /// Attach a help text description to a flag. @@ -986,294 +987,372 @@ type FlagEntry { /// ### Example: /// /// ```gleam -/// glint.int_flag("awesome_flag") -/// |> glint.flag_help("Some great text!") -/// ``` -/// -pub fn flag_help(for flag: Flag(a), of description: String) -> Flag(a) { - Flag(..flag, desc: description) -} - -/// Set the default value for a flag. -/// -/// ### Example: -/// -/// ```gleam -/// glint.int_flag("awesome_flag") -/// |> glint.flag_default(1) +/// glint.int("awesome_flag") +/// |> glint.param_help("Some great text!") /// ``` /// -pub fn flag_default(for flag: Flag(a), of default: a) -> Flag(a) { - Flag(..flag, default: Some(default)) -} - -/// Flags passed as input to a command. -/// -pub opaque type Flags { - Flags(internal: dict.Dict(String, FlagEntry)) -} - -fn insert(in flags: Flags, at name: String, insert flag: FlagEntry) -> Flags { - Flags(dict.insert(flags.internal, name, flag)) +pub fn param_help(p: Parameter(a, b), description: String) { + Parameter(..p, internal: parameter.Parameter(..p.internal, description:)) } -fn merge(into a: Flags, from b: Flags) -> Flags { - Flags(internal: dict.merge(a.internal, b.internal)) -} - -fn fold(flags: Flags, acc: acc, f: fn(acc, String, FlagEntry) -> acc) -> acc { - dict.fold(flags.internal, acc, f) -} - -fn new_flags() -> Flags { - Flags(dict.new()) -} - -/// Updates a flag value, ensuring that the new value can satisfy the required type. -/// Assumes that all flag inputs passed in start with -- -/// This function is only intended to be used from glint.execute_root -/// -fn update_flags(in flags: Flags, with flag_input: String) -> snag.Result(Flags) { - let flag_input = string.drop_start(flag_input, string.length(flag_prefix)) - - case string.split_once(flag_input, flag_delimiter) { - Ok(data) -> update_flag_value(flags, data) - Error(_) -> attempt_toggle_flag(flags, flag_input) +pub fn int(name: String) -> Parameter(Int, a) { + use params <- new_parameter(name, I, fn(s) { + s + |> parse.int + |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) + }) + use <- parameter_access_error(name) + use v <- result.try(dict.get(params, name)) + case v { + I(param) -> option.to_result(param.internal.value, Nil) + _ -> Error(Nil) } } -fn update_flag_value( - in flags: Flags, - with data: #(String, String), -) -> snag.Result(Flags) { - let #(key, input) = data - use contents <- result.try(get(flags, key)) - use value <- result.map( - compute_flag(with: input, given: contents.value) - |> result.map_error(layer_invalid_flag(_, key)), - ) - insert(flags, key, FlagEntry(..contents, value: value)) -} - -fn attempt_toggle_flag(in flags: Flags, at key: String) -> snag.Result(Flags) { - use contents <- result.try(get(flags, key)) - case contents.value { - B(FlagInternals(None, ..) as internal) -> - FlagInternals(..internal, value: Some(True)) - |> B - |> fn(val) { FlagEntry(..contents, value: val) } - |> dict.insert(into: flags.internal, for: key) - |> Flags - |> Ok - B(FlagInternals(Some(val), ..) as internal) -> - FlagInternals(..internal, value: Some(!val)) - |> B - |> fn(val) { FlagEntry(..contents, value: val) } - |> dict.insert(into: flags.internal, for: key) - |> Flags - |> Ok - _ -> Error(no_value_flag_err(key)) +pub fn ints(name: String) -> Parameter(List(Int), a) { + use params <- new_parameter(name, LI, fn(s) { + s + |> parse.ints + |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) + }) + use <- parameter_access_error(name) + use v <- result.try(dict.get(params, name)) + case v { + LI(param) -> option.to_result(param.internal.value, Nil) + _ -> Error(Nil) } } -fn access_type_error(flag_type) { - snag.error("cannot access flag as " <> flag_type) +pub fn string(name: String) -> Parameter(String, a) { + use params <- new_parameter(name, S, fn(s) { + s + |> parse.string + |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) + }) + use <- parameter_access_error(name) + use v <- result.try(dict.get(params, name)) + case v { + S(param) -> option.to_result(param.internal.value, Nil) + _ -> Error(Nil) + } } -fn flag_not_provided_error() { - snag.error("no value provided") -} +pub fn strings(name: String) -> Parameter(List(String), a) { + use params <- new_parameter(name, LS, fn(s) { + s + |> parse.strings + |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) + }) -fn construct_value( - input: String, - internal: FlagInternals(a), - constructor: fn(FlagInternals(a)) -> Value, -) -> snag.Result(Value) { - use val <- result.map(internal.parser(input)) - constructor(FlagInternals(..internal, value: Some(val))) -} - -/// Computes the new flag value given the input and the expected flag type -/// -fn compute_flag(with input: String, given current: Value) -> snag.Result(Value) { - input - |> case current { - I(internal) -> construct_value(_, internal, I) - LI(internal) -> construct_value(_, internal, LI) - F(internal) -> construct_value(_, internal, F) - LF(internal) -> construct_value(_, internal, LF) - S(internal) -> construct_value(_, internal, S) - LS(internal) -> construct_value(_, internal, LS) - B(internal) -> construct_value(_, internal, B) + use <- parameter_access_error(name) + use v <- result.try(dict.get(params, name)) + case v { + LS(param) -> option.to_result(param.internal.value, Nil) + _ -> Error(Nil) } - |> snag.context("failed to compute value for flag") } -// Error creation and manipulation functions -fn layer_invalid_flag(err: Snag, flag: String) -> Snag { - snag.layer(err, "invalid flag '" <> flag <> "'") +pub fn float(name: String) -> Parameter(Float, a) { + use params <- new_parameter(name, F, fn(s) { + s + |> parse.float + |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) + }) + use <- parameter_access_error(name) + use v <- result.try(dict.get(params, name)) + case v { + F(param) -> option.to_result(param.internal.value, Nil) + _ -> Error(Nil) + } } -fn no_value_flag_err(flag_input: String) -> Snag { - { "flag '" <> flag_input <> "' has no assigned value" } - |> snag.new() - |> layer_invalid_flag(flag_input) +pub fn floats(name: String) -> Parameter(List(Float), a) { + use params <- new_parameter(name, LF, fn(s) { + s + |> parse.floats + |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) + }) + use <- parameter_access_error(name) + use v <- result.try(dict.get(params, name)) + case v { + LF(param) -> option.to_result(param.internal.value, Nil) + _ -> Error(Nil) + } } -fn undefined_flag_err(key: String) -> Snag { - "flag provided but not defined" - |> snag.new() - |> layer_invalid_flag(key) +pub fn bool(name: String) -> Parameter(Bool, a) { + use params <- new_parameter(name, B, fn(s) { + s + |> parse.bool + |> result.map_error(fn(e) { e |> parse.error_to_string |> snag.new }) + }) + use <- parameter_access_error(name) + use v <- result.try(dict.get(params, name)) + case v { + B(param) -> option.to_result(param.internal.value, Nil) + _ -> Error(Nil) + } } -fn cannot_parse(with value: String, is kind: String) -> Snag { - { "cannot parse value '" <> value <> "' as " <> kind } - |> snag.new() +fn parameter_access_error( + name: String, + f: fn() -> Result(a, Nil), +) -> Result(a, Snag) { + case f() { + Error(Nil) -> snag.error("failed to retrieve value for: " <> name) + Ok(r) -> Ok(r) + } } -// -- FLAG ACCESS FUNCTIONS -- - -/// Access the contents for the associated flag +/// Add a [`Flag(a)`](#Flag) to a [`Command(a)`](#Command) +/// +/// The provided callback is provided a function to fetch the current flag fvalue from the command input [`Flags`](#Flags). +/// +/// This function is most ergonomic as part of a `use` chain when building commands. +/// +/// ### Example: +/// +/// ```gleam +/// ... +/// use repeat <- glint.flag( +/// glint.int("repeat") +/// |> glint.default(1) +/// |> glint.param_help("Repeat the message n-times") +/// ) +/// ... +/// use named, unnamed, flags <- glint.command() +/// let repeat_value = repeat(flags) +/// ``` /// -fn get(flags: Flags, name: String) -> snag.Result(FlagEntry) { - dict.get(flags.internal, name) - |> result.replace_error(undefined_flag_err(name)) +pub fn flag( + p: Parameter(a, Flag), + f: fn(fn(Flags, fn(a) -> Out(b)) -> Out(b)) -> Command(b), +) -> Command(b) { + let cmd = + f(fn(flags, body) { + flags.internal + |> p.getter + |> snag.context("failed to retrieve flag value") + |> try(body) + }) + Command( + ..cmd, + flags: Flags(dict.insert( + cmd.flags.internal, + p.internal.name, + p.constructor(p), + )), + ) } -fn get_value( - from flags: Flags, - at key: String, - expecting kind: fn(FlagEntry) -> snag.Result(a), -) -> snag.Result(a) { - get(flags, key) - |> result.try(kind) - |> snag.context("failed to retrieve value for flag '" <> key <> "'") +pub fn optional_flag( + p: Parameter(a, Flag), + f: fn(fn(Flags) -> Result(a, Snag)) -> Command(b), +) -> Command(b) { + let cmd = + f(fn(flags) { + flags.internal + |> p.getter + |> snag.context("failed to retrieve flag value") + }) + Command( + ..cmd, + flags: Flags(dict.insert( + cmd.flags.internal, + p.internal.name, + p.constructor(p), + )), + ) } -/// Gets the value for the associated flag. +/// Add a required argument to a [`Command(a)`](#Command). +/// The value can be retrieved from the command's [`NamedArgs`](#NamedArgs) /// -/// This function should only ever be used when fetching flags set at the group level. -/// For local flags please use the getter functions provided when calling [`glint.flag`](#flag). +/// **IMPORTANT**: /// -pub fn get_flag(from flags: Flags, for flag: Flag(a)) -> snag.Result(a) { - flag.getter(flags, flag.name) -} - -/// Gets the current value for the associated int flag +/// - Matched required arguments will **not** be present in the commmand's extra args list /// -fn get_int_flag(from flags: Flags, for name: String) -> snag.Result(Int) { - use flag <- get_value(flags, name) - case flag.value { - I(FlagInternals(value: Some(val), ..)) -> Ok(val) - I(FlagInternals(value: None, ..)) -> flag_not_provided_error() - _ -> access_type_error("int") - } -} - -/// Gets the current value for the associated ints flag +/// - All required arguments must match for a command to succeed. /// -fn get_ints_flag(from flags: Flags, for name: String) -> snag.Result(List(Int)) { - use flag <- get_value(flags, name) - case flag.value { - LI(FlagInternals(value: Some(val), ..)) -> Ok(val) - LI(FlagInternals(value: None, ..)) -> flag_not_provided_error() - _ -> access_type_error("int list") - } -} - -/// Gets the current value for the associated bool flag +/// ### Example: /// -fn get_bool_flag(from flags: Flags, for name: String) -> snag.Result(Bool) { - use flag <- get_value(flags, name) - case flag.value { - B(FlagInternals(Some(val), ..)) -> Ok(val) - B(FlagInternals(None, ..)) -> flag_not_provided_error() - _ -> access_type_error("bool") - } +/// ```gleam +/// ... +/// use first_name <- glint.named_arg(glint.string("name")) +/// ... +/// use required, extra, flags <- glint.command() +/// let first = first_name(named) +/// ``` +pub fn named_arg( + p: Parameter(a, NamedArg), + f: fn(fn(NamedArgs) -> a) -> Command(b), +) -> Command(b) { + let cmd = + f(fn(named_args) { + let assert Ok(named_arg) = p.getter(named_args.internal) + named_arg + }) + + Command(..cmd, named_args: [p.constructor(p), ..cmd.named_args]) } -/// Gets the current value for the associated string flag -/// -fn get_string_flag(from flags: Flags, for name: String) -> snag.Result(String) { - use flag <- get_value(flags, name) - case flag.value { - S(FlagInternals(value: Some(val), ..)) -> Ok(val) - S(FlagInternals(value: None, ..)) -> flag_not_provided_error() - _ -> access_type_error("string") +fn parameter_name(p: Parameters(a)) -> String { + case p { + B(value) -> value.internal.name + F(value) -> value.internal.name + LF(value) -> value.internal.name + I(value) -> value.internal.name + LI(value) -> value.internal.name + LS(value) -> value.internal.name + S(value) -> value.internal.name } } -/// Gets the current value for the associated strings flag -/// -fn get_strings_flag( - from flags: Flags, - for name: String, -) -> snag.Result(List(String)) { - use flag <- get_value(flags, name) - case flag.value { - LS(FlagInternals(value: Some(val), ..)) -> Ok(val) - LS(FlagInternals(value: None, ..)) -> flag_not_provided_error() - _ -> access_type_error("string list") +fn parse_parameter( + p: Parameters(a), + input: String, +) -> Result(#(Parameters(a), String), Snag) { + case p { + B(param) -> { + use x <- result.map(param.internal.parser(input)) + #( + Parameter( + ..param, + internal: parameter.Parameter(..param.internal, value: Some(x)), + ) + |> param.constructor, + param.internal.name, + ) + } + + F(param) -> { + use x <- result.map(param.internal.parser(input)) + #( + Parameter( + ..param, + internal: parameter.Parameter(..param.internal, value: Some(x)), + ) + |> param.constructor, + param.internal.name, + ) + } + I(param) -> { + use x <- result.map(param.internal.parser(input)) + #( + Parameter( + ..param, + internal: parameter.Parameter(..param.internal, value: Some(x)), + ) + |> param.constructor, + param.internal.name, + ) + } + LF(param) -> { + use x <- result.map(param.internal.parser(input)) + #( + Parameter( + ..param, + internal: parameter.Parameter(..param.internal, value: Some(x)), + ) + |> param.constructor, + param.internal.name, + ) + } + LI(param) -> { + use x <- result.map(param.internal.parser(input)) + #( + Parameter( + ..param, + internal: parameter.Parameter(..param.internal, value: Some(x)), + ) + |> param.constructor, + param.internal.name, + ) + } + LS(param) -> { + use x <- result.map(param.internal.parser(input)) + #( + param.constructor( + Parameter( + ..param, + internal: parameter.Parameter(..param.internal, value: Some(x)), + ), + ), + param.internal.name, + ) + } + S(param) -> { + use x <- result.map(param.internal.parser(input)) + #( + Parameter( + ..param, + internal: parameter.Parameter(..param.internal, value: Some(x)), + ) + |> param.constructor, + param.internal.name, + ) + } } } -/// Gets the current value for the associated float flag -/// -fn get_floats(from flags: Flags, for name: String) -> snag.Result(Float) { - use flag <- get_value(flags, name) - case flag.value { - F(FlagInternals(value: Some(val), ..)) -> Ok(val) - F(FlagInternals(value: None, ..)) -> flag_not_provided_error() - _ -> access_type_error("float") +fn parameter_help(p: Parameters(a)) -> String { + case p { + B(value) -> value.internal.description + F(value) -> value.internal.description + LF(value) -> value.internal.description + I(value) -> value.internal.description + LI(value) -> value.internal.description + LS(value) -> value.internal.description + S(value) -> value.internal.description } } -/// Gets the current value for the associated float flag +/// Glint's typed parameters. /// -fn get_floats_flag( - from flags: Flags, - for name: String, -) -> snag.Result(List(Float)) { - use flag <- get_value(flags, name) - case flag.value { - LF(FlagInternals(value: Some(val), ..)) -> Ok(val) - LF(FlagInternals(value: None, ..)) -> flag_not_provided_error() - _ -> access_type_error("float list") - } +/// Flags can be created using any of: +/// - [`glint.int`](#int) +/// - [`glint.ints`](#ints) +/// - [`glint.float`](#float) +/// - [`glint.floats`](#floats) +/// - [`glint.string`](#string) +/// - [`glint.strings_flag`](#strings_flag) +/// - [`glint.bool`](#bool) +/// +pub opaque type Parameter(kind, usage) { + Parameter( + internal: parameter.Parameter(kind, Snag), + constructor: fn(Parameter(kind, usage)) -> Parameters(usage), + getter: fn(dict.Dict(String, Parameters(usage))) -> Result(kind, Snag), + ) } -// traverses a Glint(a) tree for the provided path -// executes the provided function on the terminal node -// -fn update_at( - in glint: Glint(a), - at path: List(String), - do f: fn(CommandNode(a)) -> CommandNode(a), -) -> Glint(a) { - Glint( - ..glint, - cmd: do_update_at(through: glint.cmd, at: sanitize_path(path), do: f), - ) +type Parameters(a) { + I(value: Parameter(Int, a)) + LI(value: Parameter(List(Int), a)) + S(value: Parameter(String, a)) + LS(value: Parameter(List(String), a)) + B(value: Parameter(Bool, a)) + F(value: Parameter(Float, a)) + LF(value: Parameter(List(Float), a)) } -fn do_update_at( - through node: CommandNode(a), - at path: List(String), - do f: fn(CommandNode(a)) -> CommandNode(a), -) -> CommandNode(a) { - case path { - [] -> f(node) - [next, ..rest] -> { - CommandNode(..node, subcommands: { - use found <- dict.upsert(node.subcommands, next) - found - |> option.lazy_unwrap(empty_command) - |> do_update_at(rest, f) - }) - } +fn flatten(f: fn() -> Result(Out(a), Snag)) -> Out(a) { + case f() { + Error(snag) -> Failure(snag) + Ok(Success(a)) -> Success(a) + Ok(Failure(snag)) -> Failure(snag) + Ok(Info(info)) -> Info(info) } } -@external(erlang, "erlang", "halt") -@external(javascript, "node:process", "exit") -fn exit(status: Int) -> Nil +/// This is a convenience function for working with `Result(a,Snag)` inside of glint commands. +/// +/// If the result is `Ok(a)`, it will apply the provided function `f` to `a` and return the result of that function. +/// If the result is `Error(snag)`, it will return `Failure(snag)`. +/// +pub fn try(r: Result(a, Snag), f: fn(a) -> Out(b)) -> Out(b) { + case r { + Ok(a) -> f(a) + Error(snag) -> Failure(snag) + } +} diff --git a/src/glint/constraint.gleam b/src/glint/constraint.gleam index 29ccb46..442f804 100644 --- a/src/glint/constraint.gleam +++ b/src/glint/constraint.gleam @@ -14,7 +14,7 @@ pub type Constraint(a) = /// import glint /// import glint/constraint /// ... -/// glint.int_flag("my_flag") +/// glint.int("my_flag") /// |> glint.constraint(constraint.one_of([1, 2, 3, 4])) /// ``` /// @@ -45,7 +45,7 @@ pub fn one_of(allowed: List(a)) -> Constraint(a) { /// import glint /// import glint/constraint /// ... -/// glint.int_flag("my_flag") +/// glint.int("my_flag") /// |> glint.constraint(constraint.none_of([1, 2, 3, 4])) /// ``` /// @@ -82,7 +82,7 @@ pub fn none_of(disallowed: List(a)) -> Constraint(a) { /// import glint /// import glint/constraint /// ... -/// use li <- glint.flag_constraint(glint.int_flag("my_flag")) +/// use li <- glint.flag_constraint(glint.int("my_flag")) /// use i <- constraint.each() /// i |> one_of([1, 2, 3, 4]) /// ``` @@ -92,7 +92,7 @@ pub fn none_of(disallowed: List(a)) -> Constraint(a) { /// import glint /// import glint/constraint /// ... -/// glint.int_flag("my_flag") +/// glint.int("my_flag") /// |> glint.flag_constraint( /// constraint.one_of([1,2,3,4]) /// |> constraint.each diff --git a/src/glint/internal/help.gleam b/src/glint/internal/help.gleam index 2219816..d6d7ac8 100644 --- a/src/glint/internal/help.gleam +++ b/src/glint/internal/help.gleam @@ -2,25 +2,17 @@ import gleam/bool import gleam/int import gleam/list import gleam/option.{type Option, None, Some} +import gleam/pair import gleam/string -import gleam_community/ansi -import gleam_community/colour.{type Colour} -import glint/internal/utils - -/// Style heading text with the provided rgb colouring -/// this is only intended for use within glint itself. -/// -fn heading_style(heading: String, colour: Colour) -> String { - heading - |> ansi.bold - |> ansi.underline - |> ansi.italic - |> ansi.hex(colour.to_rgb_hex(colour)) -} // --- HELP: CONSTANTS --- // -pub const help_flag = Flag(Metadata("help", "Print help information"), "") +pub const help_flag = Parameter(Metadata("help", "Print help information"), "") + +pub const version_flag = Parameter( + Metadata("version", "Print the app version"), + "", +) const flags_heading = "FLAGS:" @@ -28,19 +20,23 @@ const subcommands_heading = "SUBCOMMANDS:" const usage_heading = "USAGE:" +const named_args_heading = "ARGUMENTS:" + // --- HELP: TYPES --- pub type ArgsCount { - MinArgs(Int) - EqArgs(Int) + MinArgs(name: String, help: String, cont: Int) + EqArgs(name: String, help: String, cont: Int) + NoArgs } pub type Config { Config( name: Option(String), - usage_colour: Option(Colour), - flags_colour: Option(Colour), - subcommands_colour: Option(Colour), + usage_colour: fn(String) -> String, + named_args_colour: fn(String) -> String, + flags_colour: fn(String) -> String, + subcommands_colour: fn(String) -> String, as_module: Bool, description: Option(String), indent_width: Int, @@ -60,8 +56,8 @@ pub type Metadata { /// Help type for flag metadata /// -pub type Flag { - Flag(meta: Metadata, type_: String) +pub type Parameter { + Parameter(meta: Metadata, type_: String) } /// Help type for command metadata @@ -70,35 +66,30 @@ pub type Command { // Every command has a name and description meta: Metadata, // A command can have >= 0 flags associated with it - flags: List(Flag), + flags: List(Parameter), // A command can have >= 0 subcommands associated with it subcommands: List(Metadata), // A command can have a set number of unnamed arguments - unnamed_args: Option(ArgsCount), + unnamed_args: ArgsCount, // A command can specify named arguments - named_args: List(String), + named_args: List(Parameter), ) } // -- HELP - FUNCTIONS - STRINGIFIERS -- pub fn command_help_to_string(help: Command, config: Config) -> String { - let command = case help.meta.name { - "" -> "" - s -> "Command: " <> s - } - let command_description = help.meta.description - |> utils.wordwrap(config.max_output_width) + |> wordwrap(config.max_output_width) |> string.join("\n") [ config.description |> option.unwrap(""), - command, command_description, command_help_to_usage_string(help, config), - flags_help_to_string(help.flags, config), subcommands_help_to_string(help.subcommands, config), + args_help_to_string(help.named_args, help.unnamed_args, config), + flags_help_to_string(help.flags, config), ] |> list.filter(fn(s) { s != "" }) |> string.join("\n\n") @@ -106,34 +97,21 @@ pub fn command_help_to_string(help: Command, config: Config) -> String { // -- HELP - FUNCTIONS - STRINGIFIERS - USAGE -- -/// convert a List(Flag) to a list of strings for use in usage text -/// -fn flags_help_to_usage_strings(help: List(Flag), config: Config) -> List(String) { - help - |> list.map(flag_help_to_string(_, config)) - |> list.sort(string.compare) -} - /// generate the usage help text for the flags of a command /// -fn flags_help_to_usage_string(config: Config, help: List(Flag)) -> String { - use <- bool.guard(help == [], "") - let content = - help - |> flags_help_to_usage_strings(config) - |> string.join(" ") - - "[ " <> content <> " ]" +fn flags_help_to_usage_string(help: List(Parameter)) -> String { + case help { + [] -> "" + _ -> "[ flags ]" + } } /// convert an ArgsCount to a string for usage text /// fn args_count_to_usage_string(count: ArgsCount) -> String { case count { - EqArgs(0) -> "" - EqArgs(1) -> "[ 1 argument ]" - EqArgs(n) -> "[ " <> int.to_string(n) <> " arguments ]" - MinArgs(n) -> "[ " <> int.to_string(n) <> " or more arguments ]" + NoArgs -> "" + EqArgs(name, _, _) | MinArgs(name, _, _) -> "[ " <> name <> ".. ]" } } @@ -146,7 +124,7 @@ fn command_help_to_usage_string(help: Command, config: Config) -> String { None -> "gleam run" } - let flags = flags_help_to_usage_string(config, help.flags) + let flags = flags_help_to_usage_string(help.flags) let subcommands = case list.map(help.subcommands, fn(sc) { sc.name }) |> list.sort(string.compare) @@ -158,12 +136,10 @@ fn command_help_to_usage_string(help: Command, config: Config) -> String { let named_args = help.named_args - |> list.map(fn(s) { "<" <> s <> ">" }) + |> list.map(fn(s) { "<" <> s.meta.name <> ">" }) |> string.join(" ") - let unnamed_args = - option.map(help.unnamed_args, args_count_to_usage_string) - |> option.unwrap("[ ARGS ]") + let unnamed_args = help.unnamed_args |> args_count_to_usage_string // The max width of the usage accounts for the constant indent let max_usage_width = config.max_output_width - config.indent_width @@ -172,13 +148,10 @@ fn command_help_to_usage_string(help: Command, config: Config) -> String { [app_name, help.meta.name, subcommands, named_args, unnamed_args, flags] |> list.filter(fn(s) { s != "" }) |> string.join(" ") - |> utils.wordwrap(max_usage_width) + |> wordwrap(max_usage_width) |> string.join("\n" <> string.repeat(" ", config.indent_width * 2)) - case config.usage_colour { - None -> usage_heading - Some(pretty) -> heading_style(usage_heading, pretty) - } + config.usage_colour(usage_heading) <> "\n" <> string.repeat(" ", config.indent_width) <> content @@ -186,25 +159,22 @@ fn command_help_to_usage_string(help: Command, config: Config) -> String { // -- HELP - FUNCTIONS - STRINGIFIERS - FLAGS -- -/// generate the usage help string for a command +/// generate the usage help string for a list of flags /// -fn flags_help_to_string(help: List(Flag), config: Config) -> String { +fn flags_help_to_string(help: List(Parameter), config: Config) -> String { use <- bool.guard(help == [], "") let longest_flag_length = help |> list.map(flag_help_to_string(_, config)) - |> utils.max_string_length + |> max_string_length |> int.max(config.min_first_column_width) - let heading = case config.flags_colour { - None -> flags_heading - Some(pretty) -> heading_style(flags_heading, pretty) - } + let heading = config.flags_colour(flags_heading) let content = to_spaced_indented_string( - [help_flag, ..help], + [help_flag, version_flag, ..help], fn(help) { #(flag_help_to_string(help, config), help.meta.description) }, longest_flag_length, config, @@ -215,7 +185,7 @@ fn flags_help_to_string(help: List(Flag), config: Config) -> String { /// generate the help text for a flag without a description /// -fn flag_help_to_string(help: Flag, config: Config) -> String { +fn flag_help_to_string(help: Parameter, config: Config) -> String { config.flag_prefix <> help.meta.name <> case help.type_ { @@ -234,13 +204,10 @@ fn subcommands_help_to_string(help: List(Metadata), config: Config) -> String { let longest_subcommand_length = help |> list.map(fn(h) { h.name }) - |> utils.max_string_length + |> max_string_length |> int.max(config.min_first_column_width) - let heading = case config.subcommands_colour { - None -> subcommands_heading - Some(pretty) -> heading_style(subcommands_heading, pretty) - } + let heading = config.subcommands_colour(subcommands_heading) let content = to_spaced_indented_string( @@ -253,6 +220,60 @@ fn subcommands_help_to_string(help: List(Metadata), config: Config) -> String { heading <> content } +// -- HELP - FUNCTIONS - STRINGIFIERS - NAMED ARGUMENTS -- +/// generate the usage help string for named arguments +/// +fn args_help_to_string( + named: List(Parameter), + unnamed: ArgsCount, + config: Config, +) -> String { + use <- bool.guard(named == [] && { unnamed == NoArgs }, "") + + let unnamed = case unnamed { + EqArgs(name, desc, 1) -> [#(name <> ".. 1 arg", desc)] + EqArgs(name, desc, count) -> [ + #(name <> ".. " <> int.to_string(count) <> " args", desc), + ] + MinArgs(name, desc, 1) -> [#(name <> ".. >= 1 arg", desc)] + MinArgs(name, desc, count) -> [ + #(name <> ".. >= " <> int.to_string(count) <> " args", desc), + ] + NoArgs -> [] + } + + let helps = + list.flatten([ + list.map(named, fn(help) { + #(named_arg_help_to_string(help), help.meta.description) + }), + unnamed, + ]) + + let longest_arg_length = + helps + |> list.map(pair.first) + |> max_string_length + |> int.max(config.min_first_column_width) + + let heading = config.named_args_colour(named_args_heading) + + let content = + to_spaced_indented_string(helps, fn(x) { x }, longest_arg_length, config) + + heading <> content +} + +/// generate the help text for a flag without a description +/// +fn named_arg_help_to_string(help: Parameter) -> String { + help.meta.name + <> case help.type_ { + "" -> "" + _ -> ": " <> help.type_ + } +} + /// convert a list of items to an indented string with spaced contents /// fn to_spaced_indented_string( @@ -294,7 +315,7 @@ fn format_content( config.max_output_width |> int.subtract(left_length + config.indent_width) |> int.max(config.min_first_column_width) - |> utils.wordwrap(right, _) + |> wordwrap(right, _) let right_formatted = string.join( @@ -315,3 +336,87 @@ fn format_content( wrapped, ) } + +/// Returns the length of the longest string in the list. +/// +fn max_string_length(strings: List(String)) -> Int { + use max, f <- list.fold(strings, 0) + + f + |> string.length + |> int.max(max) +} + +/// Wraps the given string so that no lines exceed the given width. Newlines in +/// the input string are retained. +/// +pub fn wordwrap(s: String, max_width: Int) -> List(String) { + use <- bool.guard(s == "", []) + use line <- list.flat_map(space_split_lines(s)) + line + |> string.split(" ") + |> do_wordwrap(max_width, "", []) +} + +fn do_wordwrap( + tokens: List(String), + max_width: Int, + line: String, + lines: List(String), +) -> List(String) { + case tokens { + // Handle the next token + [token, ..tokens] -> { + let token_length = string.length(token) + let line_length = string.length(line) + + case line, line_length + 1 + token_length <= max_width { + // When the current line is empty the next token always goes on it + // regardless of its length + "", _ -> do_wordwrap(tokens, max_width, token, lines) + + // Add the next token to the current line if it fits + _, True -> do_wordwrap(tokens, max_width, line <> " " <> token, lines) + + // Start a new line with the next token as it exceeds the max width if + // added to the current line + _, False -> do_wordwrap(tokens, max_width, token, [line, ..lines]) + } + } + + // There are no more tokens so return the final result, adding the current + // line to it if it's not empty + [] if line == "" -> list.reverse(lines) + [] -> list.reverse([line, ..lines]) + } +} + +/// split a string for consecutive newline groups +/// replaces individual newlines with a spacet +/// groups of newlines > 1 are replaced with n-2 newlines followed by a new item +fn space_split_lines(s: String) -> List(String) { + let chunks = + s + |> string.trim + |> string.to_graphemes + |> list.chunk(fn(s) { s == "\n" }) + + let lines = { + use acc, chunk <- list.fold(chunks, #([], False)) + case chunk, acc.0 { + // convert newline chunks into n-2 newlines + ["\n", "\n", ..rest], [s, ..accs] -> #( + [s <> string.concat(rest), ..accs], + True, + ) + // convert single newlines into spaces + ["\n"], [s, ..accs] -> #([s <> " ", ..accs], False) + // add the next string to the end of the last IFF the last was not a multi newline + _, [s, ..accs] if !acc.1 -> #([s <> string.concat(chunk), ..accs], False) + // add the next string as the next value in the accumulated list + _, _ -> #([string.concat(chunk), ..acc.0], False) + } + } + + list.reverse(lines.0) +} diff --git a/src/glint/internal/parameter.gleam b/src/glint/internal/parameter.gleam new file mode 100644 index 0000000..f7345aa --- /dev/null +++ b/src/glint/internal/parameter.gleam @@ -0,0 +1,22 @@ +import gleam/option +import gleam/result + +pub type Parameter(kind, error) { + Parameter( + name: String, + description: String, + parser: fn(String) -> Result(kind, error), + value: option.Option(kind), + ) +} + +pub fn add_constraint( + p: Parameter(kind, error), + constraint: fn(kind) -> Result(kind, error), +) -> Parameter(kind, error) { + Parameter(..p, parser: fn(s) { + s + |> p.parser + |> result.try(constraint) + }) +} diff --git a/src/glint/internal/parse.gleam b/src/glint/internal/parse.gleam new file mode 100644 index 0000000..bbd300c --- /dev/null +++ b/src/glint/internal/parse.gleam @@ -0,0 +1,56 @@ +import gleam/list +import gleam/result +import gleam/string +import lenient_parse + +pub type ParseError { + FailedToParse(value: String, kind: String) +} + +pub fn string(s: String) -> Result(String, ParseError) { + Ok(s) +} + +pub fn strings(s: String) -> Result(List(String), ParseError) { + s + |> string.split(",") + |> Ok +} + +pub fn int(s: String) -> Result(Int, ParseError) { + s + |> lenient_parse.to_int + |> result.replace_error(FailedToParse(s, "int")) +} + +pub fn ints(s: String) -> Result(List(Int), ParseError) { + use s <- list.try_map(string.split(s, ",")) + s + |> lenient_parse.to_int + |> result.replace_error(FailedToParse(s, "int list")) +} + +pub fn float(s: String) -> Result(Float, ParseError) { + s + |> lenient_parse.to_float + |> result.replace_error(FailedToParse(s, "float")) +} + +pub fn floats(s: String) -> Result(List(Float), ParseError) { + use s <- list.try_map(string.split(s, ",")) + s + |> lenient_parse.to_float + |> result.replace_error(FailedToParse(s, "float list")) +} + +pub fn bool(s: String) -> Result(Bool, ParseError) { + case string.lowercase(s) { + "true" | "t" | "1" -> Ok(True) + "false" | "f" | "0" -> Ok(False) + _ -> Error(FailedToParse(s, "bool")) + } +} + +pub fn error_to_string(error: ParseError) -> String { + "failed to parse value '" <> error.value <> "' as " <> error.kind +} diff --git a/src/glint/internal/utils.gleam b/src/glint/internal/utils.gleam deleted file mode 100644 index 75f9efc..0000000 --- a/src/glint/internal/utils.gleam +++ /dev/null @@ -1,88 +0,0 @@ -import gleam/bool -import gleam/int -import gleam/list -import gleam/string - -/// Returns the length of the longest string in the list. -/// -pub fn max_string_length(strings: List(String)) -> Int { - use max, f <- list.fold(strings, 0) - - f - |> string.length - |> int.max(max) -} - -/// Wraps the given string so that no lines exceed the given width. Newlines in -/// the input string are retained. -/// -pub fn wordwrap(s: String, max_width: Int) -> List(String) { - use <- bool.guard(s == "", []) - use line <- list.flat_map(space_split_lines(s)) - line - |> string.split(" ") - |> do_wordwrap(max_width, "", []) -} - -fn do_wordwrap( - tokens: List(String), - max_width: Int, - line: String, - lines: List(String), -) -> List(String) { - case tokens { - // Handle the next token - [token, ..tokens] -> { - let token_length = string.length(token) - let line_length = string.length(line) - - case line, line_length + 1 + token_length <= max_width { - // When the current line is empty the next token always goes on it - // regardless of its length - "", _ -> do_wordwrap(tokens, max_width, token, lines) - - // Add the next token to the current line if it fits - _, True -> do_wordwrap(tokens, max_width, line <> " " <> token, lines) - - // Start a new line with the next token as it exceeds the max width if - // added to the current line - _, False -> do_wordwrap(tokens, max_width, token, [line, ..lines]) - } - } - - // There are no more tokens so return the final result, adding the current - // line to it if it's not empty - [] if line == "" -> list.reverse(lines) - [] -> list.reverse([line, ..lines]) - } -} - -/// split a string for consecutive newline groups -/// replaces individual newlines with a spacet -/// groups of newlines > 1 are replaced with n-2 newlines followed by a new item -fn space_split_lines(s: String) -> List(String) { - let chunks = - s - |> string.trim - |> string.to_graphemes - |> list.chunk(fn(s) { s == "\n" }) - - let lines = { - use acc, chunk <- list.fold(chunks, #([], False)) - case chunk, acc.0 { - // convert newline chunks into n-2 newlines - ["\n", "\n", ..rest], [s, ..accs] -> #( - [s <> string.concat(rest), ..accs], - True, - ) - // convert single newlines into spaces - ["\n"], [s, ..accs] -> #([s <> " ", ..accs], False) - // add the next string to the end of the last IFF the last was not a multi newline - _, [s, ..accs] if !acc.1 -> #([s <> string.concat(chunk), ..accs], False) - // add the next string as the next value in the accumulated list - _, _ -> #([string.concat(chunk), ..acc.0], False) - } - } - - list.reverse(lines.0) -} diff --git a/src/glintio.gleam b/src/glintio.gleam new file mode 100644 index 0000000..7a0dde1 --- /dev/null +++ b/src/glintio.gleam @@ -0,0 +1,55 @@ +import gleam/io +import glint.{type Out} +import snag + +@external(erlang, "erlang", "halt") +@external(javascript, "node:process", "exit") +fn do_exit(status: Int) -> a + +/// This is a convenience function for exiting when the glint output output is Info or Failure. +/// For convenience, this function returns the inner value if the Result is Out(a). +/// +/// ### Example: +/// +/// ```gleam +/// cli +/// |> glint.run(args) +/// |> glintio.exit +/// ``` +/// +pub fn exit(res: glint.Out(a)) -> a { + case res { + glint.Success(a) -> a + glint.Info(_) -> do_exit(0) + glint.Failure(_) -> do_exit(1) + } +} + +/// Prints the result of executing a Glint application, including the command output value. +/// This function accepts as input a function to convert the command output to a String before printing it. +/// +/// If you do not wish to print the command execution result, but still want to print the help or error text, see the [print_ignore_output](#print_ignore_output) function. +pub fn print(res: Out(a), f: fn(a) -> String) -> Out(a) { + case res { + glint.Success(o) -> f(o) + glint.Info(s) -> s + glint.Failure(s) -> snag.pretty_print(s) + } + |> io.println + + res +} + +/// Prints the result of executing a Glint application. +/// This function accepts as input a function to convert the output value to a String before printing it. +/// +/// If you also wish to print the command execution result, see the [print](#print) function. +pub fn print_ignore_output(res: Out(a)) -> Out(a) { + case res { + glint.Success(_) -> Nil + glint.Info(s) -> io.println(s) + glint.Failure(s) -> s |> snag.pretty_print |> io.println + } + + res +} diff --git a/test/contraint_test.gleam b/test/contraint_test.gleam index fc28568..1532abb 100644 --- a/test/contraint_test.gleam +++ b/test/contraint_test.gleam @@ -1,96 +1,83 @@ -import gleeunit/should -import glint +import glint.{Failure, Success} import glint/constraint pub fn one_of_test() { - 1 - |> constraint.one_of([1, 2, 3]) - |> should.equal(Ok(1)) - - 1 - |> constraint.one_of([2, 3, 4]) - |> should.be_error() - - [1, 2, 3] - |> { - [5, 4, 3, 2, 1] - |> constraint.one_of - |> constraint.each - } - |> should.equal(Ok([1, 2, 3])) - - [1, 6, 3] - |> { - [5, 4, 3, 2, 1] - |> constraint.one_of - |> constraint.each - } - |> should.be_error() + assert constraint.one_of([1, 2, 3])(1) == Ok(1) + + let assert Error(_) = constraint.one_of([2, 3, 4])(1) + + assert { + [5, 4, 3, 2, 1] + |> constraint.one_of + |> constraint.each + }([1, 2, 3]) + == Ok([1, 2, 3]) + + let assert Error(_) = + { + [5, 4, 3, 2, 1] + |> constraint.one_of + |> constraint.each + }([1, 6, 3]) } pub fn none_of_test() { - 1 - |> constraint.none_of([1, 2, 3]) - |> should.be_error - - 1 - |> constraint.none_of([2, 3, 4]) - |> should.equal(Ok(1)) - - [1, 2, 3] - |> { - [4, 5, 6, 7, 8] - |> constraint.none_of - |> constraint.each - } - |> should.equal(Ok([1, 2, 3])) - - [1, 6, 3] - |> { - [4, 5, 6, 7, 8] - |> constraint.none_of - |> constraint.each - } - |> should.be_error() + let assert Error(_) = constraint.none_of([1, 2, 3])(1) + + assert constraint.none_of([2, 3, 4])(1) == Ok(1) + + assert { + [4, 5, 6, 7, 8] + |> constraint.none_of + |> constraint.each + }([1, 2, 3]) + == Ok([1, 2, 3]) + + let assert Error(_) = + { + [4, 5, 6, 7, 8] + |> constraint.none_of + |> constraint.each + }([1, 6, 3]) } pub fn flag_one_of_none_of_test() { let #(test_flag, success, failure) = #( - glint.int_flag("i") - |> glint.flag_constraint(constraint.one_of([1, 2, 3])) - |> glint.flag_constraint(constraint.none_of([4, 5, 6])), + glint.int("i") + |> glint.constraint(constraint.one_of([1, 2, 3])) + |> glint.constraint(constraint.none_of([4, 5, 6])), "1", "6", ) - glint.new() - |> glint.add([], { - use access <- glint.flag(test_flag) - use _, _, flags <- glint.command() - flags - |> access - |> should.be_ok - }) - |> glint.execute(["--i=" <> success]) - |> should.be_ok - - glint.new() - |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() - Nil - }) - |> glint.execute(["--i=" <> failure]) - |> should.be_error + let assert glint.Success(_) = + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + use flag <- access(flags) + assert flag == 1 + Success(Nil) + }) + |> glint.run(["--i=" <> success]) + + let assert Failure(_) = + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag) + use _, _, _flags <- glint.command() + Success(Nil) + }) + |> glint.run(["--i=" <> failure]) let #(test_flag, success, failure) = #( - glint.ints_flag("li") - |> glint.flag_constraint( + glint.ints("li") + |> glint.constraint( [1, 2, 3] |> constraint.one_of |> constraint.each, ) - |> glint.flag_constraint( + |> glint.constraint( [4, 5, 6] |> constraint.none_of |> constraint.each, @@ -99,61 +86,60 @@ pub fn flag_one_of_none_of_test() { "2,2,6", ) - glint.new() - |> glint.add([], { - use access <- glint.flag(test_flag) - use _, _, flags <- glint.command() - flags - |> access - |> should.be_ok - }) - |> glint.execute(["--li=" <> success]) - |> should.be_ok - - glint.new() - |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() - panic - }) - |> glint.execute(["--li=" <> failure]) - |> should.be_error + let assert Success(_) = + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + use _ <- access(flags) + Success(Nil) + }) + |> glint.run(["--li=" <> success]) + + let assert Failure(_) = + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.run(["--li=" <> failure]) let #(test_flag, success, failure) = #( - glint.float_flag("f") - |> glint.flag_constraint(constraint.one_of([1.0, 2.0, 3.0])) - |> glint.flag_constraint(constraint.none_of([4.0, 5.0, 6.0])), + glint.float("f") + |> glint.constraint(constraint.one_of([1.0, 2.0, 3.0])) + |> glint.constraint(constraint.none_of([4.0, 5.0, 6.0])), "1.0", "6.0", ) - glint.new() - |> glint.add([], { - use access <- glint.flag(test_flag) - use _, _, flags <- glint.command() - flags - |> access - |> should.be_ok - }) - |> glint.execute(["--f=" <> success]) - |> should.be_ok - - glint.new() - |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() - panic - }) - |> glint.execute(["--f=" <> failure]) - |> should.be_error + let assert Success(_) = + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + use _ <- access(flags) + Success(Nil) + }) + |> glint.run(["--f=" <> success]) + + let assert Failure(_) = + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + use _flag <- access(flags) + panic + }) + |> glint.run(["--f=" <> failure]) let #(test_flag, success, failure) = #( - glint.floats_flag("lf") - |> glint.flag_constraint( + glint.floats("lf") + |> glint.constraint( [1.0, 2.0, 3.0] |> constraint.one_of() |> constraint.each, ) - |> glint.flag_constraint( + |> glint.constraint( [4.0, 5.0, 6.0] |> constraint.none_of() |> constraint.each, @@ -161,62 +147,60 @@ pub fn flag_one_of_none_of_test() { "3.0,2.0,1.0", "2.0,3.0,6.0", ) - glint.new() - |> glint.add([], { - use access <- glint.flag(test_flag) - use _, _, flags <- glint.command() - flags - |> access - |> should.be_ok - }) - |> glint.execute(["--lf=" <> success]) - |> should.be_ok - - glint.new() - |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() - panic - }) - |> glint.execute(["--lf=" <> failure]) - |> should.be_error + let assert Success(_) = + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + use _ <- access(flags) + Success(Nil) + }) + |> glint.run(["--lf=" <> success]) + + let assert Failure(_) = + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.run(["--lf=" <> failure]) let #(test_flag, success, failure) = #( - glint.string_flag("s") - |> glint.flag_constraint(constraint.one_of(["t1", "t2", "t3"])) - |> glint.flag_constraint(constraint.none_of(["t4", "t5", "t6"])), + glint.string("s") + |> glint.constraint(constraint.one_of(["t1", "t2", "t3"])) + |> glint.constraint(constraint.none_of(["t4", "t5", "t6"])), "t3", "t4", ) - glint.new() - |> glint.add([], { - use access <- glint.flag(test_flag) - use _, _, flags <- glint.command() - flags - |> access - |> should.be_ok - }) - |> glint.execute(["--s=" <> success]) - |> should.be_ok - - glint.new() - |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() - panic - }) - |> glint.execute(["--s=" <> failure]) - |> should.be_error + let assert Success(_) = + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + use _ <- access(flags) + Success(Nil) + }) + |> glint.run(["--s=" <> success]) + + let assert Failure(_) = + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.run(["--s=" <> failure]) let #(test_flag, success, failure) = #( - glint.strings_flag("ls") - |> glint.flag_constraint( + glint.strings("ls") + |> glint.constraint( ["t1", "t2", "t3"] |> constraint.one_of |> constraint.each, ) - |> glint.flag_constraint( + |> glint.constraint( ["t4", "t5", "t6"] |> constraint.none_of |> constraint.each, @@ -225,23 +209,22 @@ pub fn flag_one_of_none_of_test() { "t2,t4,t1", ) - glint.new() - |> glint.add([], { - use access <- glint.flag(test_flag) - use _, _, flags <- glint.command() - flags - |> access - |> should.be_ok - }) - |> glint.execute(["--ls=" <> success]) - |> should.be_ok - - glint.new() - |> glint.add([], { - use _access <- glint.flag(test_flag) - use _, _, _flags <- glint.command() - panic - }) - |> glint.execute(["--ls=" <> failure]) - |> should.be_error + let assert Success(_) = + glint.new() + |> glint.add([], { + use access <- glint.flag(test_flag) + use _, _, flags <- glint.command() + use _ <- access(flags) + Success(Nil) + }) + |> glint.run(["--ls=" <> success]) + + let assert Failure(_) = + glint.new() + |> glint.add([], { + use _access <- glint.flag(test_flag) + use _, _, _flags <- glint.command() + panic + }) + |> glint.run(["--ls=" <> failure]) } diff --git a/test/examples/hello.gleam b/test/examples/hello.gleam index 72876da..b45648b 100644 --- a/test/examples/hello.gleam +++ b/test/examples/hello.gleam @@ -1,68 +1,10 @@ //// This module demonstrates a simple glint app with 2 commands -//// -//// ## Usage -//// -//// ### Running the application -//// -//// You can run this example with `gleam run -m examples/hello -- ` from the root of the repo -//// -//// The application prints: `Hello, !` -//// The `hello` application accepts at least one argument, being the names of people to say hello to. -//// - No input: `gleam run` -> prints "Hello, Joe!" -//// - One input: `gleam run Joe` -> prints "Hello, Joe!" -//// - Two inputs: `gleam run Rob Louis` -> prints "Hello, Rob and Louis!" -//// - \>2 inputs: `gleam run Rob Louis Hayleigh` -> prints "Hello, Rob, Louis and Hayleigh!" -//// -//// ### Flags -//// -//// All commands 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 Text -//// -//// Here is the help text for the root command: -//// -//// ```txt -//// It's time to say hello! -//// -//// Prints Hello, ! -//// -//// USAGE: -//// gleam run -m examples/hello ( single ) [ 1 or more arguments ] [ -//// --caps= --repeat= ] -//// -//// FLAGS: -//// --caps= Capitalize the hello message -//// --help Print help information -//// --repeat= Repeat the message n-times -//// -//// SUBCOMMANDS: -//// single Prints Hello, ! -//// ``` -//// -//// Here is the help text for the `single` command: -//// -//// ``` -//// It's time to say hello! -//// -//// Command: single -//// -//// Prints Hello, ! -//// -//// USAGE: -//// gleam run -m examples/hello single [ --caps= --repeat= ] -//// -//// FLAGS: -//// --caps= Capitalize the hello message -//// --help Print help information -//// --repeat= Repeat the message n-times -//// ``` // stdlib imports -import gleam/io +import gleam/function import gleam/list import gleam/string.{uppercase} +import glintio // external dep imports import snag @@ -77,7 +19,9 @@ import glint fn join_names(names: List(String)) -> String { case names { [] -> "" - _ -> do_join_names(names, "") + [name] -> name + [name_1, name_2] -> name_1 <> " and " <> name_2 + [name, ..names] -> do_join_names(names, name) } } @@ -98,13 +42,17 @@ pub fn capitalize(msg, caps) -> String { } /// hello is a function that says hello -pub fn hello( - primary: String, - rest: List(String), +pub fn hello(names: List(String), caps: Bool, repeat: Int) -> String { + greet("Hello", names, caps, repeat) +} + +pub fn greet( + greeting: String, + names: List(String), caps: Bool, repeat: Int, ) -> String { - { "Hello, " <> primary <> join_names(rest) <> "!" } + { greeting <> ", " <> join_names(names) <> "!" } |> capitalize(caps) |> list.repeat(repeat) |> string.join("\n") @@ -114,24 +62,24 @@ pub fn hello( /// a boolean flag with default False to control message capitalization. /// -pub fn caps_flag() -> glint.Flag(Bool) { - glint.bool_flag("caps") - |> glint.flag_default(False) - |> glint.flag_help("Capitalize the hello message") +pub fn caps_flag() -> glint.Parameter(Bool, glint.Flag) { + glint.bool("caps") + |> glint.default(False) + |> glint.param_help("Capitalize the hello message") } /// an int flag with default 1 to control how many times to repeat the message. /// this flag is constrained to values greater than 0. /// -pub fn repeat_flag() -> glint.Flag(Int) { - use n <- glint.flag_constraint( - glint.int_flag("repeat") - |> glint.flag_default(1) - |> glint.flag_help("Repeat the message n-times"), +pub fn repeat_flag() -> glint.Parameter(Int, glint.Flag) { + use n <- glint.constraint( + glint.int("repeat") + |> glint.default(1) + |> glint.param_help("Repeat the message n-times"), ) case n { _ if n > 0 -> Ok(n) - _ -> snag.error("Value must be greater than 0.") + _ -> Error(snag.new("repeat value must be a positive integer")) } } @@ -139,25 +87,41 @@ pub fn repeat_flag() -> glint.Flag(Int) { /// pub fn hello_cmd() -> glint.Command(String) { use <- glint.command_help("Prints Hello, !") - use <- glint.unnamed_args(glint.MinArgs(1)) + use <- glint.min_args("names", 1, "Names of people to greet.") use _, args, flags <- glint.command() - let assert Ok(caps) = glint.get_flag(flags, caps_flag()) - let assert Ok(repeat) = glint.get_flag(flags, repeat_flag()) - let assert [name, ..rest] = args - hello(name, rest, caps, repeat) + use caps <- glint.try(glint.get_flag(flags, caps_flag())) + use repeat <- glint.try(glint.get_flag(flags, repeat_flag())) + glint.Success(hello(args, caps, repeat)) +} + +pub fn hello_custom_cmd() -> glint.Command(String) { + use <- glint.command_help("Prints a greeting for the names provided!") + use greeting <- glint.named_arg( + glint.string("greeting") |> glint.param_help("The greeting to give."), + ) + use <- glint.min_args("names", 1, "Names of people to greet.") + + use named_args, args, flags <- glint.command() + use caps <- glint.try(glint.get_flag(flags, caps_flag())) + use repeat <- glint.try(glint.get_flag(flags, repeat_flag())) + + greeting(named_args) |> greet(args, caps, repeat) |> glint.Success } /// the command function that will be executed as the "single" command /// pub fn hello_single_cmd() -> glint.Command(String) { use <- glint.command_help("Prints Hello, !") - use <- glint.unnamed_args(glint.EqArgs(0)) - use name <- glint.named_arg("name") + use <- glint.no_args() + use name <- glint.named_arg( + glint.string("name") + |> glint.param_help("The name of the person we're saying hello to."), + ) use named_args, _, flags <- glint.command() - let assert Ok(caps) = glint.get_flag(flags, caps_flag()) - let assert Ok(repeat) = glint.get_flag(flags, repeat_flag()) + use caps <- glint.try(glint.get_flag(flags, caps_flag())) + use repeat <- glint.try(glint.get_flag(flags, repeat_flag())) let name = name(named_args) - hello(name, [], caps, repeat) + glint.Success(hello([name], caps, repeat)) } // the function that describes our cli structure @@ -166,31 +130,34 @@ pub fn app() { glint.new() // with an app name of "hello", this is used when printing help text |> glint.with_name("examples/hello") + // with an app version of "dev", this is used when the version flag is used + |> glint.with_version("dev") // apply global help text to all commands |> glint.global_help("It's time to say hello!") // show in usage text that the current app is run as a gleam module |> glint.as_module // with pretty help enabled, using the built-in colours - |> glint.pretty_help(glint.default_pretty_help()) + |> glint.with_default_header_style // with group level flags // with flag `caps` for all commands (equivalent of using glint.global_flag) |> glint.group_flag([], caps_flag()) // // with flag `repeat` for all commands (equivalent of using glint.global_flag) |> glint.group_flag([], repeat_flag()) - // with a root command that executes the `hello` function - |> glint.add( - // add the hello command to the root - at: [], - do: hello_cmd(), - ) - |> glint.add( - // add the hello single command - at: ["single"], - do: hello_single_cmd(), - ) + // add a root command + |> glint.add(at: [], do: hello_cmd()) + // add a 'single' command + |> glint.add(at: ["single"], do: hello_single_cmd()) + // add a 'custom' + |> glint.add(at: ["custom"], do: hello_custom_cmd()) } pub fn main() { - // run with a handler that prints the command output - glint.run_and_handle(app(), argv.load().arguments, io.println) + app() + // run the glint app with the command line arguments provided + |> glint.run(argv.load().arguments) + // print the result of running glint + // the command output is already a string so we can pass it as-is + |> glintio.print(function.identity) + // exit with status 1 if an error was encountered + |> glintio.exit } diff --git a/test/examples/hello_test.gleam b/test/examples/hello_test.gleam index 4b073f4..300cd20 100644 --- a/test/examples/hello_test.gleam +++ b/test/examples/hello_test.gleam @@ -1,6 +1,5 @@ import examples/hello import gleam/list -import gleeunit/should import glint type TestCase { @@ -27,14 +26,10 @@ pub fn hello_test() { ), ]) - let assert [head, ..rest] = tc.input - hello.hello(head, rest, tc.caps, tc.repeat) - |> should.equal(tc.expected) + assert hello.hello(tc.input, tc.caps, tc.repeat) == tc.expected } pub fn app_test() { - use output <- glint.run_and_handle(hello.app(), [ - "Joe", "Gleamlins", "--repeat=2", "--caps", - ]) - should.equal(output, "HELLO, JOE AND GLEAMLINS!\nHELLO, JOE AND GLEAMLINS!") + assert glint.run(hello.app(), ["Joe", "Gleamlins", "--repeat=2", "--caps"]) + == glint.Success("HELLO, JOE AND GLEAMLINS!\nHELLO, JOE AND GLEAMLINS!") } diff --git a/test/flag_test.gleam b/test/flag_test.gleam index b74f60b..db1f51d 100644 --- a/test/flag_test.gleam +++ b/test/flag_test.gleam @@ -1,461 +1,470 @@ -import gleeunit/should import glint pub fn update_flag_test() { let app = glint.new() |> glint.add([], { - use _bflag <- glint.flag(glint.bool_flag("bflag")) - use _sflag <- glint.flag(glint.string_flag("sflag")) - use _lsflag <- glint.flag(glint.strings_flag("lsflag")) - use _iflag <- glint.flag(glint.ints_flag("iflag")) - use _liflag <- glint.flag(glint.ints_flag("liflag")) - use _fflag <- glint.flag(glint.float_flag("fflag")) - use _lfflag <- glint.flag(glint.floats_flag("lfflag")) - glint.command(fn(_, _, _) { Nil }) + use _bflag <- glint.flag(glint.bool("bflag")) + use _sflag <- glint.flag(glint.string("sflag")) + use _lsflag <- glint.flag(glint.strings("lsflag")) + use _iflag <- glint.flag(glint.ints("iflag")) + use _liflag <- glint.flag(glint.ints("liflag")) + use _fflag <- glint.flag(glint.float("fflag")) + use _lfflag <- glint.flag(glint.floats("lfflag")) + glint.command(fn(_, _, _) { glint.Success(Nil) }) }) // update non-existent flag fails - app - |> glint.execute(["--not_a_flag=hello"]) - |> should.be_error() + let assert glint.Failure(_) = glint.run(app, ["--not_a_flag=hello"]) // update bool flag succeeds - app - |> glint.execute(["--bflag=true"]) - |> should.be_ok() + let assert glint.Success(_) = glint.run(app, ["--bflag=true"]) // update bool flag with non-bool value fails - app - |> glint.execute(["--bflag=zzz"]) - |> should.be_error() + let assert glint.Failure(_) = glint.run(app, ["--bflag=zzz"]) // toggle bool flag succeeds - app - |> glint.execute(["--bflag"]) - |> should.be_ok() + let assert glint.Success(Nil) = glint.run(app, ["--bflag"]) // toggle non-bool flag succeeds - app - |> glint.execute(["--sflag"]) - |> should.be_error() + let assert glint.Failure(_) = glint.run(app, ["--sflag"]) // update string flag succeeds - app - |> glint.execute(["--sflag=hello"]) - |> should.be_ok() + let assert glint.Success(Nil) = glint.run(app, ["--sflag=hello"]) // update int flag with non-int fails - app - |> glint.execute(["--iflag=hello"]) - |> should.be_error() + let assert glint.Failure(_) = glint.run(app, ["--iflag=hello"]) // update int flag with int succeeds - app - |> glint.execute(["--iflag=1"]) - |> should.be_ok() + let assert glint.Success(Nil) = glint.run(app, ["--iflag=1"]) // update int list flag with int list succeeds - app - |> glint.execute(["--liflag=1,2,3"]) - |> should.be_ok() + let assert glint.Success(Nil) = glint.run(app, ["--liflag=1,2,3"]) // update int list flag with non int list succeeds - app - |> glint.execute(["--liflag=a,b,c"]) - |> should.be_error() + let assert glint.Failure(_) = glint.run(app, ["--liflag=a,b,c"]) // update float flag with non-int fails - app - |> glint.execute(["--fflag=hello"]) - |> should.be_error() + let assert glint.Failure(_) = glint.run(app, ["--fflag=hello"]) // update float flag with int succeeds - app - |> glint.execute(["--fflag=1.0"]) - |> should.be_ok() + let assert glint.Success(Nil) = glint.run(app, ["--fflag=1.0"]) // update float list flag with int list succeeds - app - |> glint.execute(["--lfflag=1.0,2.0,3.0"]) - |> should.be_ok() + let assert glint.Success(Nil) = glint.run(app, ["--lfflag=1.0,2.0,3.0"]) // update float list flag with non int list succeeds - app - |> glint.execute(["--lfflag=a,b,c"]) - |> should.be_error() + let assert glint.Failure(_) = glint.run(app, ["--lfflag=a,b,c"]) } pub fn unsupported_flag_test() { - glint.new() - |> glint.add(["cmd"], glint.command(fn(_, _, _) { Nil })) - |> glint.execute(["--flag=1"]) - |> should.be_error() + let assert glint.Failure(_) = + glint.new() + |> glint.add(["cmd"], glint.command(fn(_, _, _) { glint.Success(Nil) })) + |> glint.run(["--flag=1"]) } pub fn flag_default_test() { let args = ["arg1", "arg2"] let flag = - glint.string_flag("flag") - |> glint.flag_default("default") - - glint.new() - |> glint.add(["cmd"], { - use flag_ <- glint.flag(flag) - use _, unnamed, flags <- glint.command() - should.equal(args, unnamed) + glint.string("flag") + |> glint.default("default") - flag_(flags) - |> should.equal(Ok("default")) - }) - |> glint.execute(["cmd", ..args]) - |> should.be_ok() + let assert glint.Success(_) = + glint.new() + |> glint.add(["cmd"], { + use flag <- glint.flag(flag) + use _, unnamed, flags <- glint.command() + assert args == unnamed + use flag <- flag(flags) + assert flag == "default" + glint.Success(Nil) + }) + |> glint.run(["cmd", ..args]) } pub fn flag_value_test() { let args = ["arg1", "arg2"] - let flag = glint.string_flag("flag") + let flag = glint.string("flag") let flag_input = "--flag=flag_value" let flag_value_should_be_set = { - use flag_ <- glint.flag(flag) + use flag <- glint.flag(flag) use _, in_args, flags <- glint.command() - should.equal(in_args, args) - - flag_(flags) - |> should.equal(Ok("flag_value")) + assert in_args == args + use flag <- flag(flags) + assert flag == "flag_value" + glint.Success(Nil) } - glint.new() - |> glint.add(["cmd"], flag_value_should_be_set) - |> glint.execute(["cmd", flag_input, ..args]) - |> should.be_ok() + let assert glint.Success(_) = + glint.new() + |> glint.add(["cmd"], flag_value_should_be_set) + |> glint.run(["cmd", flag_input, ..args]) } pub fn int_flag_test() { - let flags = glint.int_flag("flag") + let flags = glint.int("flag") // fails to parse input for flag as int, returns error let flag_input = "--flag=X" - glint.new() - |> glint.add( - [], - glint.flag(flags, fn(_flag) { glint.command(fn(_, _, _) { Nil }) }), - ) - |> glint.execute([flag_input]) - |> should.be_error() + let assert glint.Failure(_) = + glint.new() + |> glint.add( + [], + glint.flag(flags, fn(_flag) { + glint.command(fn(_, _, _) { glint.Success(Nil) }) + }), + ) + |> glint.run([flag_input]) // parses flag input as int, sets value let flag_input = "--flag=10" let expect_flag_value_of_10 = { - use flag_ <- glint.flag(flags) + use flag <- glint.flag(flags) use _, _, flags <- glint.command() - flag_(flags) - |> should.equal(Ok(10)) + use flag <- flag(flags) + assert flag == 10 + glint.Success(Nil) } - glint.new() - |> glint.add([], expect_flag_value_of_10) - |> glint.execute([flag_input]) - |> should.be_ok() + let assert glint.Success(_) = + glint.new() + |> glint.add([], expect_flag_value_of_10) + |> glint.run([flag_input]) } pub fn bool_flag_test() { - let flag = glint.bool_flag("flag") + let flag = glint.bool("flag") // fails to parse input for flag as bool, returns error let flag_input = "--flag=X" - glint.new() - |> glint.add( - [], - glint.flag(flag, fn(_flag) { glint.command(fn(_, _, _) { Nil }) }), - ) - |> glint.execute([flag_input]) - |> should.be_error() + let assert glint.Failure(_) = + glint.new() + |> glint.add( + [], + glint.flag(flag, fn(_flag) { + glint.command(fn(_, _, _) { glint.Success(Nil) }) + }), + ) + |> glint.run([flag_input]) // parses flag input as bool, sets value let flag_input = "--flag=false" let expect_flag_value_of_false = fn(flag) { glint.command(fn(_, _, flags) { - flag(flags) - |> should.equal(Ok(False)) + use flag <- flag(flags) + assert flag == False + glint.Success(Nil) }) } - glint.new() - |> glint.add([], glint.flag(flag, expect_flag_value_of_false)) - |> glint.execute([flag_input]) - |> should.be_ok() + let assert glint.Success(_) = + glint.new() + |> glint.add([], glint.flag(flag, expect_flag_value_of_false)) + |> glint.run([flag_input]) } pub fn strings_flag_test() { - let flags = glint.strings_flag("flag") + let flags = glint.strings("flag") let flag_input = "--flag=val3,val4" let expect_flag_value_list = fn(flag) { glint.command(fn(_, _, flags) { - flag(flags) - |> should.equal(Ok(["val3", "val4"])) + use flag <- flag(flags) + assert flag == ["val3", "val4"] + glint.Success(Nil) }) } - glint.new() - |> glint.add([], glint.flag(flags, expect_flag_value_list)) - |> glint.execute([flag_input]) - |> should.be_ok() + let assert glint.Success(_) = + glint.new() + |> glint.add([], glint.flag(flags, expect_flag_value_list)) + |> glint.run([flag_input]) } pub fn ints_flag_test() { - let flag = glint.ints_flag("flag") + let flag = glint.ints("flag") // fails to parse input for flag as int list, returns error let flag_input = "--flag=val3,val4" - glint.new() - |> glint.add( - [], - glint.flag(flag, fn(_) { glint.command(fn(_, _, _) { Nil }) }), - ) - |> glint.execute([flag_input]) - |> should.be_error() + let assert glint.Failure(_) = + glint.new() + |> glint.add( + [], + glint.flag(flag, fn(_) { + glint.command(fn(_, _, _) { glint.Success(Nil) }) + }), + ) + |> glint.run([flag_input]) // parses flag input as int list, sets value let flag_input = "--flag=3,4" let expect_flag_value_list = fn(flag) { glint.command(fn(_, _, flags) { - flag(flags) - |> should.equal(Ok([3, 4])) + use flag <- flag(flags) + assert flag == [3, 4] + glint.Success(Nil) }) } - glint.new() - |> glint.add([], glint.flag(flag, expect_flag_value_list)) - |> glint.execute([flag_input]) - |> should.be_ok() + let assert glint.Success(_) = + glint.new() + |> glint.add([], glint.flag(flag, expect_flag_value_list)) + |> glint.run([flag_input]) } pub fn float_flag_test() { - let flag = glint.float_flag("flag") + let flag = glint.float("flag") // fails to parse input for flag as float, returns error let flag_input = "--flag=X" - glint.new() - |> glint.add([], { - use _flag <- glint.flag(flag) - use _, _, _ <- glint.command() - Nil - }) - |> glint.execute([flag_input]) - |> should.be_error() + let assert glint.Failure(_) = + glint.new() + |> glint.add([], { + use _flag <- glint.flag(flag) + use _, _, _ <- glint.command() + glint.Success(Nil) + }) + |> glint.run([flag_input]) // parses flag input as float, sets value let flag_input = "--flag=10.0" - let expect_flag_value_of_10 = { - use flag <- glint.flag(flag) - use _, _, flags <- glint.command() - flag(flags) - |> should.equal(Ok(10.0)) + let expect_flag_value_of_10 = fn(flag) { + glint.command(fn(_, _, flags) { + use flag <- flag(flags) + assert flag == 10.0 + glint.Success(Nil) + }) } - glint.new() - |> glint.add([], expect_flag_value_of_10) - |> glint.execute([flag_input]) - |> should.be_ok() + let assert glint.Success(_) = + glint.new() + |> glint.add([], glint.flag(flag, expect_flag_value_of_10)) + |> glint.run([flag_input]) } pub fn floats_flag_test() { - let flag = glint.floats_flag("flag") + let flag = glint.floats("flag") // fails to parse input for flag as float list, returns error let flag_input = "--flag=val3,val4" - glint.new() - |> glint.add([], { - use _flag <- glint.flag(flag) - use _, _, _ <- glint.command() - Nil - }) - |> glint.execute([flag_input]) - |> should.be_error() + let assert glint.Failure(_) = + glint.new() + |> glint.add([], { + use _flag <- glint.flag(flag) + use _, _, _ <- glint.command() + glint.Success(Nil) + }) + |> glint.run([flag_input]) // parses flag input as float list, sets value let flag_input = "--flag=3.0,4.0" let expect_flag_value_list = { use flag <- glint.flag(flag) use _, _, flags <- glint.command - flag(flags) - |> should.equal(Ok([3.0, 4.0])) + use flag <- flag(flags) + assert flag == [3.0, 4.0] + glint.Success(Nil) } - glint.new() - |> glint.add([], expect_flag_value_list) - |> glint.execute([flag_input]) - |> should.be_ok() + let assert glint.Success(_) = + glint.new() + |> glint.add([], expect_flag_value_list) + |> glint.run([flag_input]) } pub fn global_flag_test() { - let flag = glint.floats_flag("flag") + let flag = glint.floats("flag") let testcase = fn(vals: List(Float)) { use _, _, flags <- glint.command() - flags - |> glint.get_flag(flag) - |> should.equal(Ok(vals)) + use flag_vals <- glint.try(glint.get_flag(flags, flag)) + assert vals == flag_vals + glint.Success(Nil) } // set global flag, pass in new value for flag - glint.new() - |> glint.group_flag([], flag) - |> glint.add(at: [], do: testcase([3.0, 4.0])) - |> glint.execute(["--flag=3.0,4.0"]) - |> should.be_ok() + let assert glint.Success(_) = + glint.new() + |> glint.group_flag([], flag) + |> glint.add(at: [], do: testcase([3.0, 4.0])) + |> glint.run(["--flag=3.0,4.0"]) // set global flag and local flag, local flag should take priority - glint.new() - |> glint.group_flag([], glint.floats_flag("flag")) - |> glint.add( - at: [], - do: glint.flag( - glint.floats_flag("flag") - |> glint.flag_default([1.0, 2.0]), - fn(_) { testcase([1.0, 2.0]) }, - ), - ) - |> glint.execute([]) - |> should.be_ok() + let assert glint.Success(_) = + glint.new() + |> glint.group_flag([], glint.floats("flag")) + |> glint.add( + at: [], + do: glint.flag( + glint.floats("flag") + |> glint.default([1.0, 2.0]), + fn(_) { testcase([1.0, 2.0]) }, + ), + ) + |> glint.run([]) // set global flag and local flag, pass in new value for flag - glint.new() - |> glint.group_flag( - [], - glint.floats_flag("flag") - |> glint.flag_default([3.0, 4.0]), - ) - |> glint.add(at: [], do: { - use _flag <- glint.flag( - glint.floats_flag("flag") - |> glint.flag_default([1.0, 2.0]), + let assert glint.Success(_) = + glint.new() + |> glint.group_flag( + [], + glint.floats("flag") + |> glint.default([3.0, 4.0]), ) + |> glint.add(at: [], do: { + use _flag <- glint.flag( + glint.floats("flag") + |> glint.default([1.0, 2.0]), + ) - testcase([5.0, 6.0]) - }) - |> glint.execute(["--flag=5.0,6.0"]) - |> should.be_ok() + testcase([5.0, 6.0]) + }) + |> glint.run(["--flag=5.0,6.0"]) } pub fn toggle_test() { // fails to parse input for flag as bool, returns error let flag_input = "--flag=X" - glint.new() - |> glint.add( - [], - glint.flag(glint.bool_flag("flag"), fn(_) { - glint.command(fn(_, _, _) { Nil }) - }), - ) - |> glint.execute([flag_input]) - |> should.be_error() + let assert glint.Failure(_) = + glint.new() + |> glint.add( + [], + glint.flag(glint.bool("flag"), fn(_) { + glint.command(fn(_, _, _) { glint.Success(Nil) }) + }), + ) + |> glint.run([flag_input]) // boolean flag is toggled, sets value to True let flag_input = "--flag" - glint.new() - |> glint.add([], { - use flag <- glint.flag(glint.bool_flag("flag")) - use _, _, flags <- glint.command() - flag(flags) - |> should.equal(Ok(True)) - }) - |> glint.execute([flag_input]) - |> should.be_ok() + let assert glint.Success(_) = + glint.new() + |> glint.add([], { + use flag <- glint.flag(glint.bool("flag")) + use _, _, flags <- glint.command() + use flag <- flag(flags) + assert flag == True + glint.Success(Nil) + }) + |> glint.run([flag_input]) // boolean flag with default of True is toggled, sets value to False let flag_input = "--flag" - glint.new() - |> glint.add([], { - use flag <- glint.flag( - glint.bool_flag("flag") - |> glint.flag_default(True), - ) - use _, _, flags <- glint.command() - flag(flags) - |> should.equal(Ok(False)) - }) - |> glint.execute([flag_input]) - |> should.be_ok() + let assert glint.Success(_) = + glint.new() + |> glint.add([], { + use flag <- glint.flag( + glint.bool("flag") + |> glint.default(True), + ) + use _, _, flags <- glint.command() + + use flag <- flag(flags) + assert flag == False + glint.Success(Nil) + }) + |> glint.run([flag_input]) // boolean flag without default toggled, sets value to True - glint.new() - |> glint.add([], { - use flag <- glint.flag(glint.bool_flag("flag")) - use _, _, flags <- glint.command() - flag(flags) - |> should.equal(Ok(True)) - }) - |> glint.execute([flag_input]) - |> should.be_ok() + let assert glint.Success(_) = + glint.new() + |> glint.add([], { + use flag <- glint.flag(glint.bool("flag")) + use _, _, flags <- glint.command() + use flag <- flag(flags) + assert flag == True + glint.Success(Nil) + }) + |> glint.run([flag_input]) // cannot toggle non-bool flag - glint.new() - |> glint.add([], { - use _flag <- glint.flag( - glint.int_flag("flag") - |> glint.flag_default(1), - ) - use _, _, _ <- glint.command() - Nil - }) - |> glint.execute([flag_input]) - |> should.be_error() + let assert glint.Failure(_) = + glint.new() + |> glint.add([], { + use _flag <- glint.flag( + glint.int("flag") + |> glint.default(1), + ) + use _, _, _ <- glint.command() + glint.Success(Nil) + }) + |> glint.run([flag_input]) } pub fn getters_test() { glint.new() |> glint.add([], { use bflag <- glint.flag( - glint.bool_flag("bflag") - |> glint.flag_default(True), + glint.bool("bflag") + |> glint.default(True), ) use sflag <- glint.flag( - glint.string_flag("sflag") - |> glint.flag_default(""), + glint.string("sflag") + |> glint.default(""), ) use lsflag <- glint.flag( - glint.strings_flag("lsflag") - |> glint.flag_default([]), + glint.strings("lsflag") + |> glint.default([]), ) use iflag <- glint.flag( - glint.int_flag("iflag") - |> glint.flag_default(1), + glint.int("iflag") + |> glint.default(1), ) use liflag <- glint.flag( - glint.ints_flag("liflag") - |> glint.flag_default([]), + glint.ints("liflag") + |> glint.default([]), ) use fflag <- glint.flag( - glint.float_flag("fflag") - |> glint.flag_default(1.0), + glint.float("fflag") + |> glint.default(1.0), ) use lfflag <- glint.flag( - glint.floats_flag("lfflag") - |> glint.flag_default([]), + glint.floats("lfflag") + |> glint.default([]), ) use _, _, flags <- glint.command() - bflag(flags) - |> should.equal(Ok(True)) + use bflag <- bflag(flags) + assert bflag == True + + use sflag <- sflag(flags) + assert sflag == "" - sflag(flags) - |> should.equal(Ok("")) + use lsflag <- lsflag(flags) + assert lsflag == [] - lsflag(flags) - |> should.equal(Ok([])) + use iflag <- iflag(flags) + assert iflag == 1 - iflag(flags) - |> should.equal(Ok(1)) + use lflag <- liflag(flags) + assert lflag == [] - liflag(flags) - |> should.equal(Ok([])) + use fflag <- fflag(flags) + assert fflag == 1.0 - fflag(flags) - |> should.equal(Ok(1.0)) + use lfflag <- lfflag(flags) + assert lfflag == [] - lfflag(flags) - |> should.equal(Ok([])) + glint.Success(Nil) }) - |> glint.execute([]) + |> glint.run([]) +} + +pub fn optional_flag_test() { + let optional_group_flag = glint.int("optional_group") + let cli = + glint.new() + |> glint.add([], { + use o <- glint.optional_flag( + glint.string("optional") + |> glint.default("default_value"), + ) + use _, _, flags <- glint.command() + assert Ok("hello") == o(flags) + assert Ok(1) == glint.get_flag(flags, optional_group_flag) + glint.Success(Nil) + }) + |> glint.group_flag([], optional_group_flag) + + assert glint.Success(Nil) + == glint.run(cli, ["--optional=hello", "--optional_group=1"]) } diff --git a/test/glint/internal/utils_test.gleam b/test/glint/internal/help_test.gleam similarity index 97% rename from test/glint/internal/utils_test.gleam rename to test/glint/internal/help_test.gleam index 721b81d..c09953b 100644 --- a/test/glint/internal/utils_test.gleam +++ b/test/glint/internal/help_test.gleam @@ -1,16 +1,12 @@ import birdie import gleam/string -import gleeunit/should -import glint/internal/utils +import glint/internal/help pub fn wordwrap_test() { - "a b c" - |> utils.wordwrap(3) - |> should.equal(["a b", "c"]) + assert help.wordwrap("a b c", 3) == ["a b", "c"] - "a\nb\n\nc\n\n\nd\n\n\n\ne" - |> utils.wordwrap(1) - |> should.equal(["a", "b", "c\n", "d\n\n", "e"]) + assert help.wordwrap("a\nb\n\nc\n\n\nd\n\n\n\ne", 1) + == ["a", "b", "c\n", "d\n\n", "e"] } pub fn big_wordwrap_test() { @@ -343,7 +339,7 @@ Nothing in this Declaration may be interpreted as implying for any State, group person any right to engage in any activity or to perform any act aimed at the destruction of any of the rights and freedoms set forth herein. " - |> utils.wordwrap(80) + |> help.wordwrap(80) |> string.join("\n") |> birdie.snap("udhr") } diff --git a/test/glint_test.gleam b/test/glint_test.gleam index 403dbf8..ee8388c 100644 --- a/test/glint_test.gleam +++ b/test/glint_test.gleam @@ -1,7 +1,6 @@ import birdie import gleeunit -import gleeunit/should -import glint.{Help, Out} +import glint import snag pub fn main() { @@ -9,56 +8,65 @@ pub fn main() { } pub fn path_clean_test() { - glint.new() - |> glint.add( - ["", " ", " cmd", "subcmd\t"], - glint.command(fn(_, _, _) { Nil }), - ) - |> glint.execute(["cmd", "subcmd"]) - |> should.be_ok() + let assert glint.Success(_) = + glint.new() + |> glint.add( + ["", " ", " cmd", "subcmd\t"], + glint.command(fn(_, _, _) { glint.Success(Nil) }), + ) + |> glint.run(["cmd", "subcmd"]) } pub fn root_command_test() { // expecting no args - glint.new() - |> glint.add( - at: [], - do: glint.command(fn(_, args, _) { should.equal(args, []) }), - ) - |> glint.execute([]) - |> should.be_ok() + let assert glint.Success(_) = + glint.new() + |> glint.add( + at: [], + do: glint.command(fn(_, args, _) { + assert args == [] + glint.Success(Nil) + }), + ) + |> glint.run([]) // expecting some args let args = ["arg1", "arg2"] - let is_args = fn(_, in_args, _) { should.equal(in_args, args) } + let is_args = fn(_, in_args, _) { + assert in_args == args + glint.Success(Nil) + } - glint.new() - |> glint.add(at: [], do: glint.command(is_args)) - |> glint.execute(args) - |> should.be_ok() + let assert glint.Success(_) = + glint.new() + |> glint.add(at: [], do: glint.command(is_args)) + |> glint.run(args) } pub fn command_routing_test() { let args = ["arg1", "arg2"] - let is_args = fn(_, in_args, _) { should.equal(in_args, args) } + let is_args = fn(_, in_args, _) { + assert in_args == args + glint.Success(Nil) + } let has_subcommand = glint.new() |> glint.add(["subcommand"], glint.command(is_args)) // execute subommand with args - has_subcommand - |> glint.execute(["subcommand", ..args]) - |> should.be_ok() + let assert glint.Success(_) = + glint.run(has_subcommand, ["subcommand", ..args]) // no root command set, will return error - has_subcommand - |> glint.execute([]) - |> should.be_error() + let assert glint.Failure(_) = glint.run(has_subcommand, []) } pub fn nested_commands_test() { let args = ["arg1", "arg2"] - let is_args = fn(_, in_args, _) { should.equal(in_args, args) } + let is_args = fn(_, in_args, _) { + assert in_args == args + glint.Success(Nil) + } let cmd = glint.new() @@ -66,65 +74,61 @@ pub fn nested_commands_test() { |> glint.add(["subcommand", "subsubcommand"], glint.command(is_args)) // call subcommand with args - cmd - |> glint.execute(["subcommand", ..args]) - |> should.be_ok() + let assert glint.Success(_) = glint.run(cmd, ["subcommand", ..args]) // call subcommand subsubcommand with args - cmd - |> glint.execute(["subcommand", "subsubcommand", ..args]) - |> should.be_ok() + let assert glint.Success(_) = + glint.run(cmd, ["subcommand", "subsubcommand", ..args]) } pub fn runner_test() { let cmd = glint.new() - |> glint.add(at: [], do: glint.command(fn(_, _, _) { Ok("success") })) + |> glint.add( + at: [], + do: glint.command(fn(_, _, _) { glint.Success("success") }), + ) |> glint.add( at: ["subcommand"], - do: glint.command(fn(_, _, _) { snag.error("failed") }), + do: glint.command(fn(_, _, _) { glint.Failure(snag.new("failed")) }), ) // command returns its own successful result - cmd - |> glint.execute([]) - |> should.equal(Ok(glint.Out(Ok("success")))) + assert glint.run(cmd, []) == glint.Success("success") // command returns its own error result - cmd - |> glint.execute(["subcommand"]) - |> should.equal(Ok(Out(snag.error("failed")))) + assert glint.run(cmd, ["subcommand"]) == glint.Failure(snag.new("failed")) } fn help() { - let nil = fn(_, _, _) { Nil } + let nil = fn(_, _, _) { glint.Success(Nil) } let global_flag = - glint.string_flag("global") - |> glint.flag_help("This is a global flag") + glint.string("global") + |> glint.param_help("This is a global flag") let flag_1 = "flag1" - |> glint.string_flag() - |> glint.flag_help("This is flag1") + |> glint.string() + |> glint.param_help("This is flag1") let flag_2 = "flag2" - |> glint.int_flag() - |> glint.flag_help("This is flag2") + |> glint.int() + |> glint.param_help("This is flag2") let flag_3 = "flag3" - |> glint.bool_flag() - |> glint.flag_help("This is flag3") + |> glint.bool() + |> glint.param_help("This is flag3") let flag_4 = "flag4" - |> glint.float_flag() - |> glint.flag_help("This is flag4") + |> glint.float() + |> glint.param_help("This is flag4") let flag_5 = "very-very-very-long-flag" - |> glint.floats_flag() - |> glint.flag_help( + |> glint.floats() + |> glint.param_help( "This is a very long flag with a very very very very very very long description", ) @@ -135,8 +139,8 @@ fn help() { |> glint.group_flag([], global_flag) |> glint.add(at: [], do: { use <- glint.command_help("This is the root command") - use _arg1 <- glint.named_arg("arg1") - use _arg2 <- glint.named_arg("arg2") + use _arg1 <- glint.named_arg(glint.string("arg1")) + use _arg2 <- glint.named_arg(glint.int("arg2")) use _flag <- glint.flag(flag_1) glint.command(nil) }) @@ -149,8 +153,8 @@ fn help() { |> glint.add(at: ["cmd1", "cmd3"], do: { use <- glint.command_help("This is cmd3") use _flag3 <- glint.flag(flag_3) - use <- glint.unnamed_args(glint.MinArgs(2)) - use _woo <- glint.named_arg("woo") + use <- glint.min_args("args", 2, "") + use _woo <- glint.named_arg(glint.bool("woo")) glint.command(nil) }) |> glint.add(at: ["cmd1", "cmd4"], do: { @@ -158,14 +162,14 @@ fn help() { "This is cmd4 which has a very very very very very very very very long description", ) use _flag4 <- glint.flag(flag_4) - use <- glint.unnamed_args(glint.EqArgs(0)) + use <- glint.no_args() glint.command(nil) }) |> glint.add(at: ["cmd2"], do: { use <- glint.command_help("This is cmd2") - use <- glint.unnamed_args(glint.EqArgs(0)) - use _arg1 <- glint.named_arg("arg1") - use _arg2 <- glint.named_arg("arg2") + use <- glint.no_args() + use _arg1 <- glint.named_arg(glint.float("arg1")) + use _arg2 <- glint.named_arg(glint.strings("arg2")) glint.command(nil) }) |> glint.add( @@ -189,74 +193,67 @@ New new new line.", ) } -fn assert_unwrap_help(res: Result(glint.Out(a), String)) -> String { - let assert Ok(Help(help)) = res +fn assert_unwrap_help(res: glint.Out(a)) -> String { + let assert glint.Info(help) = res help } pub fn help_test() { let cli = help() // execute root command - glint.execute(cli, ["a", "b"]) - |> should.equal(Ok(Out(Nil))) + assert glint.run(cli, ["a", "1"]) == glint.Success(Nil) - glint.execute(cli, ["a"]) - |> should.be_error() + let assert glint.Failure(_) = glint.run(cli, ["a"]) - glint.execute(cli, []) - |> should.be_error() + let assert glint.Failure(_) = glint.run(cli, []) - glint.execute(cli, ["cmd2"]) - |> should.be_error() + let assert glint.Failure(_) = glint.run(cli, ["cmd2"]) - glint.execute(cli, ["cmd2", "1"]) - |> should.be_error() + let assert glint.Failure(_) = glint.run(cli, ["cmd2", "1"]) - glint.execute(cli, ["cmd2", "1", "2"]) - |> should.equal(Ok(Out(Nil))) + assert glint.run(cli, ["cmd2", "1", "2"]) == glint.Success(Nil) - glint.execute(cli, ["cmd2", "1", "2", "3"]) - |> should.be_error() + let assert glint.Failure(_) = glint.run(cli, ["cmd2", "1", "2", "3"]) } pub fn root_help_test() { // help message for root command - glint.execute(help(), ["--help"]) + glint.run(help(), ["--help"]) |> assert_unwrap_help |> birdie.snap("root help") } pub fn cmd1_help_test() { // help message for command - glint.execute(help(), ["cmd1", "--help"]) + glint.run(help(), ["cmd1", "--help"]) |> assert_unwrap_help |> birdie.snap("cmd1 help") } pub fn cmd4_help_test() { // help message for nested command - glint.execute(help(), ["cmd1", "cmd4", "--help"]) + glint.run(help(), ["cmd1", "cmd4", "--help"]) |> assert_unwrap_help |> birdie.snap("cmd4 help") } pub fn cmd2_help_test() { // help message for command with no additional flags - glint.execute(help(), ["cmd2", "--help"]) + glint.run(help(), ["cmd2", "--help"]) |> assert_unwrap_help |> birdie.snap("cmd2 help") } pub fn cmd3_help_test() { // help message for command with no additional flags - glint.execute(help(), ["cmd1", "cmd3", "--help"]) + glint.run(help(), ["cmd1", "cmd3", "--help"]) |> assert_unwrap_help |> birdie.snap("cmd3 help") } pub fn cmd6_help_test() { // help message for command a subcommand whose help was set with glint.path_help - glint.execute(help(), ["cmd5", "cmd6", "--help"]) + glint.run(help(), ["cmd5", "cmd6", "--help"]) |> assert_unwrap_help |> birdie.snap("cmd6 help") } @@ -264,35 +261,35 @@ pub fn cmd6_help_test() { pub fn cmd7_help_test() { // help message for command that had help_text set with glint.glint.path_help // has no children or command runner set so no other details are available - glint.execute(help(), ["cmd5", "cmd6", "cmd7", "--help"]) + glint.run(help(), ["cmd5", "cmd6", "cmd7", "--help"]) |> assert_unwrap_help |> birdie.snap("cmd7 help") } pub fn call_help_with_residual_args_test() { // help message for command a subcommand whose help was set with glint.path_help - glint.execute(help(), ["cmd5", "cmd6", "arg", "--help"]) + glint.run(help(), ["cmd5", "cmd6", "arg", "--help"]) |> assert_unwrap_help |> birdie.snap("cmd6 help with residual args") } pub fn call_leaf_help_with_residual_args_test() { // help message for command a subcommand whose help was set with glint.path_help - glint.execute(help(), ["cmd5", "cmd6", "cmd7", "arg", "--help"]) + glint.run(help(), ["cmd5", "cmd6", "cmd7", "arg", "--help"]) |> assert_unwrap_help |> birdie.snap("cmd7 help with residual args") } pub fn global_and_group_flags_test() { let flag_f = - glint.int_flag("f") - |> glint.flag_default(2) - |> glint.flag_help("global flag example") + glint.int("f") + |> glint.default(2) + |> glint.param_help("global flag example") let sub_group_flag = "sub_group_flag" - |> glint.int_flag() - |> glint.flag_default(1) + |> glint.int() + |> glint.default(1) let cli = glint.new() @@ -300,62 +297,57 @@ pub fn global_and_group_flags_test() { |> glint.add( [], glint.command(fn(_, _, flags) { - glint.get_flag(flags, flag_f) - |> should.equal(Ok(2)) + use flag <- glint.try(glint.get_flag(flags, flag_f)) + assert flag == 2 + glint.Success(Nil) }), ) |> glint.add(["sub"], { use f <- glint.flag( "f" - |> glint.bool_flag() - |> glint.flag_default(True) - |> glint.flag_help("i decided to override the global flag"), + |> glint.bool() + |> glint.default(True) + |> glint.param_help("i decided to override the global flag"), ) use _, _, flags <- glint.command() - f(flags) - |> should.equal(Ok(True)) + use f <- f(flags) + assert f == True + glint.Success(Nil) }) |> glint.group_flag(["sub"], sub_group_flag) |> glint.add(["sub", "sub"], { use f <- glint.flag( "f" - |> glint.bool_flag() - |> glint.flag_default(True) - |> glint.flag_help("i decided to override the global flag"), + |> glint.bool() + |> glint.default(True) + |> glint.param_help("i decided to override the global flag"), ) use _, _, flags <- glint.command() - f(flags) - |> should.equal(Ok(True)) + use f <- f(flags) + assert f == True - flags - |> glint.get_flag(sub_group_flag) - |> should.equal(Ok(2)) + use flag <- glint.try(glint.get_flag(flags, sub_group_flag)) + assert flag == 2 + glint.Success(Nil) }) // root command keeps the global flag as an int - cli - |> glint.execute(["--f=2"]) - |> should.be_ok + let assert glint.Success(_) = glint.run(cli, ["--f=2"]) - cli - |> glint.execute(["--f=hello"]) - |> should.be_error + let assert glint.Failure(_) = glint.run(cli, ["--f=hello"]) // sub command overrides the global flag with a bool - cli - |> glint.execute(["sub", "--f=true"]) - |> should.be_ok + let assert glint.Success(_) = glint.run(cli, ["sub", "--f=true"]) - cli - |> glint.execute(["sub", "--f=123"]) - |> should.be_error + let assert glint.Failure(_) = glint.run(cli, ["sub", "--f=123"]) cli - |> glint.execute(["sub", "sub", "--sub_group_flag=2"]) + |> glint.run(["sub", "sub", "--sub_group_flag=2"]) } -pub fn default_pretty_help_test() { - // default_pretty_help has asserts - // we need to call the function to make sure it does not crash - glint.default_pretty_help() +pub fn version_test() { + assert glint.new() + |> glint.with_version("test") + |> glint.run(["--version"]) + == glint.Info("test") }