Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,29 @@ manually.
Booleans neither take defaults nor options. If a boolean flag receives a value,
it will be read as true unless it’s the string `false`.

### Short flag bundling

Short options can be written the way you are used to typing them: `ls -la`,
`tar -xzf archive.tar.gz`, `grep -rn pattern`. A single-dash token that matches
no option exactly is decomposed into single-character short options, so `-av` is
`-a -v`.

Boolean options continue the bundle. The first option that takes a value ends
it and reads the rest of the token as its value, or the next token if there is
nothing left:

```
-n5 ; num = 5
-avn5 ; all, verbose, num = 5
-avn 5 ; all, verbose, num = 5
```

Only single-character short names take part in this. A token that names an
option exactly is always resolved as that option first, so a multi-character
short name like `th` keeps working, and so does a long name given with a single
dash. If any letter of a bundle is not a known short option, the whole token is
rejected — nothing in it is applied.

### Positional arguments

Positional arguments are non-flag tokens matched by position. Build them with
Expand Down
69 changes: 69 additions & 0 deletions cli.carp
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,14 @@ please use [`pos-str`](#pos-str), [`pos-int`](#pos-int), or
(break))))
found))

(defn short? [m s]
(let-do [found false
vs (values m)]
(for [i 0 (length vs)]
(let [e (unsafe-nth vs i)]
(when-do (= (Pair.b (Pair.a e)) s) (set! found true) (break))))
found))

(defn set? [m s]
(let-do [found false
vs (values m)]
Expand Down Expand Up @@ -376,6 +384,21 @@ Options:" (StaticArray.unsafe-nth &System.args 0) &(options-str p) (Parser.descr
(Positional.description pos))))
(IO.println ""))))))

(private bundle-stop)
(hidden bundle-stop)
; index of the first character of `flag` that is not a boolean short option,
; or its length if they all are
(defn bundle-stop [values flag]
(let-do [n (String.length flag)
stop n]
(for [i 0 n]
(let [c (String.byte-slice flag i (Int.inc i))]
(unless-do (and (CmdMap.short? values &c)
(CmdMap.type? values &c &(Tag.TBoolean)))
(set! stop i)
(break))))
stop))

(hidden parse-from)
; the workhorse behind `parse` and `App` dispatch: parses an explicit argument
; array against `p`. Non-flag tokens fill positionals in order; flags and
Expand Down Expand Up @@ -436,6 +459,45 @@ Options:" (StaticArray.unsafe-nth &System.args 0) &(options-str p) (Parser.descr
(break))))
(or (= k "help") (= k "h"))
(do (set! res (Result.Error @"")) (break))
(and (<= (length &splt) 1)
(not (empty? k))
(not (starts-with? x "--")))
(let [n (String.length k)
stop (bundle-stop &values k)
c (if (< stop n)
(String.byte-slice k stop (Int.inc stop))
@"")
rest (if (< stop n)
(String.byte-slice k (Int.inc stop) n)
@"")
known (or (= stop n) (CmdMap.short? &values &c))]
(cond
(and (not known) (= &c "h"))
(do (set! res (Result.Error @"")) (break))
(not known)
(do
(set! res
(Result.Error (fmt "Unknown option: %s" x)))
(break))
(do
(for [j 0 stop]
(CmdMap.put! &values
&(String.byte-slice k j (Int.inc j))
"true"))
(cond
(= stop n)
(when (Maybe.just? &v) (set! i (Int.dec i)))
(not (empty? &rest))
(do
(when (Maybe.just? &v) (set! i (Int.dec i)))
(CmdMap.put! &values &c &rest))
(match v
(Maybe.Just val) (CmdMap.put! &values &c &val)
(Maybe.Nothing)
(do
(set! res
(Result.Error (fmt "No value for: %s" &c)))
(break)))))))
(do
(set! res (Result.Error (fmt "Unknown option: %s" x)))
(break))))
Expand Down Expand Up @@ -520,6 +582,13 @@ parsing — every following token becomes a positional even if it starts with `-
— and a token shaped like a negative number (`-5`, `-3.14`) is a positional, not
an unknown option.

A single-dash token that matches no option exactly is decomposed into
single-character short options, so `-av` is `-a -v`. Booleans continue the
bundle; the first option that takes a value ends it and reads the rest of the
token as its value, or the next token if nothing is left (`-n5`, `-avn5`,
`-avn 5`). If any letter is not a known short option the whole token is
rejected.

On failure it returns an `Error` with a message — except that an *empty* error
message means `--help` (or `-h`) was requested. Override that flag if you don’t
want the built-in help behaviour.")
Expand Down
6 changes: 6 additions & 0 deletions docs/CLI.html
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,12 @@ <h3 id="parse">
parsing — every following token becomes a positional even if it starts with <code>-</code>
— and a token shaped like a negative number (<code>-5</code>, <code>-3.14</code>) is a positional, not
an unknown option.</p>
<p>A single-dash token that matches no option exactly is decomposed into
single-character short options, so <code>-av</code> is <code>-a -v</code>. Booleans continue the
bundle; the first option that takes a value ends it and reads the rest of the
token as its value, or the next token if nothing is left (<code>-n5</code>, <code>-avn5</code>,
<code>-avn 5</code>). If any letter is not a known short option the whole token is
rejected.</p>
<p>On failure it returns an <code>Error</code> with a message — except that an <em>empty</em> error
message means <code>--help</code> (or <code>-h</code>) was requested. Override that flag if you don’t
want the built-in help behaviour.</p>
Expand Down
Loading