| 1 | # args.ysh
|
| 2 | #
|
| 3 | # Usage:
|
| 4 | # source --builtin args.sh
|
| 5 |
|
| 6 | const __provide__ = :| parser parseArgs |
|
| 7 |
|
| 8 | #
|
| 9 | #
|
| 10 | # parser (&spec) {
|
| 11 | # flag -v --verbose (help="Verbosely") # default is Bool, false
|
| 12 | #
|
| 13 | # flag -P --max-procs (Int, default=-1, doc='''
|
| 14 | # Run at most P processes at a time
|
| 15 | # ''')
|
| 16 | #
|
| 17 | # flag -i --invert (Bool, default=true, doc='''
|
| 18 | # Long multiline
|
| 19 | # Description
|
| 20 | # ''')
|
| 21 | #
|
| 22 | # arg src (help='Source')
|
| 23 | # arg dest (help='Dest')
|
| 24 | # arg times (help='Foo')
|
| 25 | #
|
| 26 | # rest files
|
| 27 | # }
|
| 28 | #
|
| 29 | # var args = parseArgs(spec, ARGV)
|
| 30 | #
|
| 31 | # echo "Verbose $[args.verbose]"
|
| 32 |
|
| 33 | # TODO: See list
|
| 34 | # - flag builtin:
|
| 35 | # - handle only long flag or only short flag
|
| 36 | # - flag aliases
|
| 37 | # - assert that default value has the declared type
|
| 38 |
|
| 39 | proc parser (; place ; ; block_def) {
|
| 40 | ## Create an args spec which can be passed to parseArgs.
|
| 41 | ##
|
| 42 | ## Example:
|
| 43 | ##
|
| 44 | ## # NOTE: &spec will create a variable named spec
|
| 45 | ## parser (&spec) {
|
| 46 | ## flag -v --verbose (Bool)
|
| 47 | ## }
|
| 48 | ##
|
| 49 | ## var args = parseArgs(spec, ARGV)
|
| 50 |
|
| 51 | var p = {flags: [], args: []}
|
| 52 | ctx push (p) {
|
| 53 | call io->eval(block_def, vars={flag, arg, rest})
|
| 54 | }
|
| 55 |
|
| 56 | # Validate that p.rest = [name] or null and reduce p.rest into name or null.
|
| 57 | if ('rest' in p) {
|
| 58 | if (len(p.rest) > 1) {
|
| 59 | error '`rest` was called more than once' (code=3)
|
| 60 | } else {
|
| 61 | setvar p.rest = p.rest[0]
|
| 62 | }
|
| 63 | } else {
|
| 64 | setvar p.rest = null
|
| 65 | }
|
| 66 |
|
| 67 | var names = {}
|
| 68 | for items in ([p.flags, p.args]) {
|
| 69 | for x in (items) {
|
| 70 | if (x.name in names) {
|
| 71 | error "Duplicate flag/arg name $[x.name] in spec" (code=3)
|
| 72 | }
|
| 73 |
|
| 74 | setvar names[x.name] = null
|
| 75 | }
|
| 76 | }
|
| 77 |
|
| 78 | # TODO: what about `flag --name` and then `arg name`?
|
| 79 |
|
| 80 | call place->setValue(p)
|
| 81 | }
|
| 82 |
|
| 83 | const kValidTypes = [Bool, Float, List[Float], Int, List[Int], Str, List[Str]]
|
| 84 | const kValidTypeNames = []
|
| 85 | for vt in (kValidTypes) {
|
| 86 | var name = vt.name if ('name' in propView(vt)) else vt.unique_id
|
| 87 | call kValidTypeNames->append(name)
|
| 88 | }
|
| 89 |
|
| 90 | func isValidType (type) {
|
| 91 | for valid in (kValidTypes) {
|
| 92 | if (type is valid) {
|
| 93 | return (true)
|
| 94 | }
|
| 95 | }
|
| 96 | return (false)
|
| 97 | }
|
| 98 |
|
| 99 | proc flag (short, long ; type=Bool ; default=null, help=null) {
|
| 100 | ## Declare a flag within an `arg-parse`.
|
| 101 | ##
|
| 102 | ## Examples:
|
| 103 | ##
|
| 104 | ## arg-parse (&spec) {
|
| 105 | ## flag -v --verbose
|
| 106 | ## flag -n --count (Int, default=1)
|
| 107 | ## flag -p --percent (Float, default=0.0)
|
| 108 | ## flag -f --file (Str, help="File to process")
|
| 109 | ## flag -e --exclude (List[Str], help="File to exclude")
|
| 110 | ## }
|
| 111 |
|
| 112 | if (type !== null and not isValidType(type)) {
|
| 113 | var type_names = ([null] ++ kValidTypeNames) => join(', ')
|
| 114 | error "Expected flag type to be one of: $type_names" (code=2)
|
| 115 | }
|
| 116 |
|
| 117 | # Bool has a default of false, not null
|
| 118 | if (type is Bool and default === null) {
|
| 119 | setvar default = false
|
| 120 | }
|
| 121 |
|
| 122 | var name = long => trimStart('--')
|
| 123 |
|
| 124 | ctx emit flags ({short, long, name, type, default, help})
|
| 125 | }
|
| 126 |
|
| 127 | proc arg (name ; ; help=null) {
|
| 128 | ## Declare a positional argument within an `arg-parse`.
|
| 129 | ##
|
| 130 | ## Examples:
|
| 131 | ##
|
| 132 | ## arg-parse (&spec) {
|
| 133 | ## arg name
|
| 134 | ## arg config (help="config file path")
|
| 135 | ## }
|
| 136 |
|
| 137 | ctx emit args ({name, help})
|
| 138 | }
|
| 139 |
|
| 140 | proc rest (name) {
|
| 141 | ## Take the remaining positional arguments within an `arg-parse`.
|
| 142 | ##
|
| 143 | ## Examples:
|
| 144 | ##
|
| 145 | ## arg-parse (&grepSpec) {
|
| 146 | ## arg query
|
| 147 | ## rest files
|
| 148 | ## }
|
| 149 |
|
| 150 | # We emit instead of set to detect multiple invocations of "rest"
|
| 151 | ctx emit rest (name)
|
| 152 | }
|
| 153 |
|
| 154 | func parseArgs(spec, argv) {
|
| 155 | ## Given a spec created by `parser`. Parse an array of strings `argv` per
|
| 156 | ## that spec.
|
| 157 | ##
|
| 158 | ## See `parser` for examples of use.
|
| 159 |
|
| 160 | var i = 0
|
| 161 | var positionalPos = 0
|
| 162 | var argc = len(argv)
|
| 163 | var args = {}
|
| 164 | var rest = []
|
| 165 |
|
| 166 | var value
|
| 167 | var found
|
| 168 | while (i < argc) {
|
| 169 | var arg = argv[i]
|
| 170 | if (arg.startsWith('-')) {
|
| 171 | setvar found = false
|
| 172 |
|
| 173 | for flag in (spec.flags) {
|
| 174 | if ( (flag.short and flag.short === arg) or
|
| 175 | (flag.long and flag.long === arg) ) {
|
| 176 | if (flag.type === null or flag.type is Bool) {
|
| 177 | setvar value = true
|
| 178 | } elif (flag.type is Int) {
|
| 179 | setvar i += 1
|
| 180 | if (i >= len(argv)) {
|
| 181 | error "Expected Int after '$arg'" (code=2)
|
| 182 | }
|
| 183 |
|
| 184 | try { setvar value = int(argv[i]) }
|
| 185 | if (_status !== 0) {
|
| 186 | error "Expected Int after '$arg', got '$[argv[i]]'" (code=2)
|
| 187 | }
|
| 188 | } elif (flag.type is List[Int]) {
|
| 189 | setvar i += 1
|
| 190 | if (i >= len(argv)) {
|
| 191 | error "Expected Int after '$arg'" (code=2)
|
| 192 | }
|
| 193 |
|
| 194 | setvar value = get(args, flag.name, [])
|
| 195 | try { call value->append(int(argv[i])) }
|
| 196 | if (_status !== 0) {
|
| 197 | error "Expected Int after '$arg', got '$[argv[i]]'" (code=2)
|
| 198 | }
|
| 199 | } elif (flag.type is Float) {
|
| 200 | setvar i += 1
|
| 201 | if (i >= len(argv)) {
|
| 202 | error "Expected Float after '$arg'" (code=2)
|
| 203 | }
|
| 204 |
|
| 205 | try { setvar value = float(argv[i]) }
|
| 206 | if (_status !== 0) {
|
| 207 | error "Expected Float after '$arg', got '$[argv[i]]'" (code=2)
|
| 208 | }
|
| 209 | } elif (flag.type is List[Float]) {
|
| 210 | setvar i += 1
|
| 211 | if (i >= len(argv)) {
|
| 212 | error "Expected Float after '$arg'" (code=2)
|
| 213 | }
|
| 214 |
|
| 215 | setvar value = get(args, flag.name, [])
|
| 216 | try { call value->append(float(argv[i])) }
|
| 217 | if (_status !== 0) {
|
| 218 | error "Expected Float after '$arg', got '$[argv[i]]'" (code=2)
|
| 219 | }
|
| 220 | } elif (flag.type is Str) {
|
| 221 | setvar i += 1
|
| 222 | if (i >= len(argv)) {
|
| 223 | error "Expected Str after '$arg'" (code=2)
|
| 224 | }
|
| 225 |
|
| 226 | setvar value = argv[i]
|
| 227 | } elif (flag.type is List[Str]) {
|
| 228 | setvar i += 1
|
| 229 | if (i >= len(argv)) {
|
| 230 | error "Expected Str after '$arg'" (code=2)
|
| 231 | }
|
| 232 |
|
| 233 | setvar value = get(args, flag.name, [])
|
| 234 | call value->append(argv[i])
|
| 235 | }
|
| 236 |
|
| 237 | setvar args[flag.name] = value
|
| 238 | setvar found = true
|
| 239 | break
|
| 240 | }
|
| 241 | }
|
| 242 |
|
| 243 | if (not found) {
|
| 244 | error "Unknown flag '$arg'" (code=2)
|
| 245 | }
|
| 246 | } elif (positionalPos >= len(spec.args)) {
|
| 247 | if (not spec.rest) {
|
| 248 | error "Too many arguments, unexpected '$arg'" (code=2)
|
| 249 | }
|
| 250 |
|
| 251 | call rest->append(arg)
|
| 252 | } else {
|
| 253 | var pos = spec.args[positionalPos]
|
| 254 | setvar positionalPos += 1
|
| 255 | setvar value = arg
|
| 256 | setvar args[pos.name] = value
|
| 257 | }
|
| 258 |
|
| 259 | setvar i += 1
|
| 260 | }
|
| 261 |
|
| 262 | if (spec.rest) {
|
| 263 | setvar args[spec.rest] = rest
|
| 264 | }
|
| 265 |
|
| 266 | # Set defaults for flags
|
| 267 | for flag in (spec.flags) {
|
| 268 | if (flag.name not in args) {
|
| 269 | setvar args[flag.name] = flag.default
|
| 270 | }
|
| 271 | }
|
| 272 |
|
| 273 | # Raise error on missing args
|
| 274 | for arg in (spec.args) {
|
| 275 | if (arg.name not in args) {
|
| 276 | error "Usage Error: Missing required argument $[arg.name]" (code=2)
|
| 277 | }
|
| 278 | }
|
| 279 |
|
| 280 | return (args)
|
| 281 | }
|