| 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 | }
 |