├── tests ├── config.nims └── example.nim ├── clapfn.nimble ├── LICENSE ├── README.md └── src └── clapfn.nim /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") 2 | -------------------------------------------------------------------------------- /clapfn.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "1.0.1" 4 | author = "Oliver Delancey" 5 | description = "A fast and simple command line argument parser inspired by Python's argparse." 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | 10 | 11 | # Dependencies 12 | 13 | requires "nim >= 1.2.0" 14 | -------------------------------------------------------------------------------- /tests/example.nim: -------------------------------------------------------------------------------- 1 | import tables 2 | import clapfn 3 | 4 | var parser = ArgumentParser(programName: "mcp", fullName: "My Cool Program", 5 | description: "A test program.", version: "0.0.0", 6 | author: "An Author ") 7 | 8 | # See the wiki for in-depth documentation, especially the purposes 9 | # of the various parameters. 10 | 11 | parser.addRequiredArgument("in_file", "Input file.") 12 | parser.addStoreArgument("-o", "--out", "output", "out.file", 13 | "Specify the output file.") 14 | parser.addSwitchArgument("-d", "--debug", false, "Enable debug printing.") 15 | 16 | let args = parser.parse() 17 | 18 | echo args["in_file"] 19 | echo args["out"] 20 | echo args["debug"] 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Oliver Delancey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clapfn 2 | 3 | [![Language](https://img.shields.io/badge/language-nim-yellow?style=flat-square&logo=nim")](https://nim-lang.org/) 4 | [![Nimble](https://img.shields.io/badge/nimble%20repo-clapfn-yellowgreen?style=flat-square&")](https://nimble.directory/pkg/clapfn) 5 | [![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square&logo=github")](https://github.com/oliverdelancey/clapfn/blob/master/LICENSE") 6 | 7 | `clapfn` is an easy-to-use **C**ommand **L**ine **A**rgument **P**arser **F**or **N**im. 8 | 9 | Please contact me if you have any issues using this library. This library is actively 10 | maintained and supported; I haven't made any commits lately simply because this project 11 | seems to be stable. But feel free to contact me via an issue, and I will see what I can 12 | do! 13 | 14 | ## Installation 15 | 16 | Installing is as simple as: 17 | ```bash 18 | nimble install clapfn 19 | ``` 20 | 21 | ## Usage 22 | 23 | `clapfn` is specifically designed to be straightforward to work with. 24 | ```nim 25 | import tables 26 | import clapfn 27 | 28 | var parser = ArgumentParser(programName: "mcp", fullName: "My Cool Program", 29 | description: "A test program.", version: "0.0.0", 30 | author: "An Author ") 31 | 32 | # See the wiki for in-depth documentation, especially the purposes 33 | # of the various parameters. 34 | 35 | # It is not necessary to use the argument names; they are here 36 | # simply for explanation. *All* function arguments are required. 37 | parser.addRequiredArgument(name="in_file", help="Input file.") 38 | parser.addStoreArgument(shortName="-o", longName="--out", usageInput="output", 39 | default="out.file", help="Specify the output file.") 40 | parser.addSwitchArgument(shortName="-d", longName="--debug", default=false, 41 | help="Enable debug printing.") 42 | 43 | let args = parser.parse() 44 | 45 | echo args["in_file"] 46 | echo args["out"] 47 | echo args["debug"] 48 | ``` 49 | 50 | And if you were to run `mcp --help`: 51 | ``` 52 | My Cool Program v0.0.0 53 | An Author 54 | A test program. 55 | 56 | Usage: mcp [-h] [-v] [-o output] [-d] in_file 57 | 58 | Required arguments: 59 | in_file Input file. 60 | 61 | Optional arguments: 62 | -h, --help Show this help message and exit. 63 | -v, --version Show version number and exit. 64 | -o=output, --out=output Specify the output file. 65 | -d, --debug Enable debug printing. 66 | ``` 67 | 68 | `clapfn` uses Nim's default delimiter style: 69 | ```bash 70 | # good 71 | -o:output 72 | -o=output 73 | 74 | # BAD 75 | -o output 76 | ``` 77 | 78 | The values of the command line arguments are stored in a [Table](https://nim-lang.org/docs/tables.html), and can be accessed thus: 79 | ```nim 80 | args["in_file"] 81 | args["out"] 82 | args["debug"] 83 | # etc 84 | ``` 85 | 86 | See `example.nim` for a fully functional example. 87 | 88 | ## Documentation 89 | 90 | See the [wiki](https://github.com/oliverdelancey/clapfn/wiki) for documentation. 91 | 92 | ## License 93 | 94 | This project uses the [MIT License](https://github.com/oliverdelancey/clapfn/blob/master/LICENSE) 95 | 96 | ## Contact 97 | 98 | Raise an Issue! I'll see you there. 99 | 100 | Project link: [https://github.com/oliverdelancey/clapfn](https://github.com/oliverdelancey/clapfn) 101 | -------------------------------------------------------------------------------- /src/clapfn.nim: -------------------------------------------------------------------------------- 1 | #[ 2 | clapfn v1.0.0 3 | 4 | Command 5 | Line 6 | Argument 7 | Parser 8 | For 9 | Nim 10 | 11 | author: Oliver Delancey 12 | date: 12/10/21 13 | license: MIT License 14 | ]# 15 | 16 | import macros 17 | import parseopt 18 | import sequtils 19 | import strutils 20 | import system 21 | import tables 22 | 23 | type 24 | RequiredArgument = ref object of RootObj 25 | name: string 26 | value: string 27 | help: string 28 | 29 | StoreArgument = ref object of RootObj 30 | shortName: string 31 | longName: string 32 | usageInput: string 33 | value: string 34 | help: string 35 | 36 | SwitchArgument = ref object of RootObj 37 | shortName: string 38 | longName: string 39 | value: bool 40 | help: string 41 | 42 | ArgumentParser* = ref object of RootObj 43 | programName*: string 44 | fullName*: string 45 | author*: string 46 | description*: string 47 | version*: string 48 | requiredArgs: seq[RequiredArgument] 49 | storeArgs: Table[string, StoreArgument] 50 | switchArgs: Table[string, SwitchArgument] 51 | 52 | proc removeDashes(s: string): string = 53 | var 54 | t = s 55 | i: int 56 | while true: 57 | i = t.find("-") 58 | if i == -1: 59 | break 60 | t.delete(i..i) 61 | return t 62 | 63 | proc addRequiredArgument*(argparser: ArgumentParser, name: string, 64 | help: string) = 65 | let cla = RequiredArgument(name: name, help: help) 66 | argparser.requiredArgs.add(cla) 67 | 68 | proc addStoreArgument*(argparser: ArgumentParser, shortName: string, 69 | longName: string, usageInput: string, default: string, help: string) = 70 | let cla = StoreArgument(shortName: shortName, longName: longName, 71 | usageInput: usageInput, value: default, help: help) 72 | argparser.storeArgs[removeDashes(shortName)] = cla 73 | argparser.storeArgs[removeDashes(longName)] = cla 74 | 75 | proc addSwitchArgument*(argparser: ArgumentParser, shortName: string, 76 | longName: string, default: bool, help: string) = 77 | let cla = SwitchArgument(shortName: shortName, longName: longName, 78 | value: default, help: help) 79 | argparser.switchArgs[removeDashes(shortName)] = cla 80 | argparser.switchArgs[removeDashes(longName)] = cla 81 | 82 | proc echoVersion(argparser: ArgumentParser) = # echo the version message 83 | echo argparser.fullName & " v" & argparser.version 84 | if not argparser.author.isEmptyOrWhitespace(): 85 | echo argparser.author 86 | 87 | proc echoUsage(argparser: ArgumentParser) = # echo the usage message 88 | var 89 | opts: seq[string] 90 | reqs: string 91 | 92 | # collect a sequence with the names of all optional arguments 93 | for arg in argparser.storeArgs.values: 94 | opts.add("[" & arg.shortName & "=" & arg.usageInput & "]") 95 | 96 | for arg in argparser.switchArgs.values: 97 | opts.add("[" & arg.shortName & "]") 98 | 99 | opts = deduplicate(opts) 100 | 101 | # collect a string with the name of all required arguments 102 | for _, arg in argparser.requiredArgs: 103 | reqs.add(" " & arg.name) 104 | 105 | # echo the usage message 106 | echo "Usage: " & argparser.programName & " [-h] [-v] " & opts.join(" ") & reqs 107 | 108 | proc echoHelp(argparser: ArgumentParser) = # echo the help message 109 | var 110 | reqNameCol, reqDescCol, optNameCol, optDescCol, reqSec, optSec: seq[string] 111 | example, helpMessage: string 112 | maxNCLen: int 113 | const 114 | colMargin = 2 115 | tabSize = 4 116 | let 117 | tab = repeat(" ", tabSize) 118 | 119 | for i, arg in argparser.requiredArgs: 120 | reqNameCol.add(arg.name) 121 | reqDescCol.add(arg.help) 122 | if len(arg.name) > maxNCLen: 123 | maxNCLen = len(arg.name) 124 | 125 | # insert the help entry 126 | let helpargs = "-h, --help" 127 | optNameCol.add(helpargs) 128 | optDescCol.add("Show this help message and exit.") 129 | if len(helpargs) > maxNCLen: 130 | maxNCLen = len(helpargs) 131 | 132 | # insert the version entry 133 | let verargs = "-v, --version" 134 | optNameCol.add(verargs) 135 | optDescCol.add("Show version number and exit.") 136 | if len(verargs) > maxNCLen: 137 | maxNCLen = len(verargs) 138 | 139 | for arg in argparser.storeArgs.values: 140 | example = arg.shortName & "=" & arg.usageInput & ", " & arg.longName & "=" & arg.usageInput 141 | optNameCol.add(example) 142 | optDescCol.add(arg.help) 143 | if len(example) > maxNCLen: 144 | maxNCLen = len(example) 145 | for arg in argparser.switchArgs.values: 146 | example = arg.shortName & ", " & arg.longName 147 | optNameCol.add(example) 148 | optDescCol.add(arg.help) 149 | if len(example) > maxNCLen: 150 | maxNCLen = len(example) 151 | 152 | for _, lines in zip(reqNameCol, reqDescCol): 153 | reqSec.add(tab & lines[0] & repeat(" ", maxNCLen - len(lines[0]) + 154 | colMargin) & lines[1]) 155 | for _, lines in zip(optNameCol, optDescCol): 156 | optSec.add(tab & lines[0] & repeat(" ", maxNCLen - len(lines[0]) + 157 | colMargin) & lines[1]) 158 | reqSec = deduplicate(reqSec) 159 | optSec = deduplicate(optSec) 160 | 161 | helpMessage = "Required arguments:\n" & reqSec.join("\n") & 162 | "\n\nOptional arguments:\n" & optSec.join("\n") 163 | 164 | argparser.echoVersion() 165 | echo argparser.description 166 | echo "" 167 | argparser.echoUsage() 168 | echo "" 169 | echo helpMessage 170 | 171 | 172 | proc parse*(argparser: ArgumentParser): Table[string, string] = 173 | 174 | macro badArg() = 175 | result = quote do: 176 | unRecArgs = true 177 | unRecArgsMsg.add(" " & iop.key) 178 | 179 | var 180 | iop = initOptParser() 181 | reqCount = 0 182 | unRecArgs = false 183 | unRecArgsMsg = "Error: unrecognized arguments:" 184 | 185 | while true: 186 | iop.next() 187 | case iop.kind 188 | of cmdEnd: break 189 | of cmdShortOption, cmdLongOption: 190 | if iop.val == "": # if the cmd is only a flag 191 | case iop.key 192 | of "h", "help": 193 | argparser.echoHelp() 194 | quit(0) 195 | of "v", "version": 196 | argparser.echoVersion() 197 | quit(0) 198 | else: 199 | try: 200 | argparser.switchArgs[iop.key].value = true 201 | except KeyError: 202 | badArg() 203 | else: 204 | try: 205 | argparser.storeArgs[iop.key].value = iop.val 206 | except KeyError: 207 | badArg() 208 | of cmdArgument: 209 | try: 210 | argparser.requiredArgs[reqCount].value = iop.key 211 | reqCount += 1 212 | except IndexDefect: 213 | badArg() 214 | 215 | if reqCount < len(argparser.requiredArgs): 216 | var tooFewMsg = "Error: the following arguments are required:" 217 | for _, val in argparser.requiredArgs[reqCount..^1]: 218 | tooFewMsg.add(" " & val.name) 219 | argparser.echoUsage() 220 | echo tooFewMsg 221 | quit(1) 222 | 223 | if unRecArgs: 224 | argparser.echoUsage() 225 | echo unRecArgsMsg 226 | quit(1) 227 | 228 | var combined = initTable[string, string]() 229 | for _, arg in argparser.requiredArgs: 230 | combined[arg.name] = arg.value 231 | for name, arg in argparser.storeArgs.pairs: 232 | combined[name] = arg.value 233 | for name, arg in argparser.switchArgs.pairs: 234 | combined[name] = $arg.value 235 | 236 | return combined 237 | 238 | --------------------------------------------------------------------------------