| 1 | # args.ysh
|
| 2 | #
|
| 3 | # Usage:
|
| 4 | # source --builtin args.sh
|
| 5 | #
|
| 6 | # parser (&spec) {
|
| 7 | # flag -v --verbose (help="Verbosely") # default is Bool, false
|
| 8 | #
|
| 9 | # flag -P --max-procs ('int', default=-1, doc='''
|
| 10 | # Run at most P processes at a time
|
| 11 | # ''')
|
| 12 | #
|
| 13 | # flag -i --invert ('bool', default=true, doc='''
|
| 14 | # Long multiline
|
| 15 | # Description
|
| 16 | # ''')
|
| 17 | #
|
| 18 | # arg src (help='Source')
|
| 19 | # arg dest (help='Dest')
|
| 20 | # arg times (help='Foo')
|
| 21 | #
|
| 22 | # rest files
|
| 23 | # }
|
| 24 | #
|
| 25 | # var args = parseArgs(spec, ARGV)
|
| 26 | #
|
| 27 | # echo "Verbose $[args.verbose]"
|
| 28 |
|
| 29 | # TODO: See list
|
| 30 | # - It would be nice to keep `flag` and `arg` private, injecting them into the
|
| 31 | # proc namespace only within `Args`
|
| 32 | # - We need "type object" to replace the strings 'int', 'bool', etc.
|
| 33 | # - flag builtin:
|
| 34 | # - handle only long flag or only short flag
|
| 35 | # - flag aliases
|
| 36 |
|
| 37 | proc parser (; place ; ; block_def) {
|
| 38 | ## Create an args spec which can be passed to parseArgs.
|
| 39 | ##
|
| 40 | ## Example:
|
| 41 | ##
|
| 42 | ## # NOTE: &spec will create a variable named spec
|
| 43 | ## parser (&spec) {
|
| 44 | ## flag -v --verbose ('bool')
|
| 45 | ## }
|
| 46 | ##
|
| 47 | ## var args = parseArgs(spec, ARGV)
|
| 48 |
|
| 49 | var p = {flags: [], args: []}
|
| 50 | ctx push (p; ; block_def)
|
| 51 |
|
| 52 | # Validate that p.rest = [name] or null and reduce p.rest into name or null.
|
| 53 | if ('rest' in p) {
|
| 54 | if (len(p.rest) > 1) {
|
| 55 | error '`rest` was called more than once' (code=3)
|
| 56 | } else {
|
| 57 | setvar p.rest = p.rest[0]
|
| 58 | }
|
| 59 | } else {
|
| 60 | setvar p.rest = null
|
| 61 | }
|
| 62 |
|
| 63 | var names = {}
|
| 64 | for items in ([p.flags, p.args]) {
|
| 65 | for x in (items) {
|
| 66 | if (x.name in names) {
|
| 67 | error "Duplicate flag/arg name $[x.name] in spec" (code=3)
|
| 68 | }
|
| 69 |
|
| 70 | setvar names[x.name] = null
|
| 71 | }
|
| 72 | }
|
| 73 |
|
| 74 | # TODO: what about `flag --name` and then `arg name`?
|
| 75 |
|
| 76 | call place->setValue(p)
|
| 77 | }
|
| 78 |
|
| 79 | proc flag (short, long ; type='bool' ; default=null, help=null) {
|
| 80 | ## Declare a flag within an `arg-parse`.
|
| 81 | ##
|
| 82 | ## Examples:
|
| 83 | ##
|
| 84 | ## arg-parse (&spec) {
|
| 85 | ## flag -v --verbose
|
| 86 | ## flag -n --count ('int', default=1)
|
| 87 | ## flag -f --file ('str', help="File to process")
|
| 88 | ## }
|
| 89 |
|
| 90 | # bool has a default of false, not null
|
| 91 | if (type === 'bool' and default === null) {
|
| 92 | setvar default = false
|
| 93 | }
|
| 94 |
|
| 95 | # TODO: validate `type`
|
| 96 |
|
| 97 | # TODO: Should use "trimPrefix"
|
| 98 | var name = long[2:]
|
| 99 |
|
| 100 | ctx emit flags ({short, long, name, type, default, help})
|
| 101 | }
|
| 102 |
|
| 103 | proc arg (name ; ; help=null) {
|
| 104 | ## Declare a positional argument within an `arg-parse`.
|
| 105 | ##
|
| 106 | ## Examples:
|
| 107 | ##
|
| 108 | ## arg-parse (&spec) {
|
| 109 | ## arg name
|
| 110 | ## arg config (help="config file path")
|
| 111 | ## }
|
| 112 |
|
| 113 | ctx emit args ({name, help})
|
| 114 | }
|
| 115 |
|
| 116 | proc rest (name) {
|
| 117 | ## Take the remaining positional arguments within an `arg-parse`.
|
| 118 | ##
|
| 119 | ## Examples:
|
| 120 | ##
|
| 121 | ## arg-parse (&grepSpec) {
|
| 122 | ## arg query
|
| 123 | ## rest files
|
| 124 | ## }
|
| 125 |
|
| 126 | # We emit instead of set to detect multiple invocations of "rest"
|
| 127 | ctx emit rest (name)
|
| 128 | }
|
| 129 |
|
| 130 | func parseArgs(spec, argv) {
|
| 131 | ## Given a spec created by `parser`. Parse an array of strings `argv` per
|
| 132 | ## that spec.
|
| 133 | ##
|
| 134 | ## See `parser` for examples of use.
|
| 135 |
|
| 136 | var i = 0
|
| 137 | var positionalPos = 0
|
| 138 | var argc = len(argv)
|
| 139 | var args = {}
|
| 140 | var rest = []
|
| 141 |
|
| 142 | var value
|
| 143 | var found
|
| 144 | while (i < argc) {
|
| 145 | var arg = argv[i]
|
| 146 | if (arg->startsWith('-')) {
|
| 147 | setvar found = false
|
| 148 |
|
| 149 | for flag in (spec.flags) {
|
| 150 | if ( (flag.short and flag.short === arg) or
|
| 151 | (flag.long and flag.long === arg) ) {
|
| 152 | case (flag.type) {
|
| 153 | ('bool') | (null) { setvar value = true }
|
| 154 | int {
|
| 155 | setvar i += 1
|
| 156 | if (i >= len(argv)) {
|
| 157 | error "Expected integer after '$arg'" (code=2)
|
| 158 | }
|
| 159 |
|
| 160 | try { setvar value = int(argv[i]) }
|
| 161 | if (_status !== 0) {
|
| 162 | error "Expected integer after '$arg', got '$[argv[i]]'" (code=2)
|
| 163 | }
|
| 164 | }
|
| 165 | }
|
| 166 |
|
| 167 | setvar args[flag.name] = value
|
| 168 | setvar found = true
|
| 169 | break
|
| 170 | }
|
| 171 | }
|
| 172 |
|
| 173 | if (not found) {
|
| 174 | error "Unknown flag '$arg'" (code=2)
|
| 175 | }
|
| 176 | } elif (positionalPos >= len(spec.args)) {
|
| 177 | if (not spec.rest) {
|
| 178 | error "Too many arguments, unexpected '$arg'" (code=2)
|
| 179 | }
|
| 180 |
|
| 181 | call rest->append(arg)
|
| 182 | } else {
|
| 183 | var pos = spec.args[positionalPos]
|
| 184 | setvar positionalPos += 1
|
| 185 | setvar value = arg
|
| 186 | setvar args[pos.name] = value
|
| 187 | }
|
| 188 |
|
| 189 | setvar i += 1
|
| 190 | }
|
| 191 |
|
| 192 | if (spec.rest) {
|
| 193 | setvar args[spec.rest] = rest
|
| 194 | }
|
| 195 |
|
| 196 | # Set defaults for flags
|
| 197 | for flag in (spec.flags) {
|
| 198 | if (flag.name not in args) {
|
| 199 | setvar args[flag.name] = flag.default
|
| 200 | }
|
| 201 | }
|
| 202 |
|
| 203 | # Raise error on missing args
|
| 204 | for arg in (spec.args) {
|
| 205 | if (arg.name not in args) {
|
| 206 | error "Usage Error: Missing required argument $[arg.name]" (code=2)
|
| 207 | }
|
| 208 | }
|
| 209 |
|
| 210 | return (args)
|
| 211 | }
|