├── tests ├── config.nims ├── basicmath.nim └── tbasicmath.nim ├── demo ├── git │ ├── config.nims │ ├── README.md │ ├── commands │ │ ├── flow.nim │ │ ├── flow │ │ │ └── release.nim │ │ └── root.nim │ └── git.nim └── battleship │ ├── config.nims │ └── battleship.nim ├── src ├── climate.nim └── climate │ ├── types.nim │ ├── parser.nim │ └── sugar.nim ├── .gitignore ├── .github └── workflows │ ├── tests.yml │ └── docs.yml ├── climate.nimble ├── LICENSE ├── changelog.md └── README.md /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") -------------------------------------------------------------------------------- /demo/git/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../../src") 2 | 3 | -------------------------------------------------------------------------------- /demo/battleship/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../../src") 2 | 3 | -------------------------------------------------------------------------------- /src/climate.nim: -------------------------------------------------------------------------------- 1 | import climate/[types, parser, sugar] 2 | 3 | export types, parser, sugar 4 | -------------------------------------------------------------------------------- /demo/git/README.md: -------------------------------------------------------------------------------- 1 | # Climate demo app 2 | 3 | Compile ``git.nim`` and run it with different inputs: 4 | 5 | - ./git flow init 6 | - ./git flow init -d 7 | - ./git flow release start 1.0.0 8 | - ./git flow release finish 1.0.0 9 | - ./git foo 10 | 11 | -------------------------------------------------------------------------------- /demo/git/commands/flow.nim: -------------------------------------------------------------------------------- 1 | import climate 2 | 3 | proc init*(context: Context): int = 4 | stdout.write "Initializing Git flow..." 5 | 6 | if context.cmdOptions.hasKey("d") or context.cmdOptions.hasKey("default"): 7 | stdout.write "using default settings..." 8 | 9 | echo "done!" 10 | -------------------------------------------------------------------------------- /src/climate/types.nim: -------------------------------------------------------------------------------- 1 | import std/tables 2 | 3 | export tables 4 | 5 | type 6 | Handler* = proc(context: Context): int {.nimcall.} 7 | Command* = tuple[route: string, handler: Handler] 8 | Context* = object of RootObj 9 | cmdArguments*: seq[string] 10 | cmdOptions*: Table[string, string] 11 | cmdFlags*: seq[string] 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | *.sublime-project 3 | *.sublime-workspace 4 | .idea 5 | 6 | *.exe 7 | *.html 8 | *.js 9 | *.idx 10 | htmldocs 11 | docs 12 | 13 | nimcache/ 14 | testresults/ 15 | outputExpected.txt 16 | outputGotten.txt 17 | 18 | demo/git/git 19 | demo/battleship/battleship 20 | 21 | tests/megatest.nim 22 | tests/megatest 23 | 24 | tests/basicmath 25 | tests/tbasicmath 26 | 27 | -------------------------------------------------------------------------------- /demo/git/git.nim: -------------------------------------------------------------------------------- 1 | import climate 2 | 3 | import commands/[root, flow] 4 | import commands/flow/release 5 | 6 | const commands = { 7 | "add": root.add, 8 | "reset": root.reset, 9 | "flow init": flow.init, 10 | "flow release start": release.start, 11 | "flow release finish": release.finish, 12 | } 13 | 14 | when isMainModule: 15 | quit parseCommands(commands, defaultHandler = root.root) 16 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | 8 | jobs: 9 | Docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: jiro4989/setup-nim-action@v2 14 | with: 15 | nim-version: '2.2.0' 16 | repo-token: ${{ secrets.GITHUB_TOKEN }} 17 | 18 | - name: Run Tests 19 | run: nimble test 20 | 21 | -------------------------------------------------------------------------------- /demo/git/commands/flow/release.nim: -------------------------------------------------------------------------------- 1 | import std/[strformat, with] 2 | 3 | import climate 4 | 5 | proc start*(context: Context): int = 6 | with context: 7 | arg: 8 | stdout.write fmt"Starting release {arg}..." 9 | echo "done!" 10 | 11 | proc finish*(context: Context): int = 12 | with context: 13 | arg: 14 | stdout.write fmt"Finishing release {arg}..." 15 | echo "done!" 16 | do: 17 | echo "Release name is mandatory" 18 | -------------------------------------------------------------------------------- /climate.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "1.1.3" 4 | author = "Constantine Molchanov" 5 | description = "Library for building command-line interfaces." 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 1.6.2" 13 | 14 | # Tasks 15 | 16 | before docs: 17 | rmDir "docs" 18 | 19 | task docs, "Generate docs": 20 | exec "nimble doc --outdir:docs --project --index:on src/climate" 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | 8 | jobs: 9 | Docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: jiro4989/setup-nim-action@v2 14 | with: 15 | nim-version: '2.2.0' 16 | repo-token: ${{ secrets.GITHUB_TOKEN }} 17 | 18 | - name: Build Docs 19 | run: nimble docs 20 | 21 | - name: Deploy Docs 22 | uses: peaceiris/actions-gh-pages@v3 23 | with: 24 | github_token: ${{ secrets.GITHUB_TOKEN }} 25 | publish_dir: ./docs 26 | 27 | -------------------------------------------------------------------------------- /tests/basicmath.nim: -------------------------------------------------------------------------------- 1 | import std/strutils 2 | 3 | import climate 4 | 5 | proc sum(context: Context): int = 6 | context.args: 7 | if len(args) != 2: 8 | echo "The command expects exactly two arguments." 9 | return 1 10 | 11 | echo parseInt(args[0]) + parseInt(args[1]) 12 | do: 13 | echo "The command expects exactly two arguments." 14 | return 2 15 | 16 | proc multisum(context: Context): int = 17 | var accum = 0 18 | 19 | context.args: 20 | for arg in args: 21 | accum += parseInt(arg) 22 | do: 23 | echo "The command expects at least one argument." 24 | return 2 25 | 26 | echo accum 27 | 28 | proc root(context: Context): int = 29 | echo "Available commands: sum." 30 | 31 | const commands = {"sum": sum, "multisum": multisum} 32 | 33 | quit parseCommands(commands, defaultHandler = root) 34 | -------------------------------------------------------------------------------- /demo/git/commands/root.nim: -------------------------------------------------------------------------------- 1 | import std/with 2 | 3 | import climate 4 | 5 | proc root*(context: Context): int = 6 | echo "Welcome to FakeGit!" 7 | 8 | if len(context.cmdArguments) == 0 and len(context.cmdOptions) == 0: 9 | quit "Invalid command. Run 'git --help' to see the available commands." 10 | 11 | with context: 12 | flag "version", "": 13 | echo "Version 1.0.0" 14 | 15 | flag "help", "h": 16 | echo "Usage: ..." 17 | 18 | proc add*(context: Context): int = 19 | with context: 20 | args: 21 | for filename in args: 22 | echo "Adding " & filename 23 | do: 24 | quit("No filename provided", 1) 25 | 26 | opt "tag", "T": 27 | echo "Tag: " & val 28 | do: 29 | quit("Tag is mandatory", 1) 30 | 31 | proc reset*(context: Context): int = 32 | with context: 33 | args: 34 | for filename in args: 35 | echo "Resetting " & filename 36 | 37 | echo "Reset finished" 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Constantine Molchanov 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 | -------------------------------------------------------------------------------- /src/climate/parser.nim: -------------------------------------------------------------------------------- 1 | import std/[parseopt, strutils] 2 | 3 | import types 4 | 5 | export types 6 | 7 | proc parseCommands*(commands: openArray[Command], defaultHandler: Handler = nil): int = 8 | ##[ Parse command-line params, store the arguments and options in context, and invoke the matching handler. 9 | 10 | Returns the exit code that should be returned by your app to the caller. 11 | ]## 12 | 13 | var 14 | parser = initOptParser() 15 | cmdComponents: seq[string] 16 | handler = defaultHandler 17 | context: Context 18 | route = "" 19 | 20 | block findHandler: 21 | for (kind, key, val) in getopt(parser): 22 | case kind 23 | of cmdArgument: 24 | cmdComponents.add(key) 25 | route = cmdComponents.join(" ") 26 | of cmdShortOption, cmdLongOption: 27 | context.cmdOptions[key] = val 28 | if len(val) == 0: 29 | context.cmdFlags.add(key) 30 | of cmdEnd: 31 | break 32 | 33 | for command in commands: 34 | if command.route == route: 35 | handler = command.handler 36 | break findHandler 37 | 38 | if handler.isNil: 39 | quit("Command not found", 1) 40 | 41 | if len(parser.remainingArgs) > 0: 42 | for (kind, key, val) in getopt(parser.remainingArgs): 43 | case kind 44 | of cmdArgument: 45 | context.cmdArguments.add(key) 46 | of cmdShortOption, cmdLongOption: 47 | context.cmdOptions[key] = val 48 | if len(val) == 0: 49 | context.cmdFlags.add(key) 50 | of cmdEnd: 51 | break 52 | 53 | handler(context) 54 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | - [!]—backward incompatible change 4 | - [+]—new feature 5 | - [f]—bugfix 6 | - [r]—refactoring 7 | - [t]—test suite improvement 8 | - [d]—docs improvement 9 | 10 | 11 | ## 1.1.3 (December 28, 2024) 12 | 13 | - [+] Add `flag` sugar to capture flags (i.e. options without values). 14 | - [+] Add `opt` and `flag` variants with a fallback block. 15 | - [d] Add docsrtings to the sugar templates. 16 | 17 | 18 | ## 1.1.2 (December 4, 2024) 19 | 20 | - [+] Add `arg` and `args` variants that don't require a fallback block. 21 | - [f] In `arg` and `args`, the main block would get executed after the fallback block. Now, it's either this or that. 22 | - [r] `missing` argument renamed to `fallback` to avoid collision with Nim's error message. 23 | - [t] Added commands to git demo app to demonstrate fallback-less `arg` and `args` templates. 24 | 25 | 26 | ## 1.1.1 (July 3, 2024) 27 | 28 | - [f] Fix warning about unused `val` in `sugar` submodule. 29 | 30 | 31 | ## 1.1.0 (July 3, 2024) 32 | 33 | - [!][r] Importing `climate` now imports all submodules including `sugar` and `context`. Just add `import climate` and that's it. 34 | - [+] Add new demo, Battleship. 35 | 36 | 37 | ## 1.0.3 (April 3, 2023) 38 | 39 | - [+] Add sugar to work with arguments and options to `climate/sugar`. 40 | 41 | 42 | ## 1.0.2 (March 28, 2023) 43 | 44 | - [f] Fix invalid Nim version requirement, Nim devel no longer required. 45 | 46 | 47 | ## 1.0.1 (March 6, 2023) 48 | 49 | - [+] Add `defaultHandler` param to `parseCommands` that is called when no match is found. 50 | - [+] Options that come before the command are collected along with the ones that come after it. I.e. `git flow -d init` is just as valid as `git flow init -d`. 51 | 52 | 53 | ## 1.0.0 (March 1, 2023) 54 | 55 | - 🎉 initial release. 56 | 57 | -------------------------------------------------------------------------------- /tests/tbasicmath.nim: -------------------------------------------------------------------------------- 1 | import std/[unittest, os, osproc, strutils, strformat] 2 | 3 | const clifile = "tests" / "basicmath" 4 | 5 | suite "Basic CLI": 6 | discard execCmd("nim c " & clifile) 7 | 8 | test "Two arguments expected, two given": 9 | const 10 | a = 2 11 | b = 3 12 | 13 | let (output, code) = execCmdEx &"{findExe(clifile)} sum {a} {b}" 14 | 15 | check code == 0 16 | check parseInt(strip(output)) == a + b 17 | 18 | test "Two arguments expected, three given": 19 | const 20 | a = 2 21 | b = 3 22 | c = 4 23 | 24 | let (output, code) = execCmdEx &"{findExe(clifile)} sum {a} {b} {c}" 25 | 26 | check code == 1 27 | check (strip(output)) == "The command expects exactly two arguments." 28 | 29 | test "Two arguments expected, none given": 30 | let (output, code) = execCmdEx &"{findExe(clifile)} sum" 31 | 32 | check code == 2 33 | check (strip(output)) == "The command expects exactly two arguments." 34 | 35 | test "Multiple arguments expected, multiple given": 36 | const 37 | a = 2 38 | b = 3 39 | c = 4 40 | 41 | let (output, code) = execCmdEx &"{findExe(clifile)} multisum {a} {b} {c}" 42 | 43 | check code == 0 44 | check parseInt(strip(output)) == a + b + c 45 | 46 | test "Multiple arguments expected, one given": 47 | const a = 2 48 | 49 | let (output, code) = execCmdEx &"{findExe(clifile)} multisum {a}" 50 | 51 | check code == 0 52 | check parseInt(strip(output)) == a 53 | 54 | test "Multiple arguments expected, none given": 55 | let (output, code) = execCmdEx &"{findExe(clifile)} multisum" 56 | 57 | check code == 2 58 | check (strip(output)) == "The command expects at least one argument." 59 | 60 | test "Command missing": 61 | let (output, code) = execCmdEx &"{findExe(clifile)}" 62 | 63 | check code == 0 64 | check (strip(output)) == "Available commands: sum." 65 | -------------------------------------------------------------------------------- /demo/battleship/battleship.nim: -------------------------------------------------------------------------------- 1 | import std/[strutils, strformat, with] 2 | 3 | import climate 4 | 5 | proc createShip(context: Context): int = 6 | var shipName = "Boaty McBoatFace" 7 | 8 | with context: 9 | opt "name", "n": 10 | shipName = val 11 | do: 12 | echo "HINT: to specify the ship name, use `--name|-n=` option." 13 | 14 | echo fmt"Creating ship {shipName}." 15 | 16 | proc moveShip(context: Context): int = 17 | var 18 | x = -1 19 | y = -1 20 | 21 | with context: 22 | opt "", "x": 23 | x = parseInt(val) 24 | do: 25 | echo "HINT: to specify the x coordinate, use `-x=` option." 26 | 27 | opt "", "y": 28 | y = parseInt(val) 29 | do: 30 | echo "HINT: to specify the y coordinate, use `-y=` option." 31 | 32 | echo fmt"Moving ship: x = {x}, y = {y}." 33 | 34 | proc turnShip(context: Context): int = 35 | var 36 | angle = -1 37 | counterClockWise = false 38 | 39 | with context: 40 | opt "angle", "a": 41 | angle = parseInt(val) 42 | do: 43 | quit("Angle is mandatory: --angle|-a=", 1) 44 | 45 | flag "counterclockwise", "c": 46 | counterClockWise = true 47 | do: 48 | echo "HINT: to turn the ship counterclockwise, pass `--counterclockwise|-c` flag." 49 | counterClockWise = false 50 | 51 | echo fmt"Turning ship: angle = {angle}, counterclockwise = {counterClockWise}." 52 | 53 | proc rootCommand(context: Context): int = 54 | with context: 55 | flag "help", "h": 56 | echo "Available commands: new, ship move, ship turn." 57 | return 0 58 | 59 | flag "version", "v": 60 | echo "Battleship v.0.1.0" 61 | return 0 62 | 63 | echo "Invalid command. Run 'battleship --help' to see available commands." 64 | return 1 65 | 66 | const commands = {"new": createShip, "ship move": moveShip, "ship turn": turnShip} 67 | 68 | when isMainModule: 69 | quit parseCommands(commands, defaultHandler = rootCommand) 70 | -------------------------------------------------------------------------------- /src/climate/sugar.nim: -------------------------------------------------------------------------------- 1 | import types 2 | 3 | export types 4 | 5 | template flag*( 6 | context: Context, longName, shortName: string, body, fallback: untyped 7 | ): untyped = 8 | ##[ Check that a flag defined by its long and short names is present in the context. 9 | 10 | Call ``body`` if it is, call ``fallback`` if it isn't. 11 | ]## 12 | 13 | block findFlag: 14 | for nameVariant in [longName, shortName]: 15 | if nameVariant in context.cmdFlags: 16 | body 17 | break findFlag 18 | 19 | fallback 20 | 21 | template flag*(context: Context, longName, shortName: string, body: untyped): untyped = 22 | ##[ Check that a flag defined by its long and short names is present in the context. 23 | 24 | Call ``body`` if it is, do nothing if if isn't. 25 | ]## 26 | 27 | for nameVariant in [longName, shortName]: 28 | if nameVariant in context.cmdFlags: 29 | body 30 | break 31 | 32 | template opt*( 33 | context: Context, longName, shortName: string, body, fallback: untyped 34 | ): untyped = 35 | ##[ Check that an option defined by its long and short names is present in the context. 36 | 37 | Call ``body`` and capture the option value if it is, call ``fallback`` if it isn't. 38 | ]## 39 | 40 | block findOpt: 41 | for nameVariant in [longName, shortName]: 42 | if context.cmdOptions.hasKey(nameVariant): 43 | let val {.inject, used.} = context.cmdOptions[nameVariant] 44 | body 45 | break findOpt 46 | 47 | fallback 48 | 49 | template opt*(context: Context, longName, shortName: string, body: untyped): untyped = 50 | ##[ Check that an option defined by its long and short names is present in the context. 51 | 52 | Call ``body`` and capture the option value if it is, do nothing if it isn't. 53 | ]## 54 | 55 | for nameVariant in [longName, shortName]: 56 | if context.cmdOptions.hasKey(nameVariant): 57 | let val {.inject, used.} = context.cmdOptions[nameVariant] 58 | body 59 | break 60 | 61 | template arg*(context: Context, body, fallback: untyped): untyped = 62 | ##[ Check that exactly one argument is present in the context. 63 | 64 | Call ``body`` and capture the argument value if it is, call ``fallback`` if it isn't. 65 | ]## 66 | 67 | if len(context.cmdArguments) == 0: 68 | fallback 69 | else: 70 | let arg {.inject.} = context.cmdArguments[0] 71 | body 72 | 73 | template arg*(context: Context, body: untyped): untyped = 74 | ##[ Check that exactly one argument is present in the context. 75 | 76 | Call ``body`` and capture the argument value if it is, do nothing if it isn't. 77 | ]## 78 | 79 | if len(context.cmdArguments) > 0: 80 | let arg {.inject.} = context.cmdArguments[0] 81 | body 82 | 83 | template args*(context: Context, body, fallback: untyped): untyped = 84 | ##[ Check that one or more arguments are present in the context. 85 | 86 | Call ``body`` and capture the argument values if they are, call ``fallback`` if they aren't. 87 | ]## 88 | 89 | if len(context.cmdArguments) == 0: 90 | fallback 91 | else: 92 | let args {.inject.} = context.cmdArguments 93 | body 94 | 95 | template args*(context: Context, body): untyped = 96 | ##[ Check that one or more arguments are present in the context. 97 | 98 | Call ``body`` and capture the argument values if they are, do nothing if they aren't. 99 | ]## 100 | 101 | if len(context.cmdArguments) > 0: 102 | let args {.inject.} = context.cmdArguments 103 | body 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Climate 2 | 3 | **Climate** is a library to create CLIs. It's your CLI mate 😜 4 | 5 | It allows you to think in high-level abstractions like commands and subcommands instead of parsers and tokens. 6 | 7 | [API docs index →](https://moigagoo.github.io/climate/theindex.html) 8 | 9 | 10 | # Installation 11 | 12 | Install Climate with Nimble: 13 | 14 | ``` 15 | $ nimble install -y climate 16 | ``` 17 | 18 | or add it to your .nimble file: 19 | 20 | ```nim 21 | requires "climate" 22 | ``` 23 | 24 | 25 | # Usage 26 | 27 | To create CLIs with Climate, you need to learn only three terms: **route**, **handler**, and **context**. 28 | 29 | 30 | ## Route 31 | 32 | **Route** is a unique sequence of words that correspond to a CLI command. For example, in git-flow, ``flow release start`` is a route. 33 | 34 | 35 | ## Handler 36 | 37 | **Handler** is a Nim proc invoked when the command is called by the user. It accepts a ``Context`` object and returns an ``int``. 38 | 39 | 40 | ## Context 41 | 42 | **Context** is an object that holds the values for the arguments and options from the command line. 43 | 44 | Context has a ``cmdArguments`` sequence and ``cmdOptions`` table. 45 | 46 | 47 | # Example 48 | 49 | Here's a fake git implemented with Climate: 50 | 51 | 1. Define routes: 52 | ```nim 53 | # git.nim 54 | import climate 55 | 56 | import commands/flow 57 | import commands/flow/release 58 | 59 | 60 | const commands = { 61 | "flow init": flow.init, 62 | "flow release start": release.start, 63 | "flow release finish": release.finish 64 | } 65 | ``` 66 | 67 | 2. Define handlers: 68 | 69 | ```nim 70 | # commands/flow.nim 71 | import climate/context 72 | 73 | 74 | proc init*(context: Context): int = 75 | stdout.write "Initializing Git flow..." 76 | 77 | if context.cmdOptions.hasKey("d") or context.cmdOptions.hasKey("default"): 78 | stdout.write "using default settings..." 79 | 80 | echo "done!" 81 | ``` 82 | 83 | 3. Parse the command line by calling ``parseCommands``: 84 | 85 | ```nim 86 | # git.nim 87 | quit parseCommands(commands) 88 | ``` 89 | 90 | See the complete example in ``demo`` folder. 91 | 92 | 93 | # Sugar 94 | 95 | To make it easier to work with arguments and options, Climate offers `arg(Context)`, `args(Context)`, and `opt(Context)` templates in `climate/sugar` module. 96 | 97 | 98 | ## `arg` 99 | 100 | `arg` checks that the command has been invoked with exactly one argument and captures its value; if the command was invoked with zero or more than one arguments, the fallback code is executed: 101 | 102 | ```nim 103 | proc start*(context: Context): int = 104 | context.arg: 105 | stdout.write fmt"Starting release {arg}..." 106 | echo "done!" 107 | do: 108 | echo "Release name is mandatory" 109 | return 1 110 | ``` 111 | 112 | 113 | ## `args` 114 | 115 | `args` checks that the command has been invoked with at least one argument and captures the values of the passed arguments; if the command was invoked with no arguments, the fallback code is executed: 116 | 117 | ```nim 118 | proc add*(context: Context): int = 119 | context.args: 120 | for filename in args: 121 | echo "Adding " & filename 122 | do: 123 | echo "No filename provided" 124 | return 1 125 | ``` 126 | 127 | 128 | ## `opt` 129 | 130 | `opt` checks if the command has been invoked with a certain option defined by its short or long name and captures its value: 131 | 132 | ```nim 133 | proc root*(context: Context): int = 134 | context.opt "version", "": 135 | echo "Version 1.0.0" 136 | 137 | context.opt "help", "h": 138 | echo "Usage: ..." 139 | ``` 140 | 141 | 142 | ## `flag` 143 | 144 | `flag` checks if the command has been invoked with a certain flag, i.e. an option without a value, defined by its short or long name: 145 | 146 | ```nim 147 | proc root*(context: Context): int = 148 | context.opt "version", "": 149 | echo "Version 1.0.0" 150 | 151 | context.opt "help", "h": 152 | echo "Usage: ..." 153 | ``` 154 | 155 | 156 | 157 | ## Supersugar 158 | 159 | To make it even sweeter, use `std/with`: 160 | 161 | ```nim 162 | proc root*(context: Context): int = 163 | echo "Welcome to FakeGit!" 164 | 165 | with context: 166 | opt "version", "": 167 | echo "Version 1.0.0" 168 | 169 | opt "help", "h": 170 | echo "Usage: ..." 171 | ``` 172 | 173 | --------------------------------------------------------------------------------