diff --git a/cli.carp b/cli.carp index 6de9261..eaeb5b9 100644 --- a/cli.carp +++ b/cli.carp @@ -18,13 +18,16 @@ (None) (match @b (None) true _ false) (Integer i) (match @b (Integer j) (= i j) _ false) (Floating f) (match @b (Floating g) (= f g) _ false) - (Str s) (match @b (Str t) (= s t) _ false))) + (Str s) (match @b (Str t) (= s t) _ false) + (Boolean p) (match @b (Boolean q) (= p q) _ false))) (defn format [s t] (match @t (Integer i) (Long.format s i) (Floating f) (Double.format s f) - (Str s2) (format s &s2))) + (Str s2) (format s &s2) + (Boolean b) (String.format s &(Bool.str b)) + (None) (String.format s "none"))) (defn str [t] (match @t @@ -59,8 +62,8 @@ (defmodule Tag (defn to-type [t s] (match t - (Integer) (CLI.Type.Integer (Maybe.from 0l (Long.from-string s))) - (Floating) (CLI.Type.Floating (Maybe.from 0.0 (Double.from-string s))) + (Integer) (CLI.Type.Integer (Maybe.from (Long.from-string s) 0l)) + (Floating) (CLI.Type.Floating (Maybe.from (Double.from-string s) 0.0)) (Str) (CLI.Type.Str @s) (Boolean) (CLI.Type.Boolean (/= s "false")))) @@ -145,21 +148,20 @@ k (Pair.a e) v (Pair.b (Pair.b e))] (when-do (or (= (Pair.a k) s) (= (Pair.b k) s)) - (set! res (Maybe.unsafe-from @v)) + (match @v (Maybe.Just val) (set! res val) (Maybe.Nothing) ()) (break)))) res)) - (defn in? [m s vs] + (defn in? [m s allowed] (let-do [found true vals (values m)] (for [i 0 (length vals)] (let [e (unsafe-nth vals i) - k (Pair.a e) - v (Pair.b (Pair.b e))] + k (Pair.a e)] (when (or (= (Pair.a k) s) (= (Pair.b k) s)) - (match @v + (match @(Pair.b (Pair.b e)) (Maybe.Just value) - (do (set! found (contains? vs &value)) (break)) + (do (set! found (contains? allowed &value)) (break)) (Maybe.Nothing) (break))))) found)) @@ -208,24 +210,17 @@ (hidden option-) (private option-) (defndynamic option- [t long short description required default-options] - (if (= (length default-options) 0) - (list 'CLI.Option.init - (list t) - (list 'copy long) - (list 'copy short) - (list 'copy description) - required - '(Maybe.Nothing) - '(Maybe.Nothing)) - (if (= (length default-options) 1) + (cond + (= (length default-options) 0) (list 'CLI.Option.init (list t) (list 'copy long) (list 'copy short) (list 'copy description) required - (list 'Maybe.Just (list 'to-cli-type (car default-options))) + '(Maybe.Nothing) '(Maybe.Nothing)) + (= (length default-options) 1) (list 'CLI.Option.init (list t) (list 'copy long) @@ -233,10 +228,18 @@ (list 'copy description) required (list 'Maybe.Just (list 'to-cli-type (car default-options))) - (list 'Maybe.Just - (list 'Array.copy-map - '(ref (fn [e] (to-cli-type @e))) - (cadr default-options))))))) + '(Maybe.Nothing)) + (list 'CLI.Option.init + (list t) + (list 'copy long) + (list 'copy short) + (list 'copy description) + required + (list 'Maybe.Just (list 'to-cli-type (car default-options))) + (list 'Maybe.Just + (list 'Array.copy-map + '(ref (fn [e] (to-cli-type @e))) + (cadr default-options)))))) (doc bool "creates a boolean option.") (defmacro bool [long short description] @@ -246,7 +249,7 @@ (defmacro str [long short description required :rest default-options] (CLI.option- 'CLI.Tag.Str long short description required default-options)) - (doc int "creates a integer option. The actual type is a `Long`.") + (doc int "creates an integer option. The actual type is a `Long`.") (defmacro int [long short description required :rest default-options] (CLI.option- 'CLI.Tag.Integer long @@ -255,7 +258,7 @@ required default-options)) - (doc float "creates a integer option. The actual type is a `Double`.") + (doc float "creates a floating point option. The actual type is a `Double`.") (defmacro float [long short description required :rest default-options] (CLI.option- 'CLI.Tag.Floating long @@ -285,10 +288,10 @@ Options:" (System.get-arg 0) &(options-str p) (Parser.description p))) (for [i (Option.short arg) (Option.description arg))) (when @(Option.required? arg) (IO.print " REQUIRED")) - (when (Maybe.just? (Option.default arg)) - (IO.print - &(fmt " (default: %s)" - &(str &(Maybe.unsafe-from @(Option.default arg)))))) + (match @(Option.default arg) + (Maybe.Just default-val) + (IO.print &(fmt " (default: %s)" &(str &default-val))) + (Maybe.Nothing) ()) (match @(Option.options arg) (Maybe.Just o) (IO.print &(fmt " (options: %s)" &(join ", " &(copy-map &str &o)))) @@ -303,7 +306,7 @@ the long arguments to their values. Because values can be optional, they are returned as `Maybe`. Otherwise it will return an `Error` containing an error message. If that error -mesage is empty, `--help` was requested. If you don’t want to provide a +message is empty, `--help` was requested. If you don’t want to provide a `--help` feature, you can override that flag.") (defn parse [p] (let-do [values (Parser.values p) @@ -354,16 +357,18 @@ mesage is empty, `--help` was requested. If you don’t want to provide a (fmt "Required option missing: --%s" (Option.long o)))) (break)) (Maybe.just? (Option.options o)) - (let-do [opts (Maybe.unsafe-from @(Option.options o))] - (unless-do (CmdMap.in? &values (Option.long o) &opts) - (set! res - (Result.Error - (fmt - "Option %s received an invalid option %s (Options are %s)" - (Option.long o) - &(CmdMap.get &values (Option.long o)) - &(join ", " &(copy-map &str &opts))))) - (break))) + (match @(Option.options o) + (Maybe.Just opts) + (unless-do (CmdMap.in? &values (Option.long o) &opts) + (set! res + (Result.Error + (fmt + "Option %s received an invalid option %s (Options are %s)" + (Option.long o) + &(CmdMap.get &values (Option.long o)) + &(join ", " &(copy-map &str &opts))))) + (break)) + (Maybe.Nothing) ()) ())))) (match res (Result.Success _) (Result.Success (CmdMap.to-map &values)) diff --git a/test/cli.carp b/test/cli.carp new file mode 100644 index 0000000..1ebdd25 --- /dev/null +++ b/test/cli.carp @@ -0,0 +1,362 @@ +(load "../cli.carp") +(load "Test.carp") +(use Test) + +; --------------------------------------------------------------------------- +; Type tests +; --------------------------------------------------------------------------- + +(deftest test + ; --- Type.= --- + + (assert-true test + (CLI.Type.= &(CLI.Type.Integer 42l) &(CLI.Type.Integer 42l)) + "Type.= equal integers") + + (assert-false test + (CLI.Type.= &(CLI.Type.Integer 1l) &(CLI.Type.Integer 2l)) + "Type.= different integers") + + (assert-true test + (CLI.Type.= &(CLI.Type.Floating 3.14) &(CLI.Type.Floating 3.14)) + "Type.= equal floats") + + (assert-false test + (CLI.Type.= &(CLI.Type.Floating 1.0) &(CLI.Type.Floating 2.0)) + "Type.= different floats") + + (assert-true test + (CLI.Type.= &(CLI.Type.Str @"hello") &(CLI.Type.Str @"hello")) + "Type.= equal strings") + + (assert-false test + (CLI.Type.= &(CLI.Type.Str @"a") &(CLI.Type.Str @"b")) + "Type.= different strings") + + (assert-true test + (CLI.Type.= &(CLI.Type.Boolean true) &(CLI.Type.Boolean true)) + "Type.= equal booleans true") + + (assert-true test + (CLI.Type.= &(CLI.Type.Boolean false) &(CLI.Type.Boolean false)) + "Type.= equal booleans false") + + (assert-false test + (CLI.Type.= &(CLI.Type.Boolean true) &(CLI.Type.Boolean false)) + "Type.= different booleans") + + (assert-true test + (CLI.Type.= &(CLI.Type.None) &(CLI.Type.None)) + "Type.= both None") + + (assert-false test + (CLI.Type.= &(CLI.Type.None) &(CLI.Type.Integer 0l)) + "Type.= None vs Integer") + + (assert-false test + (CLI.Type.= &(CLI.Type.Integer 0l) &(CLI.Type.Str @"0")) + "Type.= cross-type Integer vs Str") + + (assert-false test + (CLI.Type.= &(CLI.Type.Boolean true) &(CLI.Type.Str @"true")) + "Type.= cross-type Boolean vs Str") + + ; --- Type.str --- + + (assert-equal test + "42" + &(CLI.Type.str &(CLI.Type.Integer 42l)) + "Type.str integer") + + (assert-equal test + "3.140000" + &(CLI.Type.str &(CLI.Type.Floating 3.14)) + "Type.str float") + + (assert-equal test + "hello" + &(CLI.Type.str &(CLI.Type.Str @"hello")) + "Type.str string") + + (assert-equal test + "true" + &(CLI.Type.str &(CLI.Type.Boolean true)) + "Type.str boolean true") + + (assert-equal test + "false" + &(CLI.Type.str &(CLI.Type.Boolean false)) + "Type.str boolean false") + + (assert-equal test "none" &(CLI.Type.str &(CLI.Type.None)) "Type.str none") + + ; --- Type.format --- + + (assert-equal test + "42" + &(CLI.Type.format "%ld" &(CLI.Type.Integer 42l)) + "Type.format integer") + + (assert-equal test + "3.140000" + &(CLI.Type.format "%f" &(CLI.Type.Floating 3.14)) + "Type.format float") + + (assert-equal test + "hello" + &(CLI.Type.format "%s" &(CLI.Type.Str @"hello")) + "Type.format string") + + (assert-equal test + "true" + &(CLI.Type.format "%s" &(CLI.Type.Boolean true)) + "Type.format boolean") + + (assert-equal test + "none" + &(CLI.Type.format "%s" &(CLI.Type.None)) + "Type.format none") + + ; --- Type.to-* conversions --- + + (assert-equal test + 42 + (CLI.Type.to-int (CLI.Type.Integer 42l)) + "Type.to-int from Integer") + + (assert-equal test + 0 + (CLI.Type.to-int (CLI.Type.Str @"x")) + "Type.to-int from non-Integer") + + (assert-equal test + 42l + (CLI.Type.to-long (CLI.Type.Integer 42l)) + "Type.to-long from Integer") + + (assert-equal test + 0l + (CLI.Type.to-long (CLI.Type.None)) + "Type.to-long from non-Integer") + + (assert-equal test + "hi" + &(CLI.Type.to-str (CLI.Type.Str @"hi")) + "Type.to-str from Str") + + (assert-equal test + "" + &(CLI.Type.to-str (CLI.Type.Integer 1l)) + "Type.to-str from non-Str") + + (assert-true test + (CLI.Type.to-bool (CLI.Type.Boolean true)) + "Type.to-bool from Boolean true") + + (assert-false test + (CLI.Type.to-bool (CLI.Type.Boolean false)) + "Type.to-bool from Boolean false") + + (assert-false test + (CLI.Type.to-bool (CLI.Type.Str @"true")) + "Type.to-bool from non-Boolean") + + (assert-equal test + 0.0 + (CLI.Type.to-double (CLI.Type.Floating 0.0)) + "Type.to-double zero") + + (assert-equal test + 0.0 + (CLI.Type.to-double (CLI.Type.Integer 0l)) + "Type.to-double from non-Floating") + + ; --- Type.zero --- + + (assert-true test + (CLI.Type.= &(CLI.Type.None) &(CLI.Type.zero)) + "Type.zero is None") + + ; --- Tag.= --- + + (assert-true test + (CLI.Tag.= &(CLI.Tag.Integer) &(CLI.Tag.Integer)) + "Tag.= equal Integer") + + (assert-true test + (CLI.Tag.= &(CLI.Tag.Boolean) &(CLI.Tag.Boolean)) + "Tag.= equal Boolean") + + (assert-false test + (CLI.Tag.= &(CLI.Tag.Integer) &(CLI.Tag.Str)) + "Tag.= different tags") + + ; --- Tag.to-type --- + + (assert-equal test + "42" + &(CLI.Type.str &(CLI.Tag.to-type (CLI.Tag.Integer) "42")) + "Tag.to-type Integer parses") + + (assert-equal test + "hello" + &(CLI.Type.str &(CLI.Tag.to-type (CLI.Tag.Str) "hello")) + "Tag.to-type Str passes through") + + (assert-equal test + "true" + &(CLI.Type.str &(CLI.Tag.to-type (CLI.Tag.Boolean) "true")) + "Tag.to-type Boolean true") + + (assert-equal test + "true" + &(CLI.Type.str &(CLI.Tag.to-type (CLI.Tag.Boolean) "yes")) + "Tag.to-type Boolean non-false is true") + + (assert-equal test + "false" + &(CLI.Type.str &(CLI.Tag.to-type (CLI.Tag.Boolean) "false")) + "Tag.to-type Boolean false") + + ; --- CmdMap --- + + (let [m (CLI.CmdMap.new)] + (assert-false test + (CLI.CmdMap.contains? &m "x") + "CmdMap.contains? empty map")) + + (let-do [o (CLI.int "flag" "f" "desc" true) + m (CLI.CmdMap.put-empty (CLI.CmdMap.new) &o)] + (assert-true test + (CLI.CmdMap.contains? &m "flag") + "CmdMap.contains? by long name") + + (assert-true test + (CLI.CmdMap.contains? &m "f") + "CmdMap.contains? by short name") + + (assert-false test + (CLI.CmdMap.contains? &m "other") + "CmdMap.contains? missing key")) + + (let-do [o (CLI.str "name" + "n" + "a name" + false + @"default" + &[@"a" @"b" @"default"]) + m (CLI.CmdMap.put-empty (CLI.CmdMap.new) &o)] + (assert-false test + (CLI.CmdMap.set? &m "name") + "CmdMap.set? returns false when no value set") + + (do + (CLI.CmdMap.put! &m "name" "hello") + (assert-true test + (CLI.CmdMap.set? &m "name") + "CmdMap.set? returns true after put!"))) + + (let-do [o (CLI.int "count" "c" "a count" false) + m (CLI.CmdMap.put-empty (CLI.CmdMap.new) &o)] + (CLI.CmdMap.put! &m "count" "10") + (assert-equal test + "10" + &(CLI.Type.str &(CLI.CmdMap.get &m "count")) + "CmdMap.get retrieves value after put!")) + + (let-do [o (CLI.str "mode" "m" "mode" false @"a" &[@"a" @"b"]) + m (CLI.CmdMap.put-empty (CLI.CmdMap.new) &o)] + (CLI.CmdMap.put! &m "mode" "a") + (assert-true test + (CLI.CmdMap.in? &m "mode" &[(CLI.Type.Str @"a") (CLI.Type.Str @"b")]) + "CmdMap.in? value in options") + + (CLI.CmdMap.put! &m "mode" "x") + (assert-false test + (CLI.CmdMap.in? &m "mode" &[(CLI.Type.Str @"a") (CLI.Type.Str @"b")]) + "CmdMap.in? value not in options")) + + (let-do [o (CLI.int "flag" "f" "desc" false) + m (CLI.CmdMap.put-empty (CLI.CmdMap.new) &o)] + (assert-true test + (CLI.CmdMap.type? &m "flag" &(CLI.Tag.Integer)) + "CmdMap.type? correct type") + + (assert-false test + (CLI.CmdMap.type? &m "flag" &(CLI.Tag.Str)) + "CmdMap.type? wrong type")) + + (let [o (CLI.bool "verbose" "v" "be verbose") + m (CLI.CmdMap.put-empty (CLI.CmdMap.new) &o)] + (assert-true test + (CLI.CmdMap.type? &m "verbose" &(CLI.Tag.Boolean)) + "CmdMap.type? boolean")) + + ; --- CmdMap.to-map --- + + (let-do [o1 (CLI.int "flag" "f" "desc" false) + o2 (CLI.str "name" "n" "desc" false) + m (CLI.CmdMap.put-empty + (CLI.CmdMap.put-empty (CLI.CmdMap.new) &o1) + &o2)] + (CLI.CmdMap.put! &m "flag" "5") + (let-do [result (CLI.CmdMap.to-map &m)] + (assert-true test + (Map.contains? &result "flag") + "CmdMap.to-map includes set values") + + (assert-false test + (Map.contains? &result "name") + "CmdMap.to-map excludes unset values"))) + + ; --- Option construction --- + + (let-do [o (CLI.int "port" "p" "port number" true)] + (assert-equal test "port" (CLI.Option.long &o) "Option int long name") + (assert-equal test "p" (CLI.Option.short &o) "Option int short name") + (assert-true test @(CLI.Option.required? &o) "Option int required")) + + (let-do [o (CLI.str "name" "n" "a name" false @"default")] + (assert-false test @(CLI.Option.required? &o) "Option str not required") + (assert-true test + (Maybe.just? (CLI.Option.default &o)) + "Option str has default")) + + (let-do [o (CLI.bool "verbose" "v" "verbose output")] + (assert-equal test "verbose" (CLI.Option.long &o) "Option bool long name") + (assert-false test @(CLI.Option.required? &o) "Option bool not required")) + + (let [o (CLI.float "rate" "r" "rate" false 1.5)] + (assert-true test + (Maybe.just? (CLI.Option.default &o)) + "Option float has default")) + + ; --- Parser construction --- + + (let-do [p (=> (CLI.new @"test program") + (CLI.add &(CLI.int "port" "p" "port" true)) + (CLI.add &(CLI.str "host" "h" "host" false)))] + (assert-equal test + "test program" + (CLI.Parser.description &p) + "Parser description") + (assert-equal test + 2 + (Array.length (CLI.Parser.options &p)) + "Parser has two options")) + + ; --- Parser.values (initial CmdMap) --- + + (let-do [p (=> (CLI.new @"test") + (CLI.add &(CLI.int "a" "x" "desc" false)) + (CLI.add &(CLI.str "b" "y" "desc" false))) + m (CLI.Parser.values &p)] + (assert-true test + (CLI.CmdMap.contains? &m "a") + "Parser.values populates first option") + (assert-true test + (CLI.CmdMap.contains? &m "b") + "Parser.values populates second option") + (assert-false test + (CLI.CmdMap.set? &m "a") + "Parser.values options start unset")))