├── tests ├── private │ └── specialint.nim ├── help │ ├── .gitignore │ ├── snapshots │ │ ├── test_default_value_desc.txt │ │ ├── test_separator.txt │ │ ├── test_nested_cmd_lvl1Cmd1_lvl2Cmd2.txt │ │ ├── test_argument_abbr.txt │ │ ├── test_case_opt_cmdBlockProcessing.txt │ │ ├── test_longdesc_lvl1Cmd1.txt │ │ ├── test_nested_cmd_lvl1Cmd1.txt │ │ ├── test_longdesc.txt │ │ ├── test_case_opt.txt │ │ ├── test_argument.txt │ │ └── test_nested_cmd.txt │ ├── README.md │ ├── test_default_value_desc.nim │ ├── test_argument_abbr.nim │ ├── test_separator.nim │ ├── test_longdesc.nim │ ├── test_argument.nim │ ├── test_case_opt.nim │ └── test_nested_cmd.nim ├── config_files │ ├── nested_cmd.toml │ ├── current_user │ │ └── testVendor │ │ │ └── testApp.toml │ └── system_wide │ │ └── testVendor │ │ └── testApp.toml ├── issue_6.nim ├── nim.cfg ├── fail │ ├── test_abbr_duplicate_root.nim │ ├── test_name_duplicate_root.nim │ ├── test_abbr_duplicate_root_subcommand.nim │ ├── test_name_duplicate_root_subcommand.nim │ ├── test_abbr_duplicate_subcommand.nim │ └── test_name_duplicate_subcommand.nim ├── cli_example.nim ├── logger.nim ├── test_ignore.nim ├── test_qualified_ident.nim ├── test_all.nim ├── test_pragma.nim ├── command_with_args.nim ├── test_duplicates.nim ├── nested_commands.nim ├── test_envvar.nim ├── test_help.nim ├── test_argument.nim ├── test_winreg.nim ├── test_parsecmdarg.nim ├── test_nested_cmd.nim └── test_config_file.nim ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── confutils ├── toml │ ├── std │ │ ├── net.nim │ │ └── uri.nim │ └── defs.nim ├── json │ └── defs.nim ├── cli_parsing_fuzzer.nim ├── winreg │ ├── types.nim │ ├── writer.nim │ ├── winreg_serialization.nim │ ├── reader.nim │ └── utils.nim ├── std │ └── net.nim ├── defs.nim ├── cli_parser.nim ├── shell_completion.nim └── config_file.nim ├── nim.cfg ├── LICENSE-MIT ├── confutils.nimble ├── LICENSE-APACHEv2 ├── README.md └── confutils.nim /tests/private/specialint.nim: -------------------------------------------------------------------------------- 1 | type 2 | SInt* = distinct int 3 | -------------------------------------------------------------------------------- /tests/help/.gitignore: -------------------------------------------------------------------------------- 1 | /* 2 | !*/ 3 | !/*.nim 4 | !.gitignore 5 | !README.md 6 | -------------------------------------------------------------------------------- /tests/help/snapshots/test_default_value_desc.txt: -------------------------------------------------------------------------------- 1 | Usage: 2 | 3 | test_default_value_desc [OPTIONS]... 4 | 5 | The following options are available: 6 | 7 | --opt1 tcp port [=9000]. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache 2 | *.exe 3 | nimble.develop 4 | nimble.paths 5 | build/ 6 | vendor/ 7 | tests/test_all 8 | tests/test_nested_cmd 9 | tests/test_help 10 | tests/test_argument 11 | -------------------------------------------------------------------------------- /tests/config_files/nested_cmd.toml: -------------------------------------------------------------------------------- 1 | outer-arg = "toml outer-arg" 2 | outerCmd1 = { outer-arg1 = "toml outer-arg1", innerCmd1 = { inner-arg1 = "toml inner-arg1" }, innerCmd2 = { inner-arg2 = "toml inner-arg2" } } 3 | -------------------------------------------------------------------------------- /tests/issue_6.nim: -------------------------------------------------------------------------------- 1 | import confutils 2 | 3 | # TODO add test cases for https://github.com/status-im/nim-confutils/issues/6 4 | cli do (arg1: string = "", arg2: string = ""): 5 | echo "arg1: ", arg1 6 | echo "arg2: ", arg2 7 | 8 | -------------------------------------------------------------------------------- /tests/nim.cfg: -------------------------------------------------------------------------------- 1 | # Avoid some rare stack corruption while using exceptions with a SEH-enabled 2 | # toolchain: https://github.com/status-im/nimbus-eth2/issues/3121 3 | @if windows and not vcc: 4 | --define:nimRawSetjmp 5 | @end 6 | -------------------------------------------------------------------------------- /tests/fail/test_abbr_duplicate_root.nim: -------------------------------------------------------------------------------- 1 | import 2 | ../../confutils, 3 | ../../confutils/defs 4 | 5 | type 6 | TestConf* = object 7 | dataDir* {.abbr: "d" }: OutDir 8 | importDir* {.abbr: "d" }: OutDir 9 | 10 | let c = TestConf.load() 11 | -------------------------------------------------------------------------------- /tests/cli_example.nim: -------------------------------------------------------------------------------- 1 | import 2 | ../confutils 3 | 4 | cli do (foo: int, bar: string, withBaz: bool, args {.argument.}: seq[string]): 5 | echo "foo = ", foo 6 | echo "bar = ", bar 7 | echo "baz = ", withBaz 8 | for arg in args: echo "arg ", arg 9 | 10 | -------------------------------------------------------------------------------- /tests/fail/test_name_duplicate_root.nim: -------------------------------------------------------------------------------- 1 | import 2 | ../../confutils, 3 | ../../confutils/defs 4 | 5 | type 6 | TestConf* = object 7 | dataDir* {.name: "data-dir" }: OutDir 8 | importDir* {.name: "data-dir" }: OutDir 9 | 10 | let c = TestConf.load() 11 | -------------------------------------------------------------------------------- /tests/logger.nim: -------------------------------------------------------------------------------- 1 | import 2 | ../confutils 3 | 4 | type 5 | Command = enum 6 | pubsub = "A pub sub command" 7 | 8 | Conf = object 9 | logDir: string 10 | case cmd {.command.}: Command 11 | of pubsub: 12 | foo: string 13 | 14 | let c = load Conf 15 | echo c.cmd 16 | 17 | -------------------------------------------------------------------------------- /tests/help/snapshots/test_separator.txt: -------------------------------------------------------------------------------- 1 | Usage: 2 | 3 | test_separator [OPTIONS]... 4 | 5 | The following options are available: 6 | 7 | Network Options: 8 | --opt1 opt1 desc [=opt1 default]. 9 | --opt2 opt2 desc [=opt2 default]. 10 | 11 | ---------------- 12 | --opt3 opt3 desc [=opt3 default]. -------------------------------------------------------------------------------- /tests/help/snapshots/test_nested_cmd_lvl1Cmd1_lvl2Cmd2.txt: -------------------------------------------------------------------------------- 1 | Usage: 2 | 3 | test_nested_cmd lvl1Cmd1 lvl2Cmd2 [OPTIONS]... 4 | 5 | The following options are available: 6 | 7 | --top-arg1 topArg1 desc [=topArg1 default]. 8 | --lvl1-cmd1-arg1 lvl1Cmd1Arg1 desc [=lvl1Cmd1Arg1 default]. 9 | --lvl2-cmd2-arg1 lvl2Cmd2Arg1 desc [=lvl2Cmd2Arg1 default]. -------------------------------------------------------------------------------- /tests/help/README.md: -------------------------------------------------------------------------------- 1 | ## What? 2 | 3 | The way to test --help output changes is to remove all files under `snapshot` 4 | and run `./tests/test_help.nim` so the snapshots get generated again with 5 | the new content. Then you can check what changed compared to the original file. 6 | 7 | The snapshots are the output of the `*.nim --help` programs in this directory. 8 | 9 | Once your done commit the changes including the new snapshots. 10 | -------------------------------------------------------------------------------- /tests/fail/test_abbr_duplicate_root_subcommand.nim: -------------------------------------------------------------------------------- 1 | import 2 | ../../confutils, 3 | ../../confutils/defs 4 | 5 | type 6 | Command = enum 7 | noCommand 8 | 9 | TestConf* = object 10 | dataDir* {.abbr: "d" }: OutDir 11 | 12 | case cmd* {. 13 | command 14 | defaultValue: noCommand }: Command 15 | 16 | of noCommand: 17 | importDir* {.abbr: "d" }: OutDir 18 | 19 | let c = TestConf.load() 20 | -------------------------------------------------------------------------------- /tests/help/snapshots/test_argument_abbr.txt: -------------------------------------------------------------------------------- 1 | Usage: 2 | 3 | test_argument_abbr [OPTIONS]... 4 | 5 | arg1 desc. 6 | arg1 longdesc line one. 7 | longdesc line two. 8 | longdesc line three. 9 | 10 | The following options are available: 11 | 12 | -o, --opt1 opt1 desc [=opt1 default]. 13 | -p, --opt2 opt2 desc [=opt2 default]. 14 | --opt3 opt3 desc [=opt3 default]. -------------------------------------------------------------------------------- /tests/fail/test_name_duplicate_root_subcommand.nim: -------------------------------------------------------------------------------- 1 | import 2 | ../../confutils, 3 | ../../confutils/defs 4 | 5 | type 6 | Command = enum 7 | noCommand 8 | 9 | TestConf* = object 10 | dataDir* {.name: "data-dir" }: OutDir 11 | 12 | case cmd* {. 13 | command 14 | defaultValue: noCommand }: Command 15 | 16 | of noCommand: 17 | importDir* {.name: "data-dir" }: OutDir 18 | 19 | let c = TestConf.load() 20 | -------------------------------------------------------------------------------- /tests/help/snapshots/test_case_opt_cmdBlockProcessing.txt: -------------------------------------------------------------------------------- 1 | Usage: 2 | 3 | test_case_opt cmdBlockProcessing [OPTIONS]... 4 | 5 | The following options are available: 6 | 7 | -p, --pre The name of your pre-state (without .ssz) [=pre]. 8 | --blockProcessingCat block transitions. 9 | 10 | When blockProcessingCat = catAttestations, the following additional options are available: 11 | 12 | --attestation Attestation filename (without .ssz). -------------------------------------------------------------------------------- /tests/fail/test_abbr_duplicate_subcommand.nim: -------------------------------------------------------------------------------- 1 | import 2 | ../../confutils, 3 | ../../confutils/defs 4 | 5 | type 6 | Command = enum 7 | noCommand 8 | 9 | TestConf* = object 10 | dataDir* {.abbr: "d" }: OutDir 11 | 12 | case cmd* {. 13 | command 14 | defaultValue: noCommand }: Command 15 | 16 | of noCommand: 17 | importDir* {.abbr: "i" }: OutDir 18 | importKey* {.abbr: "i" }: OutDir 19 | 20 | let c = TestConf.load() 21 | -------------------------------------------------------------------------------- /.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 | nimble-version: b920dad9ed76c6619be3ec0cfbf0dde6f9e39092 14 | test-command: | 15 | nimble install -y toml_serialization json_serialization unittest2 16 | rm -f nimble.lock 17 | nimble test 18 | -------------------------------------------------------------------------------- /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 | {.deprecated: "moved to toml_serialization".} 11 | 12 | import toml_serialization/std/net as tomlnet 13 | export tomlnet 14 | -------------------------------------------------------------------------------- /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 | {.deprecated: "moved to toml_serialization".} 11 | 12 | import toml_serialization/std/uri as tomluri 13 | export tomluri 14 | -------------------------------------------------------------------------------- /tests/fail/test_name_duplicate_subcommand.nim: -------------------------------------------------------------------------------- 1 | import 2 | ../../confutils, 3 | ../../confutils/defs 4 | 5 | type 6 | Command = enum 7 | noCommand 8 | 9 | TestConf* = object 10 | dataDir* {.name: "data-dir" }: OutDir 11 | 12 | case cmd* {. 13 | command 14 | defaultValue: noCommand }: Command 15 | 16 | of noCommand: 17 | importDir* {.name: "import-dir" }: OutDir 18 | importKey* {.name: "import-dir" }: OutDir 19 | 20 | let c = TestConf.load() 21 | -------------------------------------------------------------------------------- /tests/config_files/current_user/testVendor/testApp.toml: -------------------------------------------------------------------------------- 1 | # curent user config file 2 | 3 | # log-level = "TOML CU DEBUG" 4 | 5 | log-file = "TOML CU LOGFILE" 6 | 7 | data-dir = "TOML CU DATADIR" 8 | 9 | non-interactive = true 10 | 11 | validator = ["TOML CU VALIDATORS 0"] 12 | 13 | validators-dir = "TOML CU VALIDATOR DIR" 14 | 15 | secrets-dir= "TOML CU SECRET DIR" 16 | 17 | graffiti = "0x00112233445566778899AABBCCDDEEFF" 18 | 19 | stop-at-epoch = 3 20 | 21 | # rpc-port = 1234 22 | 23 | rpc-address = "1.1.1.1" 24 | 25 | retry-delay = 3 26 | -------------------------------------------------------------------------------- /tests/config_files/system_wide/testVendor/testApp.toml: -------------------------------------------------------------------------------- 1 | # system wide config file 2 | 3 | log-level = "TOML SW DEBUG" 4 | 5 | log-file = "TOML SW LOGFILE" 6 | 7 | data-dir = "TOML SW DATADIR" 8 | 9 | non-interactive = true 10 | 11 | validator = ["TOML SW VALIDATORS 0"] 12 | 13 | validators-dir = "TOML SW VALIDATOR DIR" 14 | 15 | secrets-dir = "TOML SW SECRET DIR" 16 | 17 | graffiti = "0x00112233445566778899AABBCCDDEEFF" 18 | 19 | stop-at-epoch = 3 20 | 21 | rpc-port = 1235 22 | 23 | rpc-address = "1.1.1.1" 24 | 25 | retry-delay = 3 26 | -------------------------------------------------------------------------------- /tests/help/snapshots/test_longdesc_lvl1Cmd1.txt: -------------------------------------------------------------------------------- 1 | Usage: 2 | 3 | test_longdesc lvl1Cmd1 [OPTIONS]... 4 | 5 | The following options are available: 6 | 7 | -o, --opt1 opt1 regular description [=opt1 default]. 8 | opt1 longdesc line one. 9 | longdesc line two. 10 | longdesc line three. 11 | --opt2 opt2 regular description [=opt2 default]. 12 | opt2 longdesc line one. 13 | longdesc line two. 14 | longdesc line three. 15 | --opt3 opt3 desc [=opt3 default]. -------------------------------------------------------------------------------- /tests/test_ignore.nim: -------------------------------------------------------------------------------- 1 | import 2 | unittest2, 3 | ../confutils, 4 | ../confutils/defs 5 | 6 | type 7 | TestConf* = object 8 | dataDir* {. 9 | ignore 10 | defaultValue: "nimbus" 11 | name: "data-dir"}: string 12 | 13 | logLevel* {. 14 | defaultValue: "DEBUG" 15 | desc: "Sets the log level." 16 | name: "log-level" }: string 17 | 18 | suite "test ignore option": 19 | test "ignored option have no default value": 20 | let conf = TestConf.load() 21 | doAssert(conf.logLevel == "DEBUG") 22 | doAssert(conf.dataDir == "") 23 | -------------------------------------------------------------------------------- /nim.cfg: -------------------------------------------------------------------------------- 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 | # Avoid some rare stack corruption while using exceptions with a SEH-enabled 11 | # toolchain: https://github.com/status-im/nimbus-eth2/issues/3121 12 | @if windows and not vcc: 13 | --define:nimRawSetjmp 14 | @end 15 | -------------------------------------------------------------------------------- /tests/help/snapshots/test_nested_cmd_lvl1Cmd1.txt: -------------------------------------------------------------------------------- 1 | Usage: 2 | 3 | test_nested_cmd lvl1Cmd1 [OPTIONS]... command 4 | 5 | The following options are available: 6 | 7 | --top-arg1 topArg1 desc [=topArg1 default]. 8 | --lvl1-cmd1-arg1 lvl1Cmd1Arg1 desc [=lvl1Cmd1Arg1 default]. 9 | 10 | Available sub-commands: 11 | 12 | test_nested_cmd lvl1Cmd1 lvl2Cmd1 [OPTIONS]... 13 | 14 | The following options are available: 15 | 16 | --lvl2-cmd1-arg1 lvl2Cmd1Arg1 desc [=lvl2Cmd1Arg1 default]. 17 | 18 | test_nested_cmd lvl1Cmd1 lvl2Cmd2 [OPTIONS]... 19 | 20 | The following options are available: 21 | 22 | --lvl2-cmd2-arg1 lvl2Cmd2Arg1 desc [=lvl2Cmd2Arg1 default]. -------------------------------------------------------------------------------- /tests/help/snapshots/test_longdesc.txt: -------------------------------------------------------------------------------- 1 | Usage: 2 | 3 | test_longdesc [OPTIONS]... command 4 | 5 | The following options are available: 6 | 7 | -o, --opt1 opt1 regular description [=opt1 default]. 8 | opt1 longdesc line one. 9 | longdesc line two. 10 | longdesc line three. 11 | 12 | Available sub-commands: 13 | 14 | test_longdesc lvl1Cmd1 [OPTIONS]... 15 | 16 | The following options are available: 17 | 18 | --opt2 opt2 regular description [=opt2 default]. 19 | opt2 longdesc line one. 20 | longdesc line two. 21 | longdesc line three. 22 | --opt3 opt3 desc [=opt3 default]. -------------------------------------------------------------------------------- /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/help/test_default_value_desc.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2025 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 ../../confutils 11 | 12 | const defaultEth2TcpPort = 9000 13 | 14 | type 15 | TestConf = object 16 | opt1 {. 17 | defaultValue: defaultEth2TcpPort 18 | defaultValueDesc: $defaultEth2TcpPort 19 | desc: "tcp port" 20 | name: "opt1" }: int 21 | 22 | let c = TestConf.load(termWidth = int.high) 23 | -------------------------------------------------------------------------------- /tests/test_all.nim: -------------------------------------------------------------------------------- 1 | # nim-confutils 2 | # Copyright (c) 2020-2022 Status Research & Development GmbH 3 | # Licensed and distributed under either of 4 | # * MIT license: [LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT 5 | # * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 6 | # at your option. This file may not be copied, modified, or distributed except according to those terms. 7 | 8 | {. warning[UnusedImport]:off .} 9 | 10 | import 11 | test_ignore, 12 | test_config_file, 13 | test_envvar, 14 | test_parsecmdarg, 15 | test_pragma, 16 | test_qualified_ident, 17 | test_nested_cmd, 18 | test_help 19 | 20 | when defined(windows): 21 | import test_winreg 22 | -------------------------------------------------------------------------------- /tests/help/test_argument_abbr.nim: -------------------------------------------------------------------------------- 1 | import ../../confutils 2 | 3 | type 4 | TestConf = object 5 | arg1 {. 6 | argument 7 | desc: "arg1 desc" 8 | longDesc: 9 | "arg1 longdesc line one\n" & 10 | "longdesc line two\n" & 11 | "longdesc line three" 12 | name: "arg1" }: string 13 | opt1 {. 14 | defaultValue: "opt1 default" 15 | desc: "opt1 desc" 16 | name: "opt1" 17 | abbr: "o" }: string 18 | opt2 {. 19 | defaultValue: "opt2 default" 20 | desc: "opt2 desc" 21 | name: "opt2" 22 | abbr: "p" }: string 23 | opt3 {. 24 | defaultValue: "opt3 default" 25 | desc: "opt3 desc" 26 | name: "opt3" }: string 27 | 28 | let c = TestConf.load(termWidth = int.high) 29 | -------------------------------------------------------------------------------- /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 | {.push raises: [], gcsafe.} 11 | 12 | # Optional toml support - add `requires "toml_serialization"` to your package 13 | # dependencies before using 14 | 15 | import toml_serialization, ../defs as confutilsDefs 16 | 17 | export toml_serialization, confutilsDefs 18 | 19 | type ConfTypes = InputFile | InputDir | OutPath | OutDir | OutFile 20 | serializesAsBase(ConfTypes, Toml) 21 | 22 | {.pop.} 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /confutils/json/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 | {.push raises: [], gcsafe.} 11 | 12 | # Optional json support - add `requires "json_serialization"` to your package 13 | # dependencies before using 14 | 15 | import serialization, json_serialization, ../defs as confutilsDefs 16 | 17 | export json_serialization, confutilsDefs 18 | 19 | type ConfTypes = InputFile | InputDir | OutPath | OutDir | OutFile 20 | serializesAsBase(ConfTypes, Json) 21 | 22 | {.pop.} 23 | -------------------------------------------------------------------------------- /tests/command_with_args.nim: -------------------------------------------------------------------------------- 1 | import 2 | confutils, options 3 | 4 | type 5 | Cmd = enum 6 | fizz = "command A" 7 | buzz = "command B" 8 | 9 | Conf = object 10 | case cmd {.command.}: Cmd 11 | of fizz: 12 | option {.desc: "some option".}: Option[string] 13 | anotherOption {.desc: "another option" 14 | defaultValue: "some value".}: string 15 | thirdOption {.desc: "third option" 16 | defaultValue: "another value" 17 | defaultValueDesc: "some description".}: string 18 | arg1 {. 19 | argument 20 | desc: "argument 1" .}: string 21 | argument2 {. 22 | argument 23 | desc: "argument 2" .}: int 24 | of buzz: 25 | discard 26 | 27 | echo load(Conf) 28 | -------------------------------------------------------------------------------- /tests/help/snapshots/test_case_opt.txt: -------------------------------------------------------------------------------- 1 | Usage: 2 | 3 | test_case_opt [OPTIONS]... command 4 | 5 | The following options are available: 6 | 7 | -p, --pre The name of your pre-state (without .ssz) [=pre]. 8 | 9 | Available sub-commands: 10 | 11 | test_case_opt cmdSlotProcessing [OPTIONS]... 12 | 13 | The following options are available: 14 | 15 | -s, --num-slots The number of slots the pre-state will be advanced by [=1]. 16 | 17 | test_case_opt cmdBlockProcessing [OPTIONS]... 18 | 19 | The following options are available: 20 | 21 | --blockProcessingCat block transitions. 22 | 23 | When blockProcessingCat = catAttestations, the following additional options are available: 24 | 25 | --attestation Attestation filename (without .ssz). -------------------------------------------------------------------------------- /tests/help/snapshots/test_argument.txt: -------------------------------------------------------------------------------- 1 | Usage: 2 | 3 | test_argument command 4 | 5 | Available sub-commands: 6 | 7 | test_argument argAfterOpt [OPTIONS]... 8 | 9 | arg1 desc. 10 | 11 | The following options are available: 12 | 13 | --arg-after-opt-opt1 opt1 desc [=opt1 default]. 14 | 15 | test_argument argBeforeOpt [OPTIONS]... 16 | 17 | arg2 desc. 18 | 19 | The following options are available: 20 | 21 | --arg-before-opt-opt2 opt2 desc [=opt2 default]. 22 | 23 | test_argument argAroundOpt [OPTIONS]... 24 | 25 | arg4 desc. 26 | arg5 desc. 27 | 28 | The following options are available: 29 | 30 | --arg-around-opt-opt3 opt3 desc [=opt3 default]. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/help/snapshots/test_nested_cmd.txt: -------------------------------------------------------------------------------- 1 | Usage: 2 | 3 | test_nested_cmd [OPTIONS]... command 4 | 5 | The following options are available: 6 | 7 | --top-arg1 topArg1 desc [=topArg1 default]. 8 | --lvl1-no-command-arg1 lvl1NoCommandArg1 desc [=lvl1NoCommandArg1 default]. 9 | 10 | Available sub-commands: 11 | 12 | test_nested_cmd lvl1Cmd1 [OPTIONS]... command 13 | 14 | The following options are available: 15 | 16 | --lvl1-cmd1-arg1 lvl1Cmd1Arg1 desc [=lvl1Cmd1Arg1 default]. 17 | 18 | Available sub-commands: 19 | 20 | test_nested_cmd lvl1Cmd1 lvl2Cmd1 [OPTIONS]... 21 | 22 | The following options are available: 23 | 24 | --lvl2-cmd1-arg1 lvl2Cmd1Arg1 desc [=lvl2Cmd1Arg1 default]. 25 | 26 | test_nested_cmd lvl1Cmd1 lvl2Cmd2 [OPTIONS]... 27 | 28 | The following options are available: 29 | 30 | --lvl2-cmd2-arg1 lvl2Cmd2Arg1 desc [=lvl2Cmd2Arg1 default]. -------------------------------------------------------------------------------- /tests/help/test_separator.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2025 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 ../../confutils 11 | 12 | type 13 | Lvl1Cmd = enum 14 | lvl1Cmd1 15 | 16 | TestConf = object 17 | opt1 {. 18 | separator: "Network Options:" 19 | defaultValue: "opt1 default" 20 | desc: "opt1 desc" 21 | name: "opt1" }: string 22 | opt2 {. 23 | defaultValue: "opt2 default" 24 | desc: "opt2 desc" 25 | name: "opt2" }: string 26 | opt3 {. 27 | separator: "\p----------------" 28 | defaultValue: "opt3 default" 29 | desc: "opt3 desc" 30 | name: "opt3" }: string 31 | 32 | let c = TestConf.load(termWidth = int.high) 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/test_duplicates.nim: -------------------------------------------------------------------------------- 1 | import 2 | ../confutils, 3 | ../confutils/defs 4 | 5 | # duplicate name and abbr from different subcommand 6 | # at the same level is allowed 7 | 8 | # but hierarchical duplicate is not allowed 9 | 10 | type 11 | Command = enum 12 | noCommand 13 | subCommand 14 | 15 | BranchCmd = enum 16 | branchA 17 | branchB 18 | 19 | TestConf* = object 20 | dataDir* {.abbr: "d" }: OutDir 21 | 22 | case cmd* {. 23 | command 24 | defaultValue: noCommand }: Command 25 | 26 | of noCommand: 27 | importDir* {. 28 | abbr: "i" 29 | name: "import" 30 | }: OutDir 31 | 32 | outputDir* {. 33 | abbr: "o" 34 | name: "output" 35 | }: OutDir 36 | 37 | of subCommand: 38 | importKey* {. 39 | abbr: "i" 40 | name: "import" 41 | }: OutDir 42 | 43 | case subcmd* {. 44 | command 45 | defaultValue: branchA }: BranchCmd 46 | 47 | of branchA: 48 | outputFolder* {. 49 | abbr: "o" 50 | name: "output" 51 | }: OutDir 52 | 53 | of branchB: 54 | importFolder* {. 55 | abbr: "f" 56 | name: "import-folder" 57 | }: OutDir 58 | 59 | let c = TestConf.load() 60 | discard c 61 | -------------------------------------------------------------------------------- /tests/help/test_longdesc.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2025 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 ../../confutils 11 | 12 | type 13 | Lvl1Cmd = enum 14 | lvl1Cmd1 15 | 16 | TestConf = object 17 | opt1 {. 18 | desc: "opt1 regular description" 19 | longDesc: 20 | "opt1 longdesc line one\n" & 21 | "longdesc line two\n" & 22 | "longdesc line three" 23 | defaultValue: "opt1 default" 24 | name: "opt1" 25 | abbr: "o" }: string 26 | 27 | case cmd {.command.}: Lvl1Cmd 28 | of Lvl1Cmd.lvl1Cmd1: 29 | opt2 {. 30 | desc: "opt2 regular description" 31 | longDesc: 32 | "opt2 longdesc line one\n" & 33 | "longdesc line two\n" & 34 | "longdesc line three" 35 | defaultValue: "opt2 default" 36 | name: "opt2" }: string 37 | opt3 {. 38 | defaultValue: "opt3 default" 39 | desc: "opt3 desc" 40 | name: "opt3" }: string 41 | 42 | let c = TestConf.load(termWidth = int.high) 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/help/test_argument.nim: -------------------------------------------------------------------------------- 1 | import ../../confutils 2 | 3 | type 4 | Lvl1Cmd* = enum 5 | noCommand 6 | argAfterOpt 7 | argBeforeOpt 8 | argAroundOpt 9 | 10 | TestConf* = object 11 | case cmd* {. 12 | command 13 | defaultValue: Lvl1Cmd.noCommand }: Lvl1Cmd 14 | of Lvl1Cmd.noCommand: 15 | discard 16 | of Lvl1Cmd.argAfterOpt: 17 | opt1* {. 18 | defaultValue: "opt1 default" 19 | desc: "opt1 desc" 20 | name: "arg-after-opt-opt1" }: string 21 | arg1* {. 22 | argument 23 | desc: "arg1 desc" 24 | name: "arg-after-opt-arg1" }: string 25 | of Lvl1Cmd.argBeforeOpt: 26 | arg2* {. 27 | argument 28 | desc: "arg2 desc" 29 | name: "arg-before-opt-arg2" }: string 30 | opt2* {. 31 | defaultValue: "opt2 default" 32 | desc: "opt2 desc" 33 | name: "arg-before-opt-opt2" }: string 34 | of Lvl1Cmd.argAroundOpt: 35 | arg4* {. 36 | argument 37 | desc: "arg4 desc" 38 | name: "arg-around-opt-arg4" }: string 39 | opt3* {. 40 | defaultValue: "opt3 default" 41 | desc: "opt3 desc" 42 | name: "arg-around-opt-opt3" }: string 43 | arg5* {. 44 | argument 45 | desc: "arg5 desc" 46 | name: "arg-around-opt-arg5" }: string 47 | 48 | when isMainModule: 49 | let c = TestConf.load(termWidth = int.high) 50 | -------------------------------------------------------------------------------- /tests/help/test_case_opt.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2025 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 ../../confutils 11 | 12 | type 13 | StartupCommand* = enum 14 | noCommand 15 | cmdSlotProcessing 16 | cmdBlockProcessing 17 | 18 | BlockProcessingCat* = enum 19 | catBlockHeader 20 | catAttestations 21 | 22 | ScenarioConf* = object 23 | preState* {. 24 | desc: "The name of your pre-state (without .ssz)" 25 | name: "pre" 26 | abbr: "p" 27 | defaultValue: "pre".}: string 28 | case cmd*{. 29 | command 30 | defaultValue: noCommand }: StartupCommand 31 | of noCommand: 32 | discard 33 | of cmdSlotProcessing: 34 | numSlots* {. 35 | desc: "The number of slots the pre-state will be advanced by" 36 | name: "num-slots" 37 | abbr: "s" 38 | defaultValue: 1.}: uint64 39 | of cmdBlockProcessing: 40 | case blockProcessingCat* {. 41 | desc: "block transitions" 42 | #name: "process-blocks" # Comment this to make it work 43 | implicitlySelectable 44 | required .}: BlockProcessingCat 45 | of catBlockHeader: 46 | discard 47 | of catAttestations: 48 | attestation*{. 49 | desc: "Attestation filename (without .ssz)" 50 | name: "attestation".}: string 51 | 52 | let scenario = ScenarioConf.load(termWidth = int.high) 53 | -------------------------------------------------------------------------------- /tests/help/test_nested_cmd.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2025 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 ../../confutils 11 | 12 | type 13 | Lvl1Cmd = enum 14 | lvl1NoCommand 15 | lvl1Cmd1 16 | 17 | Lvl2Cmd = enum 18 | lvl2Cmd1 19 | lvl2Cmd2 20 | 21 | TestConf = object 22 | topArg1 {. 23 | defaultValue: "topArg1 default" 24 | desc: "topArg1 desc" 25 | name: "top-arg1" }: string 26 | 27 | case cmd {. 28 | command 29 | defaultValue: Lvl1Cmd.lvl1NoCommand }: Lvl1Cmd 30 | of Lvl1Cmd.lvl1NoCommand: 31 | lvl1NoCommandArg1 {. 32 | defaultValue: "lvl1NoCommandArg1 default" 33 | desc: "lvl1NoCommandArg1 desc" 34 | name: "lvl1-no-command-arg1" }: string 35 | of Lvl1Cmd.lvl1Cmd1: 36 | lvl1Cmd1Arg1 {. 37 | defaultValue: "lvl1Cmd1Arg1 default" 38 | desc: "lvl1Cmd1Arg1 desc" 39 | name: "lvl1-cmd1-arg1" }: string 40 | 41 | case lvl2Cmd {.command.}: Lvl2Cmd 42 | of Lvl2Cmd.lvl2Cmd1: 43 | lvl2Cmd1Arg1 {. 44 | defaultValue: "lvl2Cmd1Arg1 default" 45 | desc: "lvl2Cmd1Arg1 desc" 46 | name: "lvl2-cmd1-arg1" }: string 47 | of Lvl2Cmd.lvl2Cmd2: 48 | lvl2Cmd2Arg1 {. 49 | defaultValue: "lvl2Cmd2Arg1 default" 50 | desc: "lvl2Cmd2Arg1 desc" 51 | name: "lvl2-cmd2-arg1" }: string 52 | 53 | let c = TestConf.load(termWidth = int.high) 54 | -------------------------------------------------------------------------------- /tests/nested_commands.nim: -------------------------------------------------------------------------------- 1 | import 2 | confutils, options 3 | 4 | type 5 | OuterCmd = enum 6 | outerCmd1 7 | outerCmd2 8 | outerCmd3 9 | 10 | InnerCmd = enum 11 | innerCmd1 = "Inner cmd 1" 12 | innerCmd2 13 | 14 | OuterOpt = enum 15 | outerOpt1 = "Option1" 16 | outerOpt2 = "Option2" 17 | outerOpt3 = "Option3" 18 | 19 | InnerOpt = enum 20 | innerOpt1 21 | innerOpt2 22 | 23 | Conf = object 24 | commonOptional: Option[string] 25 | commonMandatory {. 26 | desc: "A mandatory option" 27 | abbr: "m" .}: int 28 | 29 | case opt: OuterOpt 30 | of outerOpt1: 31 | case innerOpt: InnerOpt 32 | of innerOpt1: 33 | io1Mandatory: string 34 | io1Optional: Option[int] 35 | else: 36 | discard 37 | of outerOpt2: 38 | ooMandatory: string 39 | ooOptiona {. 40 | defaultValue: "test" 41 | desc: "Outer option optional" .}: string 42 | of outerOpt3: 43 | discard 44 | 45 | case cmd {.command.}: OuterCmd 46 | of outerCmd1: 47 | case innerCmd: InnerCmd 48 | of innerCmd1: 49 | ic1Mandatory: string 50 | ic1Optional {. 51 | desc: "Delay in seconds" 52 | abbr: "s" .}: Option[int] 53 | of innerCmd2: 54 | innerArg {.argument.}: string 55 | of outerCmd2: 56 | oc2Mandatory: int 57 | of outerCmd3: 58 | x {.argument.}: string 59 | y {.argument.}: string 60 | z {.argument.}: string 61 | 62 | let conf = load Conf 63 | 64 | echo "commonOptional = ", conf.commonOptional 65 | echo "commonMandatory = ", conf.commonMandatory 66 | case conf.cmd 67 | of outerCmd2: 68 | echo "oc2Mandatory = ", conf.oc2Mandatory 69 | of outerCmd1: 70 | case conf.innerCmd: 71 | of innerCmd1: 72 | echo "ic1Mandatory = ", conf.ic1Mandatory 73 | echo "ic1Optional = ", conf.ic1Optional 74 | of innerCmd2: 75 | discard 76 | of outerCmd3: 77 | echo "outer cmd3 ", conf.x, " ", conf.y, " ", conf.z 78 | 79 | -------------------------------------------------------------------------------- /confutils/winreg/writer.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 | typetraits, options, tables, 12 | serialization, 13 | ./utils, ./types 14 | 15 | type 16 | WinregWriter* = object 17 | hKey: HKEY 18 | path: string 19 | key: seq[string] 20 | 21 | {.push gcsafe, raises: [].} 22 | 23 | proc init*(T: type WinregWriter, 24 | hKey: HKEY, path: string): T = 25 | result.hKey = hKey 26 | result.path = path 27 | 28 | proc writeValue*(w: var WinregWriter, value: auto) {.raises: [IOError].} = 29 | mixin enumInstanceSerializedFields, writeValue, writeFieldIMPL 30 | # TODO: reduce allocation 31 | 32 | when value is (SomePrimitives or range or string): 33 | let path = constructPath(w.path, w.key) 34 | discard setValue(w.hKey, path, w.key[^1], value) 35 | elif value is Option: 36 | if value.isSome: 37 | w.writeValue value.get 38 | elif value is (seq or array or openArray): 39 | when uTypeIsPrimitives(type value): 40 | let path = constructPath(w.path, w.key) 41 | discard setValue(w.hKey, path, w.key[^1], value) 42 | elif uTypeIsRecord(type value): 43 | let key = w.key[^1] 44 | for i in 0..= 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 | (if NimMajor >= 2: "" else: " -d:nimOldCaseObjects") 34 | 35 | proc build(args, path: string) = 36 | exec nimc & " " & lang & " " & cfg & " " & flags & " " & args & " " & path 37 | 38 | proc run(args, path: string) = 39 | build args & " --mm:refc -r", path 40 | if (NimMajor, NimMinor) > (1, 6): 41 | build args & " --mm:orc -r", path 42 | 43 | task test, "Run all tests": 44 | for threads in ["--threads:off", "--threads:on"]: 45 | run threads, "tests/test_all" 46 | build threads, "tests/test_duplicates" 47 | 48 | #Also iterate over every test in tests/fail, and verify they fail to compile. 49 | echo "\r\nTest Fail to Compile:" 50 | for path in listFiles(thisDir() / "tests" / "fail"): 51 | if not path.endsWith(".nim"): 52 | continue 53 | if gorgeEx(nimc & " " & lang & " " & flags & " " & path).exitCode != 0: 54 | echo " [OK] ", path.split(DirSep)[^1] 55 | else: 56 | echo " [FAILED] ", path.split(DirSep)[^1] 57 | quit(QuitFailure) 58 | 59 | echo "\r\nNimscript test:" 60 | let 61 | actualOutput = gorgeEx( 62 | nimc & " --verbosity:0 e " & flags & " " & "./tests/cli_example.nim " & 63 | "--foo=1 --bar=2 --withBaz 42").output 64 | expectedOutput = unindent""" 65 | foo = 1 66 | bar = 2 67 | baz = true 68 | arg ./tests/cli_example.nim 69 | arg 42""" 70 | if actualOutput.strip() == expectedOutput: 71 | echo " [OK] tests/cli_example.nim" 72 | else: 73 | echo " [FAILED] tests/cli_example.nim" 74 | echo actualOutput 75 | quit(QuitFailure) 76 | -------------------------------------------------------------------------------- /tests/test_help.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2025 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/[os, osproc, strutils], 12 | unittest2, 13 | stew/byteutils, 14 | ../confutils 15 | 16 | const helpPath = "tests" / "help" 17 | const snapshotsPath = helpPath / "snapshots" 18 | 19 | func normalizeHelp(s: string): string = 20 | s.replace("\x1B[0m", "") 21 | .replace("\r\n", "\n") 22 | .strip(leading = false) 23 | 24 | func cmdsToName(cmds: string): string = 25 | if cmds.len == 0: 26 | "" 27 | else: 28 | "_" & cmds.replace(" ", "_") 29 | 30 | proc cmdTest(cmdName: string, cmds = "") = 31 | let fname = helpPath / cmdName 32 | var build = "nim c --verbosity:0 --hints:off -d:confutilsNoColors" 33 | if NimMajor < 2: 34 | build.add " -d:nimOldCaseObjects" 35 | let buildRes = execCmdEx(build & " " & fname & ".nim") 36 | if buildRes.exitCode != 0: 37 | checkpoint "Build output: " & buildRes.output 38 | fail() 39 | else: 40 | let res = execCmdEx(fname & " " & cmds & " --help") 41 | let output = res.output.normalizeHelp() 42 | let snapshot = snapshotsPath / cmdName & cmds.cmdsToName() & ".txt" 43 | if res.exitCode != 0: 44 | checkpoint "Run output: " & res.output 45 | fail() 46 | elif not fileExists(snapshot): 47 | writeFile(snapshot, output) 48 | checkpoint "Snapshot created: " & snapshot 49 | fail() 50 | else: 51 | let expected = readFile(snapshot).normalizeHelp() 52 | checkpoint "Cmd output: " & $output.toBytes() 53 | checkpoint "Snapshot: " & $expected.toBytes() 54 | check output == expected 55 | 56 | suite "test --help": 57 | test "test test_nested_cmd": 58 | cmdTest("test_nested_cmd", "") 59 | 60 | test "test test_nested_cmd lvl1Cmd1": 61 | cmdTest("test_nested_cmd", "lvl1Cmd1") 62 | 63 | test "test test_nested_cmd lvl1Cmd1 lvl2Cmd2": 64 | cmdTest("test_nested_cmd", "lvl1Cmd1 lvl2Cmd2") 65 | 66 | test "test test_argument": 67 | cmdTest("test_argument", "") 68 | 69 | test "test test_argument_abbr": 70 | cmdTest("test_argument_abbr", "") 71 | 72 | test "test test_default_value_desc": 73 | cmdTest("test_default_value_desc", "") 74 | 75 | test "test test_separator": 76 | cmdTest("test_separator", "") 77 | 78 | test "test test_longdesc": 79 | cmdTest("test_longdesc", "") 80 | 81 | test "test test_longdesc lvl1Cmd1": 82 | cmdTest("test_longdesc", "lvl1Cmd1") 83 | 84 | test "test test_case_opt": 85 | cmdTest("test_case_opt", "") 86 | 87 | test "test test_case_opt cmdBlockProcessing": 88 | cmdTest("test_case_opt", "cmdBlockProcessing") 89 | -------------------------------------------------------------------------------- /tests/test_argument.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2025 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 unittest2, ../confutils, ./help/test_argument 11 | 12 | suite "test argument": 13 | test "no command": 14 | let conf = TestConf.load(cmdLine = @[]) 15 | check: 16 | conf.cmd == Lvl1Cmd.noCommand 17 | 18 | test "pass arg first to argAfterOpt": 19 | let conf = TestConf.load(cmdLine = @[ 20 | "argAfterOpt", 21 | "foo", 22 | "--arg-after-opt-opt1=bar" 23 | ]) 24 | check: 25 | conf.cmd == Lvl1Cmd.argAfterOpt 26 | conf.arg1 == "foo" 27 | conf.opt1 == "bar" 28 | 29 | test "pass arg last to argAfterOpt": 30 | let conf = TestConf.load(cmdLine = @[ 31 | "argAfterOpt", 32 | "--arg-after-opt-opt1=bar", 33 | "foo" 34 | ]) 35 | check: 36 | conf.cmd == Lvl1Cmd.argAfterOpt 37 | conf.arg1 == "foo" 38 | conf.opt1 == "bar" 39 | 40 | test "pass arg first to argBeforeOpt": 41 | let conf = TestConf.load(cmdLine = @[ 42 | "argBeforeOpt", 43 | "foo", 44 | "--arg-before-opt-opt2=bar" 45 | ]) 46 | check: 47 | conf.cmd == Lvl1Cmd.argBeforeOpt 48 | conf.arg2 == "foo" 49 | conf.opt2 == "bar" 50 | 51 | test "pass arg last to argBeforeOpt": 52 | let conf = TestConf.load(cmdLine = @[ 53 | "argBeforeOpt", 54 | "--arg-before-opt-opt2=bar", 55 | "foo" 56 | ]) 57 | check: 58 | conf.cmd == Lvl1Cmd.argBeforeOpt 59 | conf.arg2 == "foo" 60 | conf.opt2 == "bar" 61 | 62 | test "pass arg first to argAroundOpt": 63 | let conf = TestConf.load(cmdLine = @[ 64 | "argAroundOpt", 65 | "foo", 66 | "bar", 67 | "--arg-around-opt-opt3=baz" 68 | ]) 69 | check: 70 | conf.cmd == Lvl1Cmd.argAroundOpt 71 | conf.arg4 == "foo" 72 | conf.arg5 == "bar" 73 | conf.opt3 == "baz" 74 | 75 | test "pass arg last to argAroundOpt": 76 | let conf = TestConf.load(cmdLine = @[ 77 | "argAroundOpt", 78 | "--arg-around-opt-opt3=baz", 79 | "foo", 80 | "bar" 81 | ]) 82 | check: 83 | conf.cmd == Lvl1Cmd.argAroundOpt 84 | conf.arg4 == "foo" 85 | conf.arg5 == "bar" 86 | conf.opt3 == "baz" 87 | 88 | test "pass arg mix to argAroundOpt": 89 | let conf = TestConf.load(cmdLine = @[ 90 | "argAroundOpt", 91 | "foo", 92 | "--arg-around-opt-opt3=baz", 93 | "bar" 94 | ]) 95 | check: 96 | conf.cmd == Lvl1Cmd.argAroundOpt 97 | conf.arg4 == "foo" 98 | conf.arg5 == "bar" 99 | conf.opt3 == "baz" 100 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/test_parsecmdarg.nim: -------------------------------------------------------------------------------- 1 | # nim-confutils 2 | # Copyright (c) 2022-2023 Status Research & Development GmbH 3 | # Licensed and distributed under either of 4 | # * MIT license: [LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT 5 | # * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 6 | # at your option. This file may not be copied, modified, or distributed except according to those terms. 7 | 8 | import 9 | std/[sequtils], 10 | unittest2, 11 | ../confutils 12 | 13 | func testValidValues[T](lo: T = low(T), hi: T = high(T)): bool = 14 | allIt(lo .. hi, T.parseCmdArg($it) == it) 15 | 16 | func testInvalidValues[T](lo, hi: int64): bool = 17 | static: doAssert low(int64) <= low(T).int64 and high(int64) >= 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 | 109 | test "enum": 110 | type TestEnumArg {.pure.} = enum 111 | arg1 = "Arg1" 112 | arg2 113 | Arg3 114 | check: 115 | parseCmdArg(TestEnumArg, "Arg1") == TestEnumArg.arg1 116 | parseCmdArg(TestEnumArg, "arg1") == TestEnumArg.arg1 117 | parseCmdArg(TestEnumArg, "aRG1") == TestEnumArg.arg1 118 | parseCmdArg(TestEnumArg, "arg2") == TestEnumArg.arg2 119 | parseCmdArg(TestEnumArg, "Arg2") == TestEnumArg.arg2 120 | parseCmdArg(TestEnumArg, "aRG2") == TestEnumArg.arg2 121 | parseCmdArg(TestEnumArg, "arg3") == TestEnumArg.Arg3 122 | parseCmdArg(TestEnumArg, "Arg3") == TestEnumArg.Arg3 123 | parseCmdArg(TestEnumArg, "aRG3") == TestEnumArg.Arg3 124 | expect ValueError: 125 | discard parseCmdArg(TestEnumArg, "Arg123") 126 | -------------------------------------------------------------------------------- /tests/test_nested_cmd.nim: -------------------------------------------------------------------------------- 1 | # confutils 2 | # Copyright (c) 2018-2025 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/os, unittest2, toml_serialization, ../confutils 11 | 12 | const configFilePath = "tests" / "config_files" 13 | 14 | template loadFile(T, file): untyped = 15 | proc ( 16 | config: T, sources: ref SecondarySources 17 | ) {.raises: [ConfigurationError].} = 18 | sources.addConfigFile(Toml, InputFile(configFilePath / file)) 19 | 20 | type 21 | OuterCmd = enum 22 | noCommand 23 | outerCmd1 24 | 25 | InnerCmd = enum 26 | innerCmd1 = "Inner cmd 1" 27 | innerCmd2 = "Inner cmd 2" 28 | 29 | TestConf = object 30 | case cmd {. 31 | command 32 | defaultValue: OuterCmd.noCommand }: OuterCmd 33 | of OuterCmd.noCommand: 34 | outerArg {. 35 | defaultValue: "outerArg default" 36 | desc: "outerArg desc" 37 | name: "outer-arg" }: string 38 | of OuterCmd.outerCmd1: 39 | outerArg1 {. 40 | defaultValue: "outerArg1 default" 41 | desc: "outerArg1 desc" 42 | name: "outer-arg1" }: string 43 | case innerCmd {.command.}: InnerCmd 44 | of InnerCmd.innerCmd1: 45 | innerArg1 {. 46 | defaultValue: "innerArg1 default" 47 | desc: "innerArg1 desc" 48 | name: "inner-arg1" }: string 49 | of InnerCmd.innerCmd2: 50 | innerArg2 {. 51 | defaultValue: "innerArg2 default" 52 | desc: "innerArg2 desc" 53 | name: "inner-arg2" }: string 54 | 55 | suite "test nested cmd": 56 | test "no command": 57 | let conf = TestConf.load(cmdLine = @[ 58 | "--outer-arg=foobar" 59 | ]) 60 | check: 61 | conf.cmd == OuterCmd.noCommand 62 | conf.outerArg == "foobar" 63 | 64 | test "subcommand outerCmd1 innerCmd1": 65 | let conf = TestConf.load(cmdLine = @[ 66 | "outerCmd1", 67 | "innerCmd1", 68 | "--inner-arg1=foobar" 69 | ]) 70 | check: 71 | conf.cmd == OuterCmd.outerCmd1 72 | conf.innerCmd == InnerCmd.innerCmd1 73 | conf.innerArg1 == "foobar" 74 | 75 | test "subcommand outerCmd1 innerCmd2": 76 | let conf = TestConf.load(cmdLine = @[ 77 | "outerCmd1", 78 | "innerCmd2", 79 | "--inner-arg2=foobar" 80 | ]) 81 | check: 82 | conf.cmd == OuterCmd.outerCmd1 83 | conf.innerCmd == InnerCmd.innerCmd2 84 | conf.innerArg2 == "foobar" 85 | 86 | suite "test nested cmd default args": 87 | test "no command default": 88 | let conf = TestConf.load(cmdLine = newSeq[string]()) 89 | check: 90 | conf.cmd == OuterCmd.noCommand 91 | conf.outerArg == "outerArg default" 92 | 93 | test "subcommand outerCmd1 innerCmd1": 94 | let conf = TestConf.load(cmdLine = @[ 95 | "outerCmd1", 96 | "innerCmd1" 97 | ]) 98 | check: 99 | conf.cmd == OuterCmd.outerCmd1 100 | conf.innerCmd == InnerCmd.innerCmd1 101 | conf.outerArg1 == "outerArg1 default" 102 | conf.innerArg1 == "innerArg1 default" 103 | 104 | test "subcommand outerCmd1 innerCmd2": 105 | let conf = TestConf.load(cmdLine = @[ 106 | "outerCmd1", 107 | "innerCmd2" 108 | ]) 109 | check: 110 | conf.cmd == OuterCmd.outerCmd1 111 | conf.innerCmd == InnerCmd.innerCmd2 112 | conf.outerArg1 == "outerArg1 default" 113 | conf.innerArg2 == "innerArg2 default" 114 | 115 | suite "test nested cmd toml": 116 | test "no command default": 117 | let conf = TestConf.load(secondarySources = loadFile(TestConf, "nested_cmd.toml")) 118 | check: 119 | conf.cmd == OuterCmd.noCommand 120 | conf.outerArg == "toml outer-arg" 121 | 122 | test "subcommand outerCmd1 innerCmd1": 123 | let conf = TestConf.load( 124 | secondarySources = loadFile(TestConf, "nested_cmd.toml"), 125 | cmdLine = @[ 126 | "outerCmd1", 127 | "innerCmd1" 128 | ] 129 | ) 130 | check: 131 | conf.cmd == OuterCmd.outerCmd1 132 | conf.innerCmd == InnerCmd.innerCmd1 133 | conf.outerArg1 == "toml outer-arg1" 134 | conf.innerArg1 == "toml inner-arg1" 135 | 136 | test "subcommand outerCmd1 innerCmd2": 137 | let conf = TestConf.load( 138 | secondarySources = loadFile(TestConf, "nested_cmd.toml"), 139 | cmdLine = @[ 140 | "outerCmd1", 141 | "innerCmd2" 142 | ] 143 | ) 144 | check: 145 | conf.cmd == OuterCmd.outerCmd1 146 | conf.innerCmd == InnerCmd.innerCmd2 147 | conf.outerArg1 == "toml outer-arg1" 148 | conf.innerArg2 == "toml inner-arg2" 149 | -------------------------------------------------------------------------------- /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..= 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 | -------------------------------------------------------------------------------- /tests/test_config_file.nim: -------------------------------------------------------------------------------- 1 | # nim-confutils 2 | # Copyright (c) 2020-2024 Status Research & Development GmbH 3 | # Licensed and distributed under either of 4 | # * MIT license: [LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT 5 | # * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 6 | # at your option. This file may not be copied, modified, or distributed except according to those terms. 7 | 8 | import 9 | std/[options, os], 10 | unittest2, 11 | stew/byteutils, ../confutils, 12 | ../confutils/[std/net] 13 | 14 | import 15 | toml_serialization, json_serialization, 16 | ../confutils/winreg/winreg_serialization 17 | 18 | type 19 | ValidatorPrivKey = object 20 | field_a: int 21 | field_b: string 22 | 23 | GraffitiBytes = array[16, byte] 24 | 25 | VCStartUpCmd = enum 26 | VCNoCommand 27 | `import` 28 | 29 | ValidatorKeyPath = TypedInputFile[ValidatorPrivKey, Txt, "privkey"] 30 | 31 | TestConf* = object 32 | configFile* {. 33 | desc: "Loads the configuration from a config file" 34 | name: "config-file" }: Option[InputFile] 35 | 36 | logLevel* {. 37 | defaultValue: "DEBUG" 38 | desc: "Sets the log level." 39 | name: "log-level" }: string 40 | 41 | logFile* {. 42 | desc: "Specifies a path for the written Json log file" 43 | name: "log-file" }: Option[OutFile] 44 | 45 | dataDir* {. 46 | defaultValue: config.defaultDataDir() 47 | desc: "The directory where nimbus will store all blockchain data" 48 | abbr: "d" 49 | name: "data-dir" }: OutDir 50 | 51 | nonInteractive* {. 52 | desc: "Do not display interative prompts. Quit on missing configuration" 53 | name: "non-interactive" }: bool 54 | 55 | validators* {. 56 | required 57 | desc: "Attach a validator by supplying a keystore path" 58 | abbr: "v" 59 | name: "validator" }: seq[ValidatorKeyPath] 60 | 61 | validatorsDirFlag* {. 62 | desc: "A directory containing validator keystores" 63 | name: "validators-dir" }: Option[InputDir] 64 | 65 | secretsDirFlag* {. 66 | desc: "A directory containing validator keystore passwords" 67 | name: "secrets-dir" }: Option[InputDir] 68 | 69 | case cmd* {. 70 | command 71 | defaultValue: VCNoCommand }: VCStartUpCmd 72 | 73 | of VCNoCommand: 74 | graffiti* {. 75 | desc: "The graffiti value that will appear in proposed blocks. " & 76 | "You can use a 0x-prefixed hex encoded string to specify raw bytes." 77 | name: "graffiti" }: Option[GraffitiBytes] 78 | 79 | stopAtEpoch* {. 80 | defaultValue: 0 81 | desc: "A positive epoch selects the epoch at which to stop" 82 | name: "stop-at-epoch" }: uint64 83 | 84 | rpcPort* {. 85 | defaultValue: defaultEth2RpcPort 86 | desc: "HTTP port of the server to connect to for RPC - for the validator duties in the pull model" 87 | name: "rpc-port" }: Port 88 | 89 | rpcAddress* {. 90 | defaultValue: defaultAdminListenAddress(config) 91 | desc: "Address of the server to connect to for RPC - for the validator duties in the pull model" 92 | name: "rpc-address" }: IpAddress 93 | 94 | restAddress* {. 95 | defaultValue: defaultAdminListenAddress(config) 96 | desc: "Address of the server to connect to for RPC - for the validator duties in the pull model" 97 | name: "rest-address" }: IpAddress 98 | 99 | retryDelay* {. 100 | defaultValue: 10 101 | desc: "Delay in seconds between retries after unsuccessful attempts to connect to a beacon node" 102 | name: "retry-delay" }: int 103 | 104 | of `import`: 105 | 106 | blocksFile* {. 107 | argument 108 | desc: "Import RLP encoded block(s) from a file, validate, write to database and quit" 109 | defaultValue: "" 110 | name: "blocks-file" }: InputFile 111 | 112 | `type`* {. 113 | desc: "Test nnkAccQuoted of public sub command entry" 114 | defaultValue: "" 115 | name: "import-type" }: string 116 | 117 | `seq` {. 118 | desc: "Test nnkAccQuoted of private sub command entry" 119 | defaultValue: "" 120 | name: "import-seq" }: string 121 | 122 | func defaultDataDir(conf: TestConf): string = 123 | discard 124 | 125 | func parseCmdArg*(T: type GraffitiBytes, input: string): T 126 | {.raises: [ValueError, Defect].} = 127 | discard 128 | 129 | func completeCmdArg*(T: type GraffitiBytes, input: string): seq[string] = 130 | @[] 131 | 132 | func defaultAdminListenAddress*(conf: TestConf): IpAddress = 133 | (static parseIpAddress("127.0.0.1")) 134 | 135 | const 136 | defaultEth2TcpPort* = 9000 137 | defaultEth2RpcPort* = 9090 138 | 139 | const 140 | confPathCurrUser = "tests" / "config_files" / "current_user" 141 | confPathSystemWide = "tests" / "config_files" / "system_wide" 142 | 143 | # User might also need to extend the serializer capability 144 | # for each of the registered formats. 145 | # This is especially true for distinct types and some special types 146 | # not covered by the standard implementation 147 | 148 | proc readValue(r: var TomlReader, 149 | value: var (InputFile | InputDir | OutFile | OutDir | ValidatorKeyPath)) = 150 | type T = type value 151 | value = T r.parseAsString() 152 | 153 | proc readValue(r: var TomlReader, value: var IpAddress) = 154 | try: 155 | value = parseIpAddress(r.parseAsString()) 156 | except ValueError as ex: 157 | raise newException(SerializationError, ex.msg) 158 | 159 | proc readValue(r: var TomlReader, value: var Port) = 160 | value = r.parseInt(int).Port 161 | 162 | proc readValue(r: var TomlReader, value: var GraffitiBytes) = 163 | try: 164 | value = hexToByteArray[value.len](r.parseAsString()) 165 | except ValueError as ex: 166 | raise newException(SerializationError, ex.msg) 167 | 168 | proc readValue(r: var WinregReader, 169 | value: var (InputFile | InputDir | OutFile | OutDir | ValidatorKeyPath)) = 170 | type T = type value 171 | value = r.readValue(string).T 172 | 173 | proc readValue(r: var WinregReader, value: var IpAddress) {.used.} = 174 | value = parseIpAddress(r.readValue(string)) 175 | 176 | proc readValue(r: var WinregReader, value: var Port) {.used.} = 177 | value = r.readValue(int).Port 178 | 179 | proc readValue(r: var WinregReader, value: var GraffitiBytes) {.used.} = 180 | value = hexToByteArray[value.len](r.readValue(string)) 181 | 182 | proc testConfigFile() = 183 | suite "config file test suite": 184 | putEnv("PREFIX_DATA_DIR", "ENV VAR DATADIR") 185 | 186 | test "basic config file": 187 | let conf = TestConf.load( 188 | envVarsPrefix="prefix", 189 | secondarySources = proc ( 190 | config: TestConf, sources: ref SecondarySources 191 | ) {.raises: [ConfigurationError].} = 192 | if config.configFile.isSome: 193 | sources.addConfigFile(Toml, config.configFile.get) 194 | else: 195 | sources.addConfigFile(Toml, InputFile(confPathCurrUser / "testVendor" / "testApp.toml")) 196 | sources.addConfigFile(Toml, InputFile(confPathSystemWide / "testVendor" / "testApp.toml")) 197 | ) 198 | 199 | # dataDir is in env var 200 | check conf.dataDir.string == "ENV VAR DATADIR" 201 | 202 | # logFile is in current user config file 203 | check conf.logFile.isSome() 204 | check conf.logFile.get().string == "TOML CU LOGFILE" 205 | 206 | # logLevel and rpcPort are in system wide config file 207 | check conf.logLevel == "TOML SW DEBUG" 208 | check conf.rpcPort.int == 1235 209 | 210 | testConfigFile() 211 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | isIgnore: bool 42 | 43 | GeneratedFieldInfo = object 44 | isIgnore: bool 45 | isCommandOrArgument: bool 46 | path: seq[string] 47 | 48 | OriginalToGeneratedFields = OrderedTable[string, GeneratedFieldInfo] 49 | 50 | SectionParam = object 51 | isCommandOrArgument: bool 52 | isIgnore: bool 53 | defaultValue: string 54 | namePragma: string 55 | 56 | {.push gcsafe, raises: [].} 57 | 58 | func isOption(n: NimNode): bool = 59 | if n.kind != nnkBracketExpr: return false 60 | eqIdent(n[0], "Option") 61 | 62 | func makeOption(n: NimNode): NimNode = 63 | newNimNode(nnkBracketExpr).add(ident("Option"), n) 64 | 65 | template objectDecl(a): untyped = 66 | type a = object 67 | 68 | proc putRecList(n: NimNode, recList: NimNode) = 69 | recList.expectKind nnkRecList 70 | if n.kind == nnkObjectTy: 71 | n[2] = recList 72 | return 73 | for z in n: 74 | putRecList(z, recList) 75 | 76 | proc generateOptionalField(fieldName: NimNode, fieldType: NimNode): NimNode = 77 | let right = if isOption(fieldType): fieldType else: makeOption(fieldType) 78 | newIdentDefs(fieldName, right) 79 | 80 | proc traverseIdent(ident: NimNode, typ: NimNode, 81 | isDiscriminator: bool, param = SectionParam()): ConfFileSection = 82 | ident.expectKind nnkIdent 83 | ConfFileSection(fieldName: $ident, 84 | namePragma: param.namePragma, typ: typ, 85 | defaultValue: param.defaultValue, 86 | isCommandOrArgument: param.isCommandOrArgument, 87 | isDiscriminator: isDiscriminator, 88 | isIgnore: param.isIgnore) 89 | 90 | proc traversePostfix(postfix: NimNode, typ: NimNode, isDiscriminator: bool, 91 | param = SectionParam()): ConfFileSection = 92 | postfix.expectKind nnkPostfix 93 | 94 | case postfix[1].kind 95 | of nnkIdent: 96 | traverseIdent(postfix[1], typ, isDiscriminator, param) 97 | of nnkAccQuoted: 98 | traverseIdent(postfix[1][0], typ, isDiscriminator, param) 99 | else: 100 | raiseAssert "[Postfix] Unsupported child node:\n" & postfix[1].treeRepr 101 | 102 | proc shortEnumName(n: NimNode): NimNode = 103 | if n.kind == nnkDotExpr: 104 | n[1] 105 | else: 106 | n 107 | 108 | proc traversePragma(pragma: NimNode): SectionParam = 109 | pragma.expectKind nnkPragma 110 | var child: NimNode 111 | 112 | for childNode in pragma: 113 | child = childNode 114 | 115 | if child.kind == nnkCall: 116 | # A custom pragma was used more than once (e.g.: {.pragma: posixOnly, hidden.}) and the 117 | # AST is now: 118 | # ``` 119 | # Call 120 | # Sym "hidden" 121 | # ``` 122 | child = child[0] 123 | 124 | case child.kind 125 | of nnkSym: 126 | let sym = $child 127 | if sym == "command" or sym == "argument": 128 | result.isCommandOrArgument = true 129 | elif sym == "ignore": 130 | result.isIgnore = true 131 | of nnkExprColonExpr: 132 | let pragma = $child[0] 133 | if pragma == "defaultValue": 134 | result.defaultValue = repr(shortEnumName(child[1])) 135 | elif pragma == "name": 136 | result.namePragma = $child[1] 137 | else: 138 | raiseAssert "[Pragma] Unsupported child node:\n" & child.treeRepr 139 | 140 | proc traversePragmaExpr(pragmaExpr: NimNode, typ: NimNode, 141 | isDiscriminator: bool): ConfFileSection = 142 | pragmaExpr.expectKind nnkPragmaExpr 143 | let param = traversePragma(pragmaExpr[1]) 144 | 145 | case pragmaExpr[0].kind 146 | of nnkIdent: 147 | traverseIdent(pragmaExpr[0], typ, isDiscriminator, param) 148 | of nnkAccQuoted: 149 | traverseIdent(pragmaExpr[0][0], typ, isDiscriminator, param) 150 | of nnkPostfix: 151 | traversePostfix(pragmaExpr[0], typ, isDiscriminator, param) 152 | else: 153 | raiseAssert "[PragmaExpr] Unsupported expression:\n" & pragmaExpr.treeRepr 154 | 155 | proc traverseIdentDefs(identDefs: NimNode, parent: ConfFileSection, 156 | isDiscriminator: bool): seq[ConfFileSection] = 157 | identDefs.expectKind nnkIdentDefs 158 | doAssert identDefs.len > 2, "This kind of node must have at least 3 children." 159 | let typ = identDefs[^2] 160 | for child in identDefs: 161 | case child.kind 162 | of nnkIdent: 163 | result.add traverseIdent(child, typ, isDiscriminator) 164 | of nnkAccQuoted: 165 | result.add traverseIdent(child[0], typ, isDiscriminator) 166 | of nnkPostfix: 167 | result.add traversePostfix(child, typ, isDiscriminator) 168 | of nnkPragmaExpr: 169 | result.add traversePragmaExpr(child, typ, isDiscriminator) 170 | of nnkBracketExpr, nnkSym, nnkEmpty, nnkInfix, nnkCall, nnkDotExpr: 171 | discard 172 | else: 173 | raiseAssert "[IdentDefs] Unsupported child node:\n" & child.treeRepr 174 | 175 | proc traverseRecList(recList: NimNode, parent: ConfFileSection): seq[ConfFileSection] 176 | 177 | proc traverseOfBranch(ofBranch: NimNode, parent: ConfFileSection): ConfFileSection = 178 | ofBranch.expectKind nnkOfBranch 179 | result = ConfFileSection(fieldName: repr(shortEnumName(ofBranch[0])), isCaseBranch: true) 180 | for child in ofBranch: 181 | case child.kind: 182 | of nnkIdent, nnkDotExpr, nnkAccQuoted: 183 | discard 184 | of nnkRecList: 185 | result.children.add traverseRecList(child, result) 186 | else: 187 | raiseAssert "[OfBranch] Unsupported child node:\n" & child.treeRepr 188 | 189 | proc traverseRecCase(recCase: NimNode, parent: ConfFileSection): seq[ConfFileSection] = 190 | recCase.expectKind nnkRecCase 191 | for child in recCase: 192 | case child.kind 193 | of nnkIdentDefs: 194 | result.add traverseIdentDefs(child, parent, true) 195 | of nnkOfBranch: 196 | result.add traverseOfBranch(child, parent) 197 | else: 198 | raiseAssert "[RecCase] Unsupported child node:\n" & child.treeRepr 199 | 200 | proc traverseRecList(recList: NimNode, parent: ConfFileSection): seq[ConfFileSection] = 201 | recList.expectKind nnkRecList 202 | for child in recList: 203 | case child.kind 204 | of nnkIdentDefs: 205 | result.add traverseIdentDefs(child, parent, false) 206 | of nnkRecCase: 207 | result.add traverseRecCase(child, parent) 208 | of nnkNilLit: 209 | discard 210 | else: 211 | raiseAssert "[RecList] Unsupported child node:\n" & child.treeRepr 212 | 213 | proc normalize(root: ConfFileSection) = 214 | ## Moves the default case branches children one level upper in the hierarchy. 215 | ## Also removes case branches without children. 216 | var children: seq[ConfFileSection] 217 | var defaultValue = "" 218 | for child in root.children: 219 | normalize(child) 220 | if child.isDiscriminator: 221 | defaultValue = child.defaultValue 222 | if child.isCaseBranch and child.fieldName == defaultValue: 223 | for childChild in child.children: 224 | children.add childChild 225 | child.children = @[] 226 | elif child.isCaseBranch and child.children.len == 0: 227 | discard 228 | else: 229 | children.add child 230 | root.children = children 231 | 232 | proc generateConfigFileModel(ConfType: NimNode): ConfFileSection = 233 | let confTypeImpl = ConfType.getType[1].getImpl 234 | result = ConfFileSection(fieldName: $confTypeImpl[0]) 235 | result.children = traverseRecList(confTypeImpl[2][2], result) 236 | result.normalize 237 | 238 | proc getRenamedName(node: ConfFileSection): string = 239 | if node.namePragma.len == 0: node.fieldName else: node.namePragma 240 | 241 | proc generateTypes(root: ConfFileSection): seq[NimNode] = 242 | let index = result.len 243 | result.add getAst(objectDecl(genSym(nskType, root.fieldName)))[0] 244 | var recList = newNimNode(nnkRecList) 245 | for child in root.children: 246 | if child.isCommandOrArgument or child.isIgnore: 247 | continue 248 | if child.isCaseBranch: 249 | if child.children.len > 0: 250 | var types = generateTypes(child) 251 | recList.add generateOptionalField(child.fieldName.ident, types[0][0]) 252 | result.add types 253 | else: 254 | recList.add generateOptionalField(child.getRenamedName.ident, child.typ) 255 | result[index].putRecList(recList) 256 | 257 | proc generateSettersPaths(node: ConfFileSection, 258 | result: var OriginalToGeneratedFields, 259 | pathsCache: var seq[string]) = 260 | pathsCache.add node.getRenamedName 261 | if node.children.len == 0: 262 | result[node.fieldName] = GeneratedFieldInfo( 263 | isIgnore: node.isIgnore, 264 | isCommandOrArgument: node.isCommandOrArgument, 265 | path: pathsCache, 266 | ) 267 | else: 268 | for child in node.children: 269 | generateSettersPaths(child, result, pathsCache) 270 | pathsCache.del pathsCache.len - 1 271 | 272 | proc generateSettersPaths(root: ConfFileSection, pathsCache: var seq[string]): OriginalToGeneratedFields = 273 | for child in root.children: 274 | generateSettersPaths(child, result, pathsCache) 275 | 276 | template cfSetter(a, b: untyped): untyped = 277 | when a is Option: 278 | a = some(b) 279 | else: 280 | a = b 281 | 282 | proc generateSetters(confType, CF: NimNode, fieldsPaths: OriginalToGeneratedFields): 283 | (NimNode, NimNode, int) = 284 | var 285 | procs = newStmtList() 286 | assignments = newStmtList() 287 | numSetters = 0 288 | 289 | let c = "c".ident 290 | for field, param in fieldsPaths: 291 | if param.isCommandOrArgument or param.isIgnore: 292 | assignments.add quote do: 293 | result.setters[`numSetters`] = defaultConfigFileSetter 294 | inc numSetters 295 | continue 296 | 297 | var fieldPath = c 298 | var condition: NimNode 299 | for fld in param.path: 300 | fieldPath = newDotExpr(fieldPath, fld.ident) 301 | let fieldChecker = newDotExpr(fieldPath, "isSome".ident) 302 | if condition == nil: 303 | condition = fieldChecker 304 | else: 305 | condition = newNimNode(nnkInfix).add("and".ident).add(condition).add(fieldChecker) 306 | fieldPath = newDotExpr(fieldPath, "get".ident) 307 | 308 | let setterName = genSym(nskProc, field & "CFSetter") 309 | let fieldIdent = field.ident 310 | procs.add quote do: 311 | proc `setterName`(s: var `confType`, cf: ref `CF`): bool {.nimcall, gcsafe.} = 312 | for `c` in cf.data: 313 | if `condition`: 314 | cfSetter(s.`fieldIdent`, `fieldPath`) 315 | return true 316 | 317 | assignments.add quote do: 318 | result.setters[`numSetters`] = `setterName` 319 | inc numSetters 320 | 321 | result = (procs, assignments, numSetters) 322 | 323 | proc generateConfigFileSetters(confType, optType: NimNode, 324 | fieldsPaths: OriginalToGeneratedFields): NimNode = 325 | let 326 | CF = ident "SecondarySources" 327 | T = confType.getType[1] 328 | optT = optType[0][0] 329 | SetterProcType = genSym(nskType, "SetterProcType") 330 | (setterProcs, assignments, numSetters) = generateSetters(T, CF, fieldsPaths) 331 | stmtList = quote do: 332 | type 333 | `SetterProcType` = proc( 334 | s: var `T`, cf: ref `CF` 335 | ): bool {.nimcall, gcsafe, raises: [].} 336 | 337 | `CF` = object 338 | data*: seq[`optT`] 339 | setters: array[`numSetters`, `SetterProcType`] 340 | 341 | proc defaultConfigFileSetter( 342 | s: var `T`, cf: ref `CF` 343 | ): bool {.nimcall, gcsafe, raises: [], used.} = 344 | discard 345 | 346 | `setterProcs` 347 | 348 | proc new(_: type `CF`): ref `CF` = 349 | new result 350 | `assignments` 351 | 352 | new(`CF`) 353 | 354 | stmtList 355 | 356 | macro generateSecondarySources*(ConfType: type): untyped = 357 | let 358 | model = generateConfigFileModel(ConfType) 359 | modelType = generateTypes(model) 360 | var 361 | pathsCache: seq[string] 362 | 363 | result = newTree(nnkStmtList) 364 | result.add newTree(nnkTypeSection, modelType) 365 | 366 | let settersPaths = model.generateSettersPaths(pathsCache) 367 | result.add generateConfigFileSetters(ConfType, result[^1], settersPaths) 368 | 369 | {.pop.} 370 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 isOpt(opt: OptInfo): bool = 178 | opt.isCliSwitch and not opt.isHidden 179 | 180 | func hasOpts(cmd: CmdInfo): bool = 181 | for opt in cmd.opts: 182 | if opt.isOpt: 183 | return true 184 | false 185 | 186 | func hasArgs(cmd: CmdInfo): bool = 187 | for opt in cmd.opts: 188 | if opt.kind == Arg: 189 | return true 190 | false 191 | 192 | iterator args(cmd: CmdInfo): OptInfo = 193 | for opt in cmd.opts: 194 | if opt.kind == Arg: 195 | yield opt 196 | 197 | func getSubCmdDiscriminator(cmd: CmdInfo): OptInfo = 198 | for i in countdown(cmd.opts.len - 1, 0): 199 | let opt = cmd.opts[i] 200 | if opt.kind != Arg: 201 | if opt.kind == Discriminator and opt.isCommand: 202 | return opt 203 | else: 204 | return nil 205 | 206 | template hasSubCommands(cmd: CmdInfo): bool = 207 | getSubCmdDiscriminator(cmd) != nil 208 | 209 | iterator subCmds(cmd: CmdInfo): CmdInfo = 210 | let subCmdDiscriminator = cmd.getSubCmdDiscriminator 211 | if subCmdDiscriminator != nil: 212 | for cmd in subCmdDiscriminator.subCmds: 213 | yield cmd 214 | 215 | template isSubCommand(cmd: CmdInfo): bool = 216 | cmd.name.len > 0 217 | 218 | func maxNameLen(cmd: CmdInfo, commands = false): int = 219 | result = 0 220 | for opt in cmd.opts: 221 | if opt.isOpt or opt.kind == Arg: 222 | result = max(result, opt.name.len) 223 | if opt.kind == Discriminator: 224 | for subCmd in opt.subCmds: 225 | result = max(result, maxNameLen(subCmd, commands)) 226 | elif commands and opt.kind == Discriminator and opt.isCommand: 227 | for subCmd in opt.subCmds: 228 | result = max(result, maxNameLen(subCmd, commands)) 229 | 230 | func maxNameLen(cmds: openArray[CmdInfo]): int = 231 | result = 0 232 | for i, cmd in cmds: 233 | result = max(result, maxNameLen(cmd, commands = i == cmds.high)) 234 | 235 | func hasAbbrs(cmd: CmdInfo, commands = false): bool = 236 | for opt in cmd.opts: 237 | if opt.isOpt: 238 | if opt.abbr.len > 0: 239 | return true 240 | if opt.kind == Discriminator: 241 | for subCmd in opt.subCmds: 242 | if hasAbbrs(subCmd, commands): 243 | return true 244 | elif commands and opt.kind == Discriminator and opt.isCommand: 245 | for subCmd in opt.subCmds: 246 | if hasAbbrs(subCmd, commands): 247 | return true 248 | 249 | func hasAbbrs(cmds: openArray[CmdInfo]): bool = 250 | for i, cmd in cmds: 251 | if hasAbbrs(cmd, commands = i == cmds.high): 252 | return true 253 | false 254 | 255 | func humaneName(opt: OptInfo): string = 256 | if opt.name.len > 0: opt.name 257 | else: opt.abbr 258 | 259 | template padding(output: string, desiredWidth: int): string = 260 | spaces(max(desiredWidth - output.len, 0)) 261 | 262 | proc writeDesc(help: var string, 263 | appInfo: HelpAppInfo, 264 | desc, defaultValue: string) = 265 | const descSpacing = " " 266 | let 267 | descIndent = (5 + appInfo.namesWidth + descSpacing.len) 268 | remainingColumns = appInfo.terminalWidth - descIndent 269 | defaultValSuffix = if defaultValue.len == 0: "" 270 | else: " [=" & defaultValue & "]" 271 | fullDesc = desc & defaultValSuffix & "." 272 | 273 | if remainingColumns < confutils_narrow_terminal_width: 274 | helpOutput "\p ", wrapWords(fullDesc, appInfo.terminalWidth - 2, 275 | newLine = "\p ") 276 | else: 277 | let wrappingWidth = min(remainingColumns, confutils_description_width) 278 | helpOutput descSpacing, wrapWords(fullDesc, wrappingWidth, 279 | newLine = "\p" & spaces(descIndent)) 280 | 281 | proc writeLongDesc(help: var string, 282 | appInfo: HelpAppInfo, 283 | desc: string) = 284 | let lines = split(desc, {'\n', '\r'}) 285 | for line in lines: 286 | if line.len > 0: 287 | helpOutput "\p" 288 | helpOutput padding("", 5 + appInfo.namesWidth) 289 | help.writeDesc appInfo, line, "" 290 | 291 | proc describeInvocation(help: var string, 292 | cmd: CmdInfo, cmdInvocation: string, 293 | appInfo: HelpAppInfo) = 294 | helpOutput styleBright, "\p", fgCommand, cmdInvocation 295 | 296 | if cmd.opts.len > 0: 297 | if cmd.hasOpts: helpOutput " [OPTIONS]..." 298 | 299 | let subCmdDiscriminator = cmd.getSubCmdDiscriminator 300 | if subCmdDiscriminator != nil: helpOutput " command" 301 | 302 | for arg in cmd.args: 303 | helpOutput " <", arg.name, ">" 304 | 305 | helpOutput "\p" 306 | 307 | if cmd.desc.len > 0: 308 | helpOutput "\p", cmd.desc, ".\p" 309 | 310 | var argsSectionStarted = false 311 | 312 | for arg in cmd.args: 313 | if arg.desc.len > 0: 314 | if not argsSectionStarted: 315 | helpOutput "\p" 316 | argsSectionStarted = true 317 | helpOutput " " 318 | if appInfo.hasAbbrs: 319 | helpOutput " " 320 | let cliArg = "<" & arg.name & ">" 321 | helpOutput fgArg, styleBright, cliArg 322 | helpOutput padding(cliArg, appInfo.namesWidth) 323 | help.writeDesc appInfo, arg.desc, arg.defaultInHelpText 324 | help.writeLongDesc appInfo, arg.longDesc 325 | helpOutput "\p" 326 | 327 | type 328 | OptionsType = enum 329 | normalOpts 330 | defaultCmdOpts 331 | conditionalOpts 332 | 333 | proc describeOptionsList( 334 | help: var string, 335 | cmd: CmdInfo, 336 | appInfo: HelpAppInfo 337 | ) = 338 | for opt in cmd.opts: 339 | if not opt.isOpt: 340 | continue 341 | 342 | if opt.separator.len > 0: 343 | helpOutput opt.separator 344 | helpOutput "\p" 345 | 346 | # Indent all command-line switches 347 | helpOutput " " 348 | 349 | if opt.abbr.len > 0: 350 | helpOutput fgOption, styleBright, "-", opt.abbr, ", " 351 | elif appInfo.hasAbbrs: 352 | # Add additional indentatition, so all names are aligned 353 | helpOutput " " 354 | 355 | if opt.name.len > 0: 356 | let switch = "--" & opt.name 357 | helpOutput fgOption, styleBright, 358 | switch, padding(switch, appInfo.namesWidth) 359 | else: 360 | helpOutput spaces(2 + appInfo.namesWidth) 361 | 362 | if opt.desc.len > 0: 363 | help.writeDesc appInfo, 364 | opt.desc.replace("%t", opt.typename), 365 | opt.defaultInHelpText 366 | help.writeLongDesc appInfo, opt.longDesc 367 | 368 | helpOutput "\p" 369 | 370 | proc describeOptions( 371 | help: var string, 372 | cmds: openArray[CmdInfo], 373 | cmdInvocation: string, 374 | appInfo: HelpAppInfo, 375 | optionsType = normalOpts 376 | ) = 377 | if cmds.len == 0: 378 | return 379 | 380 | var hasOpts = false 381 | for c in cmds: 382 | if c.hasOpts: 383 | hasOpts = true 384 | 385 | if hasOpts: 386 | case optionsType 387 | of normalOpts: 388 | helpOutput "\pThe following options are available:\p\p" 389 | of conditionalOpts: 390 | helpOutput ", the following additional options are available:\p\p" 391 | of defaultCmdOpts: 392 | discard 393 | 394 | for c in cmds: 395 | describeOptionsList(help, c, appInfo) 396 | 397 | for c in cmds: 398 | for opt in c.opts: 399 | if opt.isOpt and opt.kind == Discriminator: 400 | for i, subCmd in opt.subCmds: 401 | if not subCmd.hasOpts: 402 | continue 403 | 404 | helpOutput "\pWhen ", styleBright, fgBlue, opt.humaneName, resetStyle, " = ", fgGreen, subCmd.name 405 | 406 | if i == opt.defaultSubCmd: 407 | helpOutput " (default)" 408 | help.describeOptions [subCmd], cmdInvocation, appInfo, conditionalOpts 409 | 410 | let cmd = cmds[^1] 411 | let subCmdDiscriminator = cmd.getSubCmdDiscriminator 412 | if subCmdDiscriminator != nil: 413 | let defaultCmdIdx = subCmdDiscriminator.defaultSubCmd 414 | if defaultCmdIdx != -1: 415 | let defaultCmd = subCmdDiscriminator.subCmds[defaultCmdIdx] 416 | help.describeOptions [defaultCmd], cmdInvocation, appInfo, defaultCmdOpts 417 | 418 | helpOutput fgSection, "\pAvailable sub-commands:\p" 419 | 420 | for i, subCmd in subCmdDiscriminator.subCmds: 421 | if i != subCmdDiscriminator.defaultSubCmd: 422 | let subCmdInvocation = cmdInvocation & " " & subCmd.name 423 | help.describeInvocation subCmd, subCmdInvocation, appInfo 424 | help.describeOptions [subCmd], subCmdInvocation, appInfo 425 | 426 | proc showHelp(help: var string, 427 | appInfo: HelpAppInfo, 428 | activeCmds: openArray[CmdInfo]) = 429 | if appInfo.copyrightBanner.len > 0: 430 | helpOutput appInfo.copyrightBanner, "\p\p" 431 | 432 | let cmd = activeCmds[^1] 433 | 434 | appInfo.maxNameLen = activeCmds.maxNameLen 435 | appInfo.hasAbbrs = activeCmds.hasAbbrs 436 | let termWidth = 437 | try: 438 | terminalWidth() 439 | except ValueError: 440 | int.high # https://github.com/nim-lang/Nim/pull/21968 441 | if appInfo.terminalWidth == 0: 442 | appInfo.terminalWidth = termWidth 443 | appInfo.namesWidth = min(minNameWidth, appInfo.maxNameLen) + descPadding 444 | 445 | var cmdInvocation = appInfo.appInvocation 446 | for i in 1 ..< activeCmds.len: 447 | cmdInvocation.add " " 448 | cmdInvocation.add activeCmds[i].name 449 | 450 | # Write out the app or script name 451 | helpOutput fgSection, "Usage: \p" 452 | help.describeInvocation cmd, cmdInvocation, appInfo 453 | help.describeOptions activeCmds, cmdInvocation, appInfo 454 | helpOutput "\p" 455 | 456 | flushOutputAndQuit QuitSuccess 457 | 458 | func getNextArgIdx(cmd: CmdInfo, consumedArgIdx: int): int = 459 | for i in 0 ..< cmd.opts.len: 460 | if cmd.opts[i].kind == Arg and cmd.opts[i].idx > consumedArgIdx: 461 | return cmd.opts[i].idx 462 | 463 | -1 464 | 465 | proc noMoreArgsError(cmd: CmdInfo): string {.raises: [].} = 466 | result = 467 | if cmd.isSubCommand: 468 | "The command '" & cmd.name & "'" 469 | else: 470 | appInvocation() 471 | result.add " does not accept" 472 | if cmd.hasArgs: result.add " additional" 473 | result.add " arguments" 474 | 475 | func findOpt(opts: openArray[OptInfo], name: string): OptInfo = 476 | for opt in opts: 477 | if cmpIgnoreStyle(opt.name, name) == 0 or 478 | cmpIgnoreStyle(opt.abbr, name) == 0: 479 | return opt 480 | 481 | func findOpt(activeCmds: openArray[CmdInfo], name: string): OptInfo = 482 | for i in countdown(activeCmds.len - 1, 0): 483 | let found = findOpt(activeCmds[i].opts, name) 484 | if found != nil: return found 485 | 486 | func findCmd(cmds: openArray[CmdInfo], name: string): CmdInfo = 487 | for cmd in cmds: 488 | if cmpIgnoreStyle(cmd.name, name) == 0: 489 | return cmd 490 | 491 | func findSubCmd(cmd: CmdInfo, name: string): CmdInfo = 492 | let subCmdDiscriminator = cmd.getSubCmdDiscriminator 493 | if subCmdDiscriminator != nil: 494 | let cmd = findCmd(subCmdDiscriminator.subCmds, name) 495 | if cmd != nil: return cmd 496 | 497 | return nil 498 | 499 | func startsWithIgnoreStyle(s: string, prefix: string): bool = 500 | # Similar in spirit to cmpIgnoreStyle, but compare only the prefix. 501 | var i = 0 502 | var j = 0 503 | 504 | while true: 505 | # Skip any underscore 506 | while i < s.len and s[i] == '_': inc i 507 | while j < prefix.len and prefix[j] == '_': inc j 508 | 509 | if j == prefix.len: 510 | # The whole prefix matches 511 | return true 512 | elif i == s.len: 513 | # We've reached the end of `s` without matching the prefix 514 | return false 515 | elif toLowerAscii(s[i]) != toLowerAscii(prefix[j]): 516 | return false 517 | 518 | inc i 519 | inc j 520 | 521 | when defined(debugCmdTree): 522 | proc printCmdTree(cmd: CmdInfo, indent = 0) = 523 | let blanks = spaces(indent) 524 | echo blanks, "> ", cmd.name 525 | 526 | for opt in cmd.opts: 527 | if opt.kind == Discriminator: 528 | for subcmd in opt.subCmds: 529 | printCmdTree(subcmd, indent + 2) 530 | else: 531 | echo blanks, " - ", opt.name, ": ", opt.typename 532 | 533 | else: 534 | template printCmdTree(cmd: CmdInfo) = discard 535 | 536 | # TODO remove the overloads here to get better "missing overload" error message 537 | proc parseCmdArg*(T: type InputDir, p: string): T {.raises: [ValueError].} = 538 | if not dirExists(p): 539 | raise newException(ValueError, "Directory doesn't exist") 540 | 541 | T(p) 542 | 543 | proc parseCmdArg*(T: type InputFile, p: string): T {.raises: [ValueError].} = 544 | # TODO this is needed only because InputFile cannot be made 545 | # an alias of TypedInputFile at the moment, because of a generics 546 | # caching issue 547 | if not fileExists(p): 548 | raise newException(ValueError, "File doesn't exist") 549 | 550 | when not defined(nimscript): 551 | try: 552 | let f = system.open(p, fmRead) 553 | close f 554 | except IOError: 555 | raise newException(ValueError, "File not accessible") 556 | 557 | T(p) 558 | 559 | proc parseCmdArg*( 560 | T: type TypedInputFile, p: string): T {.raises: [ValueError].} = 561 | var path = p 562 | when T.defaultExt.len > 0: 563 | path = path.addFileExt(T.defaultExt) 564 | 565 | if not fileExists(path): 566 | raise newException(ValueError, "File doesn't exist") 567 | 568 | when not defined(nimscript): 569 | try: 570 | let f = system.open(path, fmRead) 571 | close f 572 | except IOError: 573 | raise newException(ValueError, "File not accessible") 574 | 575 | T(path) 576 | 577 | func parseCmdArg*(T: type[OutDir|OutFile|OutPath], p: string): T = 578 | T(p) 579 | 580 | proc parseCmdArg*[T]( 581 | _: type Option[T], s: string): Option[T] {.raises: [ValueError].} = 582 | some(parseCmdArg(T, s)) 583 | 584 | template parseCmdArg*(T: type string, s: string): string = 585 | s 586 | 587 | func parseCmdArg*( 588 | T: type SomeSignedInt, s: string): T {.raises: [ValueError].} = 589 | T parseBiggestInt(s) 590 | 591 | func parseCmdArg*( 592 | T: type SomeUnsignedInt, s: string): T {.raises: [ValueError].} = 593 | T parseBiggestUInt(s) 594 | 595 | func parseCmdArg*(T: type SomeFloat, p: string): T {.raises: [ValueError].} = 596 | parseFloat(p) 597 | 598 | func parseCmdArg*(T: type bool, p: string): T {.raises: [ValueError].} = 599 | try: 600 | p.len == 0 or parseBool(p) 601 | except CatchableError: 602 | raise newException(ValueError, "'" & p & "' is not a valid boolean value. Supported values are on/off, yes/no, true/false or 1/0") 603 | 604 | func parseEnumNormalized[T: enum](s: string): T {.raises: [ValueError].} = 605 | # Note: In Nim 1.6 `parseEnum` normalizes the string except for the first 606 | # character. Nim 1.2 would normalize for all characters. In config options 607 | # the latter behaviour is required so this custom function is needed. 608 | genEnumCaseStmt(T, s, default = nil, ord(low(T)), ord(high(T)), normalize) 609 | 610 | func parseCmdArg*(T: type enum, s: string): T {.raises: [ValueError].} = 611 | parseEnumNormalized[T](s) 612 | 613 | proc parseCmdArgAux(T: type, s: string): T {.raises: [ValueError].} = 614 | # The parseCmdArg procs are allowed to raise only `ValueError`. 615 | # If you have provided your own specializations, please handle 616 | # all other exception types. 617 | mixin parseCmdArg 618 | try: 619 | parseCmdArg(T, s) 620 | except CatchableError as exc: 621 | raise newException(ValueError, exc.msg) 622 | 623 | func completeCmdArg*(T: type enum, val: string): seq[string] = 624 | for e in low(T)..high(T): 625 | let as_str = $e 626 | if startsWithIgnoreStyle(as_str, val): 627 | result.add($e) 628 | 629 | func completeCmdArg*(T: type SomeNumber, val: string): seq[string] = 630 | @[] 631 | 632 | func completeCmdArg*(T: type bool, val: string): seq[string] = 633 | @[] 634 | 635 | func completeCmdArg*(T: type string, val: string): seq[string] = 636 | @[] 637 | 638 | proc completeCmdArg*(T: type[InputFile|TypedInputFile|InputDir|OutFile|OutDir|OutPath], 639 | val: string): seq[string] = 640 | when not defined(nimscript): 641 | let (dir, name, ext) = splitFile(val) 642 | let tail = name & ext 643 | # Expand the directory component for the directory walker routine 644 | let dir_path = if dir == "": "." else: expandTilde(dir) 645 | # Dotfiles are hidden unless the user entered a dot as prefix 646 | let show_dotfiles = len(name) > 0 and name[0] == '.' 647 | 648 | try: 649 | for kind, path in walkDir(dir_path, relative=true): 650 | if not show_dotfiles and path[0] == '.': 651 | continue 652 | 653 | # Do not show files if asked for directories, on the other hand we must show 654 | # directories even if a file is requested to allow the user to select a file 655 | # inside those 656 | if type(T) is (InputDir or OutDir) and kind notin {pcDir, pcLinkToDir}: 657 | continue 658 | 659 | # Note, no normalization is needed here 660 | if path.startsWith(tail): 661 | var match = dir_path / path 662 | # Add a trailing slash so that completions can be chained 663 | if kind in {pcDir, pcLinkToDir}: 664 | match &= DirSep 665 | 666 | result.add(shellPathEscape(match)) 667 | except OSError: 668 | discard 669 | 670 | func completeCmdArg*[T](_: type seq[T], val: string): seq[string] = 671 | @[] 672 | 673 | proc completeCmdArg*[T](_: type Option[T], val: string): seq[string] = 674 | mixin completeCmdArg 675 | return completeCmdArg(type(T), val) 676 | 677 | proc completeCmdArgAux(T: type, val: string): seq[string] = 678 | mixin completeCmdArg 679 | return completeCmdArg(T, val) 680 | 681 | template setField[T]( 682 | loc: var T, val: Option[string], defaultVal: untyped): untyped = 683 | type FieldType = type(loc) 684 | loc = if isSome(val): parseCmdArgAux(FieldType, val.get) 685 | else: FieldType(defaultVal) 686 | 687 | template setField[T]( 688 | loc: var seq[T], val: Option[string], defaultVal: untyped): untyped = 689 | if val.isSome: 690 | loc.add parseCmdArgAux(type(loc[0]), val.get) 691 | else: 692 | type FieldType = type(loc) 693 | loc = FieldType(defaultVal) 694 | 695 | func makeDefaultValue*(T: type): T = 696 | default(T) 697 | 698 | func requiresInput*(T: type): bool = 699 | not ((T is seq) or (T is Option) or (T is bool)) 700 | 701 | func acceptsMultipleValues*(T: type): bool = 702 | T is seq 703 | 704 | template debugMacroResult(macroName: string) {.dirty.} = 705 | when defined(debugMacros) or defined(debugConfutils): 706 | echo "\n-------- ", macroName, " ----------------------" 707 | echo result.repr 708 | 709 | proc generateFieldSetters(RecordType: NimNode): NimNode = 710 | var recordDef = getImpl(RecordType) 711 | let makeDefaultValue = bindSym"makeDefaultValue" 712 | 713 | result = newTree(nnkStmtListExpr) 714 | var settersArray = newTree(nnkBracket) 715 | 716 | for field in recordFields(recordDef): 717 | var 718 | setterName = ident($field.name & "Setter") 719 | fieldName = field.name 720 | namePragma = field.readPragma"name" 721 | paramName = if namePragma != nil: namePragma 722 | else: fieldName 723 | configVar = ident "config" 724 | configField = newTree(nnkDotExpr, configVar, fieldName) 725 | defaultValue = field.readPragma"defaultValue" 726 | completerName = ident($field.name & "Complete") 727 | isFieldDiscriminator = newLit field.isDiscriminator 728 | 729 | if defaultValue == nil: 730 | defaultValue = newCall(makeDefaultValue, newTree(nnkTypeOfExpr, configField)) 731 | 732 | # TODO: This shouldn't be necessary. The type symbol returned from Nim should 733 | # be typed as a tyTypeDesc[tyString] instead of just `tyString`. To be filed. 734 | var fixedFieldType = newTree(nnkTypeOfExpr, field.typ) 735 | 736 | settersArray.add newTree(nnkTupleConstr, 737 | newLit($paramName), 738 | setterName, completerName, 739 | newCall(bindSym"requiresInput", fixedFieldType), 740 | newCall(bindSym"acceptsMultipleValues", fixedFieldType)) 741 | 742 | result.add quote do: 743 | {.push hint[XCannotRaiseY]: off.} 744 | 745 | result.add quote do: 746 | proc `completerName`(val: string): seq[string] {. 747 | nimcall 748 | gcsafe 749 | sideEffect 750 | raises: [] 751 | .} = 752 | return completeCmdArgAux(`fixedFieldType`, val) 753 | 754 | proc `setterName`(`configVar`: var `RecordType`, val: Option[string]) {. 755 | nimcall 756 | gcsafe 757 | sideEffect 758 | raises: [ValueError] 759 | .} = 760 | # This works as long as the object is fresh (i.e: `default(theObj)`) 761 | # and the fields are processed in order. 762 | # See https://github.com/status-im/nim-confutils/pull/117 763 | # for a general solution. 764 | when `isFieldDiscriminator`: 765 | {.cast(uncheckedAssign).}: 766 | setField(`configField`, val, `defaultValue`) 767 | else: 768 | setField(`configField`, val, `defaultValue`) 769 | 770 | result.add quote do: 771 | {.pop.} 772 | 773 | result.add settersArray 774 | debugMacroResult "Field Setters" 775 | 776 | func checkDuplicate(cmd: CmdInfo, opt: OptInfo, fieldName: NimNode) = 777 | for x in cmd.opts: 778 | if opt.name == x.name: 779 | error "duplicate name detected: " & opt.name, fieldName 780 | if opt.abbr.len > 0 and opt.abbr == x.abbr: 781 | error "duplicate abbr detected: " & opt.abbr, fieldName 782 | 783 | func validPath(path: var seq[CmdInfo], parent, node: CmdInfo): bool = 784 | for x in parent.opts: 785 | if x.kind != Discriminator: continue 786 | for y in x.subCmds: 787 | if y == node: 788 | path.add y 789 | return true 790 | if validPath(path, y, node): 791 | path.add y 792 | return true 793 | false 794 | 795 | func findPath(parent, node: CmdInfo): seq[CmdInfo] = 796 | # find valid path from parent to node 797 | result = newSeq[CmdInfo]() 798 | doAssert validPath(result, parent, node) 799 | result.add parent 800 | 801 | func toText(n: NimNode): string = 802 | if n == nil: "" 803 | elif n.kind in {nnkStrLit..nnkTripleStrLit}: n.strVal 804 | else: repr(n) 805 | 806 | proc cmdInfoFromType(T: NimNode): CmdInfo = 807 | result = CmdInfo() 808 | 809 | var 810 | recordDef = getImpl(T) 811 | discriminatorFields = newSeq[OptInfo]() 812 | fieldIdx = 0 813 | 814 | for field in recordFields(recordDef): 815 | let 816 | isImplicitlySelectable = field.readPragma"implicitlySelectable" != nil 817 | defaultValue = field.readPragma"defaultValue" 818 | defaultValueDesc = field.readPragma"defaultValueDesc" 819 | defaultInHelp = if defaultValueDesc != nil: defaultValueDesc 820 | else: defaultValue 821 | defaultInHelpText = toText(defaultInHelp) 822 | separator = field.readPragma"separator" 823 | longDesc = field.readPragma"longDesc" 824 | 825 | isHidden = field.readPragma("hidden") != nil 826 | abbr = field.readPragma"abbr" 827 | name = field.readPragma"name" 828 | desc = field.readPragma"desc" 829 | optKind = if field.isDiscriminator: Discriminator 830 | elif field.readPragma("argument") != nil: Arg 831 | else: CliSwitch 832 | 833 | var opt = OptInfo(kind: optKind, 834 | idx: fieldIdx, 835 | name: $field.name, 836 | isHidden: isHidden, 837 | hasDefault: defaultValue != nil, 838 | defaultInHelpText: defaultInHelpText, 839 | typename: field.typ.repr) 840 | 841 | if desc != nil: opt.desc = desc.strVal 842 | if name != nil: opt.name = name.strVal 843 | if abbr != nil: opt.abbr = abbr.strVal 844 | if separator != nil: opt.separator = separator.strVal 845 | if longDesc != nil: opt.longDesc = longDesc.strVal 846 | 847 | inc fieldIdx 848 | 849 | if field.isDiscriminator: 850 | discriminatorFields.add opt 851 | let cmdType = field.typ.getImpl[^1] 852 | if cmdType.kind != nnkEnumTy: 853 | error "Only enums are supported as case object discriminators", field.name 854 | 855 | opt.isImplicitlySelectable = isImplicitlySelectable 856 | opt.isCommand = field.readPragma"command" != nil 857 | 858 | for i in 1 ..< cmdType.len: 859 | let enumVal = cmdType[i] 860 | var name, desc: string 861 | if enumVal.kind == nnkEnumFieldDef: 862 | name = $enumVal[0] 863 | desc = $enumVal[1] 864 | else: 865 | name = $enumVal 866 | if defaultValue != nil and eqIdent(name, defaultValue): 867 | opt.defaultSubCmd = i - 1 868 | opt.subCmds.add CmdInfo(name: name, desc: desc) 869 | 870 | if defaultValue == nil: 871 | opt.defaultSubCmd = -1 872 | else: 873 | if opt.defaultSubCmd == -1: 874 | error "The default value is not a valid enum value", defaultValue 875 | 876 | if field.caseField != nil and field.caseBranch != nil: 877 | let fieldName = field.caseField.getFieldName 878 | var discriminator = findOpt(discriminatorFields, $fieldName) 879 | 880 | if discriminator == nil: 881 | error "Unable to find " & $fieldName 882 | 883 | if field.caseBranch.kind == nnkElse: 884 | error "Sub-command parameters cannot appear in an else branch. " & 885 | "Please specify the sub-command branch precisely", field.caseBranch[0] 886 | 887 | var branchEnumVal = field.caseBranch[0] 888 | if branchEnumVal.kind == nnkDotExpr: 889 | branchEnumVal = branchEnumVal[1] 890 | var cmd = findCmd(discriminator.subCmds, $branchEnumVal) 891 | # we respect subcommand hierarchy when looking for duplicate 892 | let path = findPath(result, cmd) 893 | for n in path: 894 | checkDuplicate(n, opt, field.name) 895 | 896 | # the reason we check for `ignore` pragma here and not using `continue` statement 897 | # is we do respect option hierarchy of subcommands 898 | if field.readPragma("ignore") == nil: 899 | cmd.opts.add opt 900 | 901 | else: 902 | checkDuplicate(result, opt, field.name) 903 | 904 | if field.readPragma("ignore") == nil: 905 | result.opts.add opt 906 | 907 | macro configurationRtti(RecordType: type): untyped = 908 | let 909 | T = RecordType.getType[1] 910 | cmdInfo = cmdInfoFromType T 911 | fieldSetters = generateFieldSetters T 912 | 913 | result = newTree(nnkPar, newLitFixed cmdInfo, fieldSetters) 914 | 915 | when hasSerialization: 916 | proc addConfigFile*(secondarySources: auto, 917 | Format: type, 918 | path: InputFile) {.raises: [ConfigurationError].} = 919 | try: 920 | secondarySources.data.add loadFile(Format, string path, 921 | type(secondarySources.data[0])) 922 | except SerializationError as err: 923 | raise newException(ConfigurationError, err.formatMsg(string path), err) 924 | except IOError as err: 925 | raise newException(ConfigurationError, 926 | "Failed to read config file at '" & string(path) & "': " & err.msg) 927 | 928 | proc addConfigFileContent*(secondarySources: auto, 929 | Format: type, 930 | content: string) {.raises: [ConfigurationError].} = 931 | try: 932 | secondarySources.data.add decode(Format, content, 933 | type(secondarySources.data[0])) 934 | except SerializationError as err: 935 | raise newException(ConfigurationError, err.formatMsg(""), err) 936 | except IOError: 937 | raiseAssert "This should not be possible" 938 | 939 | func constructEnvKey*(prefix: string, key: string): string {.raises: [].} = 940 | ## Generates env. variable names from keys and prefix following the 941 | ## IEEE Open Group env. variable spec: https://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html 942 | (prefix & "_" & key).toUpperAscii.multiReplace(("-", "_"), (" ", "_")) 943 | 944 | # On Posix there is no portable way to get the command 945 | # line from a DLL and thus the proc isn't defined in this environment. 946 | # See https://nim-lang.org/docs/os.html#commandLineParams 947 | when not declared(commandLineParams): 948 | proc commandLineParams(): seq[string] = discard 949 | 950 | proc loadImpl[C, SecondarySources]( 951 | Configuration: typedesc[C], 952 | cmdLine = commandLineParams(), 953 | version = "", 954 | copyrightBanner = "", 955 | printUsage = true, 956 | quitOnFailure = true, 957 | ignoreUnknown = false, 958 | secondarySourcesRef: ref SecondarySources, 959 | secondarySources: proc ( 960 | config: Configuration, sources: ref SecondarySources 961 | ) {.gcsafe, raises: [ConfigurationError].} = nil, 962 | envVarsPrefix = appInvocation(), 963 | termWidth = 0 964 | ): Configuration {.raises: [ConfigurationError].} = 965 | ## Loads a program configuration by parsing command-line arguments 966 | ## and a standard set of config files that can specify: 967 | ## 968 | ## - working directory settings 969 | ## - user settings 970 | ## - system-wide setttings 971 | ## 972 | ## Supports multiple config files format (INI/TOML, YAML, JSON). 973 | 974 | # This is an initial naive implementation that will be improved 975 | # over time. 976 | let (rootCmd, fieldSetters) = configurationRtti(Configuration) 977 | var fieldCounters: array[fieldSetters.len, int] 978 | 979 | printCmdTree rootCmd 980 | 981 | var activeCmds = @[rootCmd] 982 | template lastCmd: auto = activeCmds[^1] 983 | var nextArgIdx = lastCmd.getNextArgIdx(-1) 984 | 985 | var help = "" 986 | 987 | proc suggestCallingHelp = 988 | errorOutput "Try ", fgCommand, appInvocation() & " --help" 989 | errorOutput " for more information.\p" 990 | flushOutputAndQuit QuitFailure 991 | 992 | template fail(args: varargs[untyped]): untyped = 993 | if quitOnFailure: 994 | errorOutput args 995 | errorOutput "\p" 996 | suggestCallingHelp() 997 | else: 998 | # TODO: populate this string 999 | raise newException(ConfigurationError, "") 1000 | 1001 | template applySetter( 1002 | conf: Configuration, setterIdx: int, cmdLineVal: string): untyped = 1003 | when defined(nimHasWarnBareExcept): 1004 | {.push warning[BareExcept]:off.} 1005 | 1006 | try: 1007 | fieldSetters[setterIdx][1](conf, some(cmdLineVal)) 1008 | inc fieldCounters[setterIdx] 1009 | except: 1010 | fail("Error while processing the ", 1011 | fgOption, fieldSetters[setterIdx][0], 1012 | "=", cmdLineVal, resetStyle, " parameter: ", 1013 | getCurrentExceptionMsg()) 1014 | 1015 | when defined(nimHasWarnBareExcept): 1016 | {.pop.} 1017 | 1018 | when hasCompletions: 1019 | template getArgCompletions(opt: OptInfo, prefix: string): seq[string] = 1020 | fieldSetters[opt.idx][2](prefix) 1021 | 1022 | template required(opt: OptInfo): bool = 1023 | fieldSetters[opt.idx][3] and not opt.hasDefault 1024 | 1025 | template activateCmd( 1026 | conf: Configuration, discriminator: OptInfo, activatedCmd: CmdInfo) = 1027 | let cmd = activatedCmd 1028 | conf.applySetter(discriminator.idx, if cmd.desc.len > 0: cmd.desc 1029 | else: cmd.name) 1030 | activeCmds.add cmd 1031 | nextArgIdx = cmd.getNextArgIdx(-1) 1032 | 1033 | when hasCompletions: 1034 | type 1035 | ArgKindFilter = enum 1036 | argName 1037 | argAbbr 1038 | 1039 | proc showMatchingOptions(cmd: CmdInfo, prefix: string, filterKind: set[ArgKindFilter]) = 1040 | var matchingOptions: seq[OptInfo] 1041 | 1042 | if len(prefix) > 0: 1043 | # Filter the options according to the input prefix 1044 | for opt in cmd.opts: 1045 | if argName in filterKind and len(opt.name) > 0: 1046 | if startsWithIgnoreStyle(opt.name, prefix): 1047 | matchingOptions.add(opt) 1048 | if argAbbr in filterKind and len(opt.abbr) > 0: 1049 | if startsWithIgnoreStyle(opt.abbr, prefix): 1050 | matchingOptions.add(opt) 1051 | else: 1052 | matchingOptions = cmd.opts 1053 | 1054 | for opt in matchingOptions: 1055 | # The trailing '=' means the switch accepts an argument 1056 | let trailing = if opt.typename != "bool": "=" else: "" 1057 | 1058 | if argName in filterKind and len(opt.name) > 0: 1059 | try: 1060 | stdout.writeLine("--", opt.name, trailing) 1061 | except IOError: 1062 | discard 1063 | if argAbbr in filterKind and len(opt.abbr) > 0: 1064 | try: 1065 | stdout.writeLine('-', opt.abbr, trailing) 1066 | except IOError: 1067 | discard 1068 | 1069 | let completion = splitCompletionLine() 1070 | # If we're not asked to complete a command line the result is an empty list 1071 | if len(completion) != 0: 1072 | var cmdStack = @[rootCmd] 1073 | # Try to understand what the active chain of commands is without parsing the 1074 | # whole command line 1075 | for tok in completion[1..^1]: 1076 | if not tok.startsWith('-'): 1077 | let subCmd = findSubCmd(cmdStack[^1], tok) 1078 | if subCmd != nil: cmdStack.add(subCmd) 1079 | 1080 | let cur_word = completion[^1] 1081 | let prev_word = if len(completion) > 2: completion[^2] else: "" 1082 | let prev_prev_word = if len(completion) > 3: completion[^3] else: "" 1083 | 1084 | if cur_word.startsWith('-'): 1085 | # Show all the options matching the prefix input by the user 1086 | let isFullName = cur_word.startsWith("--") 1087 | var option_word = cur_word 1088 | option_word.removePrefix('-') 1089 | 1090 | for i in countdown(cmdStack.len - 1, 0): 1091 | let argFilter = 1092 | if isFullName: 1093 | {argName} 1094 | elif len(cur_word) > 1: 1095 | # If the user entered a single hypen then we show both long & short 1096 | # variants 1097 | {argAbbr} 1098 | else: 1099 | {argName, argAbbr} 1100 | 1101 | showMatchingOptions(cmdStack[i], option_word, argFilter) 1102 | elif (prev_word.startsWith('-') or 1103 | (prev_word == "=" and prev_prev_word.startsWith('-'))): 1104 | # Handle cases where we want to complete a switch choice 1105 | # -switch 1106 | # -switch= 1107 | var option_word = if len(prev_word) == 1: prev_prev_word else: prev_word 1108 | option_word.removePrefix('-') 1109 | 1110 | let opt = findOpt(cmdStack, option_word) 1111 | if opt != nil: 1112 | for arg in getArgCompletions(opt, cur_word): 1113 | try: 1114 | stdout.writeLine(arg) 1115 | except IOError: 1116 | discard 1117 | elif cmdStack[^1].hasSubCommands: 1118 | # Show all the available subcommands 1119 | for subCmd in subCmds(cmdStack[^1]): 1120 | if startsWithIgnoreStyle(subCmd.name, cur_word): 1121 | try: 1122 | stdout.writeLine(subCmd.name) 1123 | except IOError: 1124 | discard 1125 | else: 1126 | # Full options listing 1127 | for i in countdown(cmdStack.len - 1, 0): 1128 | showMatchingOptions(cmdStack[i], "", {argName, argAbbr}) 1129 | 1130 | stdout.flushFile() 1131 | 1132 | return 1133 | 1134 | proc lazyHelpAppInfo: HelpAppInfo = 1135 | HelpAppInfo( 1136 | copyrightBanner: copyrightBanner, 1137 | appInvocation: appInvocation(), 1138 | terminalWidth: termWidth) 1139 | 1140 | template processHelpAndVersionOptions(optKey: string) = 1141 | let key = optKey 1142 | if cmpIgnoreStyle(key, "help") == 0: 1143 | help.showHelp lazyHelpAppInfo(), activeCmds 1144 | elif version.len > 0 and cmpIgnoreStyle(key, "version") == 0: 1145 | help.helpOutput version, "\p" 1146 | flushOutputAndQuit QuitSuccess 1147 | 1148 | for kind, key, val in getopt(cmdLine): 1149 | when key isnot string: 1150 | let key = string(key) 1151 | case kind 1152 | of cmdLongOption, cmdShortOption: 1153 | processHelpAndVersionOptions key 1154 | 1155 | var opt = findOpt(activeCmds, key) 1156 | if opt == nil: 1157 | # We didn't find the option. 1158 | # Check if it's from the default command and activate it if necessary: 1159 | let subCmdDiscriminator = lastCmd.getSubCmdDiscriminator 1160 | if subCmdDiscriminator != nil: 1161 | if subCmdDiscriminator.defaultSubCmd != -1: 1162 | let defaultCmd = subCmdDiscriminator.subCmds[subCmdDiscriminator.defaultSubCmd] 1163 | opt = findOpt(defaultCmd.opts, key) 1164 | if opt != nil: 1165 | result.activateCmd(subCmdDiscriminator, defaultCmd) 1166 | else: 1167 | discard 1168 | 1169 | if opt != nil: 1170 | result.applySetter(opt.idx, val) 1171 | elif not ignoreUnknown: 1172 | fail "Unrecognized option '" & key & "'" 1173 | 1174 | of cmdArgument: 1175 | if lastCmd.hasSubCommands: 1176 | processHelpAndVersionOptions key 1177 | 1178 | block processArg: 1179 | let subCmdDiscriminator = lastCmd.getSubCmdDiscriminator 1180 | if subCmdDiscriminator != nil: 1181 | let subCmd = findCmd(subCmdDiscriminator.subCmds, key) 1182 | if subCmd != nil: 1183 | result.activateCmd(subCmdDiscriminator, subCmd) 1184 | break processArg 1185 | 1186 | if nextArgIdx == -1: 1187 | fail lastCmd.noMoreArgsError 1188 | 1189 | result.applySetter(nextArgIdx, key) 1190 | 1191 | if not fieldSetters[nextArgIdx][4]: 1192 | nextArgIdx = lastCmd.getNextArgIdx(nextArgIdx) 1193 | 1194 | else: 1195 | discard 1196 | 1197 | let subCmdDiscriminator = lastCmd.getSubCmdDiscriminator 1198 | if subCmdDiscriminator != nil and 1199 | subCmdDiscriminator.defaultSubCmd != -1 and 1200 | fieldCounters[subCmdDiscriminator.idx] == 0: 1201 | let defaultCmd = subCmdDiscriminator.subCmds[subCmdDiscriminator.defaultSubCmd] 1202 | result.activateCmd(subCmdDiscriminator, defaultCmd) 1203 | 1204 | # https://github.com/status-im/nim-confutils/pull/109#discussion_r1820076739 1205 | if not isNil(secondarySources): # Nim v2.0.10: `!= nil` broken in nimscript 1206 | try: 1207 | secondarySources(result, secondarySourcesRef) 1208 | except ConfigurationError as err: 1209 | fail "Failed to load secondary sources: '" & err.msg & "'" 1210 | 1211 | proc processMissingOpts( 1212 | conf: var Configuration, cmd: CmdInfo) {.raises: [ConfigurationError].} = 1213 | for opt in cmd.opts: 1214 | if fieldCounters[opt.idx] == 0: 1215 | let envKey = constructEnvKey(envVarsPrefix, opt.name) 1216 | 1217 | try: 1218 | if existsEnv(envKey): 1219 | let envContent = getEnv(envKey) 1220 | conf.applySetter(opt.idx, envContent) 1221 | elif secondarySourcesRef.setters[opt.idx](conf, secondarySourcesRef): 1222 | # all work is done in the config file setter, 1223 | # there is nothing left to do here. 1224 | discard 1225 | elif opt.hasDefault: 1226 | fieldSetters[opt.idx][1](conf, none[string]()) 1227 | elif opt.required: 1228 | fail "The required option '" & opt.name & "' was not specified" 1229 | except ValueError as err: 1230 | fail "Option '" & opt.name & "' failed to parse: '" & err.msg & "'" 1231 | 1232 | for cmd in activeCmds: 1233 | result.processMissingOpts(cmd) 1234 | 1235 | template load*( 1236 | Configuration: type, 1237 | cmdLine = commandLineParams(), 1238 | version = "", 1239 | copyrightBanner = "", 1240 | printUsage = true, 1241 | quitOnFailure = true, 1242 | ignoreUnknown = false, 1243 | secondarySources: untyped = nil, 1244 | envVarsPrefix = appInvocation(), 1245 | termWidth = 0): untyped = 1246 | block: 1247 | let secondarySourcesRef = generateSecondarySources(Configuration) 1248 | loadImpl(Configuration, cmdLine, version, 1249 | copyrightBanner, printUsage, quitOnFailure, ignoreUnknown, 1250 | secondarySourcesRef, secondarySources, envVarsPrefix, termWidth) 1251 | 1252 | func defaults*(Configuration: type): Configuration = 1253 | load(Configuration, cmdLine = @[], printUsage = false, quitOnFailure = false) 1254 | 1255 | proc dispatchImpl(cliProcSym, cliArgs, loadArgs: NimNode): NimNode = 1256 | # Here, we'll create a configuration object with fields matching 1257 | # the CLI proc params. We'll also generate a call to the designated proc 1258 | let configType = genSym(nskType, "CliConfig") 1259 | let configFields = newTree(nnkRecList) 1260 | let configVar = genSym(nskLet, "config") 1261 | var dispatchCall = newCall(cliProcSym) 1262 | 1263 | # The return type of the proc is skipped over 1264 | for i in 1 ..< cliArgs.len: 1265 | var arg = copy cliArgs[i] 1266 | 1267 | # If an argument doesn't specify a type, we infer it from the default value 1268 | if arg[1].kind == nnkEmpty: 1269 | if arg[2].kind == nnkEmpty: 1270 | error "Please provide either a default value or type of the parameter", arg 1271 | arg[1] = newCall(bindSym"typeof", arg[2]) 1272 | 1273 | # Turn any default parameters into the confutils's `defaultValue` pragma 1274 | if arg[2].kind != nnkEmpty: 1275 | if arg[0].kind != nnkPragmaExpr: 1276 | arg[0] = newTree(nnkPragmaExpr, arg[0], newTree(nnkPragma)) 1277 | arg[0][1].add newColonExpr(bindSym"defaultValue", arg[2]) 1278 | arg[2] = newEmptyNode() 1279 | 1280 | configFields.add arg 1281 | dispatchCall.add newTree(nnkDotExpr, configVar, skipPragma arg[0]) 1282 | 1283 | let cliConfigType = nnkTypeSection.newTree( 1284 | nnkTypeDef.newTree( 1285 | configType, 1286 | newEmptyNode(), 1287 | nnkObjectTy.newTree( 1288 | newEmptyNode(), 1289 | newEmptyNode(), 1290 | configFields))) 1291 | 1292 | var loadConfigCall = newCall(bindSym"load", configType) 1293 | for p in loadArgs: loadConfigCall.add p 1294 | 1295 | result = quote do: 1296 | `cliConfigType` 1297 | let `configVar` = `loadConfigCall` 1298 | `dispatchCall` 1299 | 1300 | macro dispatch*(fn: typed, args: varargs[untyped]): untyped = 1301 | if fn.kind != nnkSym or 1302 | fn.symKind notin {nskProc, nskFunc, nskMacro, nskTemplate}: 1303 | error "The first argument to `confutils.dispatch` should be a callable symbol" 1304 | 1305 | let fnImpl = fn.getImpl 1306 | result = dispatchImpl(fnImpl.name, fnImpl.params, args) 1307 | debugMacroResult "Dispatch Code" 1308 | 1309 | macro cli*(args: varargs[untyped]): untyped = 1310 | if args.len == 0: 1311 | error "The cli macro expects a do block", args 1312 | 1313 | let doBlock = args[^1] 1314 | if doBlock.kind notin {nnkDo, nnkLambda}: 1315 | error "The last argument to `confutils.cli` should be a do block", doBlock 1316 | 1317 | args.del(args.len - 1) 1318 | 1319 | # Create a new anonymous proc we'll dispatch to 1320 | let cliProcName = genSym(nskProc, "CLI") 1321 | var cliProc = newTree(nnkProcDef, cliProcName) 1322 | # Copy everything but the name from the do block: 1323 | for i in 1 ..< doBlock.len: cliProc.add doBlock[i] 1324 | 1325 | # Generate the final code 1326 | result = newStmtList(cliProc, dispatchImpl(cliProcName, cliProc.params, args)) 1327 | 1328 | # TODO: remove this once Nim supports custom pragmas on proc params 1329 | for p in cliProc.params: 1330 | if p.kind == nnkEmpty: continue 1331 | p[0] = skipPragma p[0] 1332 | 1333 | debugMacroResult "CLI Code" 1334 | 1335 | func load*(f: TypedInputFile): f.ContentType = 1336 | when f.Format is Unspecified or f.ContentType is Unspecified: 1337 | {.fatal: "To use `InputFile.load`, please specify the Format and ContentType of the file".} 1338 | 1339 | when f.Format is Txt: 1340 | # TODO: implement a proper Txt serialization format 1341 | mixin init 1342 | f.ContentType.init readFile(f.string).string 1343 | else: 1344 | mixin loadFile 1345 | loadFile(f.Format, f.string, f.ContentType) 1346 | 1347 | {.pop.} 1348 | --------------------------------------------------------------------------------