├── .gitignore ├── LICENSE ├── README.md ├── argparse.janet ├── project.janet └── test └── test1.janet /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Calvin Rose 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # argparse 2 | 3 | A moderately opinionated argument parser for 4 | [janet](https://janet-lang.org). Use this for writing 5 | CLI scripts that need to have UNIX style switches and 6 | options. 7 | 8 | ## Sample 9 | 10 | ```clojure 11 | #!/usr/bin/env janet 12 | 13 | (import argparse :prefix "") 14 | 15 | (def argparse-params 16 | ["A simple CLI tool. An example to show the capabilities of argparse." 17 | "debug" {:kind :flag 18 | :short "d" 19 | :help "Set debug mode."} 20 | "verbose" {:kind :multi 21 | :short "v" 22 | :help "Print debug information to stdout."} 23 | "key" {:kind :option 24 | :short "k" 25 | :help "An API key for getting stuff from a server." 26 | :required true} 27 | "expr" {:kind :accumulate 28 | :short "e" 29 | :help "Search for all patterns given."} 30 | "thing" {:kind :option 31 | :help "Some option?" 32 | :default "123"}]) 33 | 34 | (let [res (argparse ;argparse-params)] 35 | (unless res 36 | (os/exit 1)) 37 | (pp res)) 38 | ``` 39 | 40 | ## Installing 41 | 42 | Assuming a working install of Janet and `jpm`, you can install with 43 | `[sudo] jpm install https://github.com/janet-lang/argparse.git`, which 44 | will install the latest from master. 45 | 46 | ## Usage 47 | 48 | Call `argparse/argparse` to attempt to parse the command line args 49 | (available at `(dyn :args)`). 50 | 51 | The first argument should be a description to be displayed as help 52 | text. 53 | 54 | All subsequent options should be alternating keys and values where the 55 | keys are options to accept and the values are definitions of each option. 56 | 57 | To accept positional arguments, include a definition for the special 58 | value `:default`. For instance, to gather all positional arguments 59 | into an array, include `:default {:kind :accumulate}` in your 60 | arguments to `argparse`. 61 | 62 | Run `(doc argparse/argparse)` after importing for more information. 63 | 64 | ## License 65 | 66 | This module is licensed under the MIT/X11 License. 67 | -------------------------------------------------------------------------------- /argparse.janet: -------------------------------------------------------------------------------- 1 | ### argparse.janet 2 | ### 3 | ### A library for parsing command-line arguments 4 | ### 5 | ### Copyright 2021 © Calvin Rose 6 | 7 | (defn- pad-right 8 | "Pad a string on the right with some spaces." 9 | [str n] 10 | (def len (length str)) 11 | (if (>= len n) 12 | str 13 | (string str (string/repeat " " (- n len))))) 14 | 15 | (defn argparse 16 | "Parse (dyn :args) according to options. If the arguments are incorrect, 17 | will return nil and print usage information. 18 | 19 | Each option is a table or struct that specifies a flag or option 20 | for the script. The name of the option should be a string, specified 21 | via (argparse/argparse \"...\" op1-name {...} op2-name {...} ...). A help option 22 | and usage text is automatically generated for you.\n\n 23 | 24 | The keys in each option table are as follows:\n\n 25 | 26 | \t:kind - What kind of option is this? One of :flag, :multi, :option, or 27 | :accumulate. A flag can either be on or off, a multi is a flag that can be provided 28 | multiple times, each time adding 1 to a returned integer, an option is a key that 29 | will be set in the returned table, and accumulate means an option can be specified 30 | 0 or more times, each time appending a value to an array.\n 31 | \t:short - Single letter for shorthand access.\n 32 | \t:help - Help text for the option, explaining what it is.\n 33 | \t:default - Default value for the option.\n 34 | \t:required - Whether or not an option is required.\n 35 | \t:short-circuit - Whether or not to stop parsing and fail if this option is hit.\n 36 | \t:action - A function that will be invoked when the option is parsed.\n\n 37 | 38 | There is also a special option :default that will be invoked on arguments 39 | that do not start with a -- or -. Use this option to collect unnamed 40 | arguments to your script.\n\n 41 | 42 | After `--`, every argument is treated as an unnamed argument.\n\n 43 | 44 | Once parsed, values are accessible in the returned table by the name 45 | of the option. For example (result \"verbose\") will check if the verbose 46 | flag is enabled." 47 | [description &keys options] 48 | 49 | # Add default help option 50 | (def options (merge 51 | @{"help" {:kind :flag 52 | :short "h" 53 | :help "Show this help message." 54 | :action :help 55 | :short-circuit true}} 56 | options)) 57 | 58 | # Create shortcodes 59 | (def shortcodes @{}) 60 | (loop [[k v] :pairs options :when (string? k)] 61 | (if-let [code (v :short)] 62 | (put shortcodes (code 0) {:name k :handler v}))) 63 | 64 | # Results table and other things 65 | (def res @{:order @[]}) 66 | (def args (dyn :args)) 67 | (def arglen (length args)) 68 | (var scanning true) 69 | (var bad false) 70 | (var i 1) 71 | (var process-options? true) 72 | 73 | # Show usage 74 | (defn usage 75 | [& msg] 76 | # Only show usage once. 77 | (if bad (break)) 78 | (set bad true) 79 | (set scanning false) 80 | (unless (empty? msg) 81 | (print "usage error: " ;msg)) 82 | (def flags @"") 83 | (def opdoc @"") 84 | (def reqdoc @"") 85 | (loop [[name handler] :in (sort (pairs options))] 86 | (def short (handler :short)) 87 | (when short (buffer/push-string flags short)) 88 | (when (string? name) 89 | (def kind (handler :kind)) 90 | (def usage-prefix 91 | (string 92 | ;(if short [" -" short ", "] [" "]) 93 | "--" name 94 | ;(if (or (= :option kind) (= :accumulate kind)) 95 | [" " (or (handler :value-name) "VALUE") 96 | ;(if-let [d (handler :default)] 97 | ["=" d] 98 | [])] 99 | []))) 100 | (def usage-fragment 101 | (string 102 | (pad-right (string usage-prefix " ") 45) 103 | (if-let [h (handler :help)] h "") 104 | "\n")) 105 | (buffer/push-string (if (handler :required) reqdoc opdoc) 106 | usage-fragment))) 107 | (print "usage: " (get args 0) " [option] ... ") 108 | (print) 109 | (print description) 110 | (print) 111 | (unless (empty? reqdoc) 112 | (print " Required:") 113 | (print reqdoc)) 114 | (unless (empty? opdoc) 115 | (print " Optional:") 116 | (print opdoc))) 117 | 118 | # Handle an option 119 | (defn handle-option 120 | [name handler] 121 | (array/push (res :order) name) 122 | (case (handler :kind) 123 | :flag (put res name true) 124 | :multi (do 125 | (var count (or (get res name) 0)) 126 | (++ count) 127 | (put res name count)) 128 | :option (if-let [arg (get args i)] 129 | (do 130 | (put res name arg) 131 | (++ i)) 132 | (usage "missing argument for " name)) 133 | :accumulate (if-let [arg (get args i)] 134 | (do 135 | (def arr (or (get res name) @[])) 136 | (array/push arr arg) 137 | (++ i) 138 | (put res name arr)) 139 | (usage "missing argument for " name)) 140 | # default 141 | (usage "unknown option kind: " (handler :kind))) 142 | 143 | # Allow actions to be dispatched while scanning 144 | (when-let [action (handler :action)] 145 | (cond 146 | (= action :help) (usage) 147 | (function? action) (action))) 148 | 149 | # Early exit for things like help 150 | (when (handler :short-circuit) 151 | (set scanning false))) 152 | 153 | # Iterate command line arguments and parse them 154 | # into the run table. 155 | (while (and scanning (< i arglen)) 156 | (def arg (get args i)) 157 | (cond 158 | # `--` turns off option processing so that 159 | # the rest of arguments are treated like unnamed arguments. 160 | (and (= "--" arg) process-options?) 161 | (do 162 | (set process-options? false) 163 | (++ i)) 164 | 165 | # long name (--name) 166 | (and (string/has-prefix? "--" arg) process-options?) 167 | (let [name (string/slice arg 2) 168 | handler (get options name)] 169 | (++ i) 170 | (if handler 171 | (handle-option name handler) 172 | (usage "unknown option " name))) 173 | 174 | # short names (-flags) 175 | (and (string/has-prefix? "-" arg) process-options?) 176 | (let [flags (string/slice arg 1)] 177 | (++ i) 178 | (each flag flags 179 | (if-let [x (get shortcodes flag)] 180 | (let [{:name name :handler handler} x] 181 | (handle-option name handler)) 182 | (usage "unknown flag " arg)))) 183 | 184 | # default 185 | (if-let [handler (options :default)] 186 | (handle-option :default handler) 187 | (usage "could not handle option " arg)))) 188 | 189 | # Handle defaults, required options 190 | (loop [[name handler] :pairs options] 191 | (when (nil? (res name)) 192 | (when (handler :required) 193 | (usage "option " name " is required")) 194 | (put res name (handler :default)))) 195 | 196 | (if-not bad res)) 197 | -------------------------------------------------------------------------------- /project.janet: -------------------------------------------------------------------------------- 1 | (declare-project 2 | :name "argparse" 3 | :description "CLI argument parser for Janet" 4 | :author "Calvin Rose" 5 | :license "MIT" 6 | :url "https://github.com/janet-lang/argparse") 7 | 8 | (declare-source 9 | :source @["argparse.janet"]) 10 | -------------------------------------------------------------------------------- /test/test1.janet: -------------------------------------------------------------------------------- 1 | (import ../argparse :prefix "") 2 | 3 | (def argparse-params 4 | ["A simple CLI tool. An example to show the capabilities of argparse." 5 | "debug" {:kind :flag 6 | :short "d" 7 | :help "Set debug mode."} 8 | "verbose" {:kind :multi 9 | :short "v" 10 | :help "Print debug information to stdout."} 11 | "key" {:kind :option 12 | :short "k" 13 | :help "An API key for getting stuff from a server." 14 | :required true} 15 | "expr" {:kind :accumulate 16 | :short "e" 17 | :help "Search for all patterns given."} 18 | "thing" {:kind :option 19 | :help "Some option?" 20 | :default "123"}]) 21 | 22 | (defmacro suppress-stdout [& body] 23 | ~(with-dyns [:out (,file/open (if (,os/stat "/dev/null") "/dev/null" "nul") :w)] 24 | ,;body)) 25 | 26 | (with-dyns [:args @["testcase.janet" "-k" "100"]] 27 | (def res (suppress-stdout (argparse ;argparse-params))) 28 | (when (res "debug") (error (string "bad debug: " (res "debug")))) 29 | (when (res "verbose") (error (string "bad verbose: " (res "verbose")))) 30 | (unless (= (res "key") "100") (error (string "bad key: " (res "key")))) 31 | (when (res "expr") 32 | (error (string "bad expr: " (string/join (res "expr") " ")))) 33 | (unless (= (res "thing") "123") (error (string "bad thing: " (res "thing"))))) 34 | 35 | (with-dyns [:args @["testcase.janet" "-k" "100" "--thing"]] 36 | (def res (suppress-stdout (argparse ;argparse-params))) 37 | (when res (error "Option \"thing\" missing arg, but result is non-nil."))) 38 | 39 | (with-dyns [:args @["testcase.janet" "-k" "100" "-e" "foo" "-e"]] 40 | (def res (suppress-stdout (argparse ;argparse-params))) 41 | (when res (error "Option \"expr\" missing arg, but result is non-nil."))) 42 | 43 | (with-dyns [:args @["testcase.janet" "-k" "100" "-v" "--thing" "456" "-d" "-v" 44 | "-e" "abc" "-vvv" "-e" "def"]] 45 | (def res (suppress-stdout (argparse ;argparse-params))) 46 | (unless (res "debug") (error (string "bad debug: " (res "debug")))) 47 | (unless (= (res "verbose") 5) (error (string "bad verbose: " (res "verbose")))) 48 | (unless (= (tuple ;(res "expr")) ["abc" "def"]) 49 | (error (string "bad expr: " (string/join (res "expr") " ")))) 50 | (unless (= (res "thing") "456") (error (string "bad thing: " (res "thing")))) 51 | (unless (= (tuple ;(res :order)) 52 | ["key" "verbose" "thing" "debug" "verbose" 53 | "expr" "verbose" "verbose" "verbose" "expr"]) 54 | (error (string "bad order: " (string/join (res :order) " "))))) 55 | 56 | (with-dyns [:args @["testcase.janet" "server"]] 57 | (def res (suppress-stdout (argparse 58 | "A simple CLI tool." 59 | :default {:kind :option}))) 60 | (unless (= (res :default) "server") 61 | (error (string "bad default " (res :default))))) 62 | 63 | (with-dyns [:args @["testcase.janet" "server" "run"]] 64 | (def res (suppress-stdout (argparse 65 | "A simple CLI tool." 66 | :default {:kind :accumulate}))) 67 | (unless (and (deep= (res :default) @["server" "run"])) 68 | (error (string "bad default " (res :default))))) 69 | 70 | (with-dyns [:args @["testcase.janet" "-k" "100" "--fake"]] 71 | (def res (suppress-stdout (argparse ;argparse-params))) 72 | (when res (error "Option \"fake\" is not valid, but result is non-nil."))) 73 | 74 | (with-dyns [:args @["testcase.janet" "-l" "100" "--" "echo" "-n" "ok"]] 75 | (if-let [{"length" len :default cmd-args} 76 | (suppress-stdout (argparse "A simple CLI tool" 77 | "length" {:kind :option 78 | :short "l" 79 | :help "key"} 80 | :default {:kind :accumulate}))] 81 | (do 82 | (unless (= len "100") 83 | (error "option was not parsed correctly in the presence of `--`.")) 84 | (unless (= ["echo" "-n" "ok"] (tuple ;cmd-args)) 85 | (error "unnamed arguments after `--` were not parsed correctly."))) 86 | (error "arguments were not parsed correctly in the presence of `--`."))) 87 | --------------------------------------------------------------------------------