├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── birdie_snapshots ├── cmd1_help.accepted ├── cmd2_help.accepted ├── cmd3_help.accepted ├── cmd4_help.accepted ├── cmd6_help.accepted ├── cmd6_help_with_residual_args.accepted ├── cmd7_help.accepted ├── cmd7_help_with_residual_args.accepted ├── root_help.accepted └── udhr.accepted ├── gleam.toml ├── manifest.toml ├── renovate.json ├── src ├── glint.gleam └── glint │ ├── constraint.gleam │ └── internal │ ├── help.gleam │ └── utils.gleam └── test ├── contraint_test.gleam ├── examples ├── hello.gleam └── hello_test.gleam ├── flag_test.gleam ├── glint └── internal │ └── utils_test.gleam └── glint_test.gleam /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: GH and Hex.pm Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | jobs: 9 | release: 10 | uses: TanklesXL/gleam_actions/.github/workflows/release.yaml@main 11 | secrets: inherit 12 | with: 13 | gleam_version: 1.9.1 14 | erlang_version: 27 15 | test_erlang: true 16 | test_node: true 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_call: 9 | 10 | jobs: 11 | test: 12 | uses: TanklesXL/gleam_actions/.github/workflows/test.yaml@main 13 | with: 14 | gleam_version: 1.9.1 15 | test_node: true 16 | test_erlang: true 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.ez 3 | build 4 | erl_crash.dump 5 | .ignore_me 6 | .ignore_me.* 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased](https://github.com/TanklesXL/glint/compare/v1.1.0...HEAD) 4 | 5 | # v1 6 | 7 | ## [1.2.1](https://github.com/TanklesXL/glint/compare/v1.2.1...v1.2.0) 8 | 9 | - remove use of deprecated `list.pop` function 10 | 11 | ## [1.2.0](https://github.com/TanklesXL/glint/compare/v1.2.0...v1.1.1) 12 | 13 | - add the `glint.map_command` function 14 | 15 | ## [1.1.1](https://github.com/TanklesXL/glint/compare/v1.1.1...v1.1.0) 16 | 17 | - updated gleam stdlib to >= 0.43 18 | - fixed deprecation warnings on latest stdlib version 19 | 20 | 21 | ## [1.1.0](https://github.com/TanklesXL/glint/compare/v1.1.0...v1.0.1) 22 | 23 | - use punning internally, requires gleam >= 1.4 24 | 25 | ## [1.0.1](https://github.com/TanklesXL/glint/compare/v1.0.0...v1.0.1) 26 | 27 | - updated gleam stdlib to >=0.39 28 | - replace calls to `dict.update` with `dict.upsert` 29 | 30 | ## [1.0.0](https://github.com/TanklesXL/glint/compare/v0.18.1...v1.0.0) 31 | 32 | - replaced the pipe-based command builder API with a `use`-based one that provides better ergonomics 33 | - removal of the `glint/flag` module and named flag getter functions 34 | - flags are now handled directly by the `glint` module 35 | - constraints are now handled by the `glint/constraint` module 36 | - many functions in the `glint` module have had their names somewhat adjusted 37 | - help text formatting is now consistently aligned and more legible 38 | 39 | # v0 40 | 41 | ## [0.18.1](https://github.com/TanklesXL/glint/compare/v0.18.0...v0.18.1) 42 | 43 | - relax gleam stdlib version constraint 44 | 45 | ## [0.18.0](https://github.com/TanklesXL/glint/compare/v0.17.1...v0.18.0) 46 | 47 | - support for group flags at a given path 48 | 49 | ## [0.17.1](https://github.com/TanklesXL/glint/compare/v0.17.0...v0.17.1) 50 | 51 | - remove unused function import to silence compiler warnings 52 | 53 | ## [0.17.0](https://github.com/TanklesXL/glint/compare/v0.16.0...v0.17.0) 54 | 55 | - support gleam 1.0.0 56 | 57 | ## [0.16.0](https://github.com/TanklesXL/glint/compare/v0.15.0...v0.16.0) 58 | 59 | - `glint.CommandResult(a)` is now a `Result(Out(a), String)` instead of a `Result(Out(a),Snag)` 60 | - command exectution failures due to things like invalid flags or too few args now print help text for the current command 61 | - fix help text formatting for commands that do not include arguments 62 | - remove named args from help text usage 63 | - change `glint.count_args` to `glint.unnamed_args`, behaviour changes for this function to explicitly only check the number of unnamed arguments 64 | - remove notes section from usage text 65 | 66 | ## [0.15.0](https://github.com/TanklesXL/glint/compare/v0.14.0...v0.15.0) 67 | 68 | - support gleam >=0.34 or 1.0.x 69 | - refactor of help generation logic, no change to help text output 70 | - the `glint/flag` module loses the `flags_help` and `flag_type_help` functions 71 | - the `glint` module gains the ArgsCount type and the `count_args` function to support exact and minimum arguments count 72 | - the `glint` module gains the `named_args` function to support named arguments 73 | - the `glint.CommandInput` type gains the `.named_args` field to access named arguments 74 | - help text has been updated to support named and counted arguments 75 | 76 | ## [0.14.0](https://github.com/TanklesXL/glint/compare/v0.13.0...v0.14.0) 77 | 78 | - updated to work with gleam 0.33 79 | - removed deprecated stub api 80 | 81 | ## [0.13.0](https://github.com/TanklesXL/glint/compare/v0.12.0...v0.13.0) 82 | 83 | - clean up `flag.get_*` and `flag.get_*_values` functions 84 | - update to gleam 0.32 85 | 86 | ## [0.12.0](https://github.com/TanklesXL/glint/compare/v0.11.3...v0.12.0) 87 | 88 | - update to gleam v0.20 89 | - `flag` module now provides a getter per flag type instead of a unified one that previously returned the `Value` type. 90 | - `glint` gains the `with_print_output` function to allow printing of command output when calling `run`. 91 | - new builder api for commands and flags: 92 | - `glint.cmd` to create a command 93 | - `glint.description` to attach a description to a command 94 | - `glint.flag` to attach a flag to a command 95 | - `flag.{int, float, int_list, float_list, string, string_list, bool}` to initialize flag builders 96 | - `flag.default` to attach a default value to a flag 97 | - `flag.constraint` to attach a constraint to a flag 98 | - rename `glint.with_global_flags` to `glint.global_flags` 99 | - `glint` gains the `global_flag` and `flag_tuple` functions. 100 | 101 | ## [0.11.3](https://github.com/TanklesXL/glint/compare/v0.11.2...v0.11.3) 102 | 103 | - fixes string concat precedence bug 104 | 105 | ## [0.11.2](https://github.com/TanklesXL/glint/compare/v0.11.1...v0.11.2) 106 | 107 | - works on gleam 0.27 108 | 109 | ## [0.11.1](https://github.com/TanklesXL/glint/compare/v0.11.0...v0.11.1) - 2023-03-16 110 | 111 | - make `glint/flag.Contents` non-opaque. 112 | 113 | ## [0.11.0](https://github.com/TanklesXL/glint/compare/v0.10.0...v0.11.0) - 2023-02-08 114 | 115 | - colour for pretty help leverages the new `gleam_community/colour` and `gleam_community/ansi` packages. 116 | 117 | ## [0.10.0](https://github.com/TanklesXL/glint/compare/v0.9.0...v0.10.0) - 2022-11-29 118 | 119 | - use gleam's new `<>` string concat operator 120 | - use gleam's new `use` keyword for callback declaration 121 | 122 | ## [0.9.0](https://github.com/TanklesXL/glint/compare/v0.8.0...v0.9.0) - 2022-09-16 123 | 124 | - help text can now be set to use configurable shell colours. 125 | - added the `style` module to handle coloured help output 126 | - `glint` gains the `Glint(a)` wrapper type 127 | - `glint` gains the `Config` type 128 | - `glint.new` returns a `Glint(a)` instead of a `Command(a)` 129 | - `glint` gains helpers such as `with_config`, `default_config`, and `with_pretty_help` 130 | - `flag.get_value` renamed to `flag.get` 131 | - `glint` gains global flag support, with `glint.global_flags` 132 | 133 | ## [0.8.0](https://github.com/TanklesXL/glint/compare/v0.7.4...v0.8.0) - 2022-05-09 134 | 135 | - `flag.Contents` is no longer opaque 136 | - `glint` module gains `Stub` type to create constant commands and `add_command_from_stub` to add the resulting commands 137 | 138 | ## [0.7.4](https://github.com/TanklesXL/glint/compare/v0.7.3...v0.7.4) - 2022-05-03 139 | 140 | - refactor: negation operator in `flag.gleam` instead of bool.negate 141 | - refactor: split `flag.update_flags` into calling `update_flag_value` or `attempt_toggle_flag` 142 | 143 | ## [0.7.3](https://github.com/TanklesXL/glint/compare/v0.7.2...v0.7.3) - 2022-02-23 144 | 145 | - make `flag.Contents` opaque 146 | 147 | ## [0.7.2](https://github.com/TanklesXL/glint/compare/v0.7.1...v0.7.2) - 2022-02-22 148 | 149 | - add argument labels to `flag.get_value` 150 | 151 | ## [0.7.1](https://github.com/TanklesXL/glint/compare/v0.7.0...v0.7.1) - 2022-02-22 152 | 153 | - `flag.access_flag` renamed to `flag.access` 154 | - `flag.get_value` added 155 | 156 | ## [0.7.0](https://github.com/TanklesXL/glint/compare/v0.6.0...v0.7.0) - 2022-02-22 157 | 158 | - rename flag `*_list` functions 159 | - rename `flag.FlagValue` to `flag.Internal` 160 | - rename `flag.FlagMap` to `flag.Map` 161 | - generate help messages for commands and flags 162 | 163 | ## [0.6.0](https://github.com/TanklesXL/glint/compare/v0.5.0...v0.6.0) - 2022-02-11 164 | 165 | - `Runner` returns a generic type 166 | - sanitize command paths 167 | - make flag prefix a const 168 | - make flag delimiter a const 169 | 170 | ## [0.5.0](https://github.com/TanklesXL/glint/compare/v0.4.0...v0.5.0) - 2022-01-31 171 | 172 | - boolean flag toggle support added 173 | 174 | ## [0.4.0](https://github.com/TanklesXL/glint/compare/v0.3.0...v0.4.0) - 2022-01-28 175 | 176 | - flag string chopping has been moved into `flag` module 177 | - flags are split from args list, so flags are no longer positionally dependent 178 | - `flag` parsing functions share common bases `parse_flag` and `parse_list_flag` 179 | 180 | ## [0.3.0](https://github.com/TanklesXL/glint/compare/v0.2.0...v0.3.0) - 2022-01-13 181 | 182 | - `flag` module gains support for float and float list flags. 183 | - rename `FlagValue` constructors to be more concise. 184 | 185 | ## [0.2.0] - 2022-01-12 186 | 187 | - `flag` module gains support for string list and int list flags. 188 | 189 | ## [0.1.3] 190 | 191 | - Use `--` for flags instead of `-`. 192 | - Add `example` directory. 193 | 194 | ## [0.1.2] 195 | 196 | - README update. 197 | 198 | ## [0.1.1] 199 | 200 | - refactor `gling.add_command`. 201 | 202 | ## [0.1.0] 203 | 204 | - Initial argument parsing and flags support. 205 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # glint 2 | 3 | [![Hex Package](https://img.shields.io/hexpm/v/glint?color=ffaff3&label=%F0%9F%93%A6)](https://hex.pm/packages/glint) 4 | [![Hex.pm](https://img.shields.io/hexpm/dt/glint?color=ffaff3)](https://hex.pm/packages/glint) 5 | [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3?label=%F0%9F%93%9A)](https://hexdocs.pm/glint/) 6 | [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/tanklesxl/glint/main)](https://github.com/tanklesxl/glint/actions) 7 | 8 | Gleam command-line argument parsing with flags and automated help text generation. 9 | 10 | ## Installation 11 | 12 | To install from hex: 13 | 14 | ```sh 15 | gleam add glint 16 | ``` 17 | 18 | ## Usage 19 | 20 | Glint has 3 main concepts (see below for more details): glint itself, commands and flags. 21 | 22 | The general workflow involves 23 | 24 | 1. creating a new glint instance with `glint.new` 25 | 1. configuring it 26 | 1. creating commands with `glint.command` 27 | - attach flags with `glint.flag` 28 | - set named args with `glint.named_arg` 29 | - set unnamed args with `glint.unnamed_args` 30 | 1. attach the commands to glint with `glint.add` 31 | 1. run your glint app with `glint.run` or `glint.run_and_handle` 32 | 33 | ### Mini Example 34 | 35 | You can import `glint` as a dependency and use it to build command-line applications like the following simplified version of the [the hello world example](./test/examples/hello.gleam). 36 | 37 | ```gleam 38 | import gleam/io 39 | import gleam/list 40 | import gleam/result 41 | import gleam/string.{uppercase} 42 | import glint 43 | import argv 44 | 45 | // this function returns the builder for the caps flag 46 | fn caps_flag() -> glint.Flag(Bool) { 47 | // create a new boolean flag with key "caps" 48 | // 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 49 | glint.bool_flag("caps") 50 | // set the flag default value to False 51 | |> glint.flag_default(False) 52 | // set the flag help text 53 | |> glint.flag_help("Capitalize the hello message") 54 | } 55 | 56 | /// the glint command that will be executed 57 | /// 58 | fn hello() -> glint.Command(Nil) { 59 | // set the help text for the hello command 60 | use <- glint.command_help("Prints Hello, !") 61 | // register the caps flag with the command 62 | // the `caps` variable there is a type-safe getter for the flag value 63 | use caps <- glint.flag(caps_flag()) 64 | // start the body of the command 65 | // this is what will be executed when the command is called 66 | use _, args, flags <- glint.command() 67 | // we can assert here because the caps flag has a default 68 | // and will therefore always have a value assigned to it 69 | let assert Ok(caps) = caps(flags) 70 | // this is where the business logic of our command starts 71 | let name = case args { 72 | [] -> "Joe" 73 | [name,..] -> name 74 | } 75 | let msg = "Hello, " <> name <> "!" 76 | case caps { 77 | True -> uppercase(msg) 78 | False -> msg 79 | } 80 | |> io.println 81 | } 82 | 83 | pub fn main() { 84 | // create a new glint instance 85 | glint.new() 86 | // with an app name of "hello", this is used when printing help text 87 | |> glint.with_name("hello") 88 | // with pretty help enabled, using the built-in colours 89 | |> glint.pretty_help(glint.default_pretty_help()) 90 | // with a root command that executes the `hello` function 91 | |> glint.add(at: [], do: hello()) 92 | // execute given arguments from stdin 93 | |> glint.run(argv.load().arguments) 94 | } 95 | ``` 96 | ## Glint at-a-glance 97 | 98 | ### Glint core: `glint.Glint(a)` 99 | 100 | `glint` is conceptually quite small, your general flow will be: 101 | 102 | - create a new glint instance with `glint.new`. 103 | - configure glint with functions like `glint.with_pretty_help`. 104 | - add commands with `glint.add`. 105 | - run your cli with `glint.run`, run with a function to handle command output with `glint.run_and_handle`. 106 | 107 | ### Glint commands: `glint.Command(a)` 108 | 109 | _Note_: Glint commands are most easily set up by chaining functions with `use`. (See the above example) 110 | 111 | - Create a new command with `glint.command`. 112 | - Set the command description with `glint.command_help`. 113 | - Add a flag to a command with `glint.flag`. 114 | - Create a named argumend with `glint.named_arg`. 115 | - Set the expectation for unnamed args with `glint.unnamed_args`. 116 | 117 | ### Glint flags: `glint.Flag(a)` 118 | 119 | Glint flags are a type-safe way to provide options to your commands. 120 | 121 | - Create a new flag with a typed flag constructor function: 122 | 123 | - `glint.int_flag`: `glint.Flag(Int)` 124 | - `glint.ints_flag`: `glint.Flag(List(Int))` 125 | - `glint.float_flag`: `glint.Flag(Float)` 126 | - `glint.floats_flag`: `glint.Flag(List(Floats))` 127 | - `glint.string_flag`: `glint.Flag(String)` 128 | - `glint.strings_flag`: `glint.Flag(List(String))` 129 | - `glint.bool_flag`: `glint.Flag(Bool)` 130 | 131 | - Set the flag description with `glint.flag_help` 132 | - 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. 133 | - Add a flag to a command with `glint.flag`. 134 | - Add a `constraint.Constraint(a)` to a `glint.Flag(a)` with `glint.flag_constraint` 135 | 136 | #### Glint flag constraints: `constraint.Constraint(a)` 137 | 138 | Constraints are functions of shape `fn(a) -> Result(a, snag.Snag)` that are executed after a flag value has been successfully parsed, all constraints applied to a flag must succeed for that flag to be successfully processed. 139 | 140 | Constraints can be any function so long as it satisfies the required type signature, and are useful for ensuring that data is correctly shaped **before** your glint commands are executed. This reduces unnecessary checks polluting the business logic of your commands. 141 | 142 | Here is an example of a constraint that guarantees a processed integer flag will be a positive number. 143 | 144 | _Note_ that constraints can both nicely be set up via pipes (`|>`) or with `use`. 145 | 146 | ```gleam 147 | import glint 148 | import snag 149 | // ... 150 | // with pipes 151 | glint.int_flag("my_int") 152 | |> glint.flag_default(0) 153 | |> glint.constraint(fn(i){ 154 | case i < 0 { 155 | True -> snag.error("cannot be negative") 156 | False -> Ok(i) 157 | } 158 | }) 159 | // or 160 | // with use 161 | use i <- glint.flag_constraint( 162 | glint.int_flag("my_int") 163 | |> glint.flag_default(0) 164 | ) 165 | case i < 0 { 166 | True -> snag.error("cannot be negative") 167 | False -> Ok(i) 168 | } 169 | ``` 170 | 171 | The `glint/constraint` module provides a few helpful utilities for applying constraints, namely 172 | 173 | - `constraint.one_of`: ensures that a value is one of some list of allowed values. 174 | - `constraint.none_of`: ensures that a value is not one of some list of disallowed values. 175 | - `constraint.each`: applies a constraint on individual items in a list of values (useful for applying constraints like `one_of` and `none_of` to lists. 176 | 177 | The following example demonstrates how to constrain a `glint.Flag(List(Int))` to only allow the values 1, 2, 3 or 4 by combining `constraint.each` with `constraint.one_of` 178 | 179 | ```gleam 180 | import glint 181 | import glint/constraint 182 | import snag 183 | // ... 184 | glint.ints_flag("my_ints") 185 | |> glint.flag_default([]) 186 | |> glint.flag_constraint( 187 | [1, 2, 3, 4] 188 | |> constraint.one_of 189 | |> constraint.each 190 | ) 191 | ``` 192 | 193 | ## ✨ Complementary packages 194 | 195 | Glint works amazingly with these other packages: 196 | 197 | - [argv](https://github.com/lpil/argv), use this for cross-platform argument fetching 198 | - [gleescript](https://github.com/lpil/gleescript), use this to generate erlang escripts for your applications 199 | 200 | 201 | ## Help text 202 | 203 | Glint automatically generates help text for your commands and flags. Help text is both automatically formatted and wrapped. You can attach help text to your commands and flags with the functions described below. 204 | 205 | _**Note**_:Help text is generated and printed whenever a glint command is called with the built-in flag `--help`. It is also printed after the error text when any errors are encountered due to invalid flags or arguments. 206 | 207 | Help text descriptions can be attached to all of glint's components: 208 | 209 | - attach global help text with `glint.global_help` 210 | - attach comand help text with `glint.command_help` 211 | - attach flag help text with `glint.flag_help` 212 | - attach help text to a non-initialized command with `glint.path_help` 213 | 214 | #### Help text formatting 215 | 216 | It is not uncommon for developers to want to format long text strings in such a way that it is easier to read in a code editor. Glint accounts for this be being sensitive to multiple line breaks in a help text string. This means that text like the following: 217 | 218 | ``` 219 | A very very very very very very very long help text 220 | string that is too long to fit on one line. 221 | 222 | Here is something that gets its own line. 223 | 224 | 225 | And here is something that gets its own paragraph. 226 | ``` 227 | 228 | Will be formatted as follows(without word wrapping): 229 | 230 | ``` 231 | A very very very very very very very long help text string that is too long to fit on one line. 232 | Here is something that gets its own line. 233 | 234 | And here is something that gets its own paragraph. 235 | ``` 236 | 237 | And when wrapped will look something like the following: 238 | 239 | ``` 240 | A very very very very very very very long help 241 | text string that is too long to fit on one line. 242 | Here is something that gets its own line. 243 | 244 | And here is something that gets its own paragraph. 245 | ``` 246 | 247 | #### Help text wrapping 248 | 249 | In addition to newline formatting, glint also handles wrapping helptext so that it fits within the configured terminal width. This means that if you have a long help text string it will be adjusted to fit on additional lines if it is too long to fit on one line. Spacing is also added to keep descriptions aligned with each other. 250 | 251 | There are functions that you can use to tweak glints default wrapping behaviour, but the defaults should be sufficient for the majority of use cases. 252 | -------------------------------------------------------------------------------- /birdie_snapshots/cmd1_help.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.5 3 | title: cmd1 help 4 | file: ./test/glint_test.gleam 5 | test_name: cmd1_help_test 6 | --- 7 | Some awesome global help text! 8 | 9 | Command: cmd1 10 | 11 | This is cmd1 12 | 13 | USAGE: 14 | gleam run -m test cmd1 ( cmd3 | cmd4 ) [ ARGS ] [ --flag2= 15 | --global= --very-very-very-long-flag= ] 16 | 17 | FLAGS: 18 | --flag2= This is flag2 19 | 20 | --global= This is a global flag 21 | 22 | --help Print help information 23 | 24 | --very-very-very-long-flag= This is a very long flag with a 25 | very very very very very very long 26 | description 27 | 28 | SUBCOMMANDS: 29 | cmd3 This is cmd3 30 | 31 | cmd4 This is cmd4 which has a very very very very very very 32 | very very long description -------------------------------------------------------------------------------- /birdie_snapshots/cmd2_help.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.5 3 | title: cmd2 help 4 | file: ./test/glint_test.gleam 5 | test_name: help_test 6 | --- 7 | Some awesome global help text! 8 | 9 | Command: cmd2 10 | 11 | This is cmd2 12 | 13 | USAGE: 14 | gleam run -m test cmd2 [ --global= ] 15 | 16 | FLAGS: 17 | --global= This is a global flag 18 | --help Print help information -------------------------------------------------------------------------------- /birdie_snapshots/cmd3_help.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.5 3 | title: cmd3 help 4 | file: ./test/glint_test.gleam 5 | test_name: help_test 6 | --- 7 | Some awesome global help text! 8 | 9 | Command: cmd1 cmd3 10 | 11 | This is cmd3 12 | 13 | USAGE: 14 | gleam run -m test cmd1 cmd3 [ 2 or more arguments ] [ --flag3= 15 | --global= ] 16 | 17 | FLAGS: 18 | --flag3= This is flag3 19 | --global= This is a global flag 20 | --help Print help information -------------------------------------------------------------------------------- /birdie_snapshots/cmd4_help.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.5 3 | title: cmd4 help 4 | file: ./test/glint_test.gleam 5 | test_name: help_test 6 | --- 7 | Some awesome global help text! 8 | 9 | Command: cmd1 cmd4 10 | 11 | This is cmd4 which has a very very very very very very very very long 12 | description 13 | 14 | USAGE: 15 | gleam run -m test cmd1 cmd4 [ --flag4= --global= ] 16 | 17 | FLAGS: 18 | --flag4= This is flag4 19 | --global= This is a global flag 20 | --help Print help information -------------------------------------------------------------------------------- /birdie_snapshots/cmd6_help.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.5 3 | title: cmd6 help 4 | file: ./test/glint_test.gleam 5 | test_name: help_test 6 | --- 7 | Some awesome global help text! 8 | 9 | Command: cmd5 cmd6 10 | 11 | This is cmd6 12 | 13 | USAGE: 14 | gleam run -m test cmd5 cmd6 ( cmd7 ) [ ARGS ] [ --global= ] 15 | 16 | FLAGS: 17 | --global= This is a global flag 18 | --help Print help information 19 | 20 | SUBCOMMANDS: 21 | cmd7 This is cmd7 -------------------------------------------------------------------------------- /birdie_snapshots/cmd6_help_with_residual_args.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.8 3 | title: cmd6 help with residual args 4 | file: ./test/glint_test.gleam 5 | test_name: call_help_with_residual_args_test 6 | --- 7 | Some awesome global help text! 8 | 9 | Command: cmd5 cmd6 10 | 11 | This is cmd6 12 | 13 | USAGE: 14 | gleam run -m test cmd5 cmd6 ( cmd7 ) [ ARGS ] [ --global= ] 15 | 16 | FLAGS: 17 | --global= This is a global flag 18 | --help Print help information 19 | 20 | SUBCOMMANDS: 21 | cmd7 This is cmd7 -------------------------------------------------------------------------------- /birdie_snapshots/cmd7_help.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.5 3 | title: cmd7 help 4 | file: ./test/glint_test.gleam 5 | test_name: help_test 6 | --- 7 | Some awesome global help text! 8 | 9 | Command: cmd5 cmd6 cmd7 10 | 11 | This is cmd7 12 | 13 | USAGE: 14 | gleam run -m test cmd5 cmd6 cmd7 [ ARGS ] -------------------------------------------------------------------------------- /birdie_snapshots/cmd7_help_with_residual_args.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.8 3 | title: cmd7 help with residual args 4 | file: ./test/glint_test.gleam 5 | test_name: call_leaf_help_with_residual_args_test 6 | --- 7 | Some awesome global help text! 8 | 9 | Command: cmd5 cmd6 cmd7 10 | 11 | This is cmd7 12 | 13 | USAGE: 14 | gleam run -m test cmd5 cmd6 cmd7 [ ARGS ] -------------------------------------------------------------------------------- /birdie_snapshots/root_help.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.5 3 | title: root help 4 | file: ./test/glint_test.gleam 5 | test_name: root_help_test 6 | --- 7 | Some awesome global help text! 8 | 9 | This is the root command 10 | 11 | USAGE: 12 | gleam run -m test ( cmd1 | cmd2 | cmd5 | cmd8-very-very-very-very-long ) 13 | [ ARGS ] [ --flag1= --global= ] 14 | 15 | FLAGS: 16 | --flag1= This is flag1 17 | --global= This is a global flag 18 | --help Print help information 19 | 20 | SUBCOMMANDS: 21 | cmd1 This is cmd1 22 | 23 | cmd2 This is cmd2 24 | 25 | cmd5 26 | 27 | cmd8-very-very-very-very-long This is cmd8 with a very very very very very 28 | very very long description. Same line as 29 | prev. 30 | This should show up on a new line. 31 | 32 | New new line 33 | 34 | 35 | New new new line. -------------------------------------------------------------------------------- /birdie_snapshots/udhr.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.6 3 | title: udhr 4 | file: ./test/glint/internal/utils_test.gleam 5 | test_name: big_wordwrap_test 6 | --- 7 | Universal Declaration of Human Rights 8 | 9 | Article I 10 | 11 | All human beings are born free and equal in dignity and rights. They are endowed 12 | with reason and conscience and should act towards one another in a spirit of 13 | brotherhood. 14 | 15 | 16 | Article 2 17 | 18 | Everyone is entitled to all the rights and freedoms set forth in this 19 | Declaration, without distinction of any kind, such as race, colour, sex, 20 | language, religion, political or other opinion, national or social origin, 21 | property, birth or other status. Furthermore, no distinction shall be made on 22 | the basis of the political, jurisdictional or international status of the 23 | country or territory to which a person belongs, whether it be independent, 24 | trust, non-self-governing or under any other limitation of sovereignty. 25 | 26 | 27 | Article 3 28 | 29 | Everyone has the right to life, liberty and the security of person. 30 | 31 | 32 | Article 4 33 | 34 | No one shall be held in slavery or servitude; slavery and the slave trade shall 35 | be prohibited in all their forms. 36 | 37 | 38 | Article 5 39 | 40 | No one shall be subjected to torture or to cruel, inhuman or degrading treatment 41 | or punishment. 42 | 43 | 44 | Article 6 45 | 46 | Everyone has the right to recognition everywhere as a person before the law. 47 | 48 | 49 | Article 7 50 | 51 | All are equal before the law and are entitled without any discrimination to 52 | equal protection of the law. All are entitled to equal protection against any 53 | discrimination in violation of this Declaration and against any incitement to 54 | such discrimination. 55 | 56 | 57 | Article 8 58 | 59 | Everyone has the right to an effective remedy by the competent national 60 | tribunals for acts violating the fundamental rights granted him by the 61 | constitution or by law. 62 | 63 | 64 | Article 9 65 | 66 | No one shall be subjected to arbitrary arrest, detention or exile. 67 | 68 | 69 | Article 10 70 | 71 | Everyone is entitled in full equality to a fair and public hearing by an 72 | independent and impartial tribunal, in the determination of his rights and 73 | obligations and of any criminal charge against him. 74 | 75 | 76 | Article 11 77 | 78 | 1. Everyone charged with a penal offence has the right to be presumed innocent 79 | until proved guilty according to law in a public trial at which he has had all 80 | the guarantees necessary for his defence. 81 | 2. No one shall be held guilty of any penal offence on account of any act or 82 | omission which did not constitute a penal offence, under national or 83 | international law, at the time when it was committed. Nor shall a heavier 84 | penalty be imposed than the one that was applicable at the time the penal 85 | offence was committed. 86 | 87 | 88 | Article 12 89 | 90 | No one shall be subjected to arbitrary interference with his privacy, family, 91 | home or correspondence, nor to attacks upon his honour and reputation. Everyone 92 | has the right to the protection of the law against such interference or 93 | attacks. 94 | 95 | 96 | Article 13 97 | 98 | 1. Everyone has the right to freedom of movement and residence within the 99 | borders of each State. 100 | 2. Everyone has the right to leave any country, including his own, and to return 101 | to his country. 102 | 103 | 104 | Article 14 105 | 106 | 1. Everyone has the right to seek and to enjoy in other countries asylum from 107 | persecution. 108 | 2. This right may not be invoked in the case of prosecutions genuinely arising 109 | from non-political crimes or from acts contrary to the purposes and principles 110 | of the United Nations. 111 | 112 | 113 | Article 15 114 | 115 | 1. Everyone has the right to a nationality. 116 | 2. No one shall be arbitrarily deprived of his nationality nor denied the right 117 | to change his nationality. 118 | 119 | 120 | Article 16 121 | 122 | 1. Men and women of full age, without any limitation due to race, nationality or 123 | religion, have the right to marry and to found a family. They are entitled to 124 | equal rights as to marriage, during marriage and at its dissolution. 125 | 2. Marriage shall be entered into only with the free and full consent of the 126 | intending spouses. 127 | 3. The family is the natural and fundamental group unit of society and is 128 | entitled to protection by society and the State. 129 | 130 | Article 17 131 | 132 | 1. Everyone has the right to own property alone as well as in association with 133 | others. 134 | 2. No one shall be arbitrarily deprived of his property. 135 | 136 | 137 | Article 18 138 | 139 | Everyone has the right to freedom of thought, conscience and religion; this 140 | right includes freedom to change his religion or belief, and freedom, either 141 | alone or in community with others and in public or private, to manifest his 142 | religion or belief in teaching, practice, worship and observance. 143 | 144 | 145 | Article 19 146 | 147 | Everyone has the right to freedom of opinion and expression; this right includes 148 | freedom to hold opinions without interference and to seek, receive and impart 149 | information and ideas through any media and regardless of frontiers. 150 | 151 | 152 | Article 20 153 | 154 | 1. Everyone has the right to freedom of peaceful assembly and association. 155 | 2. No one may be compelled to belong to an association. 156 | 157 | 158 | Article 21 159 | 160 | 1. Everyone has the right to take part in the government of his country, 161 | directly or through freely chosen representatives. 162 | 2. Everyone has the right to equal access to public service in his country. 163 | 3. The will of the people shall be the basis of the authority of government; 164 | this will shall be expressed in periodic and genuine elections which shall be by 165 | universal and equal suffrage and shall be held by secret vote or by equivalent 166 | free voting procedures. 167 | 168 | Article 22 169 | 170 | Everyone, as a member of society, has the right to social security and is 171 | entitled to realization, through national effort and international co-operation 172 | and in accordance with the organization and resources of each State, of the 173 | economic, social and cultural rights indispensable for his dignity and the free 174 | development of his personality. 175 | 176 | 177 | Article 23 178 | 179 | 1. Everyone has the right to work, to free choice of employment, to just and 180 | favourable conditions of work and to protection against unemployment. 181 | 2. Everyone, without any discrimination, has the right to equal pay for equal 182 | work. 183 | 3. Everyone who works has the right to just and favourable remuneration ensuring 184 | for himself and his family an existence worthy of human dignity, and 185 | supplemented, if necessary, by other means of social protection. 186 | 4. Everyone has the right to form and to join trade unions for the protection of 187 | his interests. 188 | 189 | 190 | Article 24 191 | 192 | Everyone has the right to rest and leisure, including reasonable limitation of 193 | working hours and periodic holidays with pay. 194 | 195 | 196 | Article 25 197 | 198 | 1. Everyone has the right to a standard of living adequate for the health and 199 | well-being of himself and of his family, including food, clothing, housing and 200 | medical care and necessary social services, and the right to security in the 201 | event of unemployment, sickness, disability, widowhood, old age or other lack of 202 | livelihood in circumstances beyond his control. 203 | 2. Motherhood and childhood are entitled to special care and assistance. All 204 | children, whether born in or out of wedlock, shall enjoy the same social 205 | protection. 206 | 207 | 208 | Article 26 209 | 210 | 1. Everyone has the right to education. Education shall be free, at least in the 211 | elementary and fundamental stages. Elementary education shall be compulsory. 212 | Technical and professional education shall be made generally available and 213 | higher education shall be equally accessible to all on the basis of merit. 214 | 2. Education shall be directed to the full development of the human personality 215 | and to the strengthening of respect for human rights and fundamental freedoms. 216 | It shall promote understanding, tolerance and friendship among all nations, 217 | racial or religious groups, and shall further the activities of the United 218 | Nations for the maintenance of peace. 219 | 3. Parents have a prior right to choose the kind of education that shall be 220 | given to their children. 221 | 222 | 223 | Article 27 224 | 225 | 1. Everyone has the right freely to participate in the cultural life of the 226 | community, to enjoy the arts and to share in scientific advancement and its 227 | benefits. 228 | 2. Everyone has the right to the protection of the moral and material interests 229 | resulting from any scientific, literary or artistic production of which he is 230 | the author. 231 | 232 | 233 | Article 28 234 | 235 | Everyone is entitled to a social and international order in which the rights and 236 | freedoms set forth in this Declaration can be fully realized. 237 | 238 | 239 | Article 29 240 | 241 | 1. Everyone has duties to the community in which alone the free and full 242 | development of his personality is possible. 243 | 2. In the exercise of his rights and freedoms, everyone shall be subject only to 244 | such limitations as are determined by law solely for the purpose of securing due 245 | recognition and respect for the rights and freedoms of others and of meeting the 246 | just requirements of morality, public order and the general welfare in a 247 | democratic society. 248 | 3. These rights and freedoms may in no case be exercised contrary to the 249 | purposes and principles of the United Nations. 250 | 251 | 252 | Article 30 253 | 254 | Nothing in this Declaration may be interpreted as implying for any State, group 255 | or person any right to engage in any activity or to perform any act aimed at the 256 | destruction of any of the rights and freedoms set forth herein. -------------------------------------------------------------------------------- /gleam.toml: -------------------------------------------------------------------------------- 1 | name = "glint" 2 | version = "1.2.1" 3 | 4 | # Fill out these fields if you intend to generate HTML documentation or publishname = "glint" 5 | # your project to the Hex package manager. 6 | # 7 | licences = ["Apache-2.0"] 8 | 9 | description = "Gleam command-line argument parsing with flags and automated help text generation." 10 | 11 | repository = { type = "github", user = "TanklesXL", repo = "glint" } 12 | 13 | links = [ 14 | { title = "Hex", href = "https://hex.pm/packages/glint" }, 15 | { title = "Docs", href = "https://hexdocs.pm/glint/" }, 16 | ] 17 | 18 | gleam = ">= 1.4.0 and < 2.0.0" 19 | 20 | [dependencies] 21 | gleam_stdlib = ">= 0.43.0 and < 2.0.0" 22 | snag = ">= 1.0.0 and < 2.0.0" 23 | gleam_community_ansi = "~> 1.0" 24 | gleam_community_colour = "~> 1.0 or ~> 2.0" 25 | 26 | [dev-dependencies] 27 | gleeunit = "~> 1.0" 28 | argv = ">= 1.0.2 and < 2.0.0" 29 | birdie = ">= 1.1.8 and < 2.0.0" 30 | -------------------------------------------------------------------------------- /manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, 6 | { 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" }, 7 | { name = "edit_distance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "A1E485C69A70210223E46E63985FA1008B8B2DDA9848B7897469171B29020C05" }, 8 | { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, 9 | { name = "glance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "106111453AE9BA959184302B7DADF2E8CF322B27A7CB68EE78F3EE43FEACCE2C" }, 10 | { 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" }, 11 | { 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" }, 12 | { name = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" }, 13 | { name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" }, 14 | { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, 15 | { name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" }, 16 | { name = "gleeunit", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "0E6C83834BA65EDCAAF4FE4FB94AC697D9262D83E6F58A750D63C9F6C8A9D9FF" }, 17 | { name = "glexer", version = "2.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "5C235CBDF4DA5203AD5EAB1D6D8B456ED8162C5424FE2309CFFB7EF438B7C269" }, 18 | { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, 19 | { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" }, 20 | { name = "simplifile", version = "2.2.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C88E0EE2D509F6D86EB55161D631657675AA7684DAB83822F7E59EB93D9A60E3" }, 21 | { name = "snag", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "7E9F06390040EB5FAB392CE642771484136F2EC103A92AE11BA898C8167E6E17" }, 22 | { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, 23 | { name = "trie_again", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "5B19176F52B1BD98831B57FDC97BD1F88C8A403D6D8C63471407E78598E27184" }, 24 | ] 25 | 26 | [requirements] 27 | argv = { version = ">= 1.0.2 and < 2.0.0" } 28 | birdie = { version = ">= 1.1.8 and < 2.0.0" } 29 | gleam_community_ansi = { version = "~> 1.0" } 30 | gleam_community_colour = { version = "~> 1.0 or ~> 2.0" } 31 | gleam_stdlib = { version = ">= 0.43.0 and < 2.0.0" } 32 | gleeunit = { version = "~> 1.0" } 33 | snag = { version = ">= 1.0.0 and < 2.0.0" } 34 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended"], 4 | "rangeStrategy": "widen", 5 | "lockFileMaintenance": { "enabled": true } 6 | } 7 | -------------------------------------------------------------------------------- /src/glint.gleam: -------------------------------------------------------------------------------- 1 | import gleam 2 | import gleam/dict 3 | import gleam/float 4 | import gleam/int 5 | import gleam/io 6 | import gleam/list 7 | import gleam/option.{type Option, None, Some} 8 | import gleam/result 9 | import gleam/string 10 | import gleam_community/colour.{type Colour} 11 | import glint/constraint 12 | import glint/internal/help 13 | import snag.{type Snag} 14 | 15 | // --- CONFIGURATION --- 16 | 17 | // -- CONFIGURATION: TYPES -- 18 | 19 | /// Config for glint 20 | /// 21 | type Config { 22 | Config( 23 | pretty_help: Option(PrettyHelp), 24 | name: Option(String), 25 | as_module: Bool, 26 | description: Option(String), 27 | exit: Bool, 28 | indent_width: Int, 29 | max_output_width: Int, 30 | min_first_column_width: Int, 31 | column_gap: Int, 32 | ) 33 | } 34 | 35 | /// PrettyHelp defines the header colours to be used when styling help text 36 | /// 37 | pub type PrettyHelp { 38 | PrettyHelp(usage: Colour, flags: Colour, subcommands: Colour) 39 | } 40 | 41 | // -- CONFIGURATION: CONSTANTS -- 42 | 43 | /// default config 44 | /// 45 | const default_config = Config( 46 | pretty_help: None, 47 | name: None, 48 | as_module: False, 49 | description: None, 50 | exit: True, 51 | indent_width: 4, 52 | max_output_width: 80, 53 | min_first_column_width: 20, 54 | column_gap: 2, 55 | ) 56 | 57 | // -- CONFIGURATION: FUNCTIONS -- 58 | 59 | /// Enable custom colours for help text headers. 60 | /// 61 | /// For a pre-made style, pass in [`glint.default_pretty_help`](#default_pretty_help) 62 | /// 63 | pub fn pretty_help(glint: Glint(a), pretty: PrettyHelp) -> Glint(a) { 64 | Glint(..glint, config: Config(..glint.config, pretty_help: Some(pretty))) 65 | } 66 | 67 | /// Give the current glint application a name. 68 | /// 69 | /// The name specified here is used when generating help text for the current glint instance. 70 | /// 71 | pub fn with_name(glint: Glint(a), name: String) -> Glint(a) { 72 | Glint(..glint, config: Config(..glint.config, name: Some(name))) 73 | } 74 | 75 | /// By default, Glint exits with error status 1 when an error is encountered (eg. invalid flag or command not found) 76 | /// 77 | /// Calling this function disables that feature. 78 | /// 79 | pub fn without_exit(glint: Glint(a)) -> Glint(a) { 80 | Glint(..glint, config: Config(..glint.config, exit: False)) 81 | } 82 | 83 | /// Adjust the generated help text to reflect that the current glint app should be run as a gleam module. 84 | /// 85 | /// Use in conjunction with [`glint.with_name`](#with_name) to get usage text output like `gleam run -m ` 86 | /// 87 | pub fn as_module(glint: Glint(a)) -> Glint(a) { 88 | Glint(..glint, config: Config(..glint.config, as_module: True)) 89 | } 90 | 91 | /// Adjusts the indent width used to indent content under the usage, flags, 92 | /// and subcommands headings in the help output. 93 | /// 94 | /// Default: 4. 95 | /// 96 | pub fn with_indent_width(glint: Glint(a), indent_width: Int) -> Glint(a) { 97 | Glint(..glint, config: Config(..glint.config, indent_width:)) 98 | } 99 | 100 | /// Adjusts the output width at which help text will wrap onto a new line. 101 | /// 102 | /// Default: 80. 103 | /// 104 | pub fn with_max_output_width(glint: Glint(a), max_output_width: Int) -> Glint(a) { 105 | Glint(..glint, config: Config(..glint.config, max_output_width:)) 106 | } 107 | 108 | /// Adjusts the minimum width of the column containing flag and command names in the help output. 109 | /// 110 | /// Default: 20. 111 | /// 112 | pub fn with_min_first_column_width( 113 | glint: Glint(a), 114 | min_first_column_width: Int, 115 | ) -> Glint(a) { 116 | Glint(..glint, config: Config(..glint.config, min_first_column_width:)) 117 | } 118 | 119 | /// Adjusts the size of the gap between columns in the help output. 120 | /// 121 | /// Default: 2. 122 | /// 123 | pub fn with_column_gap(glint: Glint(a), column_gap: Int) -> Glint(a) { 124 | Glint(..glint, config: Config(..glint.config, column_gap:)) 125 | } 126 | 127 | // --- CORE --- 128 | 129 | // -- CORE: TYPES -- 130 | 131 | /// A container type for config and commands. 132 | /// 133 | /// This will be the main interaction point when setting up glint. 134 | /// To create a new one use [`glint.new`](#new). 135 | /// 136 | pub opaque type Glint(a) { 137 | Glint(config: Config, cmd: CommandNode(a)) 138 | } 139 | 140 | /// Specify the expected number of unnamed arguments with this type and the [`glint.unnamed_args`](#unnamed_args) function 141 | /// 142 | pub type ArgsCount { 143 | /// Specifies that a command must accept a specific number of unnamed arguments 144 | /// 145 | EqArgs(Int) 146 | /// Specifies that a command must accept a minimum number of unnamed arguments 147 | /// 148 | MinArgs(Int) 149 | } 150 | 151 | /// The type representing a glint command. 152 | /// 153 | /// To create a new command, use the [`glint.command`](#command) function. 154 | /// 155 | pub opaque type Command(a) { 156 | Command( 157 | do: Runner(a), 158 | flags: Flags, 159 | description: String, 160 | unnamed_args: Option(ArgsCount), 161 | named_args: List(String), 162 | ) 163 | } 164 | 165 | type InternalCommand(a) { 166 | InternalCommand( 167 | do: Runner(a), 168 | flags: Flags, 169 | unnamed_args: Option(ArgsCount), 170 | named_args: List(String), 171 | ) 172 | } 173 | 174 | /// A container for named arguments available to commands at runtime. 175 | /// 176 | pub opaque type NamedArgs { 177 | NamedArgs(internal: dict.Dict(String, String)) 178 | } 179 | 180 | /// Functions that execute when glint commands are run. 181 | /// 182 | pub type Runner(a) = 183 | fn(NamedArgs, List(String), Flags) -> a 184 | 185 | /// CommandNode tree representation. 186 | /// 187 | type CommandNode(a) { 188 | CommandNode( 189 | contents: Option(InternalCommand(a)), 190 | subcommands: dict.Dict(String, CommandNode(a)), 191 | group_flags: Flags, 192 | description: String, 193 | ) 194 | } 195 | 196 | /// Ok type for command execution 197 | /// 198 | @internal 199 | pub type Out(a) { 200 | /// Container for the command return value 201 | Out(a) 202 | /// Container for the generated help string 203 | Help(String) 204 | } 205 | 206 | // -- CORE: BUILDER FUNCTIONS -- 207 | 208 | /// Create a new glint instance. 209 | /// 210 | pub fn new() -> Glint(a) { 211 | Glint(config: default_config, cmd: empty_command()) 212 | } 213 | 214 | /// Set the help text for a specific command path. 215 | /// 216 | /// This function is intended to allow users to set the help text of commands that might not be directly instantiated, 217 | /// such as commands with no business logic associated to them but that have subcommands. 218 | /// 219 | /// Using this function should almost never be necessary, in most cases you should use [`glint.command_help`](#command_help) insstead. 220 | pub fn path_help( 221 | in glint: Glint(a), 222 | at path: List(String), 223 | put description: String, 224 | ) -> Glint(a) { 225 | use node <- update_at(in: glint, at: path) 226 | CommandNode(..node, description: description) 227 | } 228 | 229 | /// Set help text for the application as a whole. 230 | /// 231 | /// Help text set with this function wil be printed at the top of the help text for every command. 232 | /// To set help text specifically for the root command please use [`glint.command_help`](#command_help) or [`glint.path_help([],...)`](#path_help) 233 | /// 234 | /// This function allows for user-supplied newlines in long text strings. Individual newline characters are instead converted to spaces. 235 | /// This is useful for developers to format their help text in a more readable way in the source code. 236 | /// 237 | /// For formatted text to appear on a new line, use 2 newline characters. 238 | /// For formatted text to appear in a new paragraph, use 3 newline characters. 239 | /// 240 | pub fn global_help(in glint: Glint(a), of description: String) -> Glint(a) { 241 | Glint(..glint, config: Config(..glint.config, description: Some(description))) 242 | } 243 | 244 | /// Adds a new command to be run at the specified path. 245 | /// 246 | /// If the path is `[]`, the root command is set with the provided function and 247 | /// flags. 248 | /// 249 | /// Note: all command paths are sanitized by stripping whitespace and removing any empty string elements. 250 | /// 251 | /// ```gleam 252 | /// glint.new() 253 | /// |> glint.add(at: [], do: root_command()) 254 | /// |> glint.add(at: ["subcommand"], do: subcommand()) 255 | /// ... 256 | /// ``` 257 | /// 258 | pub fn add( 259 | to glint: Glint(a), 260 | at path: List(String), 261 | do command: Command(a), 262 | ) -> Glint(a) { 263 | use node <- update_at(in: glint, at: path) 264 | CommandNode( 265 | ..node, 266 | description: command.description, 267 | contents: Some(InternalCommand( 268 | do: command.do, 269 | flags: command.flags, 270 | named_args: command.named_args, 271 | unnamed_args: command.unnamed_args, 272 | )), 273 | ) 274 | } 275 | 276 | /// Helper for initializing empty commands 277 | /// 278 | fn empty_command() -> CommandNode(a) { 279 | CommandNode( 280 | contents: None, 281 | subcommands: dict.new(), 282 | group_flags: new_flags(), 283 | description: "", 284 | ) 285 | } 286 | 287 | /// Trim each path element and remove any resulting empty strings. 288 | /// 289 | fn sanitize_path(path: List(String)) -> List(String) { 290 | path 291 | |> list.map(string.trim) 292 | |> list.filter(fn(s) { s != "" }) 293 | } 294 | 295 | /// Create a [Command(a)](#Command) from a [Runner(a)](#Runner). 296 | /// 297 | /// ### Example: 298 | /// 299 | /// ```gleam 300 | /// use <- glint.command_help("Some awesome help text") 301 | /// use named_arg <- glint.named_arg("some_arg") 302 | /// use <- glint.unnamed_args(glint.EqArgs(0)) 303 | /// ... 304 | /// use named, unnamed, flags <- glint.command() 305 | /// let my_arg = named_arg(named) 306 | /// ... 307 | /// ``` 308 | pub fn command(do runner: Runner(a)) -> Command(a) { 309 | Command( 310 | do: runner, 311 | flags: new_flags(), 312 | description: "", 313 | unnamed_args: None, 314 | named_args: [], 315 | ) 316 | } 317 | 318 | /// Map the output of a [`Command`](#Command) 319 | /// 320 | /// This function can be useful when you are handling user-defined commands or commands from other packages and need to make sure the return type matches your own commands. 321 | /// 322 | pub fn map_command(command: Command(a), with fun: fn(a) -> b) -> Command(b) { 323 | Command( 324 | do: fn(named_args, args, flags) { fun(command.do(named_args, args, flags)) }, 325 | description: command.description, 326 | flags: command.flags, 327 | named_args: command.named_args, 328 | unnamed_args: command.unnamed_args, 329 | ) 330 | } 331 | 332 | /// Attach a helptext description to a [`Command(a)`](#Command) 333 | /// 334 | /// This function allows for user-supplied newlines in long text strings. Individual newline characters are instead converted to spaces. 335 | /// This is useful for developers to format their help text in a more readable way in the source code. 336 | /// 337 | /// For formatted text to appear on a new line, use 2 newline characters. 338 | /// For formatted text to appear in a new paragraph, use 3 newline characters. 339 | /// 340 | pub fn command_help(of desc: String, with f: fn() -> Command(a)) -> Command(a) { 341 | Command(..f(), description: desc) 342 | } 343 | 344 | /// Specify a specific number of unnamed args that a given command expects. 345 | /// 346 | /// Use in conjunction with [`glint.ArgsCount`](#ArgsCount) to specify either a minimum or a specific number of args. 347 | /// 348 | /// ### Example: 349 | /// 350 | /// ```gleam 351 | /// ... 352 | /// // for a command that accets only 1 unnamed argument: 353 | /// use <- glint.unnamed_args(glint.EqArgs(1)) 354 | /// ... 355 | /// named, unnamed, flags <- glint.command() 356 | /// let assert Ok([arg]) = unnamed 357 | /// ``` 358 | /// 359 | pub fn unnamed_args( 360 | of count: ArgsCount, 361 | with f: fn() -> Command(b), 362 | ) -> Command(b) { 363 | Command(..f(), unnamed_args: Some(count)) 364 | } 365 | 366 | /// Add a list of named arguments to a [`Command(a)`](#Command). The value can be retrieved from the command's [`NamedArgs`](#NamedArgs) 367 | /// 368 | /// These named arguments will be matched with the first N arguments passed to the command. 369 | /// 370 | /// 371 | /// **IMPORTANT**: 372 | /// 373 | /// - Matched named arguments will **not** be present in the commmand's unnamed args list 374 | /// 375 | /// - All named arguments must match for a command to succeed. 376 | /// 377 | /// ### Example: 378 | /// 379 | /// ```gleam 380 | /// ... 381 | /// use first_name <- glint.named_arg("first name") 382 | /// ... 383 | /// use named, unnamed, flags <- glint.command() 384 | /// let first = first_name(named) 385 | /// ``` 386 | /// 387 | pub fn named_arg( 388 | named name: String, 389 | with f: fn(fn(NamedArgs) -> String) -> Command(a), 390 | ) -> Command(a) { 391 | let cmd = { 392 | use named_args <- f() 393 | // we can assert here because the command runner will only execute if the named args match 394 | let assert Ok(arg) = dict.get(named_args.internal, name) 395 | arg 396 | } 397 | 398 | Command(..cmd, named_args: [name, ..cmd.named_args]) 399 | } 400 | 401 | /// Add a [`Flag(a)`](#Flag) to a [`Command(a)`](#Command) 402 | /// 403 | /// The provided callback is provided a function to fetch the current flag fvalue from the command input [`Flags`](#Flags). 404 | /// 405 | /// This function is most ergonomic as part of a `use` chain when building commands. 406 | /// 407 | /// ### Example: 408 | /// 409 | /// ```gleam 410 | /// ... 411 | /// use repeat <- glint.flag( 412 | /// glint.int_flag("repeat") 413 | /// |> glint.flag_default(1) 414 | /// |> glint.flag_help("Repeat the message n-times") 415 | /// ) 416 | /// ... 417 | /// use named, unnamed, flags <- glint.command() 418 | /// let repeat_value = repeat(flags) 419 | /// ``` 420 | /// 421 | pub fn flag( 422 | of flag: Flag(a), 423 | with f: fn(fn(Flags) -> snag.Result(a)) -> Command(b), 424 | ) -> Command(b) { 425 | let cmd = f(flag.getter(_, flag.name)) 426 | Command(..cmd, flags: insert(cmd.flags, flag.name, build_flag(flag))) 427 | } 428 | 429 | /// Add a flag for a group of commands. 430 | /// The provided flags will be available to all commands at or beyond the provided path 431 | /// 432 | pub fn group_flag( 433 | in glint: Glint(a), 434 | at path: List(String), 435 | of flag: Flag(_), 436 | ) -> Glint(a) { 437 | use node <- update_at(in: glint, at: path) 438 | CommandNode( 439 | ..node, 440 | group_flags: insert(node.group_flags, flag.name, build_flag(flag)), 441 | ) 442 | } 443 | 444 | // -- CORE: EXECUTION FUNCTIONS -- 445 | 446 | /// Determines which command to run and executes it. 447 | /// 448 | /// Sets any provided flags if necessary. 449 | /// 450 | /// Each value prefixed with `--` is parsed as a flag. 451 | /// 452 | /// This function does not print its output and is mainly intended for use within `glint` itself. 453 | /// If you would like to print or handle the output of a command please see the `run_and_handle` function. 454 | /// 455 | @internal 456 | pub fn execute(glint: Glint(a), args: List(String)) -> Result(Out(a), String) { 457 | // create help flag to check for 458 | let help_flag = flag_prefix <> help.help_flag.meta.name 459 | 460 | // check if help flag is present 461 | let #(help, args) = case list.partition(args, fn(s) { s == help_flag }) { 462 | // help flag not in args 463 | #([], args) -> #(False, args) 464 | // help flag in args 465 | #(_, args) -> #(True, args) 466 | } 467 | 468 | // split flags out from the args list 469 | let #(flags, args) = list.partition(args, string.starts_with(_, flag_prefix)) 470 | 471 | // search for command and execute 472 | do_execute(glint.cmd, glint.config, args, flags, help, []) 473 | } 474 | 475 | /// Find which command to execute and run it with computed flags and args 476 | /// 477 | fn do_execute( 478 | cmd: CommandNode(a), 479 | config: Config, 480 | args: List(String), 481 | flags: List(String), 482 | help: Bool, 483 | command_path: List(String), 484 | ) -> Result(Out(a), String) { 485 | case args { 486 | // when there are no more available arguments 487 | // and help flag has been passed, generate help message 488 | [] if help -> Ok(Help(cmd_help(command_path, cmd, config))) 489 | 490 | // when there are no more available arguments 491 | // run the current command 492 | [] -> execute_root(command_path, config, cmd, [], flags) |> result.map(Out) 493 | 494 | // when there are arguments remaining 495 | // check if the next one is a subcommand of the current command 496 | [arg, ..rest] -> 497 | case dict.get(cmd.subcommands, arg) { 498 | // subcommand found, continue 499 | Ok(sub_command) -> 500 | CommandNode( 501 | ..sub_command, 502 | group_flags: merge(cmd.group_flags, sub_command.group_flags), 503 | ) 504 | |> do_execute(config, rest, flags, help, [arg, ..command_path]) 505 | 506 | // subcommand not found, but help flag has been passed 507 | // generate and return help message 508 | _ if help -> Ok(Help(cmd_help(command_path, cmd, config))) 509 | 510 | // subcommand not found, but help flag has not been passed 511 | // execute the current command 512 | _ -> 513 | execute_root(command_path, config, cmd, args, flags) 514 | |> result.map(Out) 515 | } 516 | } 517 | } 518 | 519 | fn args_compare(expected: ArgsCount, actual: Int) -> snag.Result(Nil) { 520 | use err <- result.map_error(case expected { 521 | EqArgs(expected) if actual == expected -> Ok(Nil) 522 | MinArgs(expected) if actual >= expected -> Ok(Nil) 523 | EqArgs(expected) -> Error(int.to_string(expected)) 524 | MinArgs(expected) -> Error("at least " <> int.to_string(expected)) 525 | }) 526 | snag.new( 527 | "expected: " <> err <> " argument(s), provided: " <> int.to_string(actual), 528 | ) 529 | } 530 | 531 | /// Executes the current root command. 532 | /// 533 | fn execute_root( 534 | path: List(String), 535 | config: Config, 536 | cmd: CommandNode(a), 537 | args: List(String), 538 | flag_inputs: List(String), 539 | ) -> Result(a, String) { 540 | { 541 | // check if the command can actually be executed 542 | use contents <- result.try(option.to_result( 543 | cmd.contents, 544 | snag.new("command not found"), 545 | )) 546 | 547 | // merge flags and parse flag inputs 548 | use new_flags <- result.try(list.try_fold( 549 | over: flag_inputs, 550 | from: merge(cmd.group_flags, contents.flags), 551 | with: update_flags, 552 | )) 553 | 554 | // get named arguments 555 | use named_args <- result.try({ 556 | let named = list.zip(contents.named_args, args) 557 | case list.length(named) == list.length(contents.named_args) { 558 | True -> Ok(dict.from_list(named)) 559 | False -> 560 | snag.error( 561 | "unmatched named arguments: " 562 | <> { 563 | contents.named_args 564 | |> list.drop(list.length(named)) 565 | |> list.map(fn(s) { "'" <> s <> "'" }) 566 | |> string.join(", ") 567 | }, 568 | ) 569 | } 570 | }) 571 | 572 | // get unnamed arguments 573 | let args = list.drop(args, dict.size(named_args)) 574 | 575 | // validate unnamed argument quantity 576 | use _ <- result.map(case contents.unnamed_args { 577 | Some(count) -> 578 | count 579 | |> args_compare(list.length(args)) 580 | |> snag.context("invalid number of arguments provided") 581 | None -> Ok(Nil) 582 | }) 583 | 584 | // execute the command 585 | contents.do(NamedArgs(named_args), args, new_flags) 586 | } 587 | |> result.map_error(fn(err) { 588 | err 589 | |> snag.layer("failed to run command") 590 | |> snag.pretty_print 591 | <> "\nSee the following help text, available via the '--help' flag.\n\n" 592 | <> cmd_help(path, cmd, config) 593 | }) 594 | } 595 | 596 | /// Run a glint app and print any errors enountered, or the help text if requested. 597 | /// This function ignores any value returned by the command that was run. 598 | /// If you would like to do handle the command output please see the [`glint.run_and_handle`](#run_and_handle) function. 599 | /// 600 | /// IMPORTANT: This function exits with code 1 if an error was encountered. 601 | /// If this behaviour is not desired please disable it with [`glint.without_exit`](#without_exit) 602 | /// 603 | pub fn run(from glint: Glint(a), for args: List(String)) -> Nil { 604 | run_and_handle(from: glint, for: args, with: fn(_) { Nil }) 605 | } 606 | 607 | /// Run a glint app with a custom handler for command output. 608 | /// This function prints any errors enountered or the help text if requested. 609 | /// 610 | /// IMPORTANT: This function exits with code 1 if an error was encountered. 611 | /// If this behaviour is not desired please disable it with [`glint.without_exit`](#without_exit) 612 | /// 613 | pub fn run_and_handle( 614 | from glint: Glint(a), 615 | for args: List(String), 616 | with handle: fn(a) -> _, 617 | ) -> Nil { 618 | case execute(glint, args) { 619 | Error(s) -> { 620 | io.println(s) 621 | case glint.config.exit { 622 | True -> exit(1) 623 | False -> Nil 624 | } 625 | } 626 | Ok(Help(s)) -> io.println(s) 627 | Ok(Out(out)) -> { 628 | handle(out) 629 | Nil 630 | } 631 | } 632 | } 633 | 634 | /// Default colouring for help text. 635 | /// 636 | /// mint (r: 182, g: 255, b: 234) colour for usage 637 | /// 638 | /// pink (r: 255, g: 175, b: 243) colour for flags 639 | /// 640 | /// buttercup (r: 252, g: 226, b: 174) colour for subcommands 641 | /// 642 | pub fn default_pretty_help() -> PrettyHelp { 643 | let assert Ok(usage_colour) = colour.from_rgb255(182, 255, 234) 644 | let assert Ok(flags_colour) = colour.from_rgb255(255, 175, 243) 645 | let assert Ok(subcommands_colour) = colour.from_rgb255(252, 226, 174) 646 | 647 | PrettyHelp( 648 | usage: usage_colour, 649 | flags: flags_colour, 650 | subcommands: subcommands_colour, 651 | ) 652 | } 653 | 654 | // -- HELP: FUNCTIONS -- 655 | 656 | /// generate the help text for a command 657 | fn cmd_help(path: List(String), cmd: CommandNode(a), config: Config) -> String { 658 | // recreate the path of the current command 659 | // reverse the path because it is created by prepending each section as do_execute walks down the tree 660 | path 661 | |> list.reverse 662 | |> string.join(" ") 663 | |> build_command_help(cmd) 664 | |> help.command_help_to_string(build_help_config(config)) 665 | } 666 | 667 | // -- HELP - FUNCTIONS - BUILDERS -- 668 | fn build_help_config(config: Config) -> help.Config { 669 | help.Config( 670 | name: config.name, 671 | usage_colour: option.map(config.pretty_help, fn(p) { p.usage }), 672 | flags_colour: option.map(config.pretty_help, fn(p) { p.flags }), 673 | subcommands_colour: option.map(config.pretty_help, fn(p) { p.subcommands }), 674 | as_module: config.as_module, 675 | description: config.description, 676 | indent_width: config.indent_width, 677 | max_output_width: config.max_output_width, 678 | min_first_column_width: config.min_first_column_width, 679 | column_gap: config.column_gap, 680 | flag_prefix: flag_prefix, 681 | flag_delimiter: flag_delimiter, 682 | ) 683 | } 684 | 685 | /// build the help representation for a subtree of commands 686 | /// 687 | fn build_command_help(name: String, node: CommandNode(_)) -> help.Command { 688 | let #(description, flags, unnamed_args, named_args) = 689 | node.contents 690 | |> option.map(fn(cmd) { 691 | #( 692 | node.description, 693 | build_flags_help(merge(node.group_flags, cmd.flags)), 694 | cmd.unnamed_args, 695 | cmd.named_args, 696 | ) 697 | }) 698 | |> option.unwrap(#(node.description, [], None, [])) 699 | 700 | help.Command( 701 | meta: help.Metadata(name: name, description: description), 702 | flags: flags, 703 | subcommands: build_subcommands_help(node.subcommands), 704 | unnamed_args: { 705 | use args <- option.map(unnamed_args) 706 | case args { 707 | EqArgs(n) -> help.EqArgs(n) 708 | MinArgs(n) -> help.MinArgs(n) 709 | } 710 | }, 711 | named_args: named_args, 712 | ) 713 | } 714 | 715 | /// generate the string representation for the type of a flag 716 | /// 717 | fn flag_type_info(flag: FlagEntry) { 718 | case flag.value { 719 | I(_) -> "INT" 720 | B(_) -> "BOOL" 721 | F(_) -> "FLOAT" 722 | LF(_) -> "FLOAT_LIST" 723 | LI(_) -> "INT_LIST" 724 | LS(_) -> "STRING_LIST" 725 | S(_) -> "STRING" 726 | } 727 | } 728 | 729 | /// build the help representation for a list of flags 730 | /// 731 | fn build_flags_help(flags: Flags) -> List(help.Flag) { 732 | use acc, name, flag <- fold(flags, []) 733 | [ 734 | help.Flag( 735 | meta: help.Metadata(name: name, description: flag.description), 736 | type_: flag_type_info(flag), 737 | ), 738 | ..acc 739 | ] 740 | } 741 | 742 | /// build the help representation for a list of subcommands 743 | /// 744 | fn build_subcommands_help( 745 | subcommands: dict.Dict(String, CommandNode(_)), 746 | ) -> List(help.Metadata) { 747 | use acc, name, node <- dict.fold(subcommands, []) 748 | [help.Metadata(name: name, description: node.description), ..acc] 749 | } 750 | 751 | // ----- FLAGS ----- 752 | 753 | /// FlagEntry inputs must start with this prefix 754 | /// 755 | const flag_prefix = "--" 756 | 757 | /// The separation character for flag names and their values 758 | const flag_delimiter = "=" 759 | 760 | /// Supported flag types. 761 | /// 762 | type Value { 763 | /// Boolean flags, to be passed in as `--flag=true` or `--flag=false`. 764 | /// Can be toggled by omitting the desired value like `--flag`. 765 | /// Toggling will negate the existing value. 766 | /// 767 | B(FlagInternals(Bool)) 768 | 769 | /// Int flags, to be passed in as `--flag=1` 770 | /// 771 | I(FlagInternals(Int)) 772 | 773 | /// List(Int) flags, to be passed in as `--flag=1,2,3` 774 | /// 775 | LI(FlagInternals(List(Int))) 776 | 777 | /// Float flags, to be passed in as `--flag=1.0` 778 | /// 779 | F(FlagInternals(Float)) 780 | 781 | /// List(Float) flags, to be passed in as `--flag=1.0,2.0` 782 | /// 783 | LF(FlagInternals(List(Float))) 784 | 785 | /// String flags, to be passed in as `--flag=hello` 786 | /// 787 | S(FlagInternals(String)) 788 | 789 | /// List(String) flags, to be passed in as `--flag=hello,world` 790 | /// 791 | LS(FlagInternals(List(String))) 792 | } 793 | 794 | /// Glint's typed flags. 795 | /// 796 | /// Flags can be created using any of: 797 | /// - [`glint.int_flag`](#int_flag) 798 | /// - [`glint.ints_flag`](#ints_flag) 799 | /// - [`glint.float_flag`](#float_flag) 800 | /// - [`glint.floats_flag`](#floats_flag) 801 | /// - [`glint.string_flag`](#string_flag) 802 | /// - [`glint.strings_flag`](#strings_flag) 803 | /// - [`glint.bool_flag`](#bool_flag) 804 | /// 805 | pub opaque type Flag(a) { 806 | Flag( 807 | name: String, 808 | desc: String, 809 | parser: Parser(a, Snag), 810 | value: fn(FlagInternals(a)) -> Value, 811 | getter: fn(Flags, String) -> snag.Result(a), 812 | default: Option(a), 813 | ) 814 | } 815 | 816 | /// An internal representation of flag contents 817 | /// 818 | type FlagInternals(a) { 819 | FlagInternals(value: Option(a), parser: Parser(a, Snag)) 820 | } 821 | 822 | // Flag initializers 823 | 824 | type Parser(a, b) = 825 | fn(String) -> gleam.Result(a, b) 826 | 827 | /// Initialise an int flag. 828 | /// 829 | pub fn int_flag(named name: String) -> Flag(Int) { 830 | use input <- new_builder(name, I, get_int_flag) 831 | input 832 | |> int.parse 833 | |> result.replace_error(cannot_parse(input, "int")) 834 | } 835 | 836 | /// Initialise an int list flag. 837 | /// 838 | pub fn ints_flag(named name: String) -> Flag(List(Int)) { 839 | use input <- new_builder(name, LI, get_ints_flag) 840 | input 841 | |> string.split(",") 842 | |> list.try_map(int.parse) 843 | |> result.replace_error(cannot_parse(input, "int list")) 844 | } 845 | 846 | ///Initialise a float flag. 847 | /// 848 | pub fn float_flag(named name: String) -> Flag(Float) { 849 | use input <- new_builder(name, F, get_floats) 850 | input 851 | |> float.parse 852 | |> result.replace_error(cannot_parse(input, "float")) 853 | } 854 | 855 | /// Initialise a float list flag. 856 | /// 857 | pub fn floats_flag(named name: String) -> Flag(List(Float)) { 858 | use input <- new_builder(name, LF, get_floats_flag) 859 | input 860 | |> string.split(",") 861 | |> list.try_map(float.parse) 862 | |> result.replace_error(cannot_parse(input, "float list")) 863 | } 864 | 865 | /// Initialise a string flag. 866 | /// 867 | pub fn string_flag(named name: String) -> Flag(String) { 868 | new_builder(name, S, get_string_flag, fn(s) { Ok(s) }) 869 | } 870 | 871 | /// Intitialise a string list flag. 872 | /// 873 | pub fn strings_flag(named name: String) -> Flag(List(String)) { 874 | use input <- new_builder(name, LS, get_strings_flag) 875 | input 876 | |> string.split(",") 877 | |> Ok 878 | } 879 | 880 | /// Initialise a boolean flag. 881 | /// 882 | pub fn bool_flag(named name: String) -> Flag(Bool) { 883 | use input <- new_builder(name, B, get_bool_flag) 884 | case string.lowercase(input) { 885 | "true" | "t" -> Ok(True) 886 | "false" | "f" -> Ok(False) 887 | _ -> Error(cannot_parse(input, "bool")) 888 | } 889 | } 890 | 891 | /// initialize custom builders using a Value constructor and a parsing function 892 | /// 893 | fn new_builder( 894 | name: String, 895 | valuer: fn(FlagInternals(a)) -> Value, 896 | getter: fn(Flags, String) -> snag.Result(a), 897 | p: Parser(a, Snag), 898 | ) -> Flag(a) { 899 | Flag( 900 | name: name, 901 | desc: "", 902 | parser: p, 903 | value: valuer, 904 | default: None, 905 | getter: getter, 906 | ) 907 | } 908 | 909 | /// convert a (Flag(a) into its corresponding FlagEntry representation 910 | /// 911 | fn build_flag(fb: Flag(a)) -> FlagEntry { 912 | FlagEntry( 913 | value: fb.value(FlagInternals(value: fb.default, parser: fb.parser)), 914 | description: fb.desc, 915 | ) 916 | } 917 | 918 | /// Attach a constraint to a flag. 919 | /// 920 | /// As constraints are just functions, this works well as both part of a pipeline or with `use`. 921 | /// 922 | /// 923 | /// ### Pipe: 924 | /// 925 | /// ```gleam 926 | /// glint.int_flag("my_flag") 927 | /// |> glint.flag_help("An awesome flag") 928 | /// |> glint.flag_constraint(fn(i) { 929 | /// case i < 0 { 930 | /// True -> snag.error("must be greater than 0") 931 | /// False -> Ok(i) 932 | /// }}) 933 | /// ``` 934 | /// 935 | /// ### Use: 936 | /// 937 | /// ```gleam 938 | /// use i <- glint.flag_constraint( 939 | /// glint.int_flag("my_flag") 940 | /// |> glint.flag_help("An awesome flag") 941 | /// ) 942 | /// case i < 0 { 943 | /// True -> snag.error("must be greater than 0") 944 | /// False -> Ok(i) 945 | /// } 946 | /// ``` 947 | /// 948 | pub fn flag_constraint( 949 | builder: Flag(a), 950 | constraint: constraint.Constraint(a), 951 | ) -> Flag(a) { 952 | Flag(..builder, parser: wrap_with_constraint(builder.parser, constraint)) 953 | } 954 | 955 | /// attach a Constraint(a) to a Parser(a,Snag) 956 | /// this function should not be used directly unless 957 | fn wrap_with_constraint( 958 | p: Parser(a, Snag), 959 | constraint: constraint.Constraint(a), 960 | ) -> Parser(a, Snag) { 961 | fn(input: String) -> snag.Result(a) { attempt(p(input), constraint) } 962 | } 963 | 964 | fn attempt( 965 | val: gleam.Result(a, e), 966 | f: fn(a) -> gleam.Result(_, e), 967 | ) -> gleam.Result(a, e) { 968 | use a <- result.try(val) 969 | result.replace(f(a), a) 970 | } 971 | 972 | /// FlagEntry data and descriptions. 973 | /// 974 | type FlagEntry { 975 | FlagEntry(value: Value, description: String) 976 | } 977 | 978 | /// Attach a help text description to a flag. 979 | /// 980 | /// This function allows for user-supplied newlines in long text strings. Individual newline characters are instead converted to spaces. 981 | /// This is useful for developers to format their help text in a more readable way in the source code. 982 | /// 983 | /// For formatted text to appear on a new line, use 2 newline characters. 984 | /// For formatted text to appear in a new paragraph, use 3 newline characters. 985 | /// 986 | /// ### Example: 987 | /// 988 | /// ```gleam 989 | /// glint.int_flag("awesome_flag") 990 | /// |> glint.flag_help("Some great text!") 991 | /// ``` 992 | /// 993 | pub fn flag_help(for flag: Flag(a), of description: String) -> Flag(a) { 994 | Flag(..flag, desc: description) 995 | } 996 | 997 | /// Set the default value for a flag. 998 | /// 999 | /// ### Example: 1000 | /// 1001 | /// ```gleam 1002 | /// glint.int_flag("awesome_flag") 1003 | /// |> glint.flag_default(1) 1004 | /// ``` 1005 | /// 1006 | pub fn flag_default(for flag: Flag(a), of default: a) -> Flag(a) { 1007 | Flag(..flag, default: Some(default)) 1008 | } 1009 | 1010 | /// Flags passed as input to a command. 1011 | /// 1012 | pub opaque type Flags { 1013 | Flags(internal: dict.Dict(String, FlagEntry)) 1014 | } 1015 | 1016 | fn insert(in flags: Flags, at name: String, insert flag: FlagEntry) -> Flags { 1017 | Flags(dict.insert(flags.internal, name, flag)) 1018 | } 1019 | 1020 | fn merge(into a: Flags, from b: Flags) -> Flags { 1021 | Flags(internal: dict.merge(a.internal, b.internal)) 1022 | } 1023 | 1024 | fn fold(flags: Flags, acc: acc, f: fn(acc, String, FlagEntry) -> acc) -> acc { 1025 | dict.fold(flags.internal, acc, f) 1026 | } 1027 | 1028 | fn new_flags() -> Flags { 1029 | Flags(dict.new()) 1030 | } 1031 | 1032 | /// Updates a flag value, ensuring that the new value can satisfy the required type. 1033 | /// Assumes that all flag inputs passed in start with -- 1034 | /// This function is only intended to be used from glint.execute_root 1035 | /// 1036 | fn update_flags(in flags: Flags, with flag_input: String) -> snag.Result(Flags) { 1037 | let flag_input = string.drop_start(flag_input, string.length(flag_prefix)) 1038 | 1039 | case string.split_once(flag_input, flag_delimiter) { 1040 | Ok(data) -> update_flag_value(flags, data) 1041 | Error(_) -> attempt_toggle_flag(flags, flag_input) 1042 | } 1043 | } 1044 | 1045 | fn update_flag_value( 1046 | in flags: Flags, 1047 | with data: #(String, String), 1048 | ) -> snag.Result(Flags) { 1049 | let #(key, input) = data 1050 | use contents <- result.try(get(flags, key)) 1051 | use value <- result.map( 1052 | compute_flag(with: input, given: contents.value) 1053 | |> result.map_error(layer_invalid_flag(_, key)), 1054 | ) 1055 | insert(flags, key, FlagEntry(..contents, value: value)) 1056 | } 1057 | 1058 | fn attempt_toggle_flag(in flags: Flags, at key: String) -> snag.Result(Flags) { 1059 | use contents <- result.try(get(flags, key)) 1060 | case contents.value { 1061 | B(FlagInternals(None, ..) as internal) -> 1062 | FlagInternals(..internal, value: Some(True)) 1063 | |> B 1064 | |> fn(val) { FlagEntry(..contents, value: val) } 1065 | |> dict.insert(into: flags.internal, for: key) 1066 | |> Flags 1067 | |> Ok 1068 | B(FlagInternals(Some(val), ..) as internal) -> 1069 | FlagInternals(..internal, value: Some(!val)) 1070 | |> B 1071 | |> fn(val) { FlagEntry(..contents, value: val) } 1072 | |> dict.insert(into: flags.internal, for: key) 1073 | |> Flags 1074 | |> Ok 1075 | _ -> Error(no_value_flag_err(key)) 1076 | } 1077 | } 1078 | 1079 | fn access_type_error(flag_type) { 1080 | snag.error("cannot access flag as " <> flag_type) 1081 | } 1082 | 1083 | fn flag_not_provided_error() { 1084 | snag.error("no value provided") 1085 | } 1086 | 1087 | fn construct_value( 1088 | input: String, 1089 | internal: FlagInternals(a), 1090 | constructor: fn(FlagInternals(a)) -> Value, 1091 | ) -> snag.Result(Value) { 1092 | use val <- result.map(internal.parser(input)) 1093 | constructor(FlagInternals(..internal, value: Some(val))) 1094 | } 1095 | 1096 | /// Computes the new flag value given the input and the expected flag type 1097 | /// 1098 | fn compute_flag(with input: String, given current: Value) -> snag.Result(Value) { 1099 | input 1100 | |> case current { 1101 | I(internal) -> construct_value(_, internal, I) 1102 | LI(internal) -> construct_value(_, internal, LI) 1103 | F(internal) -> construct_value(_, internal, F) 1104 | LF(internal) -> construct_value(_, internal, LF) 1105 | S(internal) -> construct_value(_, internal, S) 1106 | LS(internal) -> construct_value(_, internal, LS) 1107 | B(internal) -> construct_value(_, internal, B) 1108 | } 1109 | |> snag.context("failed to compute value for flag") 1110 | } 1111 | 1112 | // Error creation and manipulation functions 1113 | fn layer_invalid_flag(err: Snag, flag: String) -> Snag { 1114 | snag.layer(err, "invalid flag '" <> flag <> "'") 1115 | } 1116 | 1117 | fn no_value_flag_err(flag_input: String) -> Snag { 1118 | { "flag '" <> flag_input <> "' has no assigned value" } 1119 | |> snag.new() 1120 | |> layer_invalid_flag(flag_input) 1121 | } 1122 | 1123 | fn undefined_flag_err(key: String) -> Snag { 1124 | "flag provided but not defined" 1125 | |> snag.new() 1126 | |> layer_invalid_flag(key) 1127 | } 1128 | 1129 | fn cannot_parse(with value: String, is kind: String) -> Snag { 1130 | { "cannot parse value '" <> value <> "' as " <> kind } 1131 | |> snag.new() 1132 | } 1133 | 1134 | // -- FLAG ACCESS FUNCTIONS -- 1135 | 1136 | /// Access the contents for the associated flag 1137 | /// 1138 | fn get(flags: Flags, name: String) -> snag.Result(FlagEntry) { 1139 | dict.get(flags.internal, name) 1140 | |> result.replace_error(undefined_flag_err(name)) 1141 | } 1142 | 1143 | fn get_value( 1144 | from flags: Flags, 1145 | at key: String, 1146 | expecting kind: fn(FlagEntry) -> snag.Result(a), 1147 | ) -> snag.Result(a) { 1148 | get(flags, key) 1149 | |> result.try(kind) 1150 | |> snag.context("failed to retrieve value for flag '" <> key <> "'") 1151 | } 1152 | 1153 | /// Gets the value for the associated flag. 1154 | /// 1155 | /// This function should only ever be used when fetching flags set at the group level. 1156 | /// For local flags please use the getter functions provided when calling [`glint.flag`](#flag). 1157 | /// 1158 | pub fn get_flag(from flags: Flags, for flag: Flag(a)) -> snag.Result(a) { 1159 | flag.getter(flags, flag.name) 1160 | } 1161 | 1162 | /// Gets the current value for the associated int flag 1163 | /// 1164 | fn get_int_flag(from flags: Flags, for name: String) -> snag.Result(Int) { 1165 | use flag <- get_value(flags, name) 1166 | case flag.value { 1167 | I(FlagInternals(value: Some(val), ..)) -> Ok(val) 1168 | I(FlagInternals(value: None, ..)) -> flag_not_provided_error() 1169 | _ -> access_type_error("int") 1170 | } 1171 | } 1172 | 1173 | /// Gets the current value for the associated ints flag 1174 | /// 1175 | fn get_ints_flag(from flags: Flags, for name: String) -> snag.Result(List(Int)) { 1176 | use flag <- get_value(flags, name) 1177 | case flag.value { 1178 | LI(FlagInternals(value: Some(val), ..)) -> Ok(val) 1179 | LI(FlagInternals(value: None, ..)) -> flag_not_provided_error() 1180 | _ -> access_type_error("int list") 1181 | } 1182 | } 1183 | 1184 | /// Gets the current value for the associated bool flag 1185 | /// 1186 | fn get_bool_flag(from flags: Flags, for name: String) -> snag.Result(Bool) { 1187 | use flag <- get_value(flags, name) 1188 | case flag.value { 1189 | B(FlagInternals(Some(val), ..)) -> Ok(val) 1190 | B(FlagInternals(None, ..)) -> flag_not_provided_error() 1191 | _ -> access_type_error("bool") 1192 | } 1193 | } 1194 | 1195 | /// Gets the current value for the associated string flag 1196 | /// 1197 | fn get_string_flag(from flags: Flags, for name: String) -> snag.Result(String) { 1198 | use flag <- get_value(flags, name) 1199 | case flag.value { 1200 | S(FlagInternals(value: Some(val), ..)) -> Ok(val) 1201 | S(FlagInternals(value: None, ..)) -> flag_not_provided_error() 1202 | _ -> access_type_error("string") 1203 | } 1204 | } 1205 | 1206 | /// Gets the current value for the associated strings flag 1207 | /// 1208 | fn get_strings_flag( 1209 | from flags: Flags, 1210 | for name: String, 1211 | ) -> snag.Result(List(String)) { 1212 | use flag <- get_value(flags, name) 1213 | case flag.value { 1214 | LS(FlagInternals(value: Some(val), ..)) -> Ok(val) 1215 | LS(FlagInternals(value: None, ..)) -> flag_not_provided_error() 1216 | _ -> access_type_error("string list") 1217 | } 1218 | } 1219 | 1220 | /// Gets the current value for the associated float flag 1221 | /// 1222 | fn get_floats(from flags: Flags, for name: String) -> snag.Result(Float) { 1223 | use flag <- get_value(flags, name) 1224 | case flag.value { 1225 | F(FlagInternals(value: Some(val), ..)) -> Ok(val) 1226 | F(FlagInternals(value: None, ..)) -> flag_not_provided_error() 1227 | _ -> access_type_error("float") 1228 | } 1229 | } 1230 | 1231 | /// Gets the current value for the associated float flag 1232 | /// 1233 | fn get_floats_flag( 1234 | from flags: Flags, 1235 | for name: String, 1236 | ) -> snag.Result(List(Float)) { 1237 | use flag <- get_value(flags, name) 1238 | case flag.value { 1239 | LF(FlagInternals(value: Some(val), ..)) -> Ok(val) 1240 | LF(FlagInternals(value: None, ..)) -> flag_not_provided_error() 1241 | _ -> access_type_error("float list") 1242 | } 1243 | } 1244 | 1245 | // traverses a Glint(a) tree for the provided path 1246 | // executes the provided function on the terminal node 1247 | // 1248 | fn update_at( 1249 | in glint: Glint(a), 1250 | at path: List(String), 1251 | do f: fn(CommandNode(a)) -> CommandNode(a), 1252 | ) -> Glint(a) { 1253 | Glint( 1254 | ..glint, 1255 | cmd: do_update_at(through: glint.cmd, at: sanitize_path(path), do: f), 1256 | ) 1257 | } 1258 | 1259 | fn do_update_at( 1260 | through node: CommandNode(a), 1261 | at path: List(String), 1262 | do f: fn(CommandNode(a)) -> CommandNode(a), 1263 | ) -> CommandNode(a) { 1264 | case path { 1265 | [] -> f(node) 1266 | [next, ..rest] -> { 1267 | CommandNode(..node, subcommands: { 1268 | use found <- dict.upsert(node.subcommands, next) 1269 | found 1270 | |> option.lazy_unwrap(empty_command) 1271 | |> do_update_at(rest, f) 1272 | }) 1273 | } 1274 | } 1275 | } 1276 | 1277 | @external(erlang, "erlang", "halt") 1278 | @external(javascript, "node:process", "exit") 1279 | fn exit(status: Int) -> Nil 1280 | -------------------------------------------------------------------------------- /src/glint/constraint.gleam: -------------------------------------------------------------------------------- 1 | import gleam/list 2 | import gleam/set 3 | import gleam/string 4 | import snag 5 | 6 | /// Constraint type for verifying flag values 7 | /// 8 | pub type Constraint(a) = 9 | fn(a) -> snag.Result(a) 10 | 11 | /// Returns a Constraint that ensures the parsed flag value is one of the allowed values. 12 | /// 13 | /// ```gleam 14 | /// import glint 15 | /// import glint/constraint 16 | /// ... 17 | /// glint.int_flag("my_flag") 18 | /// |> glint.constraint(constraint.one_of([1, 2, 3, 4])) 19 | /// ``` 20 | /// 21 | pub fn one_of(allowed: List(a)) -> Constraint(a) { 22 | let allowed_set = set.from_list(allowed) 23 | fn(val: a) -> snag.Result(a) { 24 | case set.contains(allowed_set, val) { 25 | True -> Ok(val) 26 | False -> 27 | snag.error( 28 | "invalid value '" 29 | <> string.inspect(val) 30 | <> "', must be one of: [" 31 | <> { 32 | allowed 33 | |> list.map(fn(a) { "'" <> string.inspect(a) <> "'" }) 34 | |> string.join(", ") 35 | } 36 | <> "]", 37 | ) 38 | } 39 | } 40 | } 41 | 42 | /// Returns a Constraint that ensures the parsed flag value is not one of the disallowed values. 43 | /// 44 | /// ```gleam 45 | /// import glint 46 | /// import glint/constraint 47 | /// ... 48 | /// glint.int_flag("my_flag") 49 | /// |> glint.constraint(constraint.none_of([1, 2, 3, 4])) 50 | /// ``` 51 | /// 52 | pub fn none_of(disallowed: List(a)) -> Constraint(a) { 53 | let disallowed_set = set.from_list(disallowed) 54 | fn(val: a) -> snag.Result(a) { 55 | case set.contains(disallowed_set, val) { 56 | False -> Ok(val) 57 | True -> 58 | snag.error( 59 | "invalid value '" 60 | <> string.inspect(val) 61 | <> "', must not be one of: [" 62 | <> { 63 | { 64 | disallowed 65 | |> list.map(fn(a) { "'" <> string.inspect(a) <> "'" }) 66 | |> string.join(", ") 67 | <> "]" 68 | } 69 | }, 70 | ) 71 | } 72 | } 73 | } 74 | 75 | /// This is a convenience function for applying a Constraint(a) to a List(a). 76 | /// This is useful because the default behaviour for constraints on lists is that they will apply to the list as a whole. 77 | /// 78 | /// For example, to apply one_of to all items in a `List(Int)`: 79 | /// 80 | /// Via `use`: 81 | /// ```gleam 82 | /// import glint 83 | /// import glint/constraint 84 | /// ... 85 | /// use li <- glint.flag_constraint(glint.int_flag("my_flag")) 86 | /// use i <- constraint.each() 87 | /// i |> one_of([1, 2, 3, 4]) 88 | /// ``` 89 | /// 90 | /// via a pipe: 91 | /// ```gleam 92 | /// import glint 93 | /// import glint/constraint 94 | /// ... 95 | /// glint.int_flag("my_flag") 96 | /// |> glint.flag_constraint( 97 | /// constraint.one_of([1,2,3,4]) 98 | /// |> constraint.each 99 | /// ) 100 | /// ``` 101 | /// 102 | pub fn each(constraint: Constraint(a)) -> Constraint(List(a)) { 103 | list.try_map(_, constraint) 104 | } 105 | -------------------------------------------------------------------------------- /src/glint/internal/help.gleam: -------------------------------------------------------------------------------- 1 | import gleam/bool 2 | import gleam/int 3 | import gleam/list 4 | import gleam/option.{type Option, None, Some} 5 | import gleam/string 6 | import gleam_community/ansi 7 | import gleam_community/colour.{type Colour} 8 | import glint/internal/utils 9 | 10 | /// Style heading text with the provided rgb colouring 11 | /// this is only intended for use within glint itself. 12 | /// 13 | fn heading_style(heading: String, colour: Colour) -> String { 14 | heading 15 | |> ansi.bold 16 | |> ansi.underline 17 | |> ansi.italic 18 | |> ansi.hex(colour.to_rgb_hex(colour)) 19 | } 20 | 21 | // --- HELP: CONSTANTS --- 22 | // 23 | pub const help_flag = Flag(Metadata("help", "Print help information"), "") 24 | 25 | const flags_heading = "FLAGS:" 26 | 27 | const subcommands_heading = "SUBCOMMANDS:" 28 | 29 | const usage_heading = "USAGE:" 30 | 31 | // --- HELP: TYPES --- 32 | 33 | pub type ArgsCount { 34 | MinArgs(Int) 35 | EqArgs(Int) 36 | } 37 | 38 | pub type Config { 39 | Config( 40 | name: Option(String), 41 | usage_colour: Option(Colour), 42 | flags_colour: Option(Colour), 43 | subcommands_colour: Option(Colour), 44 | as_module: Bool, 45 | description: Option(String), 46 | indent_width: Int, 47 | max_output_width: Int, 48 | min_first_column_width: Int, 49 | column_gap: Int, 50 | flag_prefix: String, 51 | flag_delimiter: String, 52 | ) 53 | } 54 | 55 | /// Common metadata for commands and flags 56 | /// 57 | pub type Metadata { 58 | Metadata(name: String, description: String) 59 | } 60 | 61 | /// Help type for flag metadata 62 | /// 63 | pub type Flag { 64 | Flag(meta: Metadata, type_: String) 65 | } 66 | 67 | /// Help type for command metadata 68 | pub type Command { 69 | Command( 70 | // Every command has a name and description 71 | meta: Metadata, 72 | // A command can have >= 0 flags associated with it 73 | flags: List(Flag), 74 | // A command can have >= 0 subcommands associated with it 75 | subcommands: List(Metadata), 76 | // A command can have a set number of unnamed arguments 77 | unnamed_args: Option(ArgsCount), 78 | // A command can specify named arguments 79 | named_args: List(String), 80 | ) 81 | } 82 | 83 | // -- HELP - FUNCTIONS - STRINGIFIERS -- 84 | pub fn command_help_to_string(help: Command, config: Config) -> String { 85 | let command = case help.meta.name { 86 | "" -> "" 87 | s -> "Command: " <> s 88 | } 89 | 90 | let command_description = 91 | help.meta.description 92 | |> utils.wordwrap(config.max_output_width) 93 | |> string.join("\n") 94 | 95 | [ 96 | config.description |> option.unwrap(""), 97 | command, 98 | command_description, 99 | command_help_to_usage_string(help, config), 100 | flags_help_to_string(help.flags, config), 101 | subcommands_help_to_string(help.subcommands, config), 102 | ] 103 | |> list.filter(fn(s) { s != "" }) 104 | |> string.join("\n\n") 105 | } 106 | 107 | // -- HELP - FUNCTIONS - STRINGIFIERS - USAGE -- 108 | 109 | /// convert a List(Flag) to a list of strings for use in usage text 110 | /// 111 | fn flags_help_to_usage_strings(help: List(Flag), config: Config) -> List(String) { 112 | help 113 | |> list.map(flag_help_to_string(_, config)) 114 | |> list.sort(string.compare) 115 | } 116 | 117 | /// generate the usage help text for the flags of a command 118 | /// 119 | fn flags_help_to_usage_string(config: Config, help: List(Flag)) -> String { 120 | use <- bool.guard(help == [], "") 121 | let content = 122 | help 123 | |> flags_help_to_usage_strings(config) 124 | |> string.join(" ") 125 | 126 | "[ " <> content <> " ]" 127 | } 128 | 129 | /// convert an ArgsCount to a string for usage text 130 | /// 131 | fn args_count_to_usage_string(count: ArgsCount) -> String { 132 | case count { 133 | EqArgs(0) -> "" 134 | EqArgs(1) -> "[ 1 argument ]" 135 | EqArgs(n) -> "[ " <> int.to_string(n) <> " arguments ]" 136 | MinArgs(n) -> "[ " <> int.to_string(n) <> " or more arguments ]" 137 | } 138 | } 139 | 140 | /// convert a Command to a styled usage block 141 | /// 142 | fn command_help_to_usage_string(help: Command, config: Config) -> String { 143 | let app_name = case config.name { 144 | Some(name) if config.as_module -> "gleam run -m " <> name 145 | Some(name) -> name 146 | None -> "gleam run" 147 | } 148 | 149 | let flags = flags_help_to_usage_string(config, help.flags) 150 | let subcommands = case 151 | list.map(help.subcommands, fn(sc) { sc.name }) 152 | |> list.sort(string.compare) 153 | |> string.join(" | ") 154 | { 155 | "" -> "" 156 | subcommands -> "( " <> subcommands <> " )" 157 | } 158 | 159 | let named_args = 160 | help.named_args 161 | |> list.map(fn(s) { "<" <> s <> ">" }) 162 | |> string.join(" ") 163 | 164 | let unnamed_args = 165 | option.map(help.unnamed_args, args_count_to_usage_string) 166 | |> option.unwrap("[ ARGS ]") 167 | 168 | // The max width of the usage accounts for the constant indent 169 | let max_usage_width = config.max_output_width - config.indent_width 170 | 171 | let content = 172 | [app_name, help.meta.name, subcommands, named_args, unnamed_args, flags] 173 | |> list.filter(fn(s) { s != "" }) 174 | |> string.join(" ") 175 | |> utils.wordwrap(max_usage_width) 176 | |> string.join("\n" <> string.repeat(" ", config.indent_width * 2)) 177 | 178 | case config.usage_colour { 179 | None -> usage_heading 180 | Some(pretty) -> heading_style(usage_heading, pretty) 181 | } 182 | <> "\n" 183 | <> string.repeat(" ", config.indent_width) 184 | <> content 185 | } 186 | 187 | // -- HELP - FUNCTIONS - STRINGIFIERS - FLAGS -- 188 | 189 | /// generate the usage help string for a command 190 | /// 191 | fn flags_help_to_string(help: List(Flag), config: Config) -> String { 192 | use <- bool.guard(help == [], "") 193 | 194 | let longest_flag_length = 195 | help 196 | |> list.map(flag_help_to_string(_, config)) 197 | |> utils.max_string_length 198 | |> int.max(config.min_first_column_width) 199 | 200 | let heading = case config.flags_colour { 201 | None -> flags_heading 202 | Some(pretty) -> heading_style(flags_heading, pretty) 203 | } 204 | 205 | let content = 206 | to_spaced_indented_string( 207 | [help_flag, ..help], 208 | fn(help) { #(flag_help_to_string(help, config), help.meta.description) }, 209 | longest_flag_length, 210 | config, 211 | ) 212 | 213 | heading <> content 214 | } 215 | 216 | /// generate the help text for a flag without a description 217 | /// 218 | fn flag_help_to_string(help: Flag, config: Config) -> String { 219 | config.flag_prefix 220 | <> help.meta.name 221 | <> case help.type_ { 222 | "" -> "" 223 | _ -> config.flag_delimiter <> "<" <> help.type_ <> ">" 224 | } 225 | } 226 | 227 | // -- HELP - FUNCTIONS - STRINGIFIERS - SUBCOMMANDS -- 228 | 229 | /// generate the styled help text for a list of subcommands 230 | /// 231 | fn subcommands_help_to_string(help: List(Metadata), config: Config) -> String { 232 | use <- bool.guard(help == [], "") 233 | 234 | let longest_subcommand_length = 235 | help 236 | |> list.map(fn(h) { h.name }) 237 | |> utils.max_string_length 238 | |> int.max(config.min_first_column_width) 239 | 240 | let heading = case config.subcommands_colour { 241 | None -> subcommands_heading 242 | Some(pretty) -> heading_style(subcommands_heading, pretty) 243 | } 244 | 245 | let content = 246 | to_spaced_indented_string( 247 | help, 248 | fn(help) { #(help.name, help.description) }, 249 | longest_subcommand_length, 250 | config, 251 | ) 252 | 253 | heading <> content 254 | } 255 | 256 | /// convert a list of items to an indented string with spaced contents 257 | /// 258 | fn to_spaced_indented_string( 259 | // items to be stringified and joined 260 | data: List(a), 261 | // function to convert each item to a tuple of (left, right) strings 262 | f: fn(a) -> #(String, String), 263 | // longest length of the first column 264 | left_length: Int, 265 | // how many spaces to indent each line 266 | config: Config, 267 | ) -> String { 268 | let left_length = left_length + config.column_gap 269 | 270 | let #(content, wrapped) = 271 | list.fold(data, #([], False), fn(acc, data) { 272 | let #(left, right) = f(data) 273 | let #(line, wrapped) = format_content(left, right, left_length, config) 274 | #([line, ..acc.0], wrapped || acc.1) 275 | }) 276 | 277 | let joiner = case wrapped { 278 | True -> "\n" 279 | False -> "" 280 | } 281 | 282 | content |> list.sort(string.compare) |> string.join(joiner) 283 | } 284 | 285 | fn format_content( 286 | left: String, 287 | right: String, 288 | left_length: Int, 289 | config: Config, 290 | ) -> #(String, Bool) { 291 | let left_formatted = string.pad_end(left, left_length, " ") 292 | 293 | let lines = 294 | config.max_output_width 295 | |> int.subtract(left_length + config.indent_width) 296 | |> int.max(config.min_first_column_width) 297 | |> utils.wordwrap(right, _) 298 | 299 | let right_formatted = 300 | string.join( 301 | lines, 302 | "\n" <> string.repeat(" ", config.indent_width + left_length), 303 | ) 304 | 305 | let wrapped = case lines { 306 | [] | [_] -> False 307 | _ -> True 308 | } 309 | 310 | #( 311 | "\n" 312 | <> string.repeat(" ", config.indent_width) 313 | <> left_formatted 314 | <> right_formatted, 315 | wrapped, 316 | ) 317 | } 318 | -------------------------------------------------------------------------------- /src/glint/internal/utils.gleam: -------------------------------------------------------------------------------- 1 | import gleam/bool 2 | import gleam/int 3 | import gleam/list 4 | import gleam/string 5 | 6 | /// Returns the length of the longest string in the list. 7 | /// 8 | pub fn max_string_length(strings: List(String)) -> Int { 9 | use max, f <- list.fold(strings, 0) 10 | 11 | f 12 | |> string.length 13 | |> int.max(max) 14 | } 15 | 16 | /// Wraps the given string so that no lines exceed the given width. Newlines in 17 | /// the input string are retained. 18 | /// 19 | pub fn wordwrap(s: String, max_width: Int) -> List(String) { 20 | use <- bool.guard(s == "", []) 21 | use line <- list.flat_map(space_split_lines(s)) 22 | line 23 | |> string.split(" ") 24 | |> do_wordwrap(max_width, "", []) 25 | } 26 | 27 | fn do_wordwrap( 28 | tokens: List(String), 29 | max_width: Int, 30 | line: String, 31 | lines: List(String), 32 | ) -> List(String) { 33 | case tokens { 34 | // Handle the next token 35 | [token, ..tokens] -> { 36 | let token_length = string.length(token) 37 | let line_length = string.length(line) 38 | 39 | case line, line_length + 1 + token_length <= max_width { 40 | // When the current line is empty the next token always goes on it 41 | // regardless of its length 42 | "", _ -> do_wordwrap(tokens, max_width, token, lines) 43 | 44 | // Add the next token to the current line if it fits 45 | _, True -> do_wordwrap(tokens, max_width, line <> " " <> token, lines) 46 | 47 | // Start a new line with the next token as it exceeds the max width if 48 | // added to the current line 49 | _, False -> do_wordwrap(tokens, max_width, token, [line, ..lines]) 50 | } 51 | } 52 | 53 | // There are no more tokens so return the final result, adding the current 54 | // line to it if it's not empty 55 | [] if line == "" -> list.reverse(lines) 56 | [] -> list.reverse([line, ..lines]) 57 | } 58 | } 59 | 60 | /// split a string for consecutive newline groups 61 | /// replaces individual newlines with a spacet 62 | /// groups of newlines > 1 are replaced with n-2 newlines followed by a new item 63 | fn space_split_lines(s: String) -> List(String) { 64 | let chunks = 65 | s 66 | |> string.trim 67 | |> string.to_graphemes 68 | |> list.chunk(fn(s) { s == "\n" }) 69 | 70 | let lines = { 71 | use acc, chunk <- list.fold(chunks, #([], False)) 72 | case chunk, acc.0 { 73 | // convert newline chunks into n-2 newlines 74 | ["\n", "\n", ..rest], [s, ..accs] -> #( 75 | [s <> string.concat(rest), ..accs], 76 | True, 77 | ) 78 | // convert single newlines into spaces 79 | ["\n"], [s, ..accs] -> #([s <> " ", ..accs], False) 80 | // add the next string to the end of the last IFF the last was not a multi newline 81 | _, [s, ..accs] if !acc.1 -> #([s <> string.concat(chunk), ..accs], False) 82 | // add the next string as the next value in the accumulated list 83 | _, _ -> #([string.concat(chunk), ..acc.0], False) 84 | } 85 | } 86 | 87 | list.reverse(lines.0) 88 | } 89 | -------------------------------------------------------------------------------- /test/contraint_test.gleam: -------------------------------------------------------------------------------- 1 | import gleeunit/should 2 | import glint 3 | import glint/constraint 4 | 5 | pub fn one_of_test() { 6 | 1 7 | |> constraint.one_of([1, 2, 3]) 8 | |> should.equal(Ok(1)) 9 | 10 | 1 11 | |> constraint.one_of([2, 3, 4]) 12 | |> should.be_error() 13 | 14 | [1, 2, 3] 15 | |> { 16 | [5, 4, 3, 2, 1] 17 | |> constraint.one_of 18 | |> constraint.each 19 | } 20 | |> should.equal(Ok([1, 2, 3])) 21 | 22 | [1, 6, 3] 23 | |> { 24 | [5, 4, 3, 2, 1] 25 | |> constraint.one_of 26 | |> constraint.each 27 | } 28 | |> should.be_error() 29 | } 30 | 31 | pub fn none_of_test() { 32 | 1 33 | |> constraint.none_of([1, 2, 3]) 34 | |> should.be_error 35 | 36 | 1 37 | |> constraint.none_of([2, 3, 4]) 38 | |> should.equal(Ok(1)) 39 | 40 | [1, 2, 3] 41 | |> { 42 | [4, 5, 6, 7, 8] 43 | |> constraint.none_of 44 | |> constraint.each 45 | } 46 | |> should.equal(Ok([1, 2, 3])) 47 | 48 | [1, 6, 3] 49 | |> { 50 | [4, 5, 6, 7, 8] 51 | |> constraint.none_of 52 | |> constraint.each 53 | } 54 | |> should.be_error() 55 | } 56 | 57 | pub fn flag_one_of_none_of_test() { 58 | let #(test_flag, success, failure) = #( 59 | glint.int_flag("i") 60 | |> glint.flag_constraint(constraint.one_of([1, 2, 3])) 61 | |> glint.flag_constraint(constraint.none_of([4, 5, 6])), 62 | "1", 63 | "6", 64 | ) 65 | 66 | glint.new() 67 | |> glint.add([], { 68 | use access <- glint.flag(test_flag) 69 | use _, _, flags <- glint.command() 70 | flags 71 | |> access 72 | |> should.be_ok 73 | }) 74 | |> glint.execute(["--i=" <> success]) 75 | |> should.be_ok 76 | 77 | glint.new() 78 | |> glint.add([], { 79 | use _access <- glint.flag(test_flag) 80 | use _, _, _flags <- glint.command() 81 | Nil 82 | }) 83 | |> glint.execute(["--i=" <> failure]) 84 | |> should.be_error 85 | 86 | let #(test_flag, success, failure) = #( 87 | glint.ints_flag("li") 88 | |> glint.flag_constraint( 89 | [1, 2, 3] 90 | |> constraint.one_of 91 | |> constraint.each, 92 | ) 93 | |> glint.flag_constraint( 94 | [4, 5, 6] 95 | |> constraint.none_of 96 | |> constraint.each, 97 | ), 98 | "1,1,1", 99 | "2,2,6", 100 | ) 101 | 102 | glint.new() 103 | |> glint.add([], { 104 | use access <- glint.flag(test_flag) 105 | use _, _, flags <- glint.command() 106 | flags 107 | |> access 108 | |> should.be_ok 109 | }) 110 | |> glint.execute(["--li=" <> success]) 111 | |> should.be_ok 112 | 113 | glint.new() 114 | |> glint.add([], { 115 | use _access <- glint.flag(test_flag) 116 | use _, _, _flags <- glint.command() 117 | panic 118 | }) 119 | |> glint.execute(["--li=" <> failure]) 120 | |> should.be_error 121 | 122 | let #(test_flag, success, failure) = #( 123 | glint.float_flag("f") 124 | |> glint.flag_constraint(constraint.one_of([1.0, 2.0, 3.0])) 125 | |> glint.flag_constraint(constraint.none_of([4.0, 5.0, 6.0])), 126 | "1.0", 127 | "6.0", 128 | ) 129 | glint.new() 130 | |> glint.add([], { 131 | use access <- glint.flag(test_flag) 132 | use _, _, flags <- glint.command() 133 | flags 134 | |> access 135 | |> should.be_ok 136 | }) 137 | |> glint.execute(["--f=" <> success]) 138 | |> should.be_ok 139 | 140 | glint.new() 141 | |> glint.add([], { 142 | use _access <- glint.flag(test_flag) 143 | use _, _, _flags <- glint.command() 144 | panic 145 | }) 146 | |> glint.execute(["--f=" <> failure]) 147 | |> should.be_error 148 | 149 | let #(test_flag, success, failure) = #( 150 | glint.floats_flag("lf") 151 | |> glint.flag_constraint( 152 | [1.0, 2.0, 3.0] 153 | |> constraint.one_of() 154 | |> constraint.each, 155 | ) 156 | |> glint.flag_constraint( 157 | [4.0, 5.0, 6.0] 158 | |> constraint.none_of() 159 | |> constraint.each, 160 | ), 161 | "3.0,2.0,1.0", 162 | "2.0,3.0,6.0", 163 | ) 164 | glint.new() 165 | |> glint.add([], { 166 | use access <- glint.flag(test_flag) 167 | use _, _, flags <- glint.command() 168 | flags 169 | |> access 170 | |> should.be_ok 171 | }) 172 | |> glint.execute(["--lf=" <> success]) 173 | |> should.be_ok 174 | 175 | glint.new() 176 | |> glint.add([], { 177 | use _access <- glint.flag(test_flag) 178 | use _, _, _flags <- glint.command() 179 | panic 180 | }) 181 | |> glint.execute(["--lf=" <> failure]) 182 | |> should.be_error 183 | 184 | let #(test_flag, success, failure) = #( 185 | glint.string_flag("s") 186 | |> glint.flag_constraint(constraint.one_of(["t1", "t2", "t3"])) 187 | |> glint.flag_constraint(constraint.none_of(["t4", "t5", "t6"])), 188 | "t3", 189 | "t4", 190 | ) 191 | 192 | glint.new() 193 | |> glint.add([], { 194 | use access <- glint.flag(test_flag) 195 | use _, _, flags <- glint.command() 196 | flags 197 | |> access 198 | |> should.be_ok 199 | }) 200 | |> glint.execute(["--s=" <> success]) 201 | |> should.be_ok 202 | 203 | glint.new() 204 | |> glint.add([], { 205 | use _access <- glint.flag(test_flag) 206 | use _, _, _flags <- glint.command() 207 | panic 208 | }) 209 | |> glint.execute(["--s=" <> failure]) 210 | |> should.be_error 211 | 212 | let #(test_flag, success, failure) = #( 213 | glint.strings_flag("ls") 214 | |> glint.flag_constraint( 215 | ["t1", "t2", "t3"] 216 | |> constraint.one_of 217 | |> constraint.each, 218 | ) 219 | |> glint.flag_constraint( 220 | ["t4", "t5", "t6"] 221 | |> constraint.none_of 222 | |> constraint.each, 223 | ), 224 | "t3,t2,t1", 225 | "t2,t4,t1", 226 | ) 227 | 228 | glint.new() 229 | |> glint.add([], { 230 | use access <- glint.flag(test_flag) 231 | use _, _, flags <- glint.command() 232 | flags 233 | |> access 234 | |> should.be_ok 235 | }) 236 | |> glint.execute(["--ls=" <> success]) 237 | |> should.be_ok 238 | 239 | glint.new() 240 | |> glint.add([], { 241 | use _access <- glint.flag(test_flag) 242 | use _, _, _flags <- glint.command() 243 | panic 244 | }) 245 | |> glint.execute(["--ls=" <> failure]) 246 | |> should.be_error 247 | } 248 | -------------------------------------------------------------------------------- /test/examples/hello.gleam: -------------------------------------------------------------------------------- 1 | //// This module demonstrates a simple glint app with 2 commands 2 | //// 3 | //// ## Usage 4 | //// 5 | //// ### Running the application 6 | //// 7 | //// You can run this example with `gleam run -m examples/hello -- ` from the root of the repo 8 | //// 9 | //// The application prints: `Hello, !` 10 | //// The `hello` application accepts at least one argument, being the names of people to say hello to. 11 | //// - No input: `gleam run` -> prints "Hello, Joe!" 12 | //// - One input: `gleam run Joe` -> prints "Hello, Joe!" 13 | //// - Two inputs: `gleam run Rob Louis` -> prints "Hello, Rob and Louis!" 14 | //// - \>2 inputs: `gleam run Rob Louis Hayleigh` -> prints "Hello, Rob, Louis and Hayleigh!" 15 | //// 16 | //// ### Flags 17 | //// 18 | //// All commands accepts two flags: 19 | //// - `--caps`: capitalizes the output, so if output would be "Hello, Joe!" it prints "HELLO, JOE!" 20 | //// - `--repeat=N`: repeats the output N times separated , so with N=2 if output would be "Hello, Joe!" it prints "Hello, Joe!\nHello, Joe!" 21 | //// 22 | //// ### Help Text 23 | //// 24 | //// Here is the help text for the root command: 25 | //// 26 | //// ```txt 27 | //// It's time to say hello! 28 | //// 29 | //// Prints Hello, ! 30 | //// 31 | //// USAGE: 32 | //// gleam run -m examples/hello ( single ) [ 1 or more arguments ] [ 33 | //// --caps= --repeat= ] 34 | //// 35 | //// FLAGS: 36 | //// --caps= Capitalize the hello message 37 | //// --help Print help information 38 | //// --repeat= Repeat the message n-times 39 | //// 40 | //// SUBCOMMANDS: 41 | //// single Prints Hello, ! 42 | //// ``` 43 | //// 44 | //// Here is the help text for the `single` command: 45 | //// 46 | //// ``` 47 | //// It's time to say hello! 48 | //// 49 | //// Command: single 50 | //// 51 | //// Prints Hello, ! 52 | //// 53 | //// USAGE: 54 | //// gleam run -m examples/hello single [ --caps= --repeat= ] 55 | //// 56 | //// FLAGS: 57 | //// --caps= Capitalize the hello message 58 | //// --help Print help information 59 | //// --repeat= Repeat the message n-times 60 | //// ``` 61 | 62 | // stdlib imports 63 | import gleam/io 64 | import gleam/list 65 | import gleam/string.{uppercase} 66 | 67 | // external dep imports 68 | import snag 69 | 70 | // glint imports 71 | import argv 72 | import glint 73 | 74 | // ----- APPLICATION LOGIC ----- 75 | 76 | /// a helper function to join a list of names 77 | fn join_names(names: List(String)) -> String { 78 | case names { 79 | [] -> "" 80 | _ -> do_join_names(names, "") 81 | } 82 | } 83 | 84 | // tail-recursive implementation of join_naemes 85 | fn do_join_names(names: List(String), acc: String) { 86 | case names { 87 | [] -> acc 88 | [a] -> acc <> " and " <> a 89 | [a, ..b] -> do_join_names(b, acc <> ", " <> a) 90 | } 91 | } 92 | 93 | pub fn capitalize(msg, caps) -> String { 94 | case caps { 95 | True -> uppercase(msg) 96 | False -> msg 97 | } 98 | } 99 | 100 | /// hello is a function that says hello 101 | pub fn hello( 102 | primary: String, 103 | rest: List(String), 104 | caps: Bool, 105 | repeat: Int, 106 | ) -> String { 107 | { "Hello, " <> primary <> join_names(rest) <> "!" } 108 | |> capitalize(caps) 109 | |> list.repeat(repeat) 110 | |> string.join("\n") 111 | } 112 | 113 | // ----- CLI SETUP ----- 114 | 115 | /// a boolean flag with default False to control message capitalization. 116 | /// 117 | pub fn caps_flag() -> glint.Flag(Bool) { 118 | glint.bool_flag("caps") 119 | |> glint.flag_default(False) 120 | |> glint.flag_help("Capitalize the hello message") 121 | } 122 | 123 | /// an int flag with default 1 to control how many times to repeat the message. 124 | /// this flag is constrained to values greater than 0. 125 | /// 126 | pub fn repeat_flag() -> glint.Flag(Int) { 127 | use n <- glint.flag_constraint( 128 | glint.int_flag("repeat") 129 | |> glint.flag_default(1) 130 | |> glint.flag_help("Repeat the message n-times"), 131 | ) 132 | case n { 133 | _ if n > 0 -> Ok(n) 134 | _ -> snag.error("Value must be greater than 0.") 135 | } 136 | } 137 | 138 | /// the command function that will be executed as the root command 139 | /// 140 | pub fn hello_cmd() -> glint.Command(String) { 141 | use <- glint.command_help("Prints Hello, !") 142 | use <- glint.unnamed_args(glint.MinArgs(1)) 143 | use _, args, flags <- glint.command() 144 | let assert Ok(caps) = glint.get_flag(flags, caps_flag()) 145 | let assert Ok(repeat) = glint.get_flag(flags, repeat_flag()) 146 | let assert [name, ..rest] = args 147 | hello(name, rest, caps, repeat) 148 | } 149 | 150 | /// the command function that will be executed as the "single" command 151 | /// 152 | pub fn hello_single_cmd() -> glint.Command(String) { 153 | use <- glint.command_help("Prints Hello, !") 154 | use <- glint.unnamed_args(glint.EqArgs(0)) 155 | use name <- glint.named_arg("name") 156 | use named_args, _, flags <- glint.command() 157 | let assert Ok(caps) = glint.get_flag(flags, caps_flag()) 158 | let assert Ok(repeat) = glint.get_flag(flags, repeat_flag()) 159 | let name = name(named_args) 160 | hello(name, [], caps, repeat) 161 | } 162 | 163 | // the function that describes our cli structure 164 | pub fn app() { 165 | // create a new glint instance 166 | glint.new() 167 | // with an app name of "hello", this is used when printing help text 168 | |> glint.with_name("examples/hello") 169 | // apply global help text to all commands 170 | |> glint.global_help("It's time to say hello!") 171 | // show in usage text that the current app is run as a gleam module 172 | |> glint.as_module 173 | // with pretty help enabled, using the built-in colours 174 | |> glint.pretty_help(glint.default_pretty_help()) 175 | // with group level flags 176 | // with flag `caps` for all commands (equivalent of using glint.global_flag) 177 | |> glint.group_flag([], caps_flag()) 178 | // // with flag `repeat` for all commands (equivalent of using glint.global_flag) 179 | |> glint.group_flag([], repeat_flag()) 180 | // with a root command that executes the `hello` function 181 | |> glint.add( 182 | // add the hello command to the root 183 | at: [], 184 | do: hello_cmd(), 185 | ) 186 | |> glint.add( 187 | // add the hello single command 188 | at: ["single"], 189 | do: hello_single_cmd(), 190 | ) 191 | } 192 | 193 | pub fn main() { 194 | // run with a handler that prints the command output 195 | glint.run_and_handle(app(), argv.load().arguments, io.println) 196 | } 197 | -------------------------------------------------------------------------------- /test/examples/hello_test.gleam: -------------------------------------------------------------------------------- 1 | import examples/hello 2 | import gleam/list 3 | import gleeunit/should 4 | import glint 5 | 6 | type TestCase { 7 | TestCase(input: List(String), caps: Bool, repeat: Int, expected: String) 8 | } 9 | 10 | pub fn hello_test() { 11 | use tc <- list.each([ 12 | TestCase(["Rob"], False, 1, "Hello, Rob!"), 13 | TestCase(["Rob"], True, 1, "HELLO, ROB!"), 14 | TestCase(["Tony", "Maria"], True, 1, "HELLO, TONY AND MARIA!"), 15 | TestCase( 16 | ["Tony", "Maria", "Nadia"], 17 | True, 18 | 1, 19 | "HELLO, TONY, MARIA AND NADIA!", 20 | ), 21 | TestCase(["Tony", "Maria"], False, 1, "Hello, Tony and Maria!"), 22 | TestCase( 23 | ["Tony", "Maria", "Nadia"], 24 | False, 25 | 1, 26 | "Hello, Tony, Maria and Nadia!", 27 | ), 28 | ]) 29 | 30 | let assert [head, ..rest] = tc.input 31 | hello.hello(head, rest, tc.caps, tc.repeat) 32 | |> should.equal(tc.expected) 33 | } 34 | 35 | pub fn app_test() { 36 | use output <- glint.run_and_handle(hello.app(), [ 37 | "Joe", "Gleamlins", "--repeat=2", "--caps", 38 | ]) 39 | should.equal(output, "HELLO, JOE AND GLEAMLINS!\nHELLO, JOE AND GLEAMLINS!") 40 | } 41 | -------------------------------------------------------------------------------- /test/flag_test.gleam: -------------------------------------------------------------------------------- 1 | import gleeunit/should 2 | import glint 3 | 4 | pub fn update_flag_test() { 5 | let app = 6 | glint.new() 7 | |> glint.add([], { 8 | use _bflag <- glint.flag(glint.bool_flag("bflag")) 9 | use _sflag <- glint.flag(glint.string_flag("sflag")) 10 | use _lsflag <- glint.flag(glint.strings_flag("lsflag")) 11 | use _iflag <- glint.flag(glint.ints_flag("iflag")) 12 | use _liflag <- glint.flag(glint.ints_flag("liflag")) 13 | use _fflag <- glint.flag(glint.float_flag("fflag")) 14 | use _lfflag <- glint.flag(glint.floats_flag("lfflag")) 15 | glint.command(fn(_, _, _) { Nil }) 16 | }) 17 | 18 | // update non-existent flag fails 19 | app 20 | |> glint.execute(["--not_a_flag=hello"]) 21 | |> should.be_error() 22 | 23 | // update bool flag succeeds 24 | app 25 | |> glint.execute(["--bflag=true"]) 26 | |> should.be_ok() 27 | 28 | // update bool flag with non-bool value fails 29 | app 30 | |> glint.execute(["--bflag=zzz"]) 31 | |> should.be_error() 32 | 33 | // toggle bool flag succeeds 34 | app 35 | |> glint.execute(["--bflag"]) 36 | |> should.be_ok() 37 | 38 | // toggle non-bool flag succeeds 39 | app 40 | |> glint.execute(["--sflag"]) 41 | |> should.be_error() 42 | 43 | // update string flag succeeds 44 | app 45 | |> glint.execute(["--sflag=hello"]) 46 | |> should.be_ok() 47 | 48 | // update int flag with non-int fails 49 | app 50 | |> glint.execute(["--iflag=hello"]) 51 | |> should.be_error() 52 | 53 | // update int flag with int succeeds 54 | app 55 | |> glint.execute(["--iflag=1"]) 56 | |> should.be_ok() 57 | 58 | // update int list flag with int list succeeds 59 | app 60 | |> glint.execute(["--liflag=1,2,3"]) 61 | |> should.be_ok() 62 | 63 | // update int list flag with non int list succeeds 64 | app 65 | |> glint.execute(["--liflag=a,b,c"]) 66 | |> should.be_error() 67 | 68 | // update float flag with non-int fails 69 | app 70 | |> glint.execute(["--fflag=hello"]) 71 | |> should.be_error() 72 | 73 | // update float flag with int succeeds 74 | app 75 | |> glint.execute(["--fflag=1.0"]) 76 | |> should.be_ok() 77 | 78 | // update float list flag with int list succeeds 79 | app 80 | |> glint.execute(["--lfflag=1.0,2.0,3.0"]) 81 | |> should.be_ok() 82 | 83 | // update float list flag with non int list succeeds 84 | app 85 | |> glint.execute(["--lfflag=a,b,c"]) 86 | |> should.be_error() 87 | } 88 | 89 | pub fn unsupported_flag_test() { 90 | glint.new() 91 | |> glint.add(["cmd"], glint.command(fn(_, _, _) { Nil })) 92 | |> glint.execute(["--flag=1"]) 93 | |> should.be_error() 94 | } 95 | 96 | pub fn flag_default_test() { 97 | let args = ["arg1", "arg2"] 98 | let flag = 99 | glint.string_flag("flag") 100 | |> glint.flag_default("default") 101 | 102 | glint.new() 103 | |> glint.add(["cmd"], { 104 | use flag_ <- glint.flag(flag) 105 | use _, unnamed, flags <- glint.command() 106 | should.equal(args, unnamed) 107 | 108 | flag_(flags) 109 | |> should.equal(Ok("default")) 110 | }) 111 | |> glint.execute(["cmd", ..args]) 112 | |> should.be_ok() 113 | } 114 | 115 | pub fn flag_value_test() { 116 | let args = ["arg1", "arg2"] 117 | let flag = glint.string_flag("flag") 118 | let flag_input = "--flag=flag_value" 119 | let flag_value_should_be_set = { 120 | use flag_ <- glint.flag(flag) 121 | use _, in_args, flags <- glint.command() 122 | should.equal(in_args, args) 123 | 124 | flag_(flags) 125 | |> should.equal(Ok("flag_value")) 126 | } 127 | 128 | glint.new() 129 | |> glint.add(["cmd"], flag_value_should_be_set) 130 | |> glint.execute(["cmd", flag_input, ..args]) 131 | |> should.be_ok() 132 | } 133 | 134 | pub fn int_flag_test() { 135 | let flags = glint.int_flag("flag") 136 | // fails to parse input for flag as int, returns error 137 | let flag_input = "--flag=X" 138 | glint.new() 139 | |> glint.add( 140 | [], 141 | glint.flag(flags, fn(_flag) { glint.command(fn(_, _, _) { Nil }) }), 142 | ) 143 | |> glint.execute([flag_input]) 144 | |> should.be_error() 145 | 146 | // parses flag input as int, sets value 147 | let flag_input = "--flag=10" 148 | let expect_flag_value_of_10 = { 149 | use flag_ <- glint.flag(flags) 150 | use _, _, flags <- glint.command() 151 | flag_(flags) 152 | |> should.equal(Ok(10)) 153 | } 154 | 155 | glint.new() 156 | |> glint.add([], expect_flag_value_of_10) 157 | |> glint.execute([flag_input]) 158 | |> should.be_ok() 159 | } 160 | 161 | pub fn bool_flag_test() { 162 | let flag = glint.bool_flag("flag") 163 | 164 | // fails to parse input for flag as bool, returns error 165 | let flag_input = "--flag=X" 166 | glint.new() 167 | |> glint.add( 168 | [], 169 | glint.flag(flag, fn(_flag) { glint.command(fn(_, _, _) { Nil }) }), 170 | ) 171 | |> glint.execute([flag_input]) 172 | |> should.be_error() 173 | 174 | // parses flag input as bool, sets value 175 | let flag_input = "--flag=false" 176 | let expect_flag_value_of_false = fn(flag) { 177 | glint.command(fn(_, _, flags) { 178 | flag(flags) 179 | |> should.equal(Ok(False)) 180 | }) 181 | } 182 | 183 | glint.new() 184 | |> glint.add([], glint.flag(flag, expect_flag_value_of_false)) 185 | |> glint.execute([flag_input]) 186 | |> should.be_ok() 187 | } 188 | 189 | pub fn strings_flag_test() { 190 | let flags = glint.strings_flag("flag") 191 | let flag_input = "--flag=val3,val4" 192 | let expect_flag_value_list = fn(flag) { 193 | glint.command(fn(_, _, flags) { 194 | flag(flags) 195 | |> should.equal(Ok(["val3", "val4"])) 196 | }) 197 | } 198 | 199 | glint.new() 200 | |> glint.add([], glint.flag(flags, expect_flag_value_list)) 201 | |> glint.execute([flag_input]) 202 | |> should.be_ok() 203 | } 204 | 205 | pub fn ints_flag_test() { 206 | let flag = glint.ints_flag("flag") 207 | 208 | // fails to parse input for flag as int list, returns error 209 | let flag_input = "--flag=val3,val4" 210 | glint.new() 211 | |> glint.add( 212 | [], 213 | glint.flag(flag, fn(_) { glint.command(fn(_, _, _) { Nil }) }), 214 | ) 215 | |> glint.execute([flag_input]) 216 | |> should.be_error() 217 | 218 | // parses flag input as int list, sets value 219 | let flag_input = "--flag=3,4" 220 | let expect_flag_value_list = fn(flag) { 221 | glint.command(fn(_, _, flags) { 222 | flag(flags) 223 | |> should.equal(Ok([3, 4])) 224 | }) 225 | } 226 | 227 | glint.new() 228 | |> glint.add([], glint.flag(flag, expect_flag_value_list)) 229 | |> glint.execute([flag_input]) 230 | |> should.be_ok() 231 | } 232 | 233 | pub fn float_flag_test() { 234 | let flag = glint.float_flag("flag") 235 | 236 | // fails to parse input for flag as float, returns error 237 | let flag_input = "--flag=X" 238 | glint.new() 239 | |> glint.add([], { 240 | use _flag <- glint.flag(flag) 241 | use _, _, _ <- glint.command() 242 | Nil 243 | }) 244 | |> glint.execute([flag_input]) 245 | |> should.be_error() 246 | 247 | // parses flag input as float, sets value 248 | let flag_input = "--flag=10.0" 249 | let expect_flag_value_of_10 = { 250 | use flag <- glint.flag(flag) 251 | use _, _, flags <- glint.command() 252 | flag(flags) 253 | |> should.equal(Ok(10.0)) 254 | } 255 | 256 | glint.new() 257 | |> glint.add([], expect_flag_value_of_10) 258 | |> glint.execute([flag_input]) 259 | |> should.be_ok() 260 | } 261 | 262 | pub fn floats_flag_test() { 263 | let flag = glint.floats_flag("flag") 264 | 265 | // fails to parse input for flag as float list, returns error 266 | let flag_input = "--flag=val3,val4" 267 | glint.new() 268 | |> glint.add([], { 269 | use _flag <- glint.flag(flag) 270 | use _, _, _ <- glint.command() 271 | Nil 272 | }) 273 | |> glint.execute([flag_input]) 274 | |> should.be_error() 275 | 276 | // parses flag input as float list, sets value 277 | let flag_input = "--flag=3.0,4.0" 278 | let expect_flag_value_list = { 279 | use flag <- glint.flag(flag) 280 | use _, _, flags <- glint.command 281 | flag(flags) 282 | |> should.equal(Ok([3.0, 4.0])) 283 | } 284 | glint.new() 285 | |> glint.add([], expect_flag_value_list) 286 | |> glint.execute([flag_input]) 287 | |> should.be_ok() 288 | } 289 | 290 | pub fn global_flag_test() { 291 | let flag = glint.floats_flag("flag") 292 | let testcase = fn(vals: List(Float)) { 293 | use _, _, flags <- glint.command() 294 | flags 295 | |> glint.get_flag(flag) 296 | |> should.equal(Ok(vals)) 297 | } 298 | 299 | // set global flag, pass in new value for flag 300 | glint.new() 301 | |> glint.group_flag([], flag) 302 | |> glint.add(at: [], do: testcase([3.0, 4.0])) 303 | |> glint.execute(["--flag=3.0,4.0"]) 304 | |> should.be_ok() 305 | 306 | // set global flag and local flag, local flag should take priority 307 | glint.new() 308 | |> glint.group_flag([], glint.floats_flag("flag")) 309 | |> glint.add( 310 | at: [], 311 | do: glint.flag( 312 | glint.floats_flag("flag") 313 | |> glint.flag_default([1.0, 2.0]), 314 | fn(_) { testcase([1.0, 2.0]) }, 315 | ), 316 | ) 317 | |> glint.execute([]) 318 | |> should.be_ok() 319 | 320 | // set global flag and local flag, pass in new value for flag 321 | glint.new() 322 | |> glint.group_flag( 323 | [], 324 | glint.floats_flag("flag") 325 | |> glint.flag_default([3.0, 4.0]), 326 | ) 327 | |> glint.add(at: [], do: { 328 | use _flag <- glint.flag( 329 | glint.floats_flag("flag") 330 | |> glint.flag_default([1.0, 2.0]), 331 | ) 332 | 333 | testcase([5.0, 6.0]) 334 | }) 335 | |> glint.execute(["--flag=5.0,6.0"]) 336 | |> should.be_ok() 337 | } 338 | 339 | pub fn toggle_test() { 340 | // fails to parse input for flag as bool, returns error 341 | let flag_input = "--flag=X" 342 | glint.new() 343 | |> glint.add( 344 | [], 345 | glint.flag(glint.bool_flag("flag"), fn(_) { 346 | glint.command(fn(_, _, _) { Nil }) 347 | }), 348 | ) 349 | |> glint.execute([flag_input]) 350 | |> should.be_error() 351 | 352 | // boolean flag is toggled, sets value to True 353 | let flag_input = "--flag" 354 | 355 | glint.new() 356 | |> glint.add([], { 357 | use flag <- glint.flag(glint.bool_flag("flag")) 358 | use _, _, flags <- glint.command() 359 | flag(flags) 360 | |> should.equal(Ok(True)) 361 | }) 362 | |> glint.execute([flag_input]) 363 | |> should.be_ok() 364 | 365 | // boolean flag with default of True is toggled, sets value to False 366 | let flag_input = "--flag" 367 | 368 | glint.new() 369 | |> glint.add([], { 370 | use flag <- glint.flag( 371 | glint.bool_flag("flag") 372 | |> glint.flag_default(True), 373 | ) 374 | use _, _, flags <- glint.command() 375 | flag(flags) 376 | |> should.equal(Ok(False)) 377 | }) 378 | |> glint.execute([flag_input]) 379 | |> should.be_ok() 380 | 381 | // boolean flag without default toggled, sets value to True 382 | glint.new() 383 | |> glint.add([], { 384 | use flag <- glint.flag(glint.bool_flag("flag")) 385 | use _, _, flags <- glint.command() 386 | flag(flags) 387 | |> should.equal(Ok(True)) 388 | }) 389 | |> glint.execute([flag_input]) 390 | |> should.be_ok() 391 | 392 | // cannot toggle non-bool flag 393 | glint.new() 394 | |> glint.add([], { 395 | use _flag <- glint.flag( 396 | glint.int_flag("flag") 397 | |> glint.flag_default(1), 398 | ) 399 | use _, _, _ <- glint.command() 400 | Nil 401 | }) 402 | |> glint.execute([flag_input]) 403 | |> should.be_error() 404 | } 405 | 406 | pub fn getters_test() { 407 | glint.new() 408 | |> glint.add([], { 409 | use bflag <- glint.flag( 410 | glint.bool_flag("bflag") 411 | |> glint.flag_default(True), 412 | ) 413 | use sflag <- glint.flag( 414 | glint.string_flag("sflag") 415 | |> glint.flag_default(""), 416 | ) 417 | use lsflag <- glint.flag( 418 | glint.strings_flag("lsflag") 419 | |> glint.flag_default([]), 420 | ) 421 | use iflag <- glint.flag( 422 | glint.int_flag("iflag") 423 | |> glint.flag_default(1), 424 | ) 425 | use liflag <- glint.flag( 426 | glint.ints_flag("liflag") 427 | |> glint.flag_default([]), 428 | ) 429 | use fflag <- glint.flag( 430 | glint.float_flag("fflag") 431 | |> glint.flag_default(1.0), 432 | ) 433 | use lfflag <- glint.flag( 434 | glint.floats_flag("lfflag") 435 | |> glint.flag_default([]), 436 | ) 437 | 438 | use _, _, flags <- glint.command() 439 | bflag(flags) 440 | |> should.equal(Ok(True)) 441 | 442 | sflag(flags) 443 | |> should.equal(Ok("")) 444 | 445 | lsflag(flags) 446 | |> should.equal(Ok([])) 447 | 448 | iflag(flags) 449 | |> should.equal(Ok(1)) 450 | 451 | liflag(flags) 452 | |> should.equal(Ok([])) 453 | 454 | fflag(flags) 455 | |> should.equal(Ok(1.0)) 456 | 457 | lfflag(flags) 458 | |> should.equal(Ok([])) 459 | }) 460 | |> glint.execute([]) 461 | } 462 | -------------------------------------------------------------------------------- /test/glint/internal/utils_test.gleam: -------------------------------------------------------------------------------- 1 | import birdie 2 | import gleam/string 3 | import gleeunit/should 4 | import glint/internal/utils 5 | 6 | pub fn wordwrap_test() { 7 | "a b c" 8 | |> utils.wordwrap(3) 9 | |> should.equal(["a b", "c"]) 10 | 11 | "a\nb\n\nc\n\n\nd\n\n\n\ne" 12 | |> utils.wordwrap(1) 13 | |> should.equal(["a", "b", "c\n", "d\n\n", "e"]) 14 | } 15 | 16 | pub fn big_wordwrap_test() { 17 | "Universal Declaration of Human Rights 18 | 19 | 20 | Article I 21 | 22 | 23 | All human beings are born free and equal in dignity and rights. They are 24 | endowed with reason and conscience and should act towards one another in a 25 | spirit of brotherhood. 26 | 27 | 28 | 29 | Article 2 30 | 31 | 32 | Everyone is entitled to all the rights and freedoms set forth in this Declaration, 33 | without distinction of any kind, such as race, colour, sex, language, religion, 34 | political or other opinion, national or social origin, property, birth or other status. 35 | Furthermore, no distinction shall be made on the basis of the political, 36 | jurisdictional or international status of the country or territory to which a person 37 | belongs, whether it be independent, trust, non-self-governing or under any other 38 | limitation of sovereignty. 39 | 40 | 41 | 42 | Article 3 43 | 44 | 45 | Everyone has the right to life, liberty and the security of person. 46 | 47 | 48 | 49 | Article 4 50 | 51 | 52 | No one shall be held in slavery or servitude; slavery and the slave trade shall be 53 | prohibited in all their forms. 54 | 55 | 56 | 57 | Article 5 58 | 59 | 60 | No one shall be subjected to torture or to cruel, inhuman or degrading treatment 61 | or punishment. 62 | 63 | 64 | 65 | Article 6 66 | 67 | 68 | Everyone has the right to recognition everywhere as a person before the law. 69 | 70 | 71 | 72 | Article 7 73 | 74 | 75 | All are equal before the law and are entitled without any discrimination to equal 76 | protection of the law. All are entitled to equal protection against any 77 | discrimination in violation of this Declaration and against any incitement to such 78 | discrimination. 79 | 80 | 81 | 82 | Article 8 83 | 84 | 85 | Everyone has the right to an effective remedy by the competent national tribunals 86 | for acts violating the fundamental rights granted him by the constitution or by law. 87 | 88 | 89 | 90 | Article 9 91 | 92 | 93 | No one shall be subjected to arbitrary arrest, detention or exile. 94 | 95 | 96 | 97 | Article 10 98 | 99 | 100 | Everyone is entitled in full equality to a fair and public hearing by an independent 101 | and impartial tribunal, in the determination of his rights and obligations and of any 102 | criminal charge against him. 103 | 104 | 105 | 106 | Article 11 107 | 108 | 109 | 1. Everyone charged with a penal offence has the right to be presumed 110 | innocent until proved guilty according to law in a public trial at which he 111 | has had all the guarantees necessary for his defence. 112 | 113 | 2. No one shall be held guilty of any penal offence on account of any act or 114 | omission which did not constitute a penal offence, under national or 115 | international law, at the time when it was committed. Nor shall a heavier 116 | penalty be imposed than the one that was applicable at the time the penal 117 | offence was committed. 118 | 119 | 120 | 121 | Article 12 122 | 123 | 124 | No one shall be subjected to arbitrary interference with his privacy, family, home 125 | or correspondence, nor to attacks upon his honour and reputation. Everyone has 126 | the right to the protection of the law against such interference or attacks. 127 | 128 | 129 | 130 | Article 13 131 | 132 | 133 | 1. Everyone has the right to freedom of movement and residence within the 134 | borders of each State. 135 | 136 | 2. Everyone has the right to leave any country, including his own, and to 137 | return to his country. 138 | 139 | 140 | 141 | Article 14 142 | 143 | 144 | 1. Everyone has the right to seek and to enjoy in other countries asylum from 145 | persecution. 146 | 147 | 2. This right may not be invoked in the case of prosecutions genuinely 148 | arising from non-political crimes or from acts contrary to the purposes and 149 | principles of the United Nations. 150 | 151 | 152 | 153 | Article 15 154 | 155 | 156 | 1. Everyone has the right to a nationality. 157 | 158 | 2. No one shall be arbitrarily deprived of his nationality nor denied the right to 159 | change his nationality. 160 | 161 | 162 | 163 | Article 16 164 | 165 | 166 | 1. Men and women of full age, without any limitation due to race, nationality 167 | or religion, have the right to marry and to found a family. They are entitled 168 | to equal rights as to marriage, during marriage and at its dissolution. 169 | 170 | 2. Marriage shall be entered into only with the free and full consent of the 171 | intending spouses. 172 | 173 | 3. The family is the natural and fundamental group unit of society and is 174 | entitled to protection by society and the State. 175 | 176 | 177 | Article 17 178 | 179 | 180 | 1. Everyone has the right to own property alone as well as in association with 181 | others. 182 | 183 | 2. No one shall be arbitrarily deprived of his property. 184 | 185 | 186 | 187 | Article 18 188 | 189 | 190 | Everyone has the right to freedom of thought, conscience and religion; this right 191 | includes freedom to change his religion or belief, and freedom, either alone or in 192 | community with others and in public or private, to manifest his religion or belief in 193 | teaching, practice, worship and observance. 194 | 195 | 196 | 197 | Article 19 198 | 199 | 200 | Everyone has the right to freedom of opinion and expression; this right includes 201 | freedom to hold opinions without interference and to seek, receive and impart 202 | information and ideas through any media and regardless of frontiers. 203 | 204 | 205 | 206 | Article 20 207 | 208 | 209 | 1. Everyone has the right to freedom of peaceful assembly and association. 210 | 211 | 2. No one may be compelled to belong to an association. 212 | 213 | 214 | 215 | Article 21 216 | 217 | 218 | 1. Everyone has the right to take part in the government of his country, 219 | directly or through freely chosen representatives. 220 | 221 | 2. Everyone has the right to equal access to public service in his country. 222 | 223 | 3. The will of the people shall be the basis of the authority of government; 224 | this will shall be expressed in periodic and genuine elections which shall 225 | be by universal and equal suffrage and shall be held by secret vote or by 226 | equivalent free voting procedures. 227 | 228 | 229 | Article 22 230 | 231 | 232 | Everyone, as a member of society, has the right to social security and is entitled 233 | to realization, through national effort and international co-operation and in 234 | accordance with the organization and resources of each State, of the economic, 235 | social and cultural rights indispensable for his dignity and the free development 236 | of his personality. 237 | 238 | 239 | 240 | Article 23 241 | 242 | 243 | 1. Everyone has the right to work, to free choice of employment, to just and 244 | favourable conditions of work and to protection against unemployment. 245 | 246 | 2. Everyone, without any discrimination, has the right to equal pay for equal 247 | work. 248 | 249 | 3. Everyone who works has the right to just and favourable remuneration 250 | ensuring for himself and his family an existence worthy of human dignity, 251 | and supplemented, if necessary, by other means of social protection. 252 | 253 | 4. Everyone has the right to form and to join trade unions for the protection of 254 | his interests. 255 | 256 | 257 | 258 | Article 24 259 | 260 | 261 | Everyone has the right to rest and leisure, including reasonable limitation of 262 | working hours and periodic holidays with pay. 263 | 264 | 265 | 266 | Article 25 267 | 268 | 269 | 1. Everyone has the right to a standard of living adequate for the health and 270 | well-being of himself and of his family, including food, clothing, housing 271 | and medical care and necessary social services, and the right to security 272 | in the event of unemployment, sickness, disability, widowhood, old age or 273 | other lack of livelihood in circumstances beyond his control. 274 | 275 | 2. Motherhood and childhood are entitled to special care and assistance. All 276 | children, whether born in or out of wedlock, shall enjoy the same social 277 | protection. 278 | 279 | 280 | 281 | Article 26 282 | 283 | 284 | 1. Everyone has the right to education. Education shall be free, at least in the 285 | elementary and fundamental stages. Elementary education shall be 286 | compulsory. Technical and professional education shall be made 287 | generally available and higher education shall be equally accessible to all 288 | on the basis of merit. 289 | 290 | 2. Education shall be directed to the full development of the human 291 | personality and to the strengthening of respect for human rights and 292 | fundamental freedoms. It shall promote understanding, tolerance and 293 | friendship among all nations, racial or religious groups, and shall further 294 | the activities of the United Nations for the maintenance of peace. 295 | 296 | 3. Parents have a prior right to choose the kind of education that shall be 297 | given to their children. 298 | 299 | 300 | 301 | Article 27 302 | 303 | 304 | 1. Everyone has the right freely to participate in the cultural life of the 305 | community, to enjoy the arts and to share in scientific advancement and 306 | its benefits. 307 | 308 | 2. Everyone has the right to the protection of the moral and material interests 309 | resulting from any scientific, literary or artistic production of which he is the 310 | author. 311 | 312 | 313 | 314 | Article 28 315 | 316 | 317 | Everyone is entitled to a social and international order in which the rights and 318 | freedoms set forth in this Declaration can be fully realized. 319 | 320 | 321 | 322 | Article 29 323 | 324 | 325 | 1. Everyone has duties to the community in which alone the free and full 326 | development of his personality is possible. 327 | 328 | 2. In the exercise of his rights and freedoms, everyone shall be subject only 329 | to such limitations as are determined by law solely for the purpose of 330 | securing due recognition and respect for the rights and freedoms of others 331 | and of meeting the just requirements of morality, public order and the 332 | general welfare in a democratic society. 333 | 334 | 3. These rights and freedoms may in no case be exercised contrary to the 335 | purposes and principles of the United Nations. 336 | 337 | 338 | 339 | Article 30 340 | 341 | 342 | Nothing in this Declaration may be interpreted as implying for any State, group or 343 | person any right to engage in any activity or to perform any act aimed at the 344 | destruction of any of the rights and freedoms set forth herein. 345 | " 346 | |> utils.wordwrap(80) 347 | |> string.join("\n") 348 | |> birdie.snap("udhr") 349 | } 350 | -------------------------------------------------------------------------------- /test/glint_test.gleam: -------------------------------------------------------------------------------- 1 | import birdie 2 | import gleeunit 3 | import gleeunit/should 4 | import glint.{Help, Out} 5 | import snag 6 | 7 | pub fn main() { 8 | gleeunit.main() 9 | } 10 | 11 | pub fn path_clean_test() { 12 | glint.new() 13 | |> glint.add( 14 | ["", " ", " cmd", "subcmd\t"], 15 | glint.command(fn(_, _, _) { Nil }), 16 | ) 17 | |> glint.execute(["cmd", "subcmd"]) 18 | |> should.be_ok() 19 | } 20 | 21 | pub fn root_command_test() { 22 | // expecting no args 23 | glint.new() 24 | |> glint.add( 25 | at: [], 26 | do: glint.command(fn(_, args, _) { should.equal(args, []) }), 27 | ) 28 | |> glint.execute([]) 29 | |> should.be_ok() 30 | 31 | // expecting some args 32 | let args = ["arg1", "arg2"] 33 | let is_args = fn(_, in_args, _) { should.equal(in_args, args) } 34 | 35 | glint.new() 36 | |> glint.add(at: [], do: glint.command(is_args)) 37 | |> glint.execute(args) 38 | |> should.be_ok() 39 | } 40 | 41 | pub fn command_routing_test() { 42 | let args = ["arg1", "arg2"] 43 | let is_args = fn(_, in_args, _) { should.equal(in_args, args) } 44 | let has_subcommand = 45 | glint.new() 46 | |> glint.add(["subcommand"], glint.command(is_args)) 47 | 48 | // execute subommand with args 49 | has_subcommand 50 | |> glint.execute(["subcommand", ..args]) 51 | |> should.be_ok() 52 | 53 | // no root command set, will return error 54 | has_subcommand 55 | |> glint.execute([]) 56 | |> should.be_error() 57 | } 58 | 59 | pub fn nested_commands_test() { 60 | let args = ["arg1", "arg2"] 61 | let is_args = fn(_, in_args, _) { should.equal(in_args, args) } 62 | 63 | let cmd = 64 | glint.new() 65 | |> glint.add(["subcommand"], glint.command(is_args)) 66 | |> glint.add(["subcommand", "subsubcommand"], glint.command(is_args)) 67 | 68 | // call subcommand with args 69 | cmd 70 | |> glint.execute(["subcommand", ..args]) 71 | |> should.be_ok() 72 | 73 | // call subcommand subsubcommand with args 74 | cmd 75 | |> glint.execute(["subcommand", "subsubcommand", ..args]) 76 | |> should.be_ok() 77 | } 78 | 79 | pub fn runner_test() { 80 | let cmd = 81 | glint.new() 82 | |> glint.add(at: [], do: glint.command(fn(_, _, _) { Ok("success") })) 83 | |> glint.add( 84 | at: ["subcommand"], 85 | do: glint.command(fn(_, _, _) { snag.error("failed") }), 86 | ) 87 | 88 | // command returns its own successful result 89 | cmd 90 | |> glint.execute([]) 91 | |> should.equal(Ok(glint.Out(Ok("success")))) 92 | 93 | // command returns its own error result 94 | cmd 95 | |> glint.execute(["subcommand"]) 96 | |> should.equal(Ok(Out(snag.error("failed")))) 97 | } 98 | 99 | fn help() { 100 | let nil = fn(_, _, _) { Nil } 101 | let global_flag = 102 | glint.string_flag("global") 103 | |> glint.flag_help("This is a global flag") 104 | 105 | let flag_1 = 106 | "flag1" 107 | |> glint.string_flag() 108 | |> glint.flag_help("This is flag1") 109 | 110 | let flag_2 = 111 | "flag2" 112 | |> glint.int_flag() 113 | |> glint.flag_help("This is flag2") 114 | let flag_3 = 115 | "flag3" 116 | |> glint.bool_flag() 117 | |> glint.flag_help("This is flag3") 118 | 119 | let flag_4 = 120 | "flag4" 121 | |> glint.float_flag() 122 | |> glint.flag_help("This is flag4") 123 | 124 | let flag_5 = 125 | "very-very-very-long-flag" 126 | |> glint.floats_flag() 127 | |> glint.flag_help( 128 | "This is a very long flag with a very very very very very very long description", 129 | ) 130 | 131 | glint.new() 132 | |> glint.with_name("test") 133 | |> glint.global_help("Some awesome global help text!") 134 | |> glint.as_module 135 | |> glint.group_flag([], global_flag) 136 | |> glint.add(at: [], do: { 137 | use <- glint.command_help("This is the root command") 138 | use _arg1 <- glint.named_arg("arg1") 139 | use _arg2 <- glint.named_arg("arg2") 140 | use _flag <- glint.flag(flag_1) 141 | glint.command(nil) 142 | }) 143 | |> glint.add(at: ["cmd1"], do: { 144 | use <- glint.command_help("This is cmd1") 145 | use _flag2 <- glint.flag(flag_2) 146 | use _flag5 <- glint.flag(flag_5) 147 | glint.command(nil) 148 | }) 149 | |> glint.add(at: ["cmd1", "cmd3"], do: { 150 | use <- glint.command_help("This is cmd3") 151 | use _flag3 <- glint.flag(flag_3) 152 | use <- glint.unnamed_args(glint.MinArgs(2)) 153 | use _woo <- glint.named_arg("woo") 154 | glint.command(nil) 155 | }) 156 | |> glint.add(at: ["cmd1", "cmd4"], do: { 157 | use <- glint.command_help( 158 | "This is cmd4 which has a very very very very very very very very long description", 159 | ) 160 | use _flag4 <- glint.flag(flag_4) 161 | use <- glint.unnamed_args(glint.EqArgs(0)) 162 | glint.command(nil) 163 | }) 164 | |> glint.add(at: ["cmd2"], do: { 165 | use <- glint.command_help("This is cmd2") 166 | use <- glint.unnamed_args(glint.EqArgs(0)) 167 | use _arg1 <- glint.named_arg("arg1") 168 | use _arg2 <- glint.named_arg("arg2") 169 | glint.command(nil) 170 | }) 171 | |> glint.add( 172 | at: ["cmd5", "cmd6"], 173 | do: glint.command_help("This is cmd6", fn() { glint.command(nil) }), 174 | ) 175 | |> glint.path_help(["cmd5", "cmd6", "cmd7"], "This is cmd7") 176 | |> glint.path_help( 177 | ["cmd8-very-very-very-very-long"], 178 | "This is cmd8 with a very very very very very very very long description. 179 | Same line as prev. 180 | 181 | This should show up on a new line. 182 | 183 | 184 | New new line 185 | 186 | 187 | 188 | New new new line.", 189 | ) 190 | } 191 | 192 | fn assert_unwrap_help(res: Result(glint.Out(a), String)) -> String { 193 | let assert Ok(Help(help)) = res 194 | help 195 | } 196 | 197 | pub fn help_test() { 198 | let cli = help() 199 | // execute root command 200 | glint.execute(cli, ["a", "b"]) 201 | |> should.equal(Ok(Out(Nil))) 202 | 203 | glint.execute(cli, ["a"]) 204 | |> should.be_error() 205 | 206 | glint.execute(cli, []) 207 | |> should.be_error() 208 | 209 | glint.execute(cli, ["cmd2"]) 210 | |> should.be_error() 211 | 212 | glint.execute(cli, ["cmd2", "1"]) 213 | |> should.be_error() 214 | 215 | glint.execute(cli, ["cmd2", "1", "2"]) 216 | |> should.equal(Ok(Out(Nil))) 217 | 218 | glint.execute(cli, ["cmd2", "1", "2", "3"]) 219 | |> should.be_error() 220 | } 221 | 222 | pub fn root_help_test() { 223 | // help message for root command 224 | glint.execute(help(), ["--help"]) 225 | |> assert_unwrap_help 226 | |> birdie.snap("root help") 227 | } 228 | 229 | pub fn cmd1_help_test() { 230 | // help message for command 231 | glint.execute(help(), ["cmd1", "--help"]) 232 | |> assert_unwrap_help 233 | |> birdie.snap("cmd1 help") 234 | } 235 | 236 | pub fn cmd4_help_test() { 237 | // help message for nested command 238 | glint.execute(help(), ["cmd1", "cmd4", "--help"]) 239 | |> assert_unwrap_help 240 | |> birdie.snap("cmd4 help") 241 | } 242 | 243 | pub fn cmd2_help_test() { 244 | // help message for command with no additional flags 245 | glint.execute(help(), ["cmd2", "--help"]) 246 | |> assert_unwrap_help 247 | |> birdie.snap("cmd2 help") 248 | } 249 | 250 | pub fn cmd3_help_test() { 251 | // help message for command with no additional flags 252 | glint.execute(help(), ["cmd1", "cmd3", "--help"]) 253 | |> assert_unwrap_help 254 | |> birdie.snap("cmd3 help") 255 | } 256 | 257 | pub fn cmd6_help_test() { 258 | // help message for command a subcommand whose help was set with glint.path_help 259 | glint.execute(help(), ["cmd5", "cmd6", "--help"]) 260 | |> assert_unwrap_help 261 | |> birdie.snap("cmd6 help") 262 | } 263 | 264 | pub fn cmd7_help_test() { 265 | // help message for command that had help_text set with glint.glint.path_help 266 | // has no children or command runner set so no other details are available 267 | glint.execute(help(), ["cmd5", "cmd6", "cmd7", "--help"]) 268 | |> assert_unwrap_help 269 | |> birdie.snap("cmd7 help") 270 | } 271 | 272 | pub fn call_help_with_residual_args_test() { 273 | // help message for command a subcommand whose help was set with glint.path_help 274 | glint.execute(help(), ["cmd5", "cmd6", "arg", "--help"]) 275 | |> assert_unwrap_help 276 | |> birdie.snap("cmd6 help with residual args") 277 | } 278 | 279 | pub fn call_leaf_help_with_residual_args_test() { 280 | // help message for command a subcommand whose help was set with glint.path_help 281 | glint.execute(help(), ["cmd5", "cmd6", "cmd7", "arg", "--help"]) 282 | |> assert_unwrap_help 283 | |> birdie.snap("cmd7 help with residual args") 284 | } 285 | 286 | pub fn global_and_group_flags_test() { 287 | let flag_f = 288 | glint.int_flag("f") 289 | |> glint.flag_default(2) 290 | |> glint.flag_help("global flag example") 291 | 292 | let sub_group_flag = 293 | "sub_group_flag" 294 | |> glint.int_flag() 295 | |> glint.flag_default(1) 296 | 297 | let cli = 298 | glint.new() 299 | |> glint.group_flag([], flag_f) 300 | |> glint.add( 301 | [], 302 | glint.command(fn(_, _, flags) { 303 | glint.get_flag(flags, flag_f) 304 | |> should.equal(Ok(2)) 305 | }), 306 | ) 307 | |> glint.add(["sub"], { 308 | use f <- glint.flag( 309 | "f" 310 | |> glint.bool_flag() 311 | |> glint.flag_default(True) 312 | |> glint.flag_help("i decided to override the global flag"), 313 | ) 314 | use _, _, flags <- glint.command() 315 | f(flags) 316 | |> should.equal(Ok(True)) 317 | }) 318 | |> glint.group_flag(["sub"], sub_group_flag) 319 | |> glint.add(["sub", "sub"], { 320 | use f <- glint.flag( 321 | "f" 322 | |> glint.bool_flag() 323 | |> glint.flag_default(True) 324 | |> glint.flag_help("i decided to override the global flag"), 325 | ) 326 | use _, _, flags <- glint.command() 327 | f(flags) 328 | |> should.equal(Ok(True)) 329 | 330 | flags 331 | |> glint.get_flag(sub_group_flag) 332 | |> should.equal(Ok(2)) 333 | }) 334 | 335 | // root command keeps the global flag as an int 336 | cli 337 | |> glint.execute(["--f=2"]) 338 | |> should.be_ok 339 | 340 | cli 341 | |> glint.execute(["--f=hello"]) 342 | |> should.be_error 343 | 344 | // sub command overrides the global flag with a bool 345 | cli 346 | |> glint.execute(["sub", "--f=true"]) 347 | |> should.be_ok 348 | 349 | cli 350 | |> glint.execute(["sub", "--f=123"]) 351 | |> should.be_error 352 | 353 | cli 354 | |> glint.execute(["sub", "sub", "--sub_group_flag=2"]) 355 | } 356 | 357 | pub fn default_pretty_help_test() { 358 | // default_pretty_help has asserts 359 | // we need to call the function to make sure it does not crash 360 | glint.default_pretty_help() 361 | } 362 | --------------------------------------------------------------------------------