├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE-APACHEv2 ├── LICENSE-MIT ├── README.md ├── config.nims ├── confutils.nim ├── confutils.nimble ├── confutils ├── cli_parser.nim ├── cli_parsing_fuzzer.nim ├── config_file.nim ├── defs.nim ├── shell_completion.nim ├── std │ └── net.nim ├── toml │ ├── defs.nim │ └── std │ │ ├── net.nim │ │ └── uri.nim └── winreg │ ├── reader.nim │ ├── types.nim │ ├── utils.nim │ ├── winreg_serialization.nim │ └── writer.nim ├── nim.cfg └── tests ├── cli_example.nim ├── command_with_args.nim ├── config_files ├── current_user │ └── testVendor │ │ └── testApp.toml └── system_wide │ └── testVendor │ └── testApp.toml ├── fail ├── test_abbr_duplicate_root.nim ├── test_abbr_duplicate_root_subcommand.nim ├── test_abbr_duplicate_subcommand.nim ├── test_name_duplicate_root.nim ├── test_name_duplicate_root_subcommand.nim └── test_name_duplicate_subcommand.nim ├── issue_6.nim ├── logger.nim ├── nested_commands.nim ├── nim.cfg ├── private └── specialint.nim ├── test_all.nim ├── test_config_file.nim ├── test_duplicates.nim ├── test_envvar.nim ├── test_ignore.nim ├── test_parsecmdarg.nim ├── test_pragma.nim ├── test_qualified_ident.nim └── test_winreg.nim /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | uses: status-im/nimbus-common-workflow/.github/workflows/common.yml@main 12 | with: 13 | test-command: | 14 | nimble install -y toml_serialization json_serialization unittest2 15 | rm -f nimble.lock 16 | nimble test 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache 2 | *.exe 3 | nimble.develop 4 | nimble.paths 5 | build/ 6 | vendor/ -------------------------------------------------------------------------------- /LICENSE-APACHEv2: -------------------------------------------------------------------------------- 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 2018 Status Research & Development GmbH 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 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Status Research & Development GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | nim-confutils 2 | ============= 3 | 4 | [![License: Apache](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | ![Github action](https://github.com/status-im/nim-confutils/workflows/CI/badge.svg) 7 | 8 | ## Introduction 9 | 10 | Confutils is a library that aims to solve the configuration problem 11 | with a holistic approach. The run-time configuration of a program 12 | is described as a plain Nim object type from which the library 13 | automatically derives the code for handling command-line options, 14 | configuration files and other platform-specific sources such as the 15 | Windows registry. 16 | 17 | The library focuses on providing a lot of compile-time configurability 18 | and extensibility with a strong adherence to the DRY principle. 19 | 20 | Let's illustrate the API with a highly annotated example. Our configuration 21 | might be described in a separate module looking like this: 22 | 23 | ```nim 24 | # config.nim 25 | import 26 | confutils/defs 27 | 28 | type 29 | NimbusConf* = object 30 | # 31 | # This is our configuration type. 32 | # 33 | # Each field will be considered a configuration option that may appear 34 | # on the command-line, whitin an environment variable or a configuration 35 | # file, or elsewhere. Custom pragmas are used to annotate the fields with 36 | # additional metadata that is used to augment the behavior of the library. 37 | # 38 | logLevel* {. 39 | defaultValue: LogLevel.INFO 40 | desc: "Sets the log level" }: LogLevel 41 | 42 | # 43 | # This program uses a CLI interface with sub-commands (similar to git). 44 | # 45 | # The `StartUpCommand` enum provides the list of available sub-commands, 46 | # but since we are specifying a default value of `noCommand`, the user 47 | # can also launch the program without entering any particular command. 48 | # The default command will also be omitted from help messages. 49 | # 50 | # Please note that the `logLevel` option above will be shared by all 51 | # sub-commands. The rest of the nested options will be relevant only 52 | # when the designated sub-command is being invoked. 53 | # 54 | case cmd* {. 55 | command 56 | defaultValue: noCommand }: StartUpCommand 57 | 58 | of noCommand: 59 | dataDir* {. 60 | defaultValue: getConfigDir() / "nimbus" 61 | desc: "The directory where nimbus will store all blockchain data." 62 | abbr: "d" }: DirPath 63 | 64 | bootstrapNodes* {. 65 | desc: "Specifies one or more bootstrap nodes to use when connecting to the network." 66 | abbr: "b" 67 | name: "bootstrap-node" }: seq[string] 68 | 69 | bootstrapNodesFile* {. 70 | defaultValue: "" 71 | desc: "Specifies a line-delimited file of bootsrap Ethereum network addresses" 72 | abbr: "f" }: InputFile 73 | 74 | tcpPort* {. 75 | desc: "TCP listening port" }: int 76 | 77 | udpPort* {. 78 | desc: "UDP listening port" }: int 79 | 80 | validators* {. 81 | required 82 | desc: "A path to a pair of public and private keys for a validator. " & 83 | "Nimbus will automatically add the extensions .privkey and .pubkey." 84 | abbr: "v" 85 | name: "validator" }: seq[PrivateValidatorData] 86 | 87 | stateSnapshot* {. 88 | desc: "Json file specifying a recent state snapshot" 89 | abbr: "s" }: Option[BeaconState] 90 | 91 | of createChain: 92 | chainStartupData* {. 93 | desc: "" 94 | abbr: "c" }: ChainStartupData 95 | 96 | outputStateFile* {. 97 | desc: "Output file where to write the initial state snapshot" 98 | name: "out" 99 | abbr: "o" }: OutFilePath 100 | 101 | StartUpCommand* = enum 102 | noCommand 103 | createChain 104 | 105 | # 106 | # The configuration can use user-defined types that feature custom 107 | # command-line parsing and serialization routines. 108 | # 109 | PrivateValidatorData* = object 110 | privKey*: ValidatorPrivKey 111 | randao*: Randao 112 | 113 | ``` 114 | 115 | Then from our main module, we just need to call `confutils.load` which must be 116 | given our configuration type as a parameter: 117 | 118 | ```nim 119 | # main.nim 120 | import 121 | confutils, config 122 | 123 | when isMainModule: 124 | let conf = NimbusConf.load() 125 | initDatabase conf.dataDir 126 | ``` 127 | 128 | And that's it - calling `load` with default parameters will first process any 129 | [command-line options](#handling-of-command-line-options) and then it will 130 | try to load any missing options from the most appropriate 131 | [configuration location](#handling-of-environment-variables-and-config-files) 132 | for the platform. Diagnostic messages will be provided for many simple 133 | configuration errors and the following help message will be produced 134 | automatically when calling the program with `program --help`: 135 | 136 | ``` 137 | Usage: beacon_node [OPTIONS] 138 | 139 | The following options are supported: 140 | 141 | --logLevel=LogLevel : Sets the log level 142 | --dataDir=DirPath : The directory where nimbus will store all blockchain data. 143 | --bootstrapNode=seq[string] : Specifies one or more bootstrap nodes to use when connecting to the network. 144 | --bootstrapNodesFile=FilePath : Specifies a line-delimited file of bootsrap Ethereum network addresses 145 | --tcpPort=int : TCP listening port 146 | --udpPort=int : UDP listening port 147 | --validator=seq[PrivateValidatorData] : A path to a pair of public and private keys for a validator. Nimbus will automatically add the extensions .privkey and .pubkey. 148 | --stateSnapshot=Option[BeaconState] : Json file specifying a recent state snapshot 149 | 150 | Available sub-commands: 151 | 152 | beacon_node createChain 153 | 154 | --out=OutFilePath : Output file where to write the initial state snapshot 155 | 156 | ``` 157 | 158 | For simpler CLI utilities, Confutils also provides the following convenience APIs: 159 | 160 | ```nim 161 | import 162 | confutils 163 | 164 | cli do (validators {. 165 | desc: "number of validators" 166 | abbr: "v" }: int, 167 | 168 | outputDir {. 169 | desc: "output dir to store the generated files" 170 | abbr: "o" }: OutPath, 171 | 172 | startupDelay {. 173 | desc: "delay in seconds before starting the simulation" } = 0): 174 | 175 | if validators < 64: 176 | echo "The number of validators must be greater than EPOCH_LENGTH (64)" 177 | quit(1) 178 | ``` 179 | 180 | ```nim 181 | import 182 | confutils 183 | 184 | proc main(foo: string, bar: int) = 185 | ... 186 | 187 | dispatch(main) 188 | ``` 189 | 190 | Under the hood, using these APIs will result in calling `load` on an anonymous 191 | configuration type having the same fields as the supplied proc params. 192 | Any additional arguments given as `cli(args) do ...` and `dispatch(fn, args)` 193 | will be passed to `load` without modification. Please note that this requires 194 | all parameters types to be concrete (non-generic). 195 | 196 | This covers the basic usage of the library and the rest of the documentation 197 | will describe the various ways the default behavior can be tweaked or extended. 198 | 199 | 200 | ## Configuration field pragmas 201 | 202 | A number of pragmas defined in `confutils/defs` can be attached to the 203 | configuration fields to control the behavior of the library. 204 | 205 | ```nim 206 | template desc*(v: string) {.pragma.} 207 | ``` 208 | 209 | A description of the configuration option that will appear in the produced 210 | help messages. 211 | 212 | ```nim 213 | template longDesc*(v: string) {.pragma.} 214 | ``` 215 | 216 | A long description text that will appear below regular desc. You can use 217 | one of {'\n', '\r'} to break it into multiple lines. But you can't use 218 | '\p' as line break. 219 | 220 | ```text 221 | -x, --name regular description [=defVal]. 222 | longdesc line one. 223 | longdesc line two. 224 | longdesc line three. 225 | ``` 226 | ----------------- 227 | 228 | ```nim 229 | template name*(v: string) {.pragma.} 230 | ``` 231 | 232 | A long name of the option. 233 | Typically, it will have to be be specified as `--longOptionName value`. 234 | See [Handling of command-line options](#handling-of-command-line-options) 235 | for more details. 236 | 237 | ----------------- 238 | 239 | ```nim 240 | template abbr*(v: string) {.pragma.} 241 | 242 | ``` 243 | 244 | A short name of the option. 245 | Typically, it will be required to be specified as `-x value`. 246 | See [Handling of command-line options](#handling-of-command-line-options) 247 | for more details. 248 | 249 | ----------------- 250 | 251 | ```nim 252 | template defaultValue*(v: untyped) {.pragma.} 253 | ``` 254 | 255 | The default value of the option if no value was supplied by the user. 256 | 257 | ----------------- 258 | 259 | ```nim 260 | template required* {.pragma.} 261 | ``` 262 | 263 | By default, all options without default values are considered required. 264 | An exception to this rule are all `seq[T]` or `Option[T]` options for 265 | which the "empty" value can be considered a reasonable default. You can 266 | also extend this behavior to other user-defined types by providing the 267 | following overloads: 268 | 269 | ```nim 270 | template hasDefault*(T: type Foo): bool = true 271 | template default*(T: type Foo): Foo = Foo(...) 272 | ``` 273 | 274 | The `required` pragma can be applied to fields having such defaultable 275 | types to make them required. 276 | 277 | ----------------- 278 | 279 | ```nim 280 | template command* {.pragma.} 281 | ``` 282 | 283 | This must be applied to an enum field that represents a possible sub-command. 284 | See the section on [sub-commands](#Using-sub-commands) for more details. 285 | 286 | ----------------- 287 | 288 | ```nim 289 | template argument* {.pragma.} 290 | ``` 291 | 292 | This field represents an argument to the program. If the program expects 293 | multiple arguments, this pragma can be applied to multiple fields or to 294 | a single `seq[T]` field depending on the desired behavior. 295 | 296 | ----------------- 297 | 298 | ```nim 299 | template separator(v: string)* {.pragma.} 300 | ``` 301 | 302 | Using this pragma, a customizable separator text will be displayed just before 303 | this field. E.g.: 304 | 305 | ```text 306 | Network Options: # this is a separator 307 | -a, --opt1 desc 308 | -b, --opt2 desc 309 | 310 | ---------------- # this is a separator too 311 | -c, --opt3 desc 312 | ``` 313 | 314 | ## Configuration field types 315 | 316 | The `confutils/defs` module provides a number of types frequently used 317 | for configuration purposes: 318 | 319 | #### `InputFile`, `InputDir` 320 | 321 | Confutils will validate that the file/directory exists and that it can 322 | be read by the current user. 323 | 324 | #### `ConfigFilePath[Format]` 325 | 326 | A file system path pointing to a configuration file in the specific format. 327 | The actual configuration can be loaded by calling `load(path, ConfigType)`. 328 | When the format is `WindowsRegistry` the path should indicate a registry key. 329 | 330 | #### `OutPath` 331 | 332 | A valid path must be given. 333 | 334 | -------------- 335 | 336 | Furthermore, you can extend the behavior of the library by providing 337 | overloads such as: 338 | 339 | ```nim 340 | proc parseCmdArg*(T: type Foo, p: string): T = 341 | ## This provides parsing and validation for fields having the `Foo` type. 342 | ## You should raise `ConfigurationError` in case of detected problems. 343 | ... 344 | 345 | proc humaneTypeName*[T](_: type MyList[T]): string = 346 | ## The returned string will be used in the help messages produced by the 347 | ## library to describe the expected type of the configuration option. 348 | mixin humaneTypeName 349 | return "list of " & humaneTypeName(T) 350 | ``` 351 | 352 | For config files, Confutils can work with any format supported by the 353 | [nim-serialization](https://github.com/status-im/nim-serialization/) library 354 | and it will use the standard serialization routines defined for the field 355 | types in this format. Fields marked with the `command` or `argument` pragmas 356 | will be ignored. 357 | 358 | ## Handling of command-line options 359 | 360 | Confutils includes parsers that can mimic several traditional styles of 361 | command line interfaces. You can select the parser being used by specifying 362 | the `CmdParser` option when calling the configuration loading APIs. 363 | 364 | The default parser of Confutils is called `MixedCmdParser`. It tries to follow 365 | the [robustness principle](https://en.wikipedia.org/wiki/Robustness_principle) 366 | by recognizing as many styles of passing command-line switches as possible. 367 | A prefix of `--` is used to indicate a long option name, while the `-` prefix 368 | uses the short option name. Multiple short options such as `-a`, `-b` and 369 | `-c` can be combined into a single `-abc` string. Both the long and the short 370 | forms can also be prefixed with `/` in the style of Windows utilities. The 371 | option names are matched in case-insensitive fashion and certain characters 372 | such as `_` and `-` will be ignored. The values can be separated from the 373 | option names with a space, colon or an equal sign. `bool` flags default to 374 | `false` and merely including them in the command line sets them to `true`. 375 | 376 | Other provided choices are `UnixCmdParser`, `WindowsCmdParser` and `NimCmdParser` 377 | which are based on more strict grammars following the most established 378 | tradition of the respective platforms. All of the discussed parsers are 379 | defined in terms of the lower-level parametric type `CustomCmdParser` that 380 | can be tweaked further for a more custom behavior. 381 | 382 | Please note that the choice of `CmdParser` will also affect the formatting 383 | of the help messages. Please see the definition of the standard [Windows][WIN_CMD] 384 | or [Posix][POSIX_CMD] command-line help syntax for mode details. 385 | 386 | [WIN_CMD]: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/command-line-syntax-key 387 | [POSIX_CMD]: http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html 388 | 389 | ### Using sub-commands 390 | 391 | As seen in the [introduction example](#introduction), Confutils makes it 392 | easy to create command-line interfaces featuring sub-commands in the style 393 | of `git` or `nimble`. The structure of the sub-command tree is encoded as 394 | a Nim case object where the sub-command name is represented by an `enum` 395 | field having the `command` pragma. Any nested fields will be considered 396 | options of the particular sub-command. The top-level fields will be shared 397 | between all sub-commands. 398 | 399 | For each available choice of command and options, Confutils will automatically 400 | provide a `help` command and the following additional switches: 401 | 402 | * `-h` will print a short syntax reminder for the command 403 | * `--help` will print a full help message (just like the `help` command) 404 | 405 | ## Handling of environment variables and config files 406 | 407 | After parsing the command line options, the default behavior of Confutils is 408 | to try to fill any missing options by examining the contents of the environment 409 | variables plus two per-user and system-wide configuration locations derived from 410 | the program name. If you want to use Confutils only as a command-line processor 411 | or a config file parser for example, you can supply an empty/nil value to the 412 | `cmdLine`, `envTable` or `configFileEnumerator` parameters of the `load` call. 413 | 414 | More specifically, the `load` call supports the following parameters: 415 | 416 | #### `cmdLine`, `envTable` 417 | 418 | The command-line parameters and the environment table of the program. 419 | By default, these will be obtained through Nim's `os` module. 420 | 421 | #### `EnvValuesFormat`, `envVarsPrefix` 422 | 423 | A nim-serialization format used to deserialize the values of environment 424 | variables. The default format is called `CmdLineFormat` and it uses the 425 | same `parseCmdArg` calls responsible for parsing the command-line. 426 | 427 | The names of the environment variables are prefixed by the name of the 428 | program by default and joined with the name of command line option, which is 429 | uppercased and characters `-` and spaces are replaced with underscore: 430 | 431 | ```nim 432 | let env_variable_name = &"{prefix}_{key}".toUpperAscii.multiReplace(("-", "_"), (" ", "_")) 433 | ``` 434 | 435 | #### `configFileEnumerator` 436 | 437 | A function responsible for returning a sequence of `ConfigFilePath` objects. 438 | To support heterogenous config file types, you can also return a tuple of 439 | sequences. The default behavior of Windows is to obtain the configuration 440 | from the Windows registry by looking at the following keys: 441 | 442 | ``` 443 | HKEY_CURRENT_USER/SOFTWARE/{appVendor}/{appName}/ 444 | HKEY_LOCAL_MACHINE/SOFTWARE/{appVendor}/{appName}/ 445 | ``` 446 | 447 | On Posix systems, the default behavior is attempt to load the configuration 448 | from the following files: 449 | 450 | ``` 451 | /$HOME/.config/{appName}.{ConfigFileFormat.extension} 452 | /etc/{appName}.{ConfigFileForamt.extension} 453 | ``` 454 | 455 | #### `ConfigFileFormat` 456 | 457 | A [nim-serialization](https://github.com/status-im/nim-serialization) format 458 | that will be used by default by Confutils. 459 | 460 | ## Customization of the help messages 461 | 462 | The `load` call offers few more optional parameters for modifying the 463 | produced help messages: 464 | 465 | #### `bannerBeforeHelp` 466 | 467 | A copyright banner or a similar message that will appear before the 468 | automatically generated help messages. 469 | 470 | #### `bannerAfterHelp` 471 | 472 | A copyright banner or a similar message that will appear after the 473 | automatically generated help messages. 474 | 475 | #### `version` 476 | 477 | If you provide this parameter, Confutils will automatically respond 478 | to the standard `--version` switch. If sub-commands are used, an 479 | additional `version` top-level command will be inserted as well. 480 | 481 | ## Compile-time options 482 | 483 | #### `confutils_colors` 484 | 485 | This option controls the use of colors appearing in the help messages 486 | produced by Confutils. Possible values are: 487 | 488 | - `NativeColors` (used by default) 489 | 490 | In this mode, Windows builds will produce output suitable for the console 491 | application in older versions of Windows. On Unix-like systems, this is 492 | equivalent to specifying `AnsiColors`. 493 | 494 | - `AnsiColors` 495 | 496 | Output suitable for terminals supporting the standard ANSI escape codes: 497 | https://en.wikipedia.org/wiki/ANSI_escape_code 498 | 499 | This includes most terminal emulators on modern Unix-like systems, 500 | Windows console replacements such as ConEmu, and the native Console 501 | and PowerShell applications on Windows 10. 502 | 503 | - `None` or `NoColors` 504 | 505 | All output will be colorless. 506 | 507 | ## Contributing 508 | 509 | The development of Confutils is sponsored by [Status.im](https://status.im/) 510 | through the use of [GitCoin](https://gitcoin.co/). Please take a look at our 511 | tracker for any issues having the [bounty][BOUNTIES] tag. 512 | 513 | When submitting pull requests, please add test cases for any new features 514 | or fixes and make sure `nimble test` is still able to execute the entire 515 | test suite successfully. 516 | 517 | [BOUNTIES]: https://github.com/status-im/nim-confutils/issues?q=is%3Aissue+is%3Aopen+label%3Abounty 518 | 519 | ## License 520 | 521 | Licensed and distributed under either of 522 | 523 | * MIT license: [LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT 524 | 525 | or 526 | 527 | * Apache License, Version 2.0, ([LICENSE-APACHEv2](LICENSE-APACHEv2) or http://www.apache.org/licenses/LICENSE-2.0) 528 | 529 | at your option. This file may not be copied, modified, or distributed except according to those terms. 530 | 531 | -------------------------------------------------------------------------------- /config.nims: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | # begin Nimble config (version 1) 11 | when fileExists("nimble.paths"): 12 | include "nimble.paths" 13 | # end Nimble config 14 | -------------------------------------------------------------------------------- /confutils.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | {.push raises: [].} 11 | 12 | import 13 | os, 14 | std/[enumutils, options, strutils, wordwrap], 15 | stew/shims/macros, 16 | confutils/[defs, cli_parser, config_file] 17 | 18 | export 19 | options, defs, config_file 20 | 21 | const 22 | hasSerialization = not defined(nimscript) 23 | useBufferedOutput = defined(nimscript) 24 | noColors = useBufferedOutput or defined(confutils_no_colors) 25 | hasCompletions = not defined(nimscript) 26 | descPadding = 6 27 | minNameWidth = 24 - descPadding 28 | 29 | when hasSerialization: 30 | import serialization 31 | export serialization 32 | 33 | when not defined(nimscript): 34 | import 35 | terminal, 36 | confutils/shell_completion 37 | 38 | type 39 | HelpAppInfo = ref object 40 | appInvocation: string 41 | copyrightBanner: string 42 | hasAbbrs: bool 43 | maxNameLen: int 44 | terminalWidth: int 45 | namesWidth: int 46 | 47 | CmdInfo = ref object 48 | name: string 49 | desc: string 50 | isHidden: bool 51 | opts: seq[OptInfo] 52 | 53 | OptKind = enum 54 | Discriminator 55 | CliSwitch 56 | Arg 57 | 58 | OptInfo = ref object 59 | name, abbr, desc, typename: string 60 | separator: string 61 | longDesc: string 62 | idx: int 63 | isHidden: bool 64 | hasDefault: bool 65 | defaultInHelpText: string 66 | case kind: OptKind 67 | of Discriminator: 68 | isCommand: bool 69 | isImplicitlySelectable: bool 70 | subCmds: seq[CmdInfo] 71 | defaultSubCmd: int 72 | else: 73 | discard 74 | 75 | const 76 | confutils_description_width {.intdefine.} = 80 77 | confutils_narrow_terminal_width {.intdefine.} = 36 78 | 79 | {.push gcsafe, raises: [].} 80 | 81 | func getFieldName(caseField: NimNode): NimNode = 82 | result = caseField 83 | if result.kind == nnkIdentDefs: result = result[0] 84 | if result.kind == nnkPragmaExpr: result = result[0] 85 | if result.kind == nnkPostfix: result = result[1] 86 | 87 | when defined(nimscript): 88 | func scriptNameParamIdx: int = 89 | for i in 1 ..< paramCount(): 90 | var param = paramStr(i) 91 | if param.len > 0 and param[0] != '-': 92 | return i 93 | 94 | proc appInvocation: string = 95 | let scriptNameIdx = scriptNameParamIdx() 96 | "nim " & (if paramCount() > scriptNameIdx: paramStr(scriptNameIdx) else: "") 97 | 98 | type stderr = object 99 | 100 | template writeLine(T: type stderr, msg: string) = 101 | echo msg 102 | 103 | proc commandLineParams(): seq[string] = 104 | for i in scriptNameParamIdx() + 1 .. paramCount(): 105 | result.add paramStr(i) 106 | 107 | # TODO: Why isn't this available in NimScript? 108 | proc getCurrentExceptionMsg(): string = 109 | "" 110 | 111 | template terminalWidth: int = 112 | 100000 113 | 114 | else: 115 | template appInvocation: string = 116 | try: 117 | getAppFilename().splitFile.name 118 | except OSError: 119 | "" 120 | 121 | when noColors: 122 | const 123 | styleBright = "" 124 | fgYellow = "" 125 | fgWhite = "" 126 | fgGreen = "" 127 | fgCyan = "" 128 | fgBlue = "" 129 | resetStyle = "" 130 | 131 | when useBufferedOutput: 132 | template helpOutput(args: varargs[string]) = 133 | for arg in args: 134 | help.add arg 135 | 136 | template errorOutput(args: varargs[string]) = 137 | helpOutput(args) 138 | 139 | template flushOutput = 140 | echo help 141 | 142 | else: 143 | template errorOutput(args: varargs[untyped]) = 144 | try: 145 | styledWrite stderr, args 146 | except IOError, ValueError: 147 | discard 148 | 149 | template helpOutput(args: varargs[untyped]) = 150 | try: 151 | styledWrite stdout, args 152 | except IOError, ValueError: 153 | discard 154 | 155 | template flushOutput = 156 | discard 157 | 158 | const 159 | fgSection = fgYellow 160 | fgDefault = fgWhite 161 | fgCommand = fgCyan 162 | fgOption = fgBlue 163 | fgArg = fgBlue 164 | 165 | # TODO: Start using these: 166 | # fgValue = fgGreen 167 | # fgType = fgYellow 168 | 169 | template flushOutputAndQuit(exitCode: int) = 170 | flushOutput 171 | quit exitCode 172 | 173 | func isCliSwitch(opt: OptInfo): bool = 174 | opt.kind == CliSwitch or 175 | (opt.kind == Discriminator and opt.isCommand == false) 176 | 177 | func hasOpts(cmd: CmdInfo): bool = 178 | for opt in cmd.opts: 179 | if opt.isCliSwitch and not opt.isHidden: 180 | return true 181 | 182 | return false 183 | 184 | func hasArgs(cmd: CmdInfo): bool = 185 | cmd.opts.len > 0 and cmd.opts[^1].kind == Arg 186 | 187 | func firstArgIdx(cmd: CmdInfo): int = 188 | # This will work correctly only if the command has arguments. 189 | result = cmd.opts.len - 1 190 | while result > 0: 191 | if cmd.opts[result - 1].kind != Arg: 192 | return 193 | dec result 194 | 195 | iterator args(cmd: CmdInfo): OptInfo = 196 | if cmd.hasArgs: 197 | for i in cmd.firstArgIdx ..< cmd.opts.len: 198 | yield cmd.opts[i] 199 | 200 | func getSubCmdDiscriminator(cmd: CmdInfo): OptInfo = 201 | for i in countdown(cmd.opts.len - 1, 0): 202 | let opt = cmd.opts[i] 203 | if opt.kind != Arg: 204 | if opt.kind == Discriminator and opt.isCommand: 205 | return opt 206 | else: 207 | return nil 208 | 209 | template hasSubCommands(cmd: CmdInfo): bool = 210 | getSubCmdDiscriminator(cmd) != nil 211 | 212 | iterator subCmds(cmd: CmdInfo): CmdInfo = 213 | let subCmdDiscriminator = cmd.getSubCmdDiscriminator 214 | if subCmdDiscriminator != nil: 215 | for cmd in subCmdDiscriminator.subCmds: 216 | yield cmd 217 | 218 | template isSubCommand(cmd: CmdInfo): bool = 219 | cmd.name.len > 0 220 | 221 | func maxNameLen(cmd: CmdInfo): int = 222 | result = 0 223 | for opt in cmd.opts: 224 | if opt.kind == Arg or opt.kind == Discriminator and opt.isCommand: 225 | continue 226 | result = max(result, opt.name.len) 227 | if opt.kind == Discriminator: 228 | for subCmd in opt.subCmds: 229 | result = max(result, subCmd.maxNameLen) 230 | 231 | func hasAbbrs(cmd: CmdInfo): bool = 232 | for opt in cmd.opts: 233 | if opt.kind == Arg or opt.kind == Discriminator and opt.isCommand: 234 | continue 235 | if opt.abbr.len > 0: 236 | return true 237 | if opt.kind == Discriminator: 238 | for subCmd in opt.subCmds: 239 | if hasAbbrs(subCmd): 240 | return true 241 | 242 | func humaneName(opt: OptInfo): string = 243 | if opt.name.len > 0: opt.name 244 | else: opt.abbr 245 | 246 | template padding(output: string, desiredWidth: int): string = 247 | spaces(max(desiredWidth - output.len, 0)) 248 | 249 | proc writeDesc(help: var string, 250 | appInfo: HelpAppInfo, 251 | desc, defaultValue: string) = 252 | const descSpacing = " " 253 | let 254 | descIndent = (5 + appInfo.namesWidth + descSpacing.len) 255 | remainingColumns = appInfo.terminalWidth - descIndent 256 | defaultValSuffix = if defaultValue.len == 0: "" 257 | else: " [=" & defaultValue & "]" 258 | fullDesc = desc & defaultValSuffix & "." 259 | 260 | if remainingColumns < confutils_narrow_terminal_width: 261 | helpOutput "\p ", wrapWords(fullDesc, appInfo.terminalWidth - 2, 262 | newLine = "\p ") 263 | else: 264 | let wrappingWidth = min(remainingColumns, confutils_description_width) 265 | helpOutput descSpacing, wrapWords(fullDesc, wrappingWidth, 266 | newLine = "\p" & spaces(descIndent)) 267 | 268 | proc writeLongDesc(help: var string, 269 | appInfo: HelpAppInfo, 270 | desc: string) = 271 | let lines = split(desc, {'\n', '\r'}) 272 | for line in lines: 273 | if line.len > 0: 274 | helpOutput "\p" 275 | helpOutput padding("", 5 + appInfo.namesWidth) 276 | help.writeDesc appInfo, line, "" 277 | 278 | proc describeInvocation(help: var string, 279 | cmd: CmdInfo, cmdInvocation: string, 280 | appInfo: HelpAppInfo) = 281 | helpOutput styleBright, "\p", fgCommand, cmdInvocation 282 | var longestArg = 0 283 | 284 | if cmd.opts.len > 0: 285 | if cmd.hasOpts: helpOutput " [OPTIONS]..." 286 | 287 | let subCmdDiscriminator = cmd.getSubCmdDiscriminator 288 | if subCmdDiscriminator != nil: helpOutput " command" 289 | 290 | for arg in cmd.args: 291 | helpOutput " <", arg.name, ">" 292 | longestArg = max(longestArg, arg.name.len) 293 | 294 | helpOutput "\p" 295 | 296 | if cmd.desc.len > 0: 297 | helpOutput "\p", cmd.desc, ".\p" 298 | 299 | var argsSectionStarted = false 300 | 301 | for arg in cmd.args: 302 | if arg.desc.len > 0: 303 | if not argsSectionStarted: 304 | helpOutput "\p" 305 | argsSectionStarted = true 306 | 307 | let cliArg = " <" & arg.humaneName & ">" 308 | helpOutput fgArg, styleBright, cliArg 309 | helpOutput padding(cliArg, 6 + appInfo.namesWidth) 310 | help.writeDesc appInfo, arg.desc, arg.defaultInHelpText 311 | help.writeLongDesc appInfo, arg.longDesc 312 | helpOutput "\p" 313 | 314 | type 315 | OptionsType = enum 316 | normalOpts 317 | defaultCmdOpts 318 | conditionalOpts 319 | 320 | proc describeOptions(help: var string, 321 | cmd: CmdInfo, cmdInvocation: string, 322 | appInfo: HelpAppInfo, optionsType = normalOpts) = 323 | if cmd.hasOpts: 324 | case optionsType 325 | of normalOpts: 326 | helpOutput "\pThe following options are available:\p\p" 327 | of conditionalOpts: 328 | helpOutput ", the following additional options are available:\p\p" 329 | of defaultCmdOpts: 330 | discard 331 | 332 | for opt in cmd.opts: 333 | if opt.kind == Arg or 334 | opt.kind == Discriminator or 335 | opt.isHidden: continue 336 | 337 | if opt.separator.len > 0: 338 | helpOutput opt.separator 339 | helpOutput "\p" 340 | 341 | # Indent all command-line switches 342 | helpOutput " " 343 | 344 | if opt.abbr.len > 0: 345 | helpOutput fgOption, styleBright, "-", opt.abbr, ", " 346 | elif appInfo.hasAbbrs: 347 | # Add additional indentatition, so all names are aligned 348 | helpOutput " " 349 | 350 | if opt.name.len > 0: 351 | let switch = "--" & opt.name 352 | helpOutput fgOption, styleBright, 353 | switch, padding(switch, appInfo.namesWidth) 354 | else: 355 | helpOutput spaces(2 + appInfo.namesWidth) 356 | 357 | if opt.desc.len > 0: 358 | help.writeDesc appInfo, 359 | opt.desc.replace("%t", opt.typename), 360 | opt.defaultInHelpText 361 | help.writeLongDesc appInfo, opt.longDesc 362 | 363 | helpOutput "\p" 364 | 365 | if opt.kind == Discriminator: 366 | for i, subCmd in opt.subCmds: 367 | if not subCmd.hasOpts: continue 368 | 369 | helpOutput "\pWhen ", styleBright, fgBlue, opt.humaneName, resetStyle, " = ", fgGreen, subCmd.name 370 | 371 | if i == opt.defaultSubCmd: helpOutput " (default)" 372 | help.describeOptions subCmd, cmdInvocation, appInfo, conditionalOpts 373 | 374 | let subCmdDiscriminator = cmd.getSubCmdDiscriminator 375 | if subCmdDiscriminator != nil: 376 | let defaultCmdIdx = subCmdDiscriminator.defaultSubCmd 377 | if defaultCmdIdx != -1: 378 | let defaultCmd = subCmdDiscriminator.subCmds[defaultCmdIdx] 379 | help.describeOptions defaultCmd, cmdInvocation, appInfo, defaultCmdOpts 380 | 381 | helpOutput fgSection, "\pAvailable sub-commands:\p" 382 | 383 | for i, subCmd in subCmdDiscriminator.subCmds: 384 | if i != subCmdDiscriminator.defaultSubCmd: 385 | let subCmdInvocation = cmdInvocation & " " & subCmd.name 386 | help.describeInvocation subCmd, subCmdInvocation, appInfo 387 | help.describeOptions subCmd, subCmdInvocation, appInfo 388 | 389 | proc showHelp(help: var string, 390 | appInfo: HelpAppInfo, 391 | activeCmds: openArray[CmdInfo]) = 392 | if appInfo.copyrightBanner.len > 0: 393 | helpOutput appInfo.copyrightBanner, "\p\p" 394 | 395 | let cmd = activeCmds[^1] 396 | 397 | appInfo.maxNameLen = cmd.maxNameLen 398 | appInfo.hasAbbrs = cmd.hasAbbrs 399 | appInfo.terminalWidth = 400 | try: 401 | terminalWidth() 402 | except ValueError: 403 | int.high # https://github.com/nim-lang/Nim/pull/21968 404 | appInfo.namesWidth = min(minNameWidth, appInfo.maxNameLen) + descPadding 405 | 406 | var cmdInvocation = appInfo.appInvocation 407 | for i in 1 ..< activeCmds.len: 408 | cmdInvocation.add " " 409 | cmdInvocation.add activeCmds[i].name 410 | 411 | # Write out the app or script name 412 | helpOutput fgSection, "Usage: \p" 413 | help.describeInvocation cmd, cmdInvocation, appInfo 414 | help.describeOptions cmd, cmdInvocation, appInfo 415 | helpOutput "\p" 416 | 417 | flushOutputAndQuit QuitSuccess 418 | 419 | func getNextArgIdx(cmd: CmdInfo, consumedArgIdx: int): int = 420 | for i in 0 ..< cmd.opts.len: 421 | if cmd.opts[i].kind == Arg and cmd.opts[i].idx > consumedArgIdx: 422 | return cmd.opts[i].idx 423 | 424 | -1 425 | 426 | proc noMoreArgsError(cmd: CmdInfo): string {.raises: [].} = 427 | result = 428 | if cmd.isSubCommand: 429 | "The command '" & cmd.name & "'" 430 | else: 431 | appInvocation() 432 | result.add " does not accept" 433 | if cmd.hasArgs: result.add " additional" 434 | result.add " arguments" 435 | 436 | func findOpt(opts: openArray[OptInfo], name: string): OptInfo = 437 | for opt in opts: 438 | if cmpIgnoreStyle(opt.name, name) == 0 or 439 | cmpIgnoreStyle(opt.abbr, name) == 0: 440 | return opt 441 | 442 | func findOpt(activeCmds: openArray[CmdInfo], name: string): OptInfo = 443 | for i in countdown(activeCmds.len - 1, 0): 444 | let found = findOpt(activeCmds[i].opts, name) 445 | if found != nil: return found 446 | 447 | func findCmd(cmds: openArray[CmdInfo], name: string): CmdInfo = 448 | for cmd in cmds: 449 | if cmpIgnoreStyle(cmd.name, name) == 0: 450 | return cmd 451 | 452 | func findSubCmd(cmd: CmdInfo, name: string): CmdInfo = 453 | let subCmdDiscriminator = cmd.getSubCmdDiscriminator 454 | if subCmdDiscriminator != nil: 455 | let cmd = findCmd(subCmdDiscriminator.subCmds, name) 456 | if cmd != nil: return cmd 457 | 458 | return nil 459 | 460 | func startsWithIgnoreStyle(s: string, prefix: string): bool = 461 | # Similar in spirit to cmpIgnoreStyle, but compare only the prefix. 462 | var i = 0 463 | var j = 0 464 | 465 | while true: 466 | # Skip any underscore 467 | while i < s.len and s[i] == '_': inc i 468 | while j < prefix.len and prefix[j] == '_': inc j 469 | 470 | if j == prefix.len: 471 | # The whole prefix matches 472 | return true 473 | elif i == s.len: 474 | # We've reached the end of `s` without matching the prefix 475 | return false 476 | elif toLowerAscii(s[i]) != toLowerAscii(prefix[j]): 477 | return false 478 | 479 | inc i 480 | inc j 481 | 482 | when defined(debugCmdTree): 483 | proc printCmdTree(cmd: CmdInfo, indent = 0) = 484 | let blanks = spaces(indent) 485 | echo blanks, "> ", cmd.name 486 | 487 | for opt in cmd.opts: 488 | if opt.kind == Discriminator: 489 | for subcmd in opt.subCmds: 490 | printCmdTree(subcmd, indent + 2) 491 | else: 492 | echo blanks, " - ", opt.name, ": ", opt.typename 493 | 494 | else: 495 | template printCmdTree(cmd: CmdInfo) = discard 496 | 497 | # TODO remove the overloads here to get better "missing overload" error message 498 | proc parseCmdArg*(T: type InputDir, p: string): T {.raises: [ValueError].} = 499 | if not dirExists(p): 500 | raise newException(ValueError, "Directory doesn't exist") 501 | 502 | T(p) 503 | 504 | proc parseCmdArg*(T: type InputFile, p: string): T {.raises: [ValueError].} = 505 | # TODO this is needed only because InputFile cannot be made 506 | # an alias of TypedInputFile at the moment, because of a generics 507 | # caching issue 508 | if not fileExists(p): 509 | raise newException(ValueError, "File doesn't exist") 510 | 511 | when not defined(nimscript): 512 | try: 513 | let f = system.open(p, fmRead) 514 | close f 515 | except IOError: 516 | raise newException(ValueError, "File not accessible") 517 | 518 | T(p) 519 | 520 | proc parseCmdArg*( 521 | T: type TypedInputFile, p: string): T {.raises: [ValueError].} = 522 | var path = p 523 | when T.defaultExt.len > 0: 524 | path = path.addFileExt(T.defaultExt) 525 | 526 | if not fileExists(path): 527 | raise newException(ValueError, "File doesn't exist") 528 | 529 | when not defined(nimscript): 530 | try: 531 | let f = system.open(path, fmRead) 532 | close f 533 | except IOError: 534 | raise newException(ValueError, "File not accessible") 535 | 536 | T(path) 537 | 538 | func parseCmdArg*(T: type[OutDir|OutFile|OutPath], p: string): T = 539 | T(p) 540 | 541 | proc parseCmdArg*[T]( 542 | _: type Option[T], s: string): Option[T] {.raises: [ValueError].} = 543 | some(parseCmdArg(T, s)) 544 | 545 | template parseCmdArg*(T: type string, s: string): string = 546 | s 547 | 548 | func parseCmdArg*( 549 | T: type SomeSignedInt, s: string): T {.raises: [ValueError].} = 550 | T parseBiggestInt(s) 551 | 552 | func parseCmdArg*( 553 | T: type SomeUnsignedInt, s: string): T {.raises: [ValueError].} = 554 | T parseBiggestUInt(s) 555 | 556 | func parseCmdArg*(T: type SomeFloat, p: string): T {.raises: [ValueError].} = 557 | parseFloat(p) 558 | 559 | func parseCmdArg*(T: type bool, p: string): T {.raises: [ValueError].} = 560 | try: 561 | p.len == 0 or parseBool(p) 562 | except CatchableError: 563 | raise newException(ValueError, "'" & p & "' is not a valid boolean value. Supported values are on/off, yes/no, true/false or 1/0") 564 | 565 | func parseCmdArg*(T: type enum, s: string): T {.raises: [ValueError].} = 566 | parseEnum[T](s) 567 | 568 | proc parseCmdArgAux(T: type, s: string): T {.raises: [ValueError].} = 569 | # The parseCmdArg procs are allowed to raise only `ValueError`. 570 | # If you have provided your own specializations, please handle 571 | # all other exception types. 572 | mixin parseCmdArg 573 | try: 574 | parseCmdArg(T, s) 575 | except CatchableError as exc: 576 | raise newException(ValueError, exc.msg) 577 | 578 | func completeCmdArg*(T: type enum, val: string): seq[string] = 579 | for e in low(T)..high(T): 580 | let as_str = $e 581 | if startsWithIgnoreStyle(as_str, val): 582 | result.add($e) 583 | 584 | func completeCmdArg*(T: type SomeNumber, val: string): seq[string] = 585 | @[] 586 | 587 | func completeCmdArg*(T: type bool, val: string): seq[string] = 588 | @[] 589 | 590 | func completeCmdArg*(T: type string, val: string): seq[string] = 591 | @[] 592 | 593 | proc completeCmdArg*(T: type[InputFile|TypedInputFile|InputDir|OutFile|OutDir|OutPath], 594 | val: string): seq[string] = 595 | when not defined(nimscript): 596 | let (dir, name, ext) = splitFile(val) 597 | let tail = name & ext 598 | # Expand the directory component for the directory walker routine 599 | let dir_path = if dir == "": "." else: expandTilde(dir) 600 | # Dotfiles are hidden unless the user entered a dot as prefix 601 | let show_dotfiles = len(name) > 0 and name[0] == '.' 602 | 603 | try: 604 | for kind, path in walkDir(dir_path, relative=true): 605 | if not show_dotfiles and path[0] == '.': 606 | continue 607 | 608 | # Do not show files if asked for directories, on the other hand we must show 609 | # directories even if a file is requested to allow the user to select a file 610 | # inside those 611 | if type(T) is (InputDir or OutDir) and kind notin {pcDir, pcLinkToDir}: 612 | continue 613 | 614 | # Note, no normalization is needed here 615 | if path.startsWith(tail): 616 | var match = dir_path / path 617 | # Add a trailing slash so that completions can be chained 618 | if kind in {pcDir, pcLinkToDir}: 619 | match &= DirSep 620 | 621 | result.add(shellPathEscape(match)) 622 | except OSError: 623 | discard 624 | 625 | func completeCmdArg*[T](_: type seq[T], val: string): seq[string] = 626 | @[] 627 | 628 | proc completeCmdArg*[T](_: type Option[T], val: string): seq[string] = 629 | mixin completeCmdArg 630 | return completeCmdArg(type(T), val) 631 | 632 | proc completeCmdArgAux(T: type, val: string): seq[string] = 633 | mixin completeCmdArg 634 | return completeCmdArg(T, val) 635 | 636 | template setField[T]( 637 | loc: var T, val: Option[string], defaultVal: untyped): untyped = 638 | type FieldType = type(loc) 639 | loc = if isSome(val): parseCmdArgAux(FieldType, val.get) 640 | else: FieldType(defaultVal) 641 | 642 | template setField[T]( 643 | loc: var seq[T], val: Option[string], defaultVal: untyped): untyped = 644 | if val.isSome: 645 | loc.add parseCmdArgAux(type(loc[0]), val.get) 646 | else: 647 | type FieldType = type(loc) 648 | loc = FieldType(defaultVal) 649 | 650 | func makeDefaultValue*(T: type): T = 651 | default(T) 652 | 653 | func requiresInput*(T: type): bool = 654 | not ((T is seq) or (T is Option) or (T is bool)) 655 | 656 | func acceptsMultipleValues*(T: type): bool = 657 | T is seq 658 | 659 | template debugMacroResult(macroName: string) {.dirty.} = 660 | when defined(debugMacros) or defined(debugConfutils): 661 | echo "\n-------- ", macroName, " ----------------------" 662 | echo result.repr 663 | 664 | func parseEnumNormalized[T: enum](s: string): T {.raises: [ValueError].} = 665 | # Note: In Nim 1.6 `parseEnum` normalizes the string except for the first 666 | # character. Nim 1.2 would normalize for all characters. In config options 667 | # the latter behaviour is required so this custom function is needed. 668 | genEnumCaseStmt(T, s, default = nil, ord(low(T)), ord(high(T)), normalize) 669 | 670 | proc generateFieldSetters(RecordType: NimNode): NimNode = 671 | var recordDef = getImpl(RecordType) 672 | let makeDefaultValue = bindSym"makeDefaultValue" 673 | 674 | result = newTree(nnkStmtListExpr) 675 | var settersArray = newTree(nnkBracket) 676 | 677 | for field in recordFields(recordDef): 678 | var 679 | setterName = ident($field.name & "Setter") 680 | fieldName = field.name 681 | namePragma = field.readPragma"name" 682 | paramName = if namePragma != nil: namePragma 683 | else: fieldName 684 | configVar = ident "config" 685 | configField = newTree(nnkDotExpr, configVar, fieldName) 686 | defaultValue = field.readPragma"defaultValue" 687 | completerName = ident($field.name & "Complete") 688 | 689 | if defaultValue == nil: 690 | defaultValue = newCall(makeDefaultValue, newTree(nnkTypeOfExpr, configField)) 691 | 692 | # TODO: This shouldn't be necessary. The type symbol returned from Nim should 693 | # be typed as a tyTypeDesc[tyString] instead of just `tyString`. To be filed. 694 | var fixedFieldType = newTree(nnkTypeOfExpr, field.typ) 695 | 696 | settersArray.add newTree(nnkTupleConstr, 697 | newLit($paramName), 698 | setterName, completerName, 699 | newCall(bindSym"requiresInput", fixedFieldType), 700 | newCall(bindSym"acceptsMultipleValues", fixedFieldType)) 701 | 702 | result.add quote do: 703 | {.push hint[XCannotRaiseY]: off.} 704 | 705 | result.add quote do: 706 | proc `completerName`(val: string): seq[string] {. 707 | nimcall 708 | gcsafe 709 | sideEffect 710 | raises: [] 711 | .} = 712 | return completeCmdArgAux(`fixedFieldType`, val) 713 | 714 | proc `setterName`(`configVar`: var `RecordType`, val: Option[string]) {. 715 | nimcall 716 | gcsafe 717 | sideEffect 718 | raises: [ValueError] 719 | .} = 720 | when `configField` is enum: 721 | # TODO: For some reason, the normal `setField` rejects enum fields 722 | # when they are used as case discriminators. File this as a bug. 723 | if isSome(val): 724 | `configField` = parseEnumNormalized[type(`configField`)](val.get) 725 | else: 726 | `configField` = `defaultValue` 727 | else: 728 | setField(`configField`, val, `defaultValue`) 729 | 730 | result.add quote do: 731 | {.pop.} 732 | 733 | result.add settersArray 734 | debugMacroResult "Field Setters" 735 | 736 | func checkDuplicate(cmd: CmdInfo, opt: OptInfo, fieldName: NimNode) = 737 | for x in cmd.opts: 738 | if opt.name == x.name: 739 | error "duplicate name detected: " & opt.name, fieldName 740 | if opt.abbr.len > 0 and opt.abbr == x.abbr: 741 | error "duplicate abbr detected: " & opt.abbr, fieldName 742 | 743 | func validPath(path: var seq[CmdInfo], parent, node: CmdInfo): bool = 744 | for x in parent.opts: 745 | if x.kind != Discriminator: continue 746 | for y in x.subCmds: 747 | if y == node: 748 | path.add y 749 | return true 750 | if validPath(path, y, node): 751 | path.add y 752 | return true 753 | false 754 | 755 | func findPath(parent, node: CmdInfo): seq[CmdInfo] = 756 | # find valid path from parent to node 757 | result = newSeq[CmdInfo]() 758 | doAssert validPath(result, parent, node) 759 | result.add parent 760 | 761 | func toText(n: NimNode): string = 762 | if n == nil: "" 763 | elif n.kind in {nnkStrLit..nnkTripleStrLit}: n.strVal 764 | else: repr(n) 765 | 766 | proc cmdInfoFromType(T: NimNode): CmdInfo = 767 | result = CmdInfo() 768 | 769 | var 770 | recordDef = getImpl(T) 771 | discriminatorFields = newSeq[OptInfo]() 772 | fieldIdx = 0 773 | 774 | for field in recordFields(recordDef): 775 | let 776 | isImplicitlySelectable = field.readPragma"implicitlySelectable" != nil 777 | defaultValue = field.readPragma"defaultValue" 778 | defaultValueDesc = field.readPragma"defaultValueDesc" 779 | defaultInHelp = if defaultValueDesc != nil: defaultValueDesc 780 | else: defaultValue 781 | defaultInHelpText = toText(defaultInHelp) 782 | separator = field.readPragma"separator" 783 | longDesc = field.readPragma"longDesc" 784 | 785 | isHidden = field.readPragma("hidden") != nil 786 | abbr = field.readPragma"abbr" 787 | name = field.readPragma"name" 788 | desc = field.readPragma"desc" 789 | optKind = if field.isDiscriminator: Discriminator 790 | elif field.readPragma("argument") != nil: Arg 791 | else: CliSwitch 792 | 793 | var opt = OptInfo(kind: optKind, 794 | idx: fieldIdx, 795 | name: $field.name, 796 | isHidden: isHidden, 797 | hasDefault: defaultValue != nil, 798 | defaultInHelpText: defaultInHelpText, 799 | typename: field.typ.repr) 800 | 801 | if desc != nil: opt.desc = desc.strVal 802 | if name != nil: opt.name = name.strVal 803 | if abbr != nil: opt.abbr = abbr.strVal 804 | if separator != nil: opt.separator = separator.strVal 805 | if longDesc != nil: opt.longDesc = longDesc.strVal 806 | 807 | inc fieldIdx 808 | 809 | if field.isDiscriminator: 810 | discriminatorFields.add opt 811 | let cmdType = field.typ.getImpl[^1] 812 | if cmdType.kind != nnkEnumTy: 813 | error "Only enums are supported as case object discriminators", field.name 814 | 815 | opt.isImplicitlySelectable = isImplicitlySelectable 816 | opt.isCommand = field.readPragma"command" != nil 817 | 818 | for i in 1 ..< cmdType.len: 819 | let enumVal = cmdType[i] 820 | var name, desc: string 821 | if enumVal.kind == nnkEnumFieldDef: 822 | name = $enumVal[0] 823 | desc = $enumVal[1] 824 | else: 825 | name = $enumVal 826 | if defaultValue != nil and eqIdent(name, defaultValue): 827 | opt.defaultSubCmd = i - 1 828 | opt.subCmds.add CmdInfo(name: name, desc: desc) 829 | 830 | if defaultValue == nil: 831 | opt.defaultSubCmd = -1 832 | else: 833 | if opt.defaultSubCmd == -1: 834 | error "The default value is not a valid enum value", defaultValue 835 | 836 | if field.caseField != nil and field.caseBranch != nil: 837 | let fieldName = field.caseField.getFieldName 838 | var discriminator = findOpt(discriminatorFields, $fieldName) 839 | 840 | if discriminator == nil: 841 | error "Unable to find " & $fieldName 842 | 843 | if field.caseBranch.kind == nnkElse: 844 | error "Sub-command parameters cannot appear in an else branch. " & 845 | "Please specify the sub-command branch precisely", field.caseBranch[0] 846 | 847 | var branchEnumVal = field.caseBranch[0] 848 | if branchEnumVal.kind == nnkDotExpr: 849 | branchEnumVal = branchEnumVal[1] 850 | var cmd = findCmd(discriminator.subCmds, $branchEnumVal) 851 | # we respect subcommand hierarchy when looking for duplicate 852 | let path = findPath(result, cmd) 853 | for n in path: 854 | checkDuplicate(n, opt, field.name) 855 | 856 | # the reason we check for `ignore` pragma here and not using `continue` statement 857 | # is we do respect option hierarchy of subcommands 858 | if field.readPragma("ignore") == nil: 859 | cmd.opts.add opt 860 | 861 | else: 862 | checkDuplicate(result, opt, field.name) 863 | 864 | if field.readPragma("ignore") == nil: 865 | result.opts.add opt 866 | 867 | macro configurationRtti(RecordType: type): untyped = 868 | let 869 | T = RecordType.getType[1] 870 | cmdInfo = cmdInfoFromType T 871 | fieldSetters = generateFieldSetters T 872 | 873 | result = newTree(nnkPar, newLitFixed cmdInfo, fieldSetters) 874 | 875 | when hasSerialization: 876 | proc addConfigFile*(secondarySources: auto, 877 | Format: type, 878 | path: InputFile) {.raises: [ConfigurationError].} = 879 | try: 880 | secondarySources.data.add loadFile(Format, string path, 881 | type(secondarySources.data[0])) 882 | except SerializationError as err: 883 | raise newException(ConfigurationError, err.formatMsg(string path), err) 884 | except IOError as err: 885 | raise newException(ConfigurationError, 886 | "Failed to read config file at '" & string(path) & "': " & err.msg) 887 | 888 | proc addConfigFileContent*(secondarySources: auto, 889 | Format: type, 890 | content: string) {.raises: [ConfigurationError].} = 891 | try: 892 | secondarySources.data.add decode(Format, content, 893 | type(secondarySources.data[0])) 894 | except SerializationError as err: 895 | raise newException(ConfigurationError, err.formatMsg(""), err) 896 | except IOError: 897 | raiseAssert "This should not be possible" 898 | 899 | func constructEnvKey*(prefix: string, key: string): string {.raises: [].} = 900 | ## Generates env. variable names from keys and prefix following the 901 | ## IEEE Open Group env. variable spec: https://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html 902 | (prefix & "_" & key).toUpperAscii.multiReplace(("-", "_"), (" ", "_")) 903 | 904 | # On Posix there is no portable way to get the command 905 | # line from a DLL and thus the proc isn't defined in this environment. 906 | # See https://nim-lang.org/docs/os.html#commandLineParams 907 | when not declared(commandLineParams): 908 | proc commandLineParams(): seq[string] = discard 909 | 910 | proc loadImpl[C, SecondarySources]( 911 | Configuration: typedesc[C], 912 | cmdLine = commandLineParams(), 913 | version = "", 914 | copyrightBanner = "", 915 | printUsage = true, 916 | quitOnFailure = true, 917 | secondarySourcesRef: ref SecondarySources, 918 | secondarySources: proc ( 919 | config: Configuration, sources: ref SecondarySources 920 | ) {.gcsafe, raises: [ConfigurationError].} = nil, 921 | envVarsPrefix = appInvocation() 922 | ): Configuration {.raises: [ConfigurationError].} = 923 | ## Loads a program configuration by parsing command-line arguments 924 | ## and a standard set of config files that can specify: 925 | ## 926 | ## - working directory settings 927 | ## - user settings 928 | ## - system-wide setttings 929 | ## 930 | ## Supports multiple config files format (INI/TOML, YAML, JSON). 931 | 932 | # This is an initial naive implementation that will be improved 933 | # over time. 934 | let (rootCmd, fieldSetters) = configurationRtti(Configuration) 935 | var fieldCounters: array[fieldSetters.len, int] 936 | 937 | printCmdTree rootCmd 938 | 939 | var activeCmds = @[rootCmd] 940 | template lastCmd: auto = activeCmds[^1] 941 | var nextArgIdx = lastCmd.getNextArgIdx(-1) 942 | 943 | var help = "" 944 | 945 | proc suggestCallingHelp = 946 | errorOutput "Try ", fgCommand, appInvocation() & " --help" 947 | errorOutput " for more information.\p" 948 | flushOutputAndQuit QuitFailure 949 | 950 | template fail(args: varargs[untyped]): untyped = 951 | if quitOnFailure: 952 | errorOutput args 953 | errorOutput "\p" 954 | suggestCallingHelp() 955 | else: 956 | # TODO: populate this string 957 | raise newException(ConfigurationError, "") 958 | 959 | template applySetter( 960 | conf: Configuration, setterIdx: int, cmdLineVal: string): untyped = 961 | when defined(nimHasWarnBareExcept): 962 | {.push warning[BareExcept]:off.} 963 | 964 | try: 965 | fieldSetters[setterIdx][1](conf, some(cmdLineVal)) 966 | inc fieldCounters[setterIdx] 967 | except: 968 | fail("Error while processing the ", 969 | fgOption, fieldSetters[setterIdx][0], 970 | "=", cmdLineVal, resetStyle, " parameter: ", 971 | getCurrentExceptionMsg()) 972 | 973 | when defined(nimHasWarnBareExcept): 974 | {.pop.} 975 | 976 | when hasCompletions: 977 | template getArgCompletions(opt: OptInfo, prefix: string): seq[string] = 978 | fieldSetters[opt.idx][2](prefix) 979 | 980 | template required(opt: OptInfo): bool = 981 | fieldSetters[opt.idx][3] and not opt.hasDefault 982 | 983 | template activateCmd( 984 | conf: Configuration, discriminator: OptInfo, activatedCmd: CmdInfo) = 985 | let cmd = activatedCmd 986 | conf.applySetter(discriminator.idx, if cmd.desc.len > 0: cmd.desc 987 | else: cmd.name) 988 | activeCmds.add cmd 989 | nextArgIdx = cmd.getNextArgIdx(-1) 990 | 991 | when hasCompletions: 992 | type 993 | ArgKindFilter = enum 994 | argName 995 | argAbbr 996 | 997 | proc showMatchingOptions(cmd: CmdInfo, prefix: string, filterKind: set[ArgKindFilter]) = 998 | var matchingOptions: seq[OptInfo] 999 | 1000 | if len(prefix) > 0: 1001 | # Filter the options according to the input prefix 1002 | for opt in cmd.opts: 1003 | if argName in filterKind and len(opt.name) > 0: 1004 | if startsWithIgnoreStyle(opt.name, prefix): 1005 | matchingOptions.add(opt) 1006 | if argAbbr in filterKind and len(opt.abbr) > 0: 1007 | if startsWithIgnoreStyle(opt.abbr, prefix): 1008 | matchingOptions.add(opt) 1009 | else: 1010 | matchingOptions = cmd.opts 1011 | 1012 | for opt in matchingOptions: 1013 | # The trailing '=' means the switch accepts an argument 1014 | let trailing = if opt.typename != "bool": "=" else: "" 1015 | 1016 | if argName in filterKind and len(opt.name) > 0: 1017 | try: 1018 | stdout.writeLine("--", opt.name, trailing) 1019 | except IOError: 1020 | discard 1021 | if argAbbr in filterKind and len(opt.abbr) > 0: 1022 | try: 1023 | stdout.writeLine('-', opt.abbr, trailing) 1024 | except IOError: 1025 | discard 1026 | 1027 | let completion = splitCompletionLine() 1028 | # If we're not asked to complete a command line the result is an empty list 1029 | if len(completion) != 0: 1030 | var cmdStack = @[rootCmd] 1031 | # Try to understand what the active chain of commands is without parsing the 1032 | # whole command line 1033 | for tok in completion[1..^1]: 1034 | if not tok.startsWith('-'): 1035 | let subCmd = findSubCmd(cmdStack[^1], tok) 1036 | if subCmd != nil: cmdStack.add(subCmd) 1037 | 1038 | let cur_word = completion[^1] 1039 | let prev_word = if len(completion) > 2: completion[^2] else: "" 1040 | let prev_prev_word = if len(completion) > 3: completion[^3] else: "" 1041 | 1042 | if cur_word.startsWith('-'): 1043 | # Show all the options matching the prefix input by the user 1044 | let isFullName = cur_word.startsWith("--") 1045 | var option_word = cur_word 1046 | option_word.removePrefix('-') 1047 | 1048 | for i in countdown(cmdStack.len - 1, 0): 1049 | let argFilter = 1050 | if isFullName: 1051 | {argName} 1052 | elif len(cur_word) > 1: 1053 | # If the user entered a single hypen then we show both long & short 1054 | # variants 1055 | {argAbbr} 1056 | else: 1057 | {argName, argAbbr} 1058 | 1059 | showMatchingOptions(cmdStack[i], option_word, argFilter) 1060 | elif (prev_word.startsWith('-') or 1061 | (prev_word == "=" and prev_prev_word.startsWith('-'))): 1062 | # Handle cases where we want to complete a switch choice 1063 | # -switch 1064 | # -switch= 1065 | var option_word = if len(prev_word) == 1: prev_prev_word else: prev_word 1066 | option_word.removePrefix('-') 1067 | 1068 | let opt = findOpt(cmdStack, option_word) 1069 | if opt != nil: 1070 | for arg in getArgCompletions(opt, cur_word): 1071 | try: 1072 | stdout.writeLine(arg) 1073 | except IOError: 1074 | discard 1075 | elif cmdStack[^1].hasSubCommands: 1076 | # Show all the available subcommands 1077 | for subCmd in subCmds(cmdStack[^1]): 1078 | if startsWithIgnoreStyle(subCmd.name, cur_word): 1079 | try: 1080 | stdout.writeLine(subCmd.name) 1081 | except IOError: 1082 | discard 1083 | else: 1084 | # Full options listing 1085 | for i in countdown(cmdStack.len - 1, 0): 1086 | showMatchingOptions(cmdStack[i], "", {argName, argAbbr}) 1087 | 1088 | stdout.flushFile() 1089 | 1090 | return 1091 | 1092 | proc lazyHelpAppInfo: HelpAppInfo = 1093 | HelpAppInfo( 1094 | copyrightBanner: copyrightBanner, 1095 | appInvocation: appInvocation()) 1096 | 1097 | template processHelpAndVersionOptions(optKey: string) = 1098 | let key = optKey 1099 | if cmpIgnoreStyle(key, "help") == 0: 1100 | help.showHelp lazyHelpAppInfo(), activeCmds 1101 | elif version.len > 0 and cmpIgnoreStyle(key, "version") == 0: 1102 | help.helpOutput version, "\p" 1103 | flushOutputAndQuit QuitSuccess 1104 | 1105 | for kind, key, val in getopt(cmdLine): 1106 | when key isnot string: 1107 | let key = string(key) 1108 | case kind 1109 | of cmdLongOption, cmdShortOption: 1110 | processHelpAndVersionOptions key 1111 | 1112 | var opt = findOpt(activeCmds, key) 1113 | if opt == nil: 1114 | # We didn't find the option. 1115 | # Check if it's from the default command and activate it if necessary: 1116 | let subCmdDiscriminator = lastCmd.getSubCmdDiscriminator 1117 | if subCmdDiscriminator != nil: 1118 | if subCmdDiscriminator.defaultSubCmd != -1: 1119 | let defaultCmd = subCmdDiscriminator.subCmds[subCmdDiscriminator.defaultSubCmd] 1120 | opt = findOpt(defaultCmd.opts, key) 1121 | if opt != nil: 1122 | result.activateCmd(subCmdDiscriminator, defaultCmd) 1123 | else: 1124 | discard 1125 | 1126 | if opt != nil: 1127 | result.applySetter(opt.idx, val) 1128 | else: 1129 | fail "Unrecognized option '" & key & "'" 1130 | 1131 | of cmdArgument: 1132 | if lastCmd.hasSubCommands: 1133 | processHelpAndVersionOptions key 1134 | 1135 | block processArg: 1136 | let subCmdDiscriminator = lastCmd.getSubCmdDiscriminator 1137 | if subCmdDiscriminator != nil: 1138 | let subCmd = findCmd(subCmdDiscriminator.subCmds, key) 1139 | if subCmd != nil: 1140 | result.activateCmd(subCmdDiscriminator, subCmd) 1141 | break processArg 1142 | 1143 | if nextArgIdx == -1: 1144 | fail lastCmd.noMoreArgsError 1145 | 1146 | result.applySetter(nextArgIdx, key) 1147 | 1148 | if not fieldSetters[nextArgIdx][4]: 1149 | nextArgIdx = lastCmd.getNextArgIdx(nextArgIdx) 1150 | 1151 | else: 1152 | discard 1153 | 1154 | let subCmdDiscriminator = lastCmd.getSubCmdDiscriminator 1155 | if subCmdDiscriminator != nil and 1156 | subCmdDiscriminator.defaultSubCmd != -1 and 1157 | fieldCounters[subCmdDiscriminator.idx] == 0: 1158 | let defaultCmd = subCmdDiscriminator.subCmds[subCmdDiscriminator.defaultSubCmd] 1159 | result.activateCmd(subCmdDiscriminator, defaultCmd) 1160 | 1161 | # https://github.com/status-im/nim-confutils/pull/109#discussion_r1820076739 1162 | if not isNil(secondarySources): # Nim v2.0.10: `!= nil` broken in nimscript 1163 | try: 1164 | secondarySources(result, secondarySourcesRef) 1165 | except ConfigurationError as err: 1166 | fail "Failed to load secondary sources: '" & err.msg & "'" 1167 | 1168 | proc processMissingOpts( 1169 | conf: var Configuration, cmd: CmdInfo) {.raises: [ConfigurationError].} = 1170 | for opt in cmd.opts: 1171 | if fieldCounters[opt.idx] == 0: 1172 | let envKey = constructEnvKey(envVarsPrefix, opt.name) 1173 | 1174 | try: 1175 | if existsEnv(envKey): 1176 | let envContent = getEnv(envKey) 1177 | conf.applySetter(opt.idx, envContent) 1178 | elif secondarySourcesRef.setters[opt.idx](conf, secondarySourcesRef): 1179 | # all work is done in the config file setter, 1180 | # there is nothing left to do here. 1181 | discard 1182 | elif opt.hasDefault: 1183 | fieldSetters[opt.idx][1](conf, none[string]()) 1184 | elif opt.required: 1185 | fail "The required option '" & opt.name & "' was not specified" 1186 | except ValueError as err: 1187 | fail "Option '" & opt.name & "' failed to parse: '" & err.msg & "'" 1188 | 1189 | for cmd in activeCmds: 1190 | result.processMissingOpts(cmd) 1191 | 1192 | template load*( 1193 | Configuration: type, 1194 | cmdLine = commandLineParams(), 1195 | version = "", 1196 | copyrightBanner = "", 1197 | printUsage = true, 1198 | quitOnFailure = true, 1199 | secondarySources: untyped = nil, 1200 | envVarsPrefix = appInvocation()): untyped = 1201 | block: 1202 | let secondarySourcesRef = generateSecondarySources(Configuration) 1203 | loadImpl(Configuration, cmdLine, version, 1204 | copyrightBanner, printUsage, quitOnFailure, 1205 | secondarySourcesRef, secondarySources, envVarsPrefix) 1206 | 1207 | func defaults*(Configuration: type): Configuration = 1208 | load(Configuration, cmdLine = @[], printUsage = false, quitOnFailure = false) 1209 | 1210 | proc dispatchImpl(cliProcSym, cliArgs, loadArgs: NimNode): NimNode = 1211 | # Here, we'll create a configuration object with fields matching 1212 | # the CLI proc params. We'll also generate a call to the designated proc 1213 | let configType = genSym(nskType, "CliConfig") 1214 | let configFields = newTree(nnkRecList) 1215 | let configVar = genSym(nskLet, "config") 1216 | var dispatchCall = newCall(cliProcSym) 1217 | 1218 | # The return type of the proc is skipped over 1219 | for i in 1 ..< cliArgs.len: 1220 | var arg = copy cliArgs[i] 1221 | 1222 | # If an argument doesn't specify a type, we infer it from the default value 1223 | if arg[1].kind == nnkEmpty: 1224 | if arg[2].kind == nnkEmpty: 1225 | error "Please provide either a default value or type of the parameter", arg 1226 | arg[1] = newCall(bindSym"typeof", arg[2]) 1227 | 1228 | # Turn any default parameters into the confutils's `defaultValue` pragma 1229 | if arg[2].kind != nnkEmpty: 1230 | if arg[0].kind != nnkPragmaExpr: 1231 | arg[0] = newTree(nnkPragmaExpr, arg[0], newTree(nnkPragma)) 1232 | arg[0][1].add newColonExpr(bindSym"defaultValue", arg[2]) 1233 | arg[2] = newEmptyNode() 1234 | 1235 | configFields.add arg 1236 | dispatchCall.add newTree(nnkDotExpr, configVar, skipPragma arg[0]) 1237 | 1238 | let cliConfigType = nnkTypeSection.newTree( 1239 | nnkTypeDef.newTree( 1240 | configType, 1241 | newEmptyNode(), 1242 | nnkObjectTy.newTree( 1243 | newEmptyNode(), 1244 | newEmptyNode(), 1245 | configFields))) 1246 | 1247 | var loadConfigCall = newCall(bindSym"load", configType) 1248 | for p in loadArgs: loadConfigCall.add p 1249 | 1250 | result = quote do: 1251 | `cliConfigType` 1252 | let `configVar` = `loadConfigCall` 1253 | `dispatchCall` 1254 | 1255 | macro dispatch*(fn: typed, args: varargs[untyped]): untyped = 1256 | if fn.kind != nnkSym or 1257 | fn.symKind notin {nskProc, nskFunc, nskMacro, nskTemplate}: 1258 | error "The first argument to `confutils.dispatch` should be a callable symbol" 1259 | 1260 | let fnImpl = fn.getImpl 1261 | result = dispatchImpl(fnImpl.name, fnImpl.params, args) 1262 | debugMacroResult "Dispatch Code" 1263 | 1264 | macro cli*(args: varargs[untyped]): untyped = 1265 | if args.len == 0: 1266 | error "The cli macro expects a do block", args 1267 | 1268 | let doBlock = args[^1] 1269 | if doBlock.kind notin {nnkDo, nnkLambda}: 1270 | error "The last argument to `confutils.cli` should be a do block", doBlock 1271 | 1272 | args.del(args.len - 1) 1273 | 1274 | # Create a new anonymous proc we'll dispatch to 1275 | let cliProcName = genSym(nskProc, "CLI") 1276 | var cliProc = newTree(nnkProcDef, cliProcName) 1277 | # Copy everything but the name from the do block: 1278 | for i in 1 ..< doBlock.len: cliProc.add doBlock[i] 1279 | 1280 | # Generate the final code 1281 | result = newStmtList(cliProc, dispatchImpl(cliProcName, cliProc.params, args)) 1282 | 1283 | # TODO: remove this once Nim supports custom pragmas on proc params 1284 | for p in cliProc.params: 1285 | if p.kind == nnkEmpty: continue 1286 | p[0] = skipPragma p[0] 1287 | 1288 | debugMacroResult "CLI Code" 1289 | 1290 | func load*(f: TypedInputFile): f.ContentType = 1291 | when f.Format is Unspecified or f.ContentType is Unspecified: 1292 | {.fatal: "To use `InputFile.load`, please specify the Format and ContentType of the file".} 1293 | 1294 | when f.Format is Txt: 1295 | # TODO: implement a proper Txt serialization format 1296 | mixin init 1297 | f.ContentType.init readFile(f.string).string 1298 | else: 1299 | mixin loadFile 1300 | loadFile(f.Format, f.string, f.ContentType) 1301 | 1302 | {.pop.} 1303 | -------------------------------------------------------------------------------- /confutils.nimble: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import os, strutils 11 | mode = ScriptMode.Verbose 12 | 13 | packageName = "confutils" 14 | version = "0.1.0" 15 | author = "Status Research & Development GmbH" 16 | description = "Simplified handling of command line options and config files" 17 | license = "Apache License 2.0" 18 | skipDirs = @["tests"] 19 | 20 | requires "nim >= 1.6.0", 21 | "stew", 22 | "serialization" 23 | 24 | let nimc = getEnv("NIMC", "nim") # Which nim compiler to use 25 | let lang = getEnv("NIMLANG", "c") # Which backend (c/cpp/js) 26 | let flags = getEnv("NIMFLAGS", "") # Extra flags for the compiler 27 | let verbose = getEnv("V", "") notin ["", "0"] 28 | 29 | let cfg = 30 | " --styleCheck:usages --styleCheck:error" & 31 | (if verbose: "" else: " --verbosity:0 --hints:off") & 32 | " --skipParentCfg --skipUserCfg --outdir:build --nimcache:build/nimcache -f" 33 | 34 | proc build(args, path: string) = 35 | exec nimc & " " & lang & " " & cfg & " " & flags & " " & args & " " & path 36 | 37 | proc run(args, path: string) = 38 | build args & " --mm:refc -r", path 39 | if (NimMajor, NimMinor) > (1, 6): 40 | build args & " --mm:orc -r", path 41 | 42 | task test, "Run all tests": 43 | for threads in ["--threads:off", "--threads:on"]: 44 | run threads, "tests/test_all" 45 | build threads, "tests/test_duplicates" 46 | 47 | #Also iterate over every test in tests/fail, and verify they fail to compile. 48 | echo "\r\nTest Fail to Compile:" 49 | for path in listFiles(thisDir() / "tests" / "fail"): 50 | if not path.endsWith(".nim"): 51 | continue 52 | if gorgeEx(nimc & " " & lang & " " & flags & " " & path).exitCode != 0: 53 | echo " [OK] ", path.split(DirSep)[^1] 54 | else: 55 | echo " [FAILED] ", path.split(DirSep)[^1] 56 | quit(QuitFailure) 57 | 58 | echo "\r\nNimscript test:" 59 | let 60 | actualOutput = gorgeEx( 61 | nimc & " --verbosity:0 e " & flags & " " & "./tests/cli_example.nim " & 62 | "--foo=1 --bar=2 --withBaz 42").output 63 | expectedOutput = unindent""" 64 | foo = 1 65 | bar = 2 66 | baz = true 67 | arg ./tests/cli_example.nim 68 | arg 42""" 69 | if actualOutput.strip() == expectedOutput: 70 | echo " [OK] tests/cli_example.nim" 71 | else: 72 | echo " [FAILED] tests/cli_example.nim" 73 | echo actualOutput 74 | quit(QuitFailure) 75 | -------------------------------------------------------------------------------- /confutils/cli_parser.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | # Parts taken from Nim's Runtime Library (c) Copyright 2015 Andreas Rumpf 11 | 12 | type 13 | CmdLineKind* = enum ## The detected command line token. 14 | cmdEnd, ## End of command line reached 15 | cmdArgument, ## An argument such as a filename 16 | cmdLongOption, ## A long option such as --option 17 | cmdShortOption ## A short option such as -c 18 | 19 | OptParser* = object of RootObj ## Implementation of the command line parser. 20 | pos*: int 21 | inShortState: bool 22 | allowWhitespaceAfterColon: bool 23 | shortNoVal: set[char] 24 | longNoVal: seq[string] 25 | cmds: seq[string] 26 | idx: int 27 | kind*: CmdLineKind ## The detected command line token 28 | key*, val*: string ## Key and value pair; the key is the option 29 | ## or the argument, and the value is not "" if 30 | ## the option was given a value 31 | 32 | {.push gcsafe, raises: [].} 33 | 34 | func parseWord(s: string, i: int, w: var string, 35 | delim: set[char] = {'\t', ' '}): int = 36 | result = i 37 | if result < s.len and s[result] == '\"': 38 | inc(result) 39 | while result < s.len: 40 | if s[result] == '"': 41 | inc result 42 | break 43 | add(w, s[result]) 44 | inc(result) 45 | else: 46 | while result < s.len and s[result] notin delim: 47 | add(w, s[result]) 48 | inc(result) 49 | 50 | func initOptParser*(cmds: seq[string], shortNoVal: set[char]={}, 51 | longNoVal: seq[string] = @[]; 52 | allowWhitespaceAfterColon = true): OptParser = 53 | result.pos = 0 54 | result.idx = 0 55 | result.inShortState = false 56 | result.shortNoVal = shortNoVal 57 | result.longNoVal = longNoVal 58 | result.allowWhitespaceAfterColon = allowWhitespaceAfterColon 59 | result.cmds = cmds 60 | result.kind = cmdEnd 61 | result.key = "" 62 | result.val = "" 63 | 64 | func handleShortOption(p: var OptParser; cmd: string) = 65 | var i = p.pos 66 | p.kind = cmdShortOption 67 | if i < cmd.len: 68 | add(p.key, cmd[i]) 69 | inc(i) 70 | p.inShortState = true 71 | while i < cmd.len and cmd[i] in {'\t', ' '}: 72 | inc(i) 73 | p.inShortState = false 74 | if i < cmd.len and cmd[i] in {':', '='} or 75 | card(p.shortNoVal) > 0 and p.key[0] notin p.shortNoVal: 76 | if i < cmd.len and cmd[i] in {':', '='}: 77 | inc(i) 78 | p.inShortState = false 79 | while i < cmd.len and cmd[i] in {'\t', ' '}: inc(i) 80 | p.val = substr(cmd, i) 81 | p.pos = 0 82 | inc p.idx 83 | else: 84 | p.pos = i 85 | if i >= cmd.len: 86 | p.inShortState = false 87 | p.pos = 0 88 | inc p.idx 89 | 90 | func next*(p: var OptParser) = 91 | ## Parses the next token. 92 | ## 93 | ## ``p.kind`` describes what kind of token has been parsed. ``p.key`` and 94 | ## ``p.val`` are set accordingly. 95 | if p.idx >= p.cmds.len: 96 | p.kind = cmdEnd 97 | return 98 | 99 | var i = p.pos 100 | while i < p.cmds[p.idx].len and p.cmds[p.idx][i] in {'\t', ' '}: inc(i) 101 | p.pos = i 102 | setLen(p.key, 0) 103 | setLen(p.val, 0) 104 | if p.inShortState: 105 | p.inShortState = false 106 | if i >= p.cmds[p.idx].len: 107 | inc(p.idx) 108 | p.pos = 0 109 | if p.idx >= p.cmds.len: 110 | p.kind = cmdEnd 111 | return 112 | else: 113 | handleShortOption(p, p.cmds[p.idx]) 114 | return 115 | 116 | if i < p.cmds[p.idx].len and p.cmds[p.idx][i] == '-': 117 | inc(i) 118 | if i < p.cmds[p.idx].len and p.cmds[p.idx][i] == '-': 119 | p.kind = cmdLongOption 120 | inc(i) 121 | i = parseWord(p.cmds[p.idx], i, p.key, {' ', '\t', ':', '='}) 122 | while i < p.cmds[p.idx].len and p.cmds[p.idx][i] in {'\t', ' '}: inc(i) 123 | if i < p.cmds[p.idx].len and p.cmds[p.idx][i] in {':', '='}: 124 | inc(i) 125 | while i < p.cmds[p.idx].len and p.cmds[p.idx][i] in {'\t', ' '}: inc(i) 126 | # if we're at the end, use the next command line option: 127 | if p.allowWhitespaceAfterColon and i >= p.cmds[p.idx].len and 128 | p.idx + 1 < p.cmds.len and p.cmds[p.idx + 1][0] != '-': 129 | inc p.idx 130 | i = 0 131 | if p.idx < p.cmds.len: 132 | p.val = p.cmds[p.idx].substr(i) 133 | elif len(p.longNoVal) > 0 and p.key notin p.longNoVal and p.idx+1 < p.cmds.len: 134 | p.val = p.cmds[p.idx+1] 135 | inc p.idx 136 | else: 137 | p.val = "" 138 | inc p.idx 139 | p.pos = 0 140 | else: 141 | p.pos = i 142 | handleShortOption(p, p.cmds[p.idx]) 143 | else: 144 | p.kind = cmdArgument 145 | p.key = p.cmds[p.idx] 146 | inc p.idx 147 | p.pos = 0 148 | 149 | iterator getopt*(p: var OptParser): tuple[kind: CmdLineKind, key, val: string] = 150 | p.pos = 0 151 | p.idx = 0 152 | while true: 153 | next(p) 154 | if p.kind == cmdEnd: break 155 | yield (p.kind, p.key, p.val) 156 | 157 | iterator getopt*(cmds: seq[string], 158 | shortNoVal: set[char]={}, longNoVal: seq[string] = @[]): 159 | tuple[kind: CmdLineKind, key, val: string] = 160 | var p = initOptParser(cmds, shortNoVal=shortNoVal, longNoVal=longNoVal) 161 | while true: 162 | next(p) 163 | if p.kind == cmdEnd: break 164 | yield (p.kind, p.key, p.val) 165 | 166 | {.pop.} 167 | -------------------------------------------------------------------------------- /confutils/cli_parsing_fuzzer.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import 11 | strutils, 12 | stew/byteutils, testutils/fuzzing, 13 | ../confutils 14 | 15 | {.push gcsafe, raises: [].} 16 | 17 | template fuzzCliParsing*(Conf: type) = 18 | test: 19 | block: 20 | try: 21 | let cfg = Conf.load(cmdLine = split(fromBytes(string, payload)), 22 | printUsage = false, 23 | quitOnFailure = false) 24 | except ConfigurationError as err: 25 | discard 26 | 27 | {.pop.} 28 | -------------------------------------------------------------------------------- /confutils/config_file.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import 11 | std/[tables, macrocache], 12 | stew/shims/macros 13 | 14 | {.warning[UnusedImport]:off.} 15 | import 16 | std/typetraits, 17 | ./defs 18 | 19 | #[ 20 | Overview of this module: 21 | - Create temporary configuration object with all fields optional. 22 | - Load this temporary object from every registered config files 23 | including env vars and windows regs if available. 24 | - If the CLI parser detect missing opt, it will try to obtain 25 | the value from temporary object starting from the first registered 26 | config file format. 27 | - If none of them have the missing value, it will load the default value 28 | from `defaultValue` pragma. 29 | ]# 30 | 31 | type 32 | ConfFileSection = ref object 33 | children: seq[ConfFileSection] 34 | fieldName: string 35 | namePragma: string 36 | typ: NimNode 37 | defaultValue: string 38 | isCommandOrArgument: bool 39 | isCaseBranch: bool 40 | isDiscriminator: bool 41 | 42 | GeneratedFieldInfo = tuple 43 | isCommandOrArgument: bool 44 | path: seq[string] 45 | 46 | OriginalToGeneratedFields = OrderedTable[string, GeneratedFieldInfo] 47 | 48 | {.push gcsafe, raises: [].} 49 | 50 | func isOption(n: NimNode): bool = 51 | if n.kind != nnkBracketExpr: return false 52 | eqIdent(n[0], "Option") 53 | 54 | func makeOption(n: NimNode): NimNode = 55 | newNimNode(nnkBracketExpr).add(ident("Option"), n) 56 | 57 | template objectDecl(a): untyped = 58 | type a = object 59 | 60 | proc putRecList(n: NimNode, recList: NimNode) = 61 | recList.expectKind nnkRecList 62 | if n.kind == nnkObjectTy: 63 | n[2] = recList 64 | return 65 | for z in n: 66 | putRecList(z, recList) 67 | 68 | proc generateOptionalField(fieldName: NimNode, fieldType: NimNode): NimNode = 69 | let right = if isOption(fieldType): fieldType else: makeOption(fieldType) 70 | newIdentDefs(fieldName, right) 71 | 72 | proc traverseIdent(ident: NimNode, typ: NimNode, isDiscriminator: bool, 73 | isCommandOrArgument = false, defaultValue = "", 74 | namePragma = ""): ConfFileSection = 75 | ident.expectKind nnkIdent 76 | ConfFileSection(fieldName: $ident, namePragma: namePragma, typ: typ, 77 | defaultValue: defaultValue, isCommandOrArgument: isCommandOrArgument, 78 | isDiscriminator: isDiscriminator) 79 | 80 | proc traversePostfix(postfix: NimNode, typ: NimNode, isDiscriminator: bool, 81 | isCommandOrArgument = false, defaultValue = "", 82 | namePragma = ""): ConfFileSection = 83 | postfix.expectKind nnkPostfix 84 | 85 | case postfix[1].kind 86 | of nnkIdent: 87 | traverseIdent(postfix[1], typ, isDiscriminator, isCommandOrArgument, 88 | defaultValue, namePragma) 89 | of nnkAccQuoted: 90 | traverseIdent(postfix[1][0], typ, isDiscriminator, isCommandOrArgument, 91 | defaultValue, namePragma) 92 | else: 93 | raiseAssert "[Postfix] Unsupported child node:\n" & postfix[1].treeRepr 94 | 95 | proc shortEnumName(n: NimNode): NimNode = 96 | if n.kind == nnkDotExpr: 97 | n[1] 98 | else: 99 | n 100 | 101 | proc traversePragma(pragma: NimNode): 102 | tuple[isCommandOrArgument: bool, defaultValue, namePragma: string] = 103 | pragma.expectKind nnkPragma 104 | var child: NimNode 105 | 106 | for childNode in pragma: 107 | child = childNode 108 | 109 | if child.kind == nnkCall: 110 | # A custom pragma was used more than once (e.g.: {.pragma: posixOnly, hidden.}) and the 111 | # AST is now: 112 | # ``` 113 | # Call 114 | # Sym "hidden" 115 | # ``` 116 | child = child[0] 117 | 118 | case child.kind 119 | of nnkSym: 120 | let sym = $child 121 | if sym == "command" or sym == "argument": 122 | result.isCommandOrArgument = true 123 | of nnkExprColonExpr: 124 | let pragma = $child[0] 125 | if pragma == "defaultValue": 126 | result.defaultValue = repr(shortEnumName(child[1])) 127 | elif pragma == "name": 128 | result.namePragma = $child[1] 129 | else: 130 | raiseAssert "[Pragma] Unsupported child node:\n" & child.treeRepr 131 | 132 | proc traversePragmaExpr(pragmaExpr: NimNode, typ: NimNode, 133 | isDiscriminator: bool): ConfFileSection = 134 | pragmaExpr.expectKind nnkPragmaExpr 135 | let (isCommandOrArgument, defaultValue, namePragma) = 136 | traversePragma(pragmaExpr[1]) 137 | 138 | case pragmaExpr[0].kind 139 | of nnkIdent: 140 | traverseIdent(pragmaExpr[0], typ, isDiscriminator, isCommandOrArgument, 141 | defaultValue, namePragma) 142 | of nnkAccQuoted: 143 | traverseIdent(pragmaExpr[0][0], typ, isDiscriminator, isCommandOrArgument, 144 | defaultValue, namePragma) 145 | of nnkPostfix: 146 | traversePostfix(pragmaExpr[0], typ, isDiscriminator, isCommandOrArgument, 147 | defaultValue, namePragma) 148 | else: 149 | raiseAssert "[PragmaExpr] Unsupported expression:\n" & pragmaExpr.treeRepr 150 | 151 | proc traverseIdentDefs(identDefs: NimNode, parent: ConfFileSection, 152 | isDiscriminator: bool): seq[ConfFileSection] = 153 | identDefs.expectKind nnkIdentDefs 154 | doAssert identDefs.len > 2, "This kind of node must have at least 3 children." 155 | let typ = identDefs[^2] 156 | for child in identDefs: 157 | case child.kind 158 | of nnkIdent: 159 | result.add traverseIdent(child, typ, isDiscriminator) 160 | of nnkAccQuoted: 161 | result.add traverseIdent(child[0], typ, isDiscriminator) 162 | of nnkPostfix: 163 | result.add traversePostfix(child, typ, isDiscriminator) 164 | of nnkPragmaExpr: 165 | result.add traversePragmaExpr(child, typ, isDiscriminator) 166 | of nnkBracketExpr, nnkSym, nnkEmpty, nnkInfix, nnkCall, nnkDotExpr: 167 | discard 168 | else: 169 | raiseAssert "[IdentDefs] Unsupported child node:\n" & child.treeRepr 170 | 171 | proc traverseRecList(recList: NimNode, parent: ConfFileSection): seq[ConfFileSection] 172 | 173 | proc traverseOfBranch(ofBranch: NimNode, parent: ConfFileSection): ConfFileSection = 174 | ofBranch.expectKind nnkOfBranch 175 | result = ConfFileSection(fieldName: repr(shortEnumName(ofBranch[0])), isCaseBranch: true) 176 | for child in ofBranch: 177 | case child.kind: 178 | of nnkIdent, nnkDotExpr, nnkAccQuoted: 179 | discard 180 | of nnkRecList: 181 | result.children.add traverseRecList(child, result) 182 | else: 183 | raiseAssert "[OfBranch] Unsupported child node:\n" & child.treeRepr 184 | 185 | proc traverseRecCase(recCase: NimNode, parent: ConfFileSection): seq[ConfFileSection] = 186 | recCase.expectKind nnkRecCase 187 | for child in recCase: 188 | case child.kind 189 | of nnkIdentDefs: 190 | result.add traverseIdentDefs(child, parent, true) 191 | of nnkOfBranch: 192 | result.add traverseOfBranch(child, parent) 193 | else: 194 | raiseAssert "[RecCase] Unsupported child node:\n" & child.treeRepr 195 | 196 | proc traverseRecList(recList: NimNode, parent: ConfFileSection): seq[ConfFileSection] = 197 | recList.expectKind nnkRecList 198 | for child in recList: 199 | case child.kind 200 | of nnkIdentDefs: 201 | result.add traverseIdentDefs(child, parent, false) 202 | of nnkRecCase: 203 | result.add traverseRecCase(child, parent) 204 | of nnkNilLit: 205 | discard 206 | else: 207 | raiseAssert "[RecList] Unsupported child node:\n" & child.treeRepr 208 | 209 | proc normalize(root: ConfFileSection) = 210 | ## Moves the default case branches children one level upper in the hierarchy. 211 | ## Also removes case branches without children. 212 | var children: seq[ConfFileSection] 213 | var defaultValue = "" 214 | for child in root.children: 215 | normalize(child) 216 | if child.isDiscriminator: 217 | defaultValue = child.defaultValue 218 | if child.isCaseBranch and child.fieldName == defaultValue: 219 | for childChild in child.children: 220 | children.add childChild 221 | child.children = @[] 222 | elif child.isCaseBranch and child.children.len == 0: 223 | discard 224 | else: 225 | children.add child 226 | root.children = children 227 | 228 | proc generateConfigFileModel(ConfType: NimNode): ConfFileSection = 229 | let confTypeImpl = ConfType.getType[1].getImpl 230 | result = ConfFileSection(fieldName: $confTypeImpl[0]) 231 | result.children = traverseRecList(confTypeImpl[2][2], result) 232 | result.normalize 233 | 234 | proc getRenamedName(node: ConfFileSection): string = 235 | if node.namePragma.len == 0: node.fieldName else: node.namePragma 236 | 237 | proc generateTypes(root: ConfFileSection): seq[NimNode] = 238 | let index = result.len 239 | result.add getAst(objectDecl(genSym(nskType, root.fieldName)))[0] 240 | var recList = newNimNode(nnkRecList) 241 | for child in root.children: 242 | if child.isCommandOrArgument: 243 | continue 244 | if child.isCaseBranch: 245 | if child.children.len > 0: 246 | var types = generateTypes(child) 247 | recList.add generateOptionalField(child.fieldName.ident, types[0][0]) 248 | result.add types 249 | else: 250 | recList.add generateOptionalField(child.getRenamedName.ident, child.typ) 251 | result[index].putRecList(recList) 252 | 253 | proc generateSettersPaths(node: ConfFileSection, 254 | result: var OriginalToGeneratedFields, 255 | pathsCache: var seq[string]) = 256 | pathsCache.add node.getRenamedName 257 | if node.children.len == 0: 258 | result[node.fieldName] = (node.isCommandOrArgument, pathsCache) 259 | else: 260 | for child in node.children: 261 | generateSettersPaths(child, result, pathsCache) 262 | pathsCache.del pathsCache.len - 1 263 | 264 | proc generateSettersPaths(root: ConfFileSection, pathsCache: var seq[string]): OriginalToGeneratedFields = 265 | for child in root.children: 266 | generateSettersPaths(child, result, pathsCache) 267 | 268 | template cfSetter(a, b: untyped): untyped = 269 | when a is Option: 270 | a = some(b) 271 | else: 272 | a = b 273 | 274 | proc generateSetters(confType, CF: NimNode, fieldsPaths: OriginalToGeneratedFields): 275 | (NimNode, NimNode, int) = 276 | var 277 | procs = newStmtList() 278 | assignments = newStmtList() 279 | numSetters = 0 280 | 281 | let c = "c".ident 282 | for field, (isCommandOrArgument, path) in fieldsPaths: 283 | if isCommandOrArgument: 284 | assignments.add quote do: 285 | result.setters[`numSetters`] = defaultConfigFileSetter 286 | inc numSetters 287 | continue 288 | 289 | var fieldPath = c 290 | var condition: NimNode 291 | for fld in path: 292 | fieldPath = newDotExpr(fieldPath, fld.ident) 293 | let fieldChecker = newDotExpr(fieldPath, "isSome".ident) 294 | if condition == nil: 295 | condition = fieldChecker 296 | else: 297 | condition = newNimNode(nnkInfix).add("and".ident).add(condition).add(fieldChecker) 298 | fieldPath = newDotExpr(fieldPath, "get".ident) 299 | 300 | let setterName = genSym(nskProc, field & "CFSetter") 301 | let fieldIdent = field.ident 302 | procs.add quote do: 303 | proc `setterName`(s: var `confType`, cf: ref `CF`): bool {.nimcall, gcsafe.} = 304 | for `c` in cf.data: 305 | if `condition`: 306 | cfSetter(s.`fieldIdent`, `fieldPath`) 307 | return true 308 | 309 | assignments.add quote do: 310 | result.setters[`numSetters`] = `setterName` 311 | inc numSetters 312 | 313 | result = (procs, assignments, numSetters) 314 | 315 | proc generateConfigFileSetters(confType, optType: NimNode, 316 | fieldsPaths: OriginalToGeneratedFields): NimNode = 317 | let 318 | CF = ident "SecondarySources" 319 | T = confType.getType[1] 320 | optT = optType[0][0] 321 | SetterProcType = genSym(nskType, "SetterProcType") 322 | (setterProcs, assignments, numSetters) = generateSetters(T, CF, fieldsPaths) 323 | stmtList = quote do: 324 | type 325 | `SetterProcType` = proc( 326 | s: var `T`, cf: ref `CF` 327 | ): bool {.nimcall, gcsafe, raises: [].} 328 | 329 | `CF` = object 330 | data*: seq[`optT`] 331 | setters: array[`numSetters`, `SetterProcType`] 332 | 333 | proc defaultConfigFileSetter( 334 | s: var `T`, cf: ref `CF` 335 | ): bool {.nimcall, gcsafe, raises: [], used.} = 336 | discard 337 | 338 | `setterProcs` 339 | 340 | proc new(_: type `CF`): ref `CF` = 341 | new result 342 | `assignments` 343 | 344 | new(`CF`) 345 | 346 | stmtList 347 | 348 | macro generateSecondarySources*(ConfType: type): untyped = 349 | let 350 | model = generateConfigFileModel(ConfType) 351 | modelType = generateTypes(model) 352 | var 353 | pathsCache: seq[string] 354 | 355 | result = newTree(nnkStmtList) 356 | result.add newTree(nnkTypeSection, modelType) 357 | 358 | let settersPaths = model.generateSettersPaths(pathsCache) 359 | result.add generateConfigFileSetters(ConfType, result[^1], settersPaths) 360 | 361 | {.pop.} 362 | -------------------------------------------------------------------------------- /confutils/defs.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import 11 | std/options 12 | 13 | type 14 | ConfigurationError* = object of CatchableError 15 | 16 | TypedInputFile*[ContentType = Unspecified, 17 | Format = Unspecified, 18 | defaultExt: static string] = distinct string 19 | 20 | # InputFile* = TypedInputFile[Unspecified, Unspecified, ""] 21 | # TODO temporary work-around, see parseCmdArg 22 | InputFile* = distinct string 23 | 24 | InputDir* = distinct string 25 | OutPath* = distinct string 26 | OutDir* = distinct string 27 | OutFile* = distinct string 28 | 29 | RestOfCmdLine* = distinct string 30 | SubCommandArgs* = distinct string 31 | 32 | Flag* = object 33 | name*: string 34 | 35 | FlagWithValue* = object 36 | name*: string 37 | value*: string 38 | 39 | FlagWithOptionalValue* = object 40 | name*: string 41 | value*: Option[string] 42 | 43 | Unspecified* = object 44 | Txt* = object 45 | 46 | SomeDistinctString = InputFile|InputDir|OutPath|OutDir|OutFile 47 | 48 | {.push gcsafe, raises: [].} 49 | 50 | template `/`*(dir: InputDir|OutDir, path: string): auto = 51 | string(dir) / path 52 | 53 | template `$`*(x: SomeDistinctString): string = 54 | string(x) 55 | 56 | template desc*(v: string) {.pragma.} 57 | template longDesc*(v: string) {.pragma.} 58 | template name*(v: string) {.pragma.} 59 | template abbr*(v: string) {.pragma.} 60 | template separator*(v: string) {.pragma.} 61 | template defaultValue*(v: untyped) {.pragma.} 62 | template defaultValueDesc*(v: string) {.pragma.} 63 | template required* {.pragma.} 64 | template command* {.pragma.} 65 | template argument* {.pragma.} 66 | template hidden* {.pragma.} 67 | template ignore* {.pragma.} 68 | template inlineConfiguration* {.pragma.} 69 | 70 | template implicitlySelectable* {.pragma.} 71 | ## This can be applied to a case object discriminator 72 | ## to allow the value of the discriminator to be determined 73 | ## implicitly when the user specifies any of the sub-options 74 | ## that depend on the disciminator value. 75 | 76 | {.pop.} 77 | -------------------------------------------------------------------------------- /confutils/shell_completion.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | ## A simple lexer meant to tokenize an input string as a shell would do. 11 | import lexbase 12 | import options 13 | import streams 14 | import os 15 | import strutils 16 | 17 | type 18 | ShellLexer = object of BaseLexer 19 | preserveTrailingWs: bool 20 | mergeWordBreaks: bool 21 | wordBreakChars: string 22 | 23 | const 24 | WORDBREAKS = "\"'@><=;|&(:" 25 | SAFE_CHARS = {'a'..'z', 'A'..'Z', '0'..'9', '@', '%', '+', '=', ':', ',', '.', '/', '-'} 26 | 27 | {.push gcsafe, raises: [].} 28 | 29 | proc open(l: var ShellLexer, 30 | input: Stream, 31 | wordBreakChars: string = WORDBREAKS, 32 | preserveTrailingWs = true) {.gcsafe, raises: [IOError, OSError].} = 33 | lexbase.open(l, input) 34 | l.preserveTrailingWs = preserveTrailingWs 35 | l.mergeWordBreaks = false 36 | l.wordBreakChars = wordBreakChars 37 | 38 | proc parseQuoted(l: var ShellLexer, 39 | pos: int, 40 | isSingle: bool, 41 | output: var string): int {.gcsafe, raises: [IOError, OSError].} = 42 | var pos = pos 43 | while true: 44 | case l.buf[pos]: 45 | of '\c': pos = lexbase.handleCR(l, pos) 46 | of '\L': pos = lexbase.handleLF(l, pos) 47 | of lexbase.EndOfFile: break 48 | of '\\': 49 | # Consume the backslash and the following character 50 | inc(pos) 51 | if (isSingle and l.buf[pos] in {'\''}) or 52 | (not isSingle and l.buf[pos] in {'$', '`', '\\', '"'}): 53 | # Escape the character 54 | output.add(l.buf[pos]) 55 | else: 56 | # Rewrite the escape sequence as-is 57 | output.add('\\') 58 | output.add(l.buf[pos]) 59 | inc(pos) 60 | of '\"': 61 | inc(pos) 62 | if isSingle: output.add('\"') 63 | else: break 64 | of '\'': 65 | inc(pos) 66 | if isSingle: break 67 | else: output.add('\'') 68 | else: 69 | output.add(l.buf[pos]) 70 | inc(pos) 71 | return pos 72 | 73 | proc getTok(l: var ShellLexer): Option[string] {.gcsafe, raises: [IOError, OSError].} = 74 | var pos = l.bufpos 75 | 76 | # Skip the initial whitespace 77 | while true: 78 | case l.buf[pos]: 79 | of '\c': pos = lexbase.handleCR(l, pos) 80 | of '\L': pos = lexbase.handleLF(l, pos) 81 | of '#': 82 | # Skip everything until EOF/EOL 83 | while l.buf[pos] notin {'\c', '\L', lexbase.EndOfFile}: 84 | inc(pos) 85 | of lexbase.EndOfFile: 86 | # If we did eat up some whitespace return an empty token, this is needed 87 | # to find out if the string ends with whitespace. 88 | if l.preserveTrailingWs and l.bufpos != pos: 89 | l.bufpos = pos 90 | return some("") 91 | return none(string) 92 | of ' ', '\t': 93 | inc(pos) 94 | else: 95 | break 96 | 97 | var tokLit = "" 98 | # Parse the next token 99 | while true: 100 | case l.buf[pos]: 101 | of '\c': pos = lexbase.handleCR(l, pos) 102 | of '\L': pos = lexbase.handleLF(l, pos) 103 | of '\'': 104 | # Single-quoted string 105 | inc(pos) 106 | pos = parseQuoted(l, pos, true, tokLit) 107 | of '"': 108 | # Double-quoted string 109 | inc(pos) 110 | pos = parseQuoted(l, pos, false, tokLit) 111 | of '\\': 112 | # Escape sequence 113 | inc(pos) 114 | if l.buf[pos] != lexbase.EndOfFile: 115 | tokLit.add(l.buf[pos]) 116 | inc(pos) 117 | of '#', ' ', '\t', lexbase.EndOfFile: 118 | break 119 | else: 120 | let ch = l.buf[pos] 121 | if ch notin l.wordBreakChars: 122 | tokLit.add(l.buf[pos]) 123 | inc(pos) 124 | # Merge together runs of adjacent word-breaking characters if requested 125 | elif l.mergeWordBreaks: 126 | while l.buf[pos] in l.wordBreakChars: 127 | tokLit.add(l.buf[pos]) 128 | inc(pos) 129 | l.mergeWordBreaks = false 130 | break 131 | else: 132 | l.mergeWordBreaks = true 133 | break 134 | 135 | l.bufpos = pos 136 | return some(tokLit) 137 | 138 | proc splitCompletionLine*(): seq[string] = 139 | let comp_line = os.getEnv("COMP_LINE") 140 | var comp_point = 141 | try: 142 | parseInt(os.getEnv("COMP_POINT", "0")) 143 | except ValueError: 144 | return @[] 145 | 146 | if comp_point == len(comp_line): 147 | comp_point -= 1 148 | 149 | if comp_point < 0 or comp_point > len(comp_line): 150 | return @[] 151 | 152 | # Take the useful part only 153 | var strm = newStringStream(comp_line[0..comp_point]) 154 | 155 | # Split the resulting string 156 | var l: ShellLexer 157 | try: 158 | l.open(strm) 159 | while true: 160 | let token = l.getTok() 161 | if token.isNone(): 162 | break 163 | result.add(token.get()) 164 | except IOError, OSError: 165 | return @[] 166 | 167 | proc shellQuote*(word: string): string = 168 | if len(word) == 0: 169 | return "''" 170 | 171 | if allCharsInSet(word, SAFE_CHARS): 172 | return word 173 | 174 | result.add('\'') 175 | for ch in word: 176 | if ch == '\'': result.add('\\') 177 | result.add(ch) 178 | 179 | result.add('\'') 180 | 181 | proc shellPathEscape*(path: string): string = 182 | if allCharsInSet(path, SAFE_CHARS): 183 | return path 184 | 185 | for ch in path: 186 | if ch notin SAFE_CHARS: 187 | result.add('\\') 188 | result.add(ch) 189 | 190 | {.pop.} 191 | 192 | when isMainModule: 193 | # Test data lifted from python's shlex unit-tests 194 | const data = """ 195 | foo bar|foo|bar| 196 | foo bar|foo|bar| 197 | foo bar |foo|bar| 198 | foo bar bla fasel|foo|bar|bla|fasel| 199 | x y z xxxx|x|y|z|xxxx| 200 | \x bar|x|bar| 201 | \ x bar| x|bar| 202 | \ bar| bar| 203 | foo \x bar|foo|x|bar| 204 | foo \ x bar|foo| x|bar| 205 | foo \ bar|foo| bar| 206 | foo "bar" bla|foo|bar|bla| 207 | "foo" "bar" "bla"|foo|bar|bla| 208 | "foo" bar "bla"|foo|bar|bla| 209 | "foo" bar bla|foo|bar|bla| 210 | foo 'bar' bla|foo|bar|bla| 211 | 'foo' 'bar' 'bla'|foo|bar|bla| 212 | 'foo' bar 'bla'|foo|bar|bla| 213 | 'foo' bar bla|foo|bar|bla| 214 | blurb foo"bar"bar"fasel" baz|blurb|foobarbarfasel|baz| 215 | blurb foo'bar'bar'fasel' baz|blurb|foobarbarfasel|baz| 216 | ""|| 217 | ''|| 218 | foo "" bar|foo||bar| 219 | foo '' bar|foo||bar| 220 | foo "" "" "" bar|foo||||bar| 221 | foo '' '' '' bar|foo||||bar| 222 | \"|"| 223 | "\""|"| 224 | "foo\ bar"|foo\ bar| 225 | "foo\\ bar"|foo\ bar| 226 | "foo\\ bar\""|foo\ bar"| 227 | "foo\\" bar\"|foo\|bar"| 228 | "foo\\ bar\" dfadf"|foo\ bar" dfadf| 229 | "foo\\\ bar\" dfadf"|foo\\ bar" dfadf| 230 | "foo\\\x bar\" dfadf"|foo\\x bar" dfadf| 231 | "foo\x bar\" dfadf"|foo\x bar" dfadf| 232 | \'|'| 233 | 'foo\ bar'|foo\ bar| 234 | 'foo\\ bar'|foo\\ bar| 235 | "foo\\\x bar\" df'a\ 'df"|foo\\x bar" df'a\ 'df| 236 | \"foo|"foo| 237 | \"foo\x|"foox| 238 | "foo\x"|foo\x| 239 | "foo\ "|foo\ | 240 | foo\ xx|foo xx| 241 | foo\ x\x|foo xx| 242 | foo\ x\x\"|foo xx"| 243 | "foo\ x\x"|foo\ x\x| 244 | "foo\ x\x\\"|foo\ x\x\| 245 | "foo\ x\x\\""foobar"|foo\ x\x\foobar| 246 | "foo\ x\x\\"\'"foobar"|foo\ x\x\'foobar| 247 | "foo\ x\x\\"\'"fo'obar"|foo\ x\x\'fo'obar| 248 | "foo\ x\x\\"\'"fo'obar" 'don'\''t'|foo\ x\x\'fo'obar|don't| 249 | "foo\ x\x\\"\'"fo'obar" 'don'\''t' \\|foo\ x\x\'fo'obar|don't|\| 250 | 'foo\ bar'|foo\ bar| 251 | 'foo\\ bar'|foo\\ bar| 252 | foo\ bar|foo bar| 253 | :-) ;-)|:-)|;-)| 254 | áéíóú|áéíóú| 255 | """ 256 | var corpus = newStringStream(data) 257 | var line = "" 258 | while corpus.readLine(line): 259 | let chunks = line.split('|') 260 | let expr = chunks[0] 261 | let expected = chunks[1..^2] 262 | 263 | var l: ShellLexer 264 | var strm = newStringStream(expr) 265 | var got: seq[string] 266 | l.open(strm, wordBreakChars="", preserveTrailingWs=false) 267 | while true: 268 | let x = l.getTok() 269 | if x.isNone(): 270 | break 271 | got.add(x.get()) 272 | 273 | if got != expected: 274 | echo "got ", got 275 | echo "expected ", expected 276 | doAssert(false) 277 | 278 | doAssert(shellQuote("") == "''") 279 | doAssert(shellQuote("\\\"") == "'\\\"'") 280 | doAssert(shellQuote("foobar") == "foobar") 281 | doAssert(shellQuote("foo$bar") == "'foo$bar'") 282 | doAssert(shellQuote("foo bar") == "'foo bar'") 283 | doAssert(shellQuote("foo'bar") == "'foo\\'bar'") 284 | -------------------------------------------------------------------------------- /confutils/std/net.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import std/net 11 | from std/parseutils import parseInt 12 | export net 13 | 14 | {.push gcsafe, raises: [].} 15 | 16 | func parseCmdArg*(T: type IpAddress, s: string): T {.gcsafe, raises: [ValueError].} = 17 | parseIpAddress(s) 18 | 19 | func completeCmdArg*(T: type IpAddress, val: string): seq[string] = 20 | # TODO: Maybe complete the local IP address? 21 | @[] 22 | 23 | func parseCmdArg*(T: type Port, s: string): T {.gcsafe, raises: [ValueError].} = 24 | template fail = 25 | raise newException(ValueError, 26 | "The supplied port must be an integer value in the range 1-65535") 27 | 28 | var intVal: int 29 | let parsedChars = try: parseInt(s, intVal) 30 | except CatchableError: fail() 31 | 32 | if parsedChars != len(s) or intVal < 1 or intVal > 65535: 33 | fail() 34 | 35 | return Port(intVal) 36 | 37 | func completeCmdArg*(T: type Port, val: string): seq[string] = 38 | @[] 39 | 40 | {.pop.} 41 | -------------------------------------------------------------------------------- /confutils/toml/defs.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import 11 | toml_serialization, ../defs as confutilsDefs 12 | 13 | export 14 | toml_serialization, confutilsDefs 15 | 16 | {.push gcsafe, raises: [].} 17 | 18 | template readConfutilsType(T: type) = 19 | template readValue*(r: var TomlReader, val: var T) = 20 | val = T r.readValue(string) 21 | 22 | readConfutilsType InputFile 23 | readConfutilsType InputDir 24 | readConfutilsType OutPath 25 | readConfutilsType OutDir 26 | readConfutilsType OutFile 27 | 28 | {.pop.} 29 | -------------------------------------------------------------------------------- /confutils/toml/std/net.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import 11 | std/net, 12 | toml_serialization, toml_serialization/lexer 13 | 14 | export 15 | net, toml_serialization 16 | 17 | {.push gcsafe, raises: [].} 18 | 19 | proc readValue*(r: var TomlReader, val: var IpAddress) 20 | {.raises: [SerializationError, IOError].} = 21 | val = try: parseIpAddress(r.readValue(string)) 22 | except ValueError as err: 23 | r.lex.raiseUnexpectedValue("IP address " & err.msg) 24 | 25 | proc readValue*(r: var TomlReader, val: var Port) 26 | {.raises: [SerializationError, IOError].} = 27 | let port = try: r.readValue(uint16) 28 | except ValueError as exc: 29 | r.lex.raiseUnexpectedValue("Port " & exc.msg) 30 | 31 | val = Port port 32 | 33 | {.pop.} 34 | -------------------------------------------------------------------------------- /confutils/toml/std/uri.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import 11 | std/uri, 12 | toml_serialization, toml_serialization/lexer 13 | 14 | export 15 | uri, toml_serialization 16 | 17 | {.push gcsafe, raises: [].} 18 | 19 | proc readValue*(r: var TomlReader, val: var Uri) 20 | {.raises: [SerializationError, IOError].} = 21 | val = try: parseUri(r.readValue(string)) 22 | except ValueError as err: 23 | r.lex.raiseUnexpectedValue("URI " & err.msg) 24 | 25 | {.pop.} 26 | -------------------------------------------------------------------------------- /confutils/winreg/reader.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import 11 | tables, typetraits, options, 12 | serialization/[object_serialization, errors], 13 | ./utils, ./types 14 | 15 | type 16 | WinregReader* = object 17 | hKey: HKEY 18 | path: string 19 | key: seq[string] 20 | 21 | WinregReaderError* = object of WinregError 22 | 23 | GenericWinregReaderError* = object of WinregReaderError 24 | deserializedField*: string 25 | innerException*: ref CatchableError 26 | 27 | {.push gcsafe, raises: [].} 28 | 29 | proc handleReadException*(r: WinregReader, 30 | Record: type, 31 | fieldName: string, 32 | field: auto, 33 | err: ref CatchableError) {.gcsafe, raises: [WinregError].} = 34 | var ex = new GenericWinregReaderError 35 | ex.deserializedField = fieldName 36 | ex.innerException = err 37 | raise ex 38 | 39 | proc init*(T: type WinregReader, 40 | hKey: HKEY, path: string): T = 41 | result.hKey = hKey 42 | result.path = path 43 | 44 | proc readValue*[T](r: var WinregReader, value: var T) 45 | {.gcsafe, raises: [SerializationError, IOError].} = 46 | mixin readValue 47 | # TODO: reduce allocation 48 | 49 | when T is (SomePrimitives or range or string): 50 | let path = constructPath(r.path, r.key) 51 | discard getValue(r.hKey, path, r.key[^1], value) 52 | 53 | elif T is Option: 54 | template getUnderlyingType[T](_: Option[T]): untyped = T 55 | type UT = getUnderlyingType(value) 56 | let path = constructPath(r.path, r.key) 57 | if pathExists(r.hKey, path, r.key[^1]): 58 | value = some(r.readValue(UT)) 59 | 60 | elif T is (seq or array): 61 | when uTypeIsPrimitives(T): 62 | let path = constructPath(r.path, r.key) 63 | discard getValue(r.hKey, path, r.key[^1], value) 64 | 65 | else: 66 | let key = r.key[^1] 67 | for i in 0.. 0: 74 | const fields = T.fieldReadersTable(WinregReader) 75 | var expectedFieldPos = 0 76 | r.key.add "" 77 | value.enumInstanceSerializedFields(fieldName, field): 78 | when T is tuple: 79 | r.key[^1] = $expectedFieldPos 80 | var reader = fields[expectedFieldPos].reader 81 | expectedFieldPos += 1 82 | 83 | else: 84 | r.key[^1] = fieldName 85 | var reader = findFieldReader(fields, fieldName, expectedFieldPos) 86 | 87 | if reader != nil: 88 | reader(value, r) 89 | discard r.key.pop() 90 | 91 | else: 92 | const typeName = typetraits.name(T) 93 | {.fatal: "Failed to convert from Winreg an unsupported type: " & typeName.} 94 | 95 | {.pop.} 96 | -------------------------------------------------------------------------------- /confutils/winreg/types.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import 11 | serialization/errors 12 | 13 | type 14 | HKEY* = distinct uint 15 | RegType* = distinct int32 16 | WinregError* = object of SerializationError 17 | 18 | const 19 | HKEY_CLASSES_ROOT* = HKEY(0x80000000'u) 20 | HKEY_CURRENT_USER* = HKEY(0x80000001'u) 21 | HKEY_LOCAL_MACHINE* = HKEY(0x80000002'u) 22 | HKEY_USERS* = HKEY(0x80000003'u) 23 | 24 | HKLM* = HKEY_LOCAL_MACHINE 25 | HKCU* = HKEY_CURRENT_USER 26 | HKCR* = HKEY_CLASSES_ROOT 27 | HKU* = HKEY_USERS 28 | 29 | {.push gcsafe, raises: [].} 30 | 31 | proc `==`*(a, b: HKEY): bool {.borrow.} 32 | proc `==`*(a, b: RegType): bool {.borrow.} 33 | 34 | {.pop.} 35 | -------------------------------------------------------------------------------- /confutils/winreg/utils.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import 11 | strutils, 12 | ./types 13 | 14 | type 15 | SomePrimitives* = SomeInteger | enum | bool | SomeFloat | char 16 | 17 | const 18 | REG_SZ* = RegType(1) 19 | REG_BINARY* = RegType(3) 20 | REG_DWORD* = RegType(4) 21 | REG_QWORD* = RegType(11) 22 | 23 | RT_SZ* = 0x00000002 24 | RT_BINARY* = 0x00000008 25 | RT_DWORD* = 0x00000010 26 | RT_QWORD* = 0x00000040 27 | RT_ANY* = 0x0000ffff 28 | 29 | {.push gcsafe, raises: [].} 30 | 31 | proc regGetValue(hKey: HKEY, lpSubKey, lpValue: cstring, 32 | dwFlags: int32, pdwType: ptr RegType, 33 | pvData: pointer, pcbData: ptr int32): int32 {. 34 | importc: "RegGetValueA", dynlib: "Advapi32.dll", stdcall.} 35 | 36 | proc regSetValue(hKey: HKEY, lpSubKey, lpValueName: cstring, 37 | dwType: RegType; lpData: pointer; cbData: int32): int32 {. 38 | importc: "RegSetKeyValueA", dynlib: "Advapi32.dll", stdcall.} 39 | 40 | template call(f) = 41 | if f != 0: 42 | return false 43 | 44 | template safeCast(destType: type, src: typed): auto = 45 | when sizeof(src) < sizeof(destType): 46 | destType(src) 47 | else: 48 | cast[destType](src) 49 | 50 | proc setValue*(hKey: HKEY, path, key: string, val: SomePrimitives): bool = 51 | when sizeof(val) < 8: 52 | var dw = int32.safeCast(val) 53 | call regSetValue(hKey, path, key, REG_DWORD, dw.addr, sizeof(dw).int32) 54 | else: 55 | var dw = int64.safeCast(val) 56 | call regSetValue(hKey, path, key, REG_QWORD, dw.addr, sizeof(dw).int32) 57 | result = true 58 | 59 | proc setValue*[T: SomePrimitives](hKey: HKEY, path, key: string, val: openArray[T]): bool = 60 | call regSetValue(hKey, path, key, REG_BINARY, val[0].unsafeAddr, int32(val.len * sizeof(T))) 61 | result = true 62 | 63 | proc getValue*(hKey: HKEY, path, key: string, outVal: var string): bool = 64 | var size: int32 65 | call regGetValue(hKey, path, key, RT_BINARY, nil, nil, addr size) 66 | outVal.setLen(size) 67 | call regGetValue(hKey, path, key, RT_BINARY, nil, outVal[0].addr, addr size) 68 | result = true 69 | 70 | proc getValue*[T: SomePrimitives](hKey: HKEY, path, key: string, outVal: var seq[T]): bool = 71 | var size: int32 72 | call regGetValue(hKey, path, key, RT_BINARY, nil, nil, addr size) 73 | outVal.setLen(size div sizeof(T)) 74 | call regGetValue(hKey, path, key, RT_BINARY, nil, outVal[0].addr, addr size) 75 | result = true 76 | 77 | proc getValue*[N, T: SomePrimitives](hKey: HKEY, path, key: string, outVal: var array[N, T]): bool = 78 | var size: int32 79 | call regGetValue(hKey, path, key, RT_BINARY, nil, nil, addr size) 80 | if outVal.len != size div sizeof(T): 81 | return false 82 | call regGetValue(hKey, path, key, RT_BINARY, nil, outVal[0].addr, addr size) 83 | result = true 84 | 85 | proc getValue*(hKey: HKEY, path, key: string, outVal: var SomePrimitives): bool = 86 | when sizeof(outVal) < 8: 87 | type T = type outVal 88 | var val: int32 89 | var valSize = sizeof(val).int32 90 | call regGetValue(hKey, path, key, RT_DWORD, nil, val.addr, valSize.addr) 91 | outVal = cast[T](val) 92 | else: 93 | var valSize = sizeof(outVal).int32 94 | call regGetValue(hKey, path, key, RT_QWORD, nil, outVal.addr, valSize.addr) 95 | result = true 96 | 97 | proc pathExists*(hKey: HKEY, path, key: string): bool {.inline.} = 98 | result = regGetValue(hKey, path, key, RT_ANY, nil, nil, nil) == 0 99 | 100 | proc parseWinregPath*(input: string): (HKEY, string) = 101 | let pos = input.find('\\') 102 | if pos < 0: return 103 | 104 | result[1] = input.substr(pos + 1) 105 | case input.substr(0, pos - 1) 106 | of "HKEY_CLASSES_ROOT", "HKCR": 107 | result[0] = HKCR 108 | of "HKEY_CURRENT_USER", "HKCU": 109 | result[0] = HKCU 110 | of "HKEY_LOCAL_MACHINE", "HKLM": 111 | result[0] = HKLM 112 | of "HKEY_USERS", "HKU": 113 | result[0] = HKU 114 | else: 115 | discard 116 | 117 | proc `$`*(hKey: HKEY): string = 118 | case hKey 119 | of HKCR: result = "HKEY_CLASSES_ROOT" 120 | of HKCU: result = "HKEY_CURRENT_USER" 121 | of HKLM: result = "HKEY_LOCAL_MACHINE" 122 | of HKU : result = "HKEY_USERS" 123 | else: discard 124 | 125 | template uTypeIsPrimitives*[T](_: type seq[T]): bool = 126 | when T is SomePrimitives: 127 | true 128 | else: 129 | false 130 | 131 | template uTypeIsPrimitives*[N, T](_: type array[N, T]): bool = 132 | when T is SomePrimitives: 133 | true 134 | else: 135 | false 136 | 137 | template uTypeIsPrimitives*[T](_: type openArray[T]): bool = 138 | when T is SomePrimitives: 139 | true 140 | else: 141 | false 142 | 143 | template uTypeIsRecord*(_: typed): bool = 144 | false 145 | 146 | template uTypeIsRecord*[T](_: type seq[T]): bool = 147 | when T is (object or tuple): 148 | true 149 | else: 150 | false 151 | 152 | template uTypeIsRecord*[N, T](_: type array[N, T]): bool = 153 | when T is (object or tuple): 154 | true 155 | else: 156 | false 157 | 158 | func constructPath*(root: string, keys: openArray[string]): string = 159 | if keys.len <= 1: 160 | return root 161 | var size = root.len + 1 162 | for i in 0..= high(T).int64 18 | allIt( 19 | lo .. hi, 20 | try: 21 | when T is SomeUnsignedInt: 22 | # TODO https://github.com/status-im/nim-confutils/issues/45 23 | it != T.parseCmdArg($it).int64 24 | else: 25 | discard it != T.parseCmdArg($it).int64 26 | false 27 | except RangeDefect: 28 | true) 29 | 30 | const span = 300000 31 | 32 | suite "parseCmdArg": 33 | # For 8 and 16-bit integer types, there aren't many valid possibilities. Test 34 | # them all. 35 | test "int8": 36 | const 37 | lowBase = int16(low(int8)) - 1 38 | highBase = int16(high(int8)) + 1 39 | check: 40 | testInvalidValues[int8](lowBase * 2, lowBase) 41 | testValidValues[int8]() 42 | testInvalidValues[int8](highBase, highBase + span) 43 | 44 | test "int16": 45 | check: testValidValues[int16]() 46 | 47 | test "int32": 48 | check: 49 | testValidValues[int32](-span, span) 50 | # https://github.com/nim-lang/Nim/issues/16353 so target high(T) - 1 51 | testValidValues[int32](high(int32) - span, high(int32) - 1) 52 | 53 | test "int64": 54 | const 55 | highBase = int64(high(int32)) + 1 56 | lowBase = int64(low(int32)) - 1 57 | check: 58 | testValidValues[int64](low(int64), low(int64) + span) 59 | testValidValues[int64](lowBase - span, lowBase) 60 | testValidValues[int64](-span, span) 61 | testValidValues[int64](highBase, highBase + span) 62 | 63 | # https://github.com/nim-lang/Nim/issues/16353 so target high(T) - 1 64 | testValidValues[int64](high(int64) - span, high(int64) - 1) 65 | 66 | test "uint8": 67 | const highBase = int16(high(uint8)) + 1 68 | check: 69 | testValidValues[uint8]() 70 | testInvalidValues[uint8](highBase, highBase + span) 71 | 72 | test "uint16": 73 | const highBase = int32(high(uint16)) + 1 74 | check: 75 | testValidValues[uint16]() 76 | testInvalidValues[uint16](highBase, highBase + span) 77 | 78 | test "uint32": 79 | const highBase = int64(high(uint32)) + 1 80 | check: 81 | testValidValues[uint32](0, 2000000) 82 | 83 | # https://github.com/nim-lang/Nim/issues/16353 so target high(T) - 1 84 | testValidValues[uint32](high(uint32) - span, high(uint32) - 1) 85 | testInvalidValues[uint32](highBase, highBase + span) 86 | 87 | test "uint64": 88 | const highBase = uint64(high(uint32)) + 1 89 | check: 90 | testValidValues[uint64](0, span) 91 | testValidValues[uint64](highBase, highBase + span) 92 | 93 | # https://github.com/nim-lang/Nim/issues/16353 so target high(T) - 1 94 | testValidValues[uint64](high(uint64) - span, high(uint64) - 1) 95 | 96 | test "bool": 97 | for trueish in ["y", "yes", "true", "1", "on"]: 98 | check: bool.parseCmdArg(trueish) 99 | for falsey in ["n", "no", "false", "0", "off"]: 100 | check: not bool.parseCmdArg(falsey) 101 | for invalid in ["2", "-1", "ncd"]: 102 | check: 103 | try: 104 | discard bool.parseCmdArg(invalid) 105 | false 106 | except ValueError: 107 | true 108 | -------------------------------------------------------------------------------- /tests/test_pragma.nim: -------------------------------------------------------------------------------- 1 | import 2 | unittest2, 3 | ../confutils, 4 | ../confutils/defs 5 | 6 | {.pragma: customPragma, hidden.} 7 | 8 | type 9 | TestConf* = object 10 | statusBarEnabled* {. 11 | customPragma 12 | desc: "Display a status bar at the bottom of the terminal screen" 13 | defaultValue: true 14 | name: "status-bar" }: bool 15 | 16 | statusBarEnabled2* {. 17 | customPragma 18 | desc: "Display a status bar at the bottom of the terminal screen" 19 | defaultValue: true 20 | name: "status-bar2" }: bool 21 | 22 | suite "test custom pragma": 23 | test "funny AST when called twice": 24 | let conf = TestConf.load() 25 | doAssert(conf.statusBarEnabled == true) 26 | doAssert(conf.statusBarEnabled2 == true) 27 | 28 | -------------------------------------------------------------------------------- /tests/test_qualified_ident.nim: -------------------------------------------------------------------------------- 1 | import 2 | std/[strutils], 3 | unittest2, 4 | ../confutils, 5 | ./private/specialint 6 | 7 | type 8 | TestConf* = object 9 | La1* {. 10 | desc: "La1" 11 | name: "la1" }: SInt 12 | 13 | La2* {. 14 | desc: "La2" 15 | name: "la2" }: specialint.SInt 16 | 17 | func parseCmdArg(T: type specialint.SInt, p: string): T = 18 | parseInt(p).T 19 | 20 | func completeCmdArg(T: type specialint.SInt, val: string): seq[string] = 21 | @[] 22 | 23 | suite "Qualified Ident": 24 | test "Qualified Ident": 25 | let conf = TestConf.load(@["--la1:123", "--la2:456"]) 26 | check conf.La1.int == 123 27 | check conf.La2.int == 456 28 | -------------------------------------------------------------------------------- /tests/test_winreg.nim: -------------------------------------------------------------------------------- 1 | import 2 | std/[options, os], 3 | unittest2, 4 | ../confutils/winreg/winreg_serialization, 5 | ../confutils/winreg/utils 6 | 7 | type 8 | Fruit = enum 9 | Apple 10 | 11 | const 12 | commonPath = "SOFTWARE\\nimbus" 13 | 14 | template readWrite(key: string, val: typed) = 15 | test key: 16 | var ok = setValue(HKCU, commonPath, key, val) 17 | check ok == true 18 | var outVal: type val 19 | ok = getValue(HKCU, commonPath, key, outVal) 20 | check ok == true 21 | check outVal == val 22 | 23 | suite "winreg utils test suite": 24 | readWrite("some number", 123'u32) 25 | readWrite("some number 64", 123'u64) 26 | readWrite("some bytes", @[1.byte, 2.byte]) 27 | readWrite("some int list", @[4,5,6]) 28 | readWrite("some array", [1.byte, 2.byte, 4.byte]) 29 | readWrite("some string", "hello world") 30 | readWrite("some enum", Apple) 31 | readWrite("some boolean", true) 32 | readWrite("some float32", 1.234'f32) 33 | readWrite("some float64", 1.234'f64) 34 | 35 | test "parse winregpath": 36 | let (hKey, path) = parseWinregPath("HKEY_CLASSES_ROOT\\" & commonPath) 37 | check hKey == HKCR 38 | check path == commonPath 39 | 40 | type 41 | ValidIpAddress {.requiresInit.} = object 42 | value: string 43 | 44 | TestObject = object 45 | address: Option[ValidIpAddress] 46 | 47 | proc readValue(r: var WinregReader, value: var ValidIpAddress) = 48 | r.readValue(value.value) 49 | 50 | proc writeValue( 51 | w: var WinregWriter, value: ValidIpAddress) {.raises: [IOError].} = 52 | w.writeValue(value.value) 53 | 54 | suite "optional fields test suite": 55 | test "optional field with requiresInit pragma": 56 | 57 | var z = TestObject(address: some(ValidIpAddress(value: "1.2.3.4"))) 58 | Winreg.saveFile("HKCU" / commonPath, z) 59 | var x = Winreg.loadFile("HKCU" / commonPath, TestObject) 60 | check x.address.isSome 61 | check x.address.get().value == "1.2.3.4" 62 | 63 | type 64 | Class = enum 65 | Truck 66 | MPV 67 | SUV 68 | 69 | Fuel = enum 70 | Gasoline 71 | Diesel 72 | 73 | Engine = object 74 | cylinder: int 75 | valve: int16 76 | fuel: Fuel 77 | 78 | Suspension = object 79 | dist: int 80 | length: int 81 | 82 | Vehicle = object 83 | name: string 84 | color: int 85 | class: Class 86 | engine: Engine 87 | wheel: int 88 | suspension: array[3, Suspension] 89 | door: array[4, int] 90 | antennae: Option[int] 91 | bumper: Option[string] 92 | 93 | suite "winreg encoder test suite": 94 | test "basic encoder and decoder": 95 | let v = Vehicle( 96 | name: "buggy", 97 | color: 213, 98 | class: MPV, 99 | engine: Engine( 100 | cylinder: 3, 101 | valve: 2, 102 | fuel: Diesel 103 | ), 104 | wheel: 6, 105 | door: [1,2,3,4], 106 | suspension: [ 107 | Suspension(dist: 1, length: 5), 108 | Suspension(dist: 2, length: 6), 109 | Suspension(dist: 3, length: 7) 110 | ], 111 | bumper: some("Chromium") 112 | ) 113 | 114 | Winreg.encode(HKCU, commonPath, v) 115 | let x = Winreg.decode(HKCU, commonPath, Vehicle) 116 | check x == v 117 | check x.antennae.isNone 118 | check x.bumper.get() == "Chromium" 119 | --------------------------------------------------------------------------------