├── .gitignore ├── README.md ├── lib ├── argument_parser.ex └── argument_parser │ ├── builder.ex │ ├── builder │ ├── escript.ex │ └── mix.ex │ ├── help.ex │ └── parser.ex ├── mix.exs └── test ├── argument_parser_builder_test.exs ├── argument_parser_error_test.exs ├── argument_parser_help_test.exs ├── argument_parser_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | *.swp 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [`Elixir.ArgumentParser`](#Elixir.ArgumentParser) 2 | 3 | [`Elixir.ArgumentParser.Builder`](#Elixir.ArgumentParser.Builder) 4 | 5 | [`Elixir.ArgumentParser.Builder.Mix`](#Elixir.ArgumentParser.Builder.Mix) 6 | 7 | [`Elixir.ArgumentParser.Builder.Escript`](#Elixir.ArgumentParser.Builder.Escript) 8 | 9 | # ArgumentParser 10 | 11 | 12 | 13 | * [Description](#description) 14 | * [Types](#types) 15 | * [Functions](#functions) 16 | 17 | ## Description 18 | 19 | Tool for accepting command-line arguments, intended to be functionally similar to python's argparse 20 | 21 | ## Parser ## 22 | 23 | [`ArgumentParser`](ArgumentParser.html#content) is a struct with the following attributes: 24 | 25 | :flags | A list of Flag Arguments 26 | :positional | A list of Positional Arguments 27 | :description | A string to print before generated help 28 | :epilog | A string to print after generated help 29 | :add_help | Print help when -h or --help is passed. Default true 30 | :strict | Throw an error when an unexpected argument is found. Default false 31 | 32 | An ArgumentParser can be created with `new`. Positional args can be added 33 | with `add_arg`, flags with `add_flag`. When the parser is setup use 34 | `parse` to parse. 35 | 36 | ## Arguments ## 37 | 38 | There are 2 types or arguments: positional and flag 39 | 40 | Arguments are option lists where the first element is the name of the argument 41 | as an atom. This name will be the key of the map returned by `parse`. 42 | 43 | Arguments take the following options: 44 | * [`alias`](#alias) 45 | * [`action`](#action) 46 | * [`choices`](#choices) 47 | * [`required`](#required) 48 | * [`default`](#default) 49 | * [`help`](#help) 50 | * [`metavar`](#metavar) 51 | 52 | ### Flag Arguments ### 53 | 54 | Flags can be either long form `--flag` or single character alias `-f` 55 | Alias flags can be grouped: `-flag` == `-f -l -a -g` 56 | Grouping only works if flags take no args. 57 | 58 | ### Positional Arguments ### 59 | 60 | Positional arguments have as their first element an atom which is their name 61 | Positional arguments will be consumed in the order they are defined. 62 | For example: 63 | 64 | iex>ArgumentParser.parse( 65 | ...> ArgumentParser.new(positional: [[:one], [:two]]), 66 | ...> ["foo", "bar"]) 67 | {:ok, %{one: "foo", two: "bar"}} 68 | 69 | ## Actions 70 | 71 | Valid actions are 72 | 73 | {:store, nargs} | collects [nargs] arguments and sets as string 74 | {:store, convert} | collects one argument and applys [convert] to it 75 | {:store, nargs, convert} | collects [nargs] arguments and applys [convert] to them 76 | {:store_const, term} | sets value to [term] when flag is present 77 | :store_true | sets value to true when flag is present 78 | :store_false | sets value to false when flag is present 79 | :count | stores a count of # of times the flag is used 80 | :help | print help and exit 81 | {:version, version_sting} | print version_sting and exit 82 | 83 | The default action is `{:store, 1}`. 84 | 85 | Examples: 86 | 87 | iex> ArgumentParser.new(flags: [[:tru, action: :store_true], 88 | ...> [:fls, action: :store_false], 89 | ...> [:cst, action: {:store_const, Foo}]]) |> 90 | ...> ArgumentParser.parse(~w[--tru --fls --cst]) 91 | {:ok, %{tru: true, fls: false, cst: Foo}} 92 | 93 | iex> ArgumentParser.new() |> 94 | ...> ArgumentParser.add_flag(:cnt, action: :count) |> 95 | ...> ArgumentParser.add_arg(:star, action: {:store, :*}) |> 96 | ...> ArgumentParser.parse(~w[--cnt one two --cnt]) 97 | {:ok, %{cnt: 2, star: ["one", "two"]}} 98 | 99 | ### nargs 100 | 101 | nargs can be: 102 | 103 | postitive integer N | collect the next [N] arguments 104 | :* | collect remaining arguments until a flag argument in encountered 105 | :+ | same as :* but thows an error if no arguments are collected 106 | :'?' | collect one argument if there is any left 107 | :remainder | collect all remaining args regardless of type 108 | 109 | `:store` is the same as `{:store, 1}` 110 | 111 | iex> ArgumentParser.new() |> 112 | ...> ArgumentParser.add_flag(:star, action: {:store, :*}) |> 113 | ...> ArgumentParser.add_arg(:rmdr, action: {:store, :remainder}) |> 114 | ...> ArgumentParser.parse(~w[one two --apnd bar]) 115 | {:ok, %{star: [], rmdr: ["one", "two", "--apnd", "bar"]}} 116 | 117 | ### convert 118 | 119 | Convert can be any function with an arity of 1. 120 | If nargs is 1 or :'?' a String will be passed, otherwise a list of String will be 121 | 122 | iex> ArgumentParser.new(positional: [ 123 | ...> [:hex, action: {:store, &String.to_integer(&1, 16)}]]) |> 124 | ...> ArgumentParser.parse(["BADA55"]) 125 | {:ok, %{hex: 12245589}} 126 | 127 | ### choices 128 | 129 | A list of terms. If an argument is passed that does not match the coices an 130 | error will be returned. 131 | 132 | iex> ArgumentParser.new() |> 133 | ...> ArgumentParser.add_arg([:foo, choices: ["a", "b", "c"]]) |> 134 | ...> ArgumentParser.parse(["foo", "x"], :false) 135 | {:error, "value for foo should be one of [\"a\", \"b\", \"c\"], got foo"} 136 | 137 | ### required 138 | 139 | If true an error will be thown if a value is not set. Defaults to false. 140 | 141 | __flags only__ 142 | 143 | ### default 144 | 145 | Default value. 146 | 147 | iex> ArgumentParser.new(positional: [[:dft, default: :foo]]) |> 148 | ...> ArgumentParser.parse([]) 149 | {:ok, %{dft: :foo}} 150 | 151 | ### help 152 | 153 | String to print for this flag's entry in the generated help output 154 | 155 | 156 | ## Types 157 | 158 |
action :: 159 | :store | 160 | {:store, nargs} | 161 | {:store, convert} | 162 | {:store, nargs, convert} | 163 | {:store_const, term} | 164 | :store_true | 165 | :store_false | 166 | :count | 167 | :help | 168 | {:version, String.t} 169 | 170 | argument :: [atom | argument_option] 171 | 172 | argument_option :: 173 | {:alias, atom} | 174 | {:action, action} | 175 | {:choices, term} | 176 | {:required, boolean} | 177 | {:default, term} | 178 | {:help, String.t} | 179 | {:metavar, atom} 180 | 181 | convert :: (String.t -> term) 182 | 183 | nargs :: pos_integer | :"?" | :* | :+ | :remainder 184 | 185 | t :: %ArgumentParser{add_help: boolean, description: String.t, epilog: String.t, flags: [argument], positional: [argument], strict: boolean} 186 | 187 |## Functions 188 | 189 | ### print_help(parser) 190 | 191 | Generate help string for a parser 192 | 193 | Will print the description, 194 | followed by a generated description of all arguments 195 | followed by an epilog. 196 | 197 | 198 | ### parse(parser, args, print_and_exit \\ true) 199 | 200 | Parse arguments according to the passed ArgumentParser. 201 | 202 | Usually returns an `{:ok, result}` tuple. Exceptions are: 203 | 204 | ### if error was encountered during parsing 205 | 206 | If `print_and_exit` is `:true` a helpful error message is sent to stdout and 207 | the process exits. 208 | If `print_and_exit` is `:false` an `{:error, reason}` tuple is returned. 209 | 210 | ### if help or version message should be printed 211 | 212 | If `print_and_exit` is `:true` the message is sent to stdout and 213 | the process exits. 214 | If `print_and_exit` is `:false` a `{:message, string}` tuple is returned. 215 | 216 | 217 | ### new(arguments \\ []) 218 | 219 | Create a new ArgumentParser 220 | 221 | example: 222 | 223 | iex> ArgumentParser.new(description: "Lorem Ipsum") 224 | %ArgumentParser{description: "Lorem Ipsum", add_help: :true, 225 | epilog: "", flags: [], positional: []} 226 | 227 | 228 | ### add_flag(parser, name, opts) 229 | 230 | ### add_flag(parser, flag) 231 | 232 | Append a flag arg to an ArgumentParser. 233 | 234 | example: 235 | 236 | iex> ArgumentParser.new(description: "Lorem Ipsum") |> 237 | ...> ArgumentParser.add_flag(:foo, required: :false, action: :store_true) 238 | %ArgumentParser{description: "Lorem Ipsum", add_help: :true, epilog: "", 239 | flags: [[:foo, required: :false, action: :store_true]], 240 | positional: []} 241 | 242 | 243 | ### add_arg(parser, name, opts) 244 | 245 | ### add_arg(parser, arg) 246 | 247 | Append a positional arg to an ArgumentParser. 248 | 249 | example: 250 | 251 | iex> ArgumentParser.new(description: "Lorem Ipsum") |> 252 | ...> ArgumentParser.add_arg(:foo, required: :false, action: :store_true) 253 | %ArgumentParser{description: "Lorem Ipsum", add_help: :true, 254 | epilog: "", flags: [], 255 | positional: [[:foo, required: :false, action: :store_true]]} 256 | 257 | 258 | # ArgumentParser.Builder 259 | 260 | 261 | 262 | * [Description](#description) 263 | 264 | ## Description 265 | 266 | Utility for easily creating modules that parse args using ArgumentParser. 267 | 268 | `@arg` and `@flag` attributes can be used to define arguments similar to 269 | the `add_flag/2` and `add_arg/2` functions. 270 | 271 | Will create a private `parse` function. 272 | 273 | The first argument to the parser function should be a list of binarys. 274 | the second option is the `print_and_exit` flag, which defaults to `:true`. 275 | 276 | parse([binary], :true) :: {:ok, map()} 277 | parse([binary], :false) :: {:ok, map()} | {:error, term} | {:message, 278 | iodata} 279 | 280 | When the `print_and_exit` flag is `:true` messages and errors will be printed 281 | to stdout and the process will exit. 282 | 283 | ArgumentParser options can be passed in the `use` options. 284 | 285 | use ArgumentParser.Builder, add_help: :false, strict: :true 286 | 287 | If `:description` is not passed the `@shortdoc` or `@moduledoc` will be used 288 | if present. 289 | 290 | If no `@moduledoc` is defined for the module then the help message for the 291 | argument parser will be set as the `@moduledoc`. To disable this behaviour 292 | explicitly use `@moduledoc :false`. 293 | 294 | Example: 295 | 296 | defmodule Script.Example do 297 | use ArgumentParser.Builder 298 | @arg [:name] 299 | @flag [:bar, alias: :b, help: "get some beer at the bar"] 300 | 301 | def run(args) do 302 | {:ok, parsed} = parse(args) 303 | main(parsed) 304 | end 305 | 306 | def main(%{name: "Homer"}) do 307 | IO.puts("No Homers!") 308 | end 309 | def main(%{name: name, bar: bar}) do 310 | IO.puts("Hey #{name} let's go to #{bar}!") 311 | end 312 | end 313 | 314 | # ArgumentParser.Builder.Mix 315 | 316 | 317 | 318 | * [Description](#description) 319 | * [Callbacks](#callbacks) 320 | 321 | ## Description 322 | 323 | Similar to `ArgumentParser.Builder`, but will automatically create a `run` 324 | function that parsed the args and calls the `main` function with the result. 325 | 326 | defmodule Mix.Task.Drinks do 327 | use ArgumentParser.Builder.Mix 328 | @flag [:coffee, default: "black"] 329 | @flag [:tea, default: "green"] 330 | 331 | def main(%{coffee: coffee, tea: tea}) do 332 | IO.puts("Today we have %{coffee} coffee and #{tea} tea") 333 | end 334 | end 335 | 336 | 337 | $ mix drinks --tea puer 338 | Today we have black coffee and puer tea 339 | 340 | 341 | ## Callbacks 342 | 343 | ### main(arg0) 344 | 345 | When `run` is called the arguments will be parsed and the result passed to 346 | `main` 347 | 348 | 349 | # ArgumentParser.Builder.Escript 350 | 351 | 352 | 353 | * [Description](#description) 354 | * [Callbacks](#callbacks) 355 | 356 | ## Description 357 | 358 | `use` this module to have an ArgumentParser automatically parse your escript 359 | args. The macro will define a `main` function which parses script args and 360 | calls the `run` callback with the result` 361 | 362 | example: 363 | 364 | defmodule Foo.Escript do 365 | use ArgumentParser.Builder.Escript, add_help: :false 366 | @flag [:bar, alias: :b, action: :count] 367 | 368 | def run(%{bar: bar}) do 369 | IO.puts("#{bar} bars" 370 | end 371 | end 372 | 373 | 374 | $ mix escript.build 375 | $ ./foo -bbb 376 | 3 bars 377 | 378 | ## Callbacks 379 | 380 | ### run(arg0) 381 | 382 | When `main` is called the arguments will be parsed and the result passed to 383 | `run` 384 | -------------------------------------------------------------------------------- /lib/argument_parser.ex: -------------------------------------------------------------------------------- 1 | defmodule ArgumentParser do 2 | @moduledoc """ 3 | Tool for accepting command-line arguments, intended to be functionally similar to python's argparse 4 | 5 | ## Parser ## 6 | 7 | `ArgumentParser` is a struct with the following attributes: 8 | 9 | :flags | A list of Flag Arguments 10 | :positional | A list of Positional Arguments 11 | :description | A string to print before generated help 12 | :epilog | A string to print after generated help 13 | :add_help | Print help when -h or --help is passed. Default true 14 | :strict | Throw an error when an unexpected argument is found. Default false 15 | 16 | An ArgumentParser can be created with `new`. Positional args can be added 17 | with `add_arg`, flags with `add_flag`. When the parser is setup use 18 | `parse` to parse. 19 | 20 | ## Arguments ## 21 | 22 | There are 2 types or arguments: positional and flag 23 | 24 | Arguments are option lists where the first element is the name of the argument 25 | as an atom. This name will be the key of the map returned by `parse`. 26 | 27 | Arguments take the following options: 28 | * [`alias`](#alias) 29 | * [`action`](#action) 30 | * [`choices`](#choices) 31 | * [`required`](#required) 32 | * [`default`](#default) 33 | * [`help`](#help) 34 | * [`metavar`](#metavar) 35 | 36 | ### Flag Arguments ### 37 | 38 | Flags can be either long form `--flag` or single character alias `-f` 39 | Alias flags can be grouped: `-flag` == `-f -l -a -g` 40 | Grouping only works if flags take no args. 41 | 42 | ### Positional Arguments ### 43 | 44 | Positional arguments have as their first element an atom which is their name 45 | Positional arguments will be consumed in the order they are defined. 46 | For example: 47 | 48 | iex>ArgumentParser.parse( 49 | ...> ArgumentParser.new(positional: [[:one], [:two]]), 50 | ...> ["foo", "bar"]) 51 | {:ok, %{one: "foo", two: "bar"}} 52 | 53 | ## Actions 54 | 55 | Valid actions are 56 | 57 | {:store, nargs} | collects [nargs] arguments and sets as string 58 | {:store, convert} | collects one argument and applys [convert] to it 59 | {:store, nargs, convert} | collects [nargs] arguments and applys [convert] to them 60 | {:store_const, term} | sets value to [term] when flag is present 61 | :store_true | sets value to true when flag is present 62 | :store_false | sets value to false when flag is present 63 | :count | stores a count of # of times the flag is used 64 | :help | print help and exit 65 | {:version, version_sting} | print version_sting and exit 66 | 67 | The default action is `{:store, 1}`. 68 | 69 | Examples: 70 | 71 | iex> ArgumentParser.new(flags: [[:tru, action: :store_true], 72 | ...> [:fls, action: :store_false], 73 | ...> [:cst, action: {:store_const, Foo}]]) |> 74 | ...> ArgumentParser.parse(~w[--tru --fls --cst]) 75 | {:ok, %{tru: true, fls: false, cst: Foo}} 76 | 77 | iex> ArgumentParser.new() |> 78 | ...> ArgumentParser.add_flag(:cnt, action: :count) |> 79 | ...> ArgumentParser.add_arg(:star, action: {:store, :*}) |> 80 | ...> ArgumentParser.parse(~w[--cnt one two --cnt]) 81 | {:ok, %{cnt: 2, star: ["one", "two"]}} 82 | 83 | ### nargs 84 | 85 | nargs can be: 86 | 87 | postitive integer N | collect the next [N] arguments 88 | :* | collect remaining arguments until a flag argument in encountered 89 | :+ | same as :* but thows an error if no arguments are collected 90 | :'?' | collect one argument if there is any left 91 | :remainder | collect all remaining args regardless of type 92 | 93 | `:store` is the same as `{:store, 1}` 94 | 95 | iex> ArgumentParser.new() |> 96 | ...> ArgumentParser.add_flag(:star, action: {:store, :*}) |> 97 | ...> ArgumentParser.add_arg(:rmdr, action: {:store, :remainder}) |> 98 | ...> ArgumentParser.parse(~w[one two --apnd bar]) 99 | {:ok, %{star: [], rmdr: ["one", "two", "--apnd", "bar"]}} 100 | 101 | ### convert 102 | 103 | Convert can be any function with an arity of 1. 104 | If nargs is 1 or :'?' a String will be passed, otherwise a list of String will be 105 | 106 | iex> ArgumentParser.new(positional: [ 107 | ...> [:hex, action: {:store, &String.to_integer(&1, 16)}]]) |> 108 | ...> ArgumentParser.parse(["BADA55"]) 109 | {:ok, %{hex: 12245589}} 110 | 111 | ### choices 112 | 113 | A list of terms. If an argument is passed that does not match the coices an 114 | error will be returned. 115 | 116 | iex> ArgumentParser.new() |> 117 | ...> ArgumentParser.add_arg([:foo, choices: ["a", "b", "c"]]) |> 118 | ...> ArgumentParser.parse(["foo", "x"], :false) 119 | {:error, "value for foo should be one of [\\"a\\", \\"b\\", \\"c\\"], got foo"} 120 | 121 | ### required 122 | 123 | If true an error will be thown if a value is not set. Defaults to false. 124 | 125 | __flags only__ 126 | 127 | ### default 128 | 129 | Default value. 130 | 131 | iex> ArgumentParser.new(positional: [[:dft, default: :foo]]) |> 132 | ...> ArgumentParser.parse([]) 133 | {:ok, %{dft: :foo}} 134 | 135 | ### help 136 | 137 | String to print for this flag's entry in the generated help output 138 | 139 | """ 140 | alias ArgumentParser.Parser 141 | alias ArgumentParser.Help 142 | defstruct flags: [], 143 | positional: [], 144 | description: "", 145 | epilog: "", 146 | add_help: true, 147 | strict: true 148 | 149 | @type t :: %__MODULE__{ 150 | flags: [argument], 151 | positional: [argument], 152 | description: String.t, 153 | epilog: String.t, 154 | add_help: boolean, 155 | strict: boolean} 156 | 157 | @type argument :: [atom | argument_option] 158 | 159 | @type argument_option :: 160 | {:alias, atom} | 161 | {:action, action} | 162 | {:choices, term} | 163 | {:required, boolean} | 164 | {:default, term} | 165 | {:help, String.t} | 166 | {:metavar, atom} 167 | 168 | @type action :: 169 | :store | 170 | {:store, nargs} | 171 | {:store, convert} | 172 | {:store, nargs, convert} | 173 | {:store_const, term} | 174 | :store_true | 175 | :store_false | 176 | :count | 177 | :help | 178 | {:version, String.t} 179 | 180 | @type nargs :: pos_integer | :'?' | :* | :+ | :remainder 181 | 182 | @type convert :: ((String.t) -> term) 183 | 184 | @doc """ 185 | Create a new ArgumentParser 186 | 187 | example: 188 | 189 | iex> ArgumentParser.new(description: "Lorem Ipsum") 190 | %ArgumentParser{description: "Lorem Ipsum", add_help: :true, 191 | epilog: "", flags: [], positional: []} 192 | """ 193 | @spec new(Dict.t) :: t 194 | def new(arguments \\ []) do 195 | struct(__MODULE__, arguments) 196 | end 197 | 198 | @doc """ 199 | Append a flag arg to an ArgumentParser. 200 | 201 | example: 202 | 203 | iex> ArgumentParser.new(description: "Lorem Ipsum") |> 204 | ...> ArgumentParser.add_flag(:foo, required: :false, action: :store_true) 205 | %ArgumentParser{description: "Lorem Ipsum", add_help: :true, epilog: "", 206 | flags: [[:foo, required: :false, action: :store_true]], 207 | positional: []} 208 | """ 209 | def add_flag(parser, [name | _] = flag) when is_atom(name) do 210 | flags = [flag | parser.flags] 211 | %{parser | flags: flags} 212 | end 213 | def add_flag(parser, name, opts) when is_atom(name) and is_list(opts) do 214 | add_flag(parser, [name | opts]) 215 | end 216 | 217 | @doc """ 218 | Append a positional arg to an ArgumentParser. 219 | 220 | example: 221 | 222 | iex> ArgumentParser.new(description: "Lorem Ipsum") |> 223 | ...> ArgumentParser.add_arg(:foo, required: :false, action: :store_true) 224 | %ArgumentParser{description: "Lorem Ipsum", add_help: :true, 225 | epilog: "", flags: [], 226 | positional: [[:foo, required: :false, action: :store_true]]} 227 | """ 228 | def add_arg(parser, [name | _] = arg) when is_atom(name) do 229 | args = parser.positional ++ [arg] 230 | %{parser | positional: args} 231 | end 232 | def add_arg(parser, name, opts) when is_atom(name) and is_list(opts) do 233 | add_arg(parser, [name | opts]) 234 | end 235 | 236 | @doc ~S""" 237 | Generate help string for a parser 238 | 239 | Will print the description, 240 | followed by a generated description of all arguments 241 | followed by an epilog. 242 | """ 243 | @spec print_help(t) :: String.t 244 | def print_help(parser) do 245 | Help.describe(parser) 246 | end 247 | 248 | @doc ~S""" 249 | Parse arguments according to the passed ArgumentParser. 250 | 251 | Usually returns an `{:ok, result}` tuple. Exceptions are: 252 | 253 | ### if error was encountered during parsing 254 | 255 | If `print_and_exit` is `:true` a helpful error message is sent to stdout and 256 | the process exits. 257 | If `print_and_exit` is `:false` an `{:error, reason}` tuple is returned. 258 | 259 | ### if help or version message should be printed 260 | 261 | If `print_and_exit` is `:true` the message is sent to stdout and 262 | the process exits. 263 | If `print_and_exit` is `:false` a `{:message, string}` tuple is returned. 264 | """ 265 | 266 | @spec parse(t, [String.t], boolean) :: Parser.result 267 | def parse(parser, args, print_and_exit \\ :true) 268 | when is_list(args) and is_boolean(print_and_exit) do 269 | Parser.parse(args, parser) |> 270 | handle(print_and_exit, parser) 271 | end 272 | 273 | defp handle({:ok, result}, _, _) do 274 | {:ok, result} 275 | end 276 | defp handle({:message, msg}, :true, _) do 277 | IO.puts(msg) 278 | exit(:normal) 279 | end 280 | defp handle({:error, reason}, :true, parser) do 281 | IO.puts("error: #{inspect(reason)}") 282 | IO.puts(Help.describe_short(parser)) 283 | exit(:normal) 284 | end 285 | defp handle(result, :false, _) do 286 | result 287 | end 288 | end 289 | -------------------------------------------------------------------------------- /lib/argument_parser/builder.ex: -------------------------------------------------------------------------------- 1 | defmodule ArgumentParser.Builder do 2 | @moduledoc ~S""" 3 | Utility for easily creating modules that parse args using ArgumentParser. 4 | 5 | `@arg` and `@flag` attributes can be used to define arguments similar to 6 | the `add_flag/2` and `add_arg/2` functions. 7 | 8 | Will create a private `parse` function. 9 | 10 | The first argument to the parser function should be a list of binarys. 11 | the second option is the `print_and_exit` flag, which defaults to `:true`. 12 | 13 | parse([binary], :true) :: {:ok, map()} 14 | parse([binary], :false) :: {:ok, map()} | {:error, term} | {:message, 15 | iodata} 16 | 17 | When the `print_and_exit` flag is `:true` messages and errors will be printed 18 | to stdout and the process will exit. 19 | 20 | ArgumentParser options can be passed in the `use` options. 21 | 22 | use ArgumentParser.Builder, add_help: :false, strict: :true 23 | 24 | If `:description` is not passed the `@shortdoc` or `@moduledoc` will be used 25 | if present. 26 | 27 | If no `@moduledoc` is defined for the module then the help message for the 28 | argument parser will be set as the `@moduledoc`. To disable this behaviour 29 | explicitly use `@moduledoc :false`. 30 | 31 | Example: 32 | 33 | defmodule Script.Example do 34 | use ArgumentParser.Builder 35 | @arg [:name] 36 | @flag [:bar, alias: :b, help: "get some beer at the bar"] 37 | 38 | def run(args) do 39 | {:ok, parsed} = parse(args) 40 | main(parsed) 41 | end 42 | 43 | def main(%{name: "Homer"}) do 44 | IO.puts("No Homers!") 45 | end 46 | def main(%{name: name, bar: bar}) do 47 | IO.puts("Hey #{name} let's go to #{bar}!") 48 | end 49 | end 50 | """ 51 | 52 | @doc :false 53 | defmacro __using__(opts) do 54 | setup(opts) 55 | end 56 | 57 | @doc :false 58 | def setup(opts) do 59 | quote do 60 | Enum.each( 61 | [:shortdoc, :recursive], 62 | &Module.register_attribute(__MODULE__, &1, persist: true)) 63 | Enum.each( 64 | [:arg, :flag], 65 | &Module.register_attribute(__MODULE__, &1, accumulate: true)) 66 | @argparse_opts unquote(opts) 67 | @before_compile ArgumentParser.Builder 68 | end 69 | end 70 | 71 | @doc :false 72 | defmacro __before_compile__(env) do 73 | attr = &Module.get_attribute(env.module, &1) 74 | args = attr.(:arg) |> Enum.reverse() 75 | flags = attr.(:flag) |> Enum.reverse() 76 | opts = attr.(:argparse_opts) 77 | moddoc = attr.(:moduledoc) 78 | desc = cond do 79 | Dict.has_key?(opts, :description) -> opts[:description] 80 | String.valid?(sd = attr.(:shortdoc)) -> sd 81 | String.valid?(moddoc) -> moddoc 82 | true -> "" 83 | end 84 | 85 | parser = opts |> 86 | Dict.merge(flags: flags, positional: args, description: desc) |> 87 | ArgumentParser.new() 88 | 89 | if moddoc == nil do 90 | Module.put_attribute(env.module, :moduledoc, 91 | {env.line, ArgumentParser.print_help(parser)}) 92 | end 93 | 94 | quote do 95 | defp parse(arguments, exit \\ :true) do 96 | ArgumentParser.parse(unquote(Macro.escape(parser)), arguments, exit) 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/argument_parser/builder/escript.ex: -------------------------------------------------------------------------------- 1 | defmodule ArgumentParser.Builder.Escript do 2 | @moduledoc ~S""" 3 | `use` this module to have an ArgumentParser automatically parse your escript 4 | args. The macro will define a `main` function which parses script args and 5 | calls the `run` callback with the result` 6 | 7 | example: 8 | 9 | defmodule Foo.Escript do 10 | use ArgumentParser.Builder.Escript, add_help: :false 11 | @flag [:bar, alias: :b, action: :count] 12 | 13 | def run(%{bar: bar}) do 14 | IO.puts("#{bar} bars" 15 | end 16 | end 17 | 18 | 19 | $ mix escript.build 20 | $ ./foo -bbb 21 | 3 bars 22 | """ 23 | 24 | @doc """ 25 | When `main` is called the arguments will be parsed and the result passed to 26 | `run` 27 | """ 28 | @callback run(map()) :: any 29 | defmacro __using__(opts) do 30 | quote do 31 | unquote(ArgumentParser.Builder.setup(opts)) 32 | @behaviour ArgumentParser.Builder.Escript 33 | 34 | def main(args) do 35 | {:ok, parsed} = parse(args) 36 | run(parsed) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/argument_parser/builder/mix.ex: -------------------------------------------------------------------------------- 1 | defmodule ArgumentParser.Builder.Mix do 2 | @moduledoc ~S""" 3 | Similar to `ArgumentParser.Builder`, but will automatically create a `run` 4 | function that parsed the args and calls the `main` function with the result. 5 | 6 | defmodule Mix.Task.Drinks do 7 | use ArgumentParser.Builder.Mix 8 | @flag [:coffee, default: "black"] 9 | @flag [:tea, default: "green"] 10 | 11 | def main(%{coffee: coffee, tea: tea}) do 12 | IO.puts("Today we have %{coffee} coffee and #{tea} tea") 13 | end 14 | end 15 | 16 | 17 | $ mix drinks --tea puer 18 | Today we have black coffee and puer tea 19 | 20 | """ 21 | 22 | @doc """ 23 | When `run` is called the arguments will be parsed and the result passed to 24 | `main` 25 | """ 26 | @callback main(map()) :: any 27 | defmacro __using__(opts) do 28 | quote do 29 | unquote(ArgumentParser.Builder.setup(opts)) 30 | @behaviour ArgumentParser.Builder.Mix 31 | 32 | @doc :false 33 | def run(args) do 34 | {:ok, parsed} = parse(args) 35 | main(parsed) 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/argument_parser/help.ex: -------------------------------------------------------------------------------- 1 | defmodule ArgumentParser.Help do 2 | @moduledoc :false 3 | 4 | @doc :false 5 | def describe(%ArgumentParser{} = parser) do 6 | """ 7 | #{parser.description} 8 | #{format_body(parser)} 9 | #{parser.epilog} 10 | """ |> String.strip() 11 | end 12 | 13 | @doc :false 14 | def describe_short(%ArgumentParser{} = parser) do 15 | ["Usage:", short_options(parser), format_position_head(parser.positional)] 16 | end 17 | 18 | defp short_options(%{flags: flags}) do 19 | case Enum.reduce(flags, {[], []}, &sort_by_alias/2) do 20 | {[], []} -> [] 21 | {[], names} -> [" ", names] 22 | {aliases, []} -> [" -", aliases] 23 | {aliases, names} -> [" -", aliases, " ", names] 24 | end 25 | end 26 | 27 | defp sort_by_alias([name | opts], {aliases, names}) do 28 | case Keyword.fetch(opts, :alias) do 29 | {:ok, a} -> {[Atom.to_string(a)|aliases], names} 30 | :error -> {aliases, ["--" <> Atom.to_string(name)|names]} 31 | end 32 | end 33 | 34 | defp format_body(parser) do 35 | flags = if parser.add_help do 36 | parser.flags ++ [[:help, alias: :h, action: :help, 37 | help: "Display this message and exit"]] 38 | else 39 | parser.flags 40 | end 41 | 42 | [ "Usage:", 43 | format_position_head(parser.positional), 44 | "\n", 45 | format_args(:positional, parser.positional), 46 | "\nOptions:\n", 47 | format_args(:flag, flags) ] 48 | end 49 | 50 | defp format_position_head([]) do 51 | [] 52 | end 53 | defp format_position_head([[name | options] | rest]) do 54 | mv = format_metavars( 55 | Keyword.get(options, :action, :store), 56 | Keyword.get(options, :metavar, Atom.to_string(name))) 57 | [mv | format_position_head(rest)] 58 | end 59 | 60 | defp format_metavars(:store, mv) do 61 | [?\ , mv] 62 | end 63 | defp format_metavars(t, mv) when is_tuple(t) do 64 | format_metavars(elem(t, 1), mv) 65 | end 66 | defp format_metavars(n, mv) when is_number(n) do 67 | (for _ <- 1..n, do: "#{ mv}") |> Enum.join 68 | end 69 | defp format_metavars(:*, mv) do 70 | " [#{mv} ...]" 71 | end 72 | defp format_metavars(:+, mv) do 73 | " #{mv} [#{mv} ...]" 74 | end 75 | defp format_metavars(action, mv) 76 | when action in [:'?', :store_true, :store_false, :store_const] do 77 | " [#{mv}]" 78 | end 79 | defp format_metavars(_, _) do 80 | "" 81 | end 82 | 83 | defp longest_name(args) do 84 | Stream.map(args, &Kernel.hd/1) |> 85 | Stream.map(&Atom.to_string/1) |> 86 | Stream.map(&String.length/1) |> 87 | Enum.max() 88 | end 89 | 90 | defp format_args(_, []), do: "" 91 | defp format_args(type, args) do 92 | Enum.map(args, &format_arg(type, &1, longest_name(args))) 93 | end 94 | 95 | defp format_arg(:positional, [name | options], name_len) do 96 | [:io_lib.format(' ~-#{name_len}s', [name]), format_arg_help(options)] 97 | end 98 | defp format_arg(:flag, [name | options], name_len) do 99 | [ " ", 100 | (if alias = options[:alias], do: "-#{alias} ", else: ""), 101 | :io_lib.format('--~-#{name_len}s', [name]), 102 | format_arg_help(options)] 103 | end 104 | 105 | defp format_arg_help(options) do 106 | [ if choices = Keyword.get(options, :choices) do 107 | " one of #{inspect(choices)}" 108 | else "" end, 109 | if default = Keyword.get(options, :default) do 110 | " default: #{default}" 111 | else "" end, 112 | if help = Keyword.get(options, :help) do 113 | " #{help}" 114 | else "" end, 115 | "\n" ] 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/argument_parser/parser.ex: -------------------------------------------------------------------------------- 1 | defmodule ArgumentParser.Parser do 2 | alias ArgumentParser, as: AP 3 | @moduledoc :false 4 | 5 | @narg_atoms [:'?', :*, :+, :remainder] 6 | 7 | @type result :: {:ok, map()} | {:error, term} | {:message, binary} 8 | 9 | defmacrop list_action?(action), do: 10 | quote do: 11 | is_tuple(unquote(action)) and 12 | :erlang.element(2, unquote(action)) == :* 13 | 14 | @doc :false 15 | @spec parse([String.t], AP.t) :: result 16 | def parse(args, %AP{} = parser) when is_list(args) do 17 | parse(args, parser, %{}) |> check_arguments(parser) 18 | end 19 | 20 | @spec check_arguments(result, AP.t) :: result 21 | # Check for required args and set defaults 22 | defp check_arguments({:ok, parsed}, parser) do 23 | missing_args = Stream.concat(parser.positional, parser.flags) |> 24 | Stream.map(fn(a) -> {a, key_for(a)} end) |> 25 | Stream.reject(&Dict.has_key?(parsed, elem(&1, 1))) 26 | Enum.reduce(missing_args, {:ok, parsed}, &check_argument/2) 27 | end 28 | defp check_arguments(other, _parser) do 29 | other 30 | end 31 | 32 | defp check_argument(_argument, {:error, reason}) do 33 | {:error, reason} 34 | end 35 | defp check_argument({argument, key}, {:ok, parsed}) do 36 | if Dict.has_key?(parsed, key) do 37 | {:ok, parsed} 38 | else 39 | get_default(argument, key, parsed) 40 | end 41 | end 42 | 43 | defp get_default(argument, key, parsed) do 44 | default = case Keyword.fetch(argument, :default) do 45 | {:ok, value} -> value 46 | :error -> default_by_action(Dict.get(argument, :action)) 47 | end 48 | if (default == :none) do 49 | if Keyword.get(argument, :required) do 50 | {:error, "missing required arg #{key}"} 51 | else 52 | {:ok, parsed} 53 | end 54 | else 55 | {:ok, Dict.put(parsed, key, default)} 56 | end 57 | end 58 | 59 | defp default_by_action(:count), do: 0 60 | defp default_by_action(:store_true), do: :false 61 | defp default_by_action(:store_false), do: :true 62 | defp default_by_action(action) when list_action?(action), do: [] 63 | defp default_by_action(_), do: :none 64 | 65 | defp key_for([name | _]) when is_atom(name) do 66 | name 67 | end 68 | 69 | @spec parse([String.t], AP.t, map()) :: result 70 | defp parse([], _parser, parsed) do 71 | {:ok, parsed} 72 | end 73 | # --help 74 | defp parse([h | _], %{add_help: true} = parser, _) 75 | when h in ["-h", "--help"] do 76 | {:message, AP.print_help(parser)} 77 | end 78 | # --[flag] 79 | defp parse([<-, ?-, arg :: binary>> | rest], 80 | parser, parsed) do 81 | get_flag_by_name(String.to_atom(arg), parser.flags, parser.strict) |> 82 | apply_argument(rest, parsed, parser) |> 83 | check_if_done(parser) 84 | end 85 | # -[alias]+ (e.g. -aux -> -a -u -x) 86 | defp parse([<-, aliased :: binary>> | rest], 87 | parser, parsed) do 88 | unalias_and_apply(aliased, rest, parsed, parser) |> 89 | check_if_done(parser) 90 | end 91 | # positional args 92 | defp parse(args, 93 | %{positional: [head | tail]} = parser, 94 | parsed) do 95 | apply_argument(head, args, parsed, parser) |> 96 | check_if_done(%{parser | positional: tail}) 97 | end 98 | # unexpected positional arg 99 | defp parse([head | tail], parser, parsed) do 100 | if parser.strict do 101 | {:error, "unexpected argument: #{head}"} 102 | else 103 | parse(tail, parser, Dict.put(parsed, String.to_atom(head), true)) 104 | end 105 | end 106 | 107 | defp check_if_done({:ok, args, parsed}, parser) do 108 | parse(args, parser, parsed) 109 | end 110 | defp check_if_done(other, _parser) do 111 | other 112 | end 113 | 114 | defp apply_argument({:error, _} = err, _, _, _) do 115 | err 116 | end 117 | defp apply_argument(argument, args, parsed, parser) do 118 | key = key_for(argument) 119 | Keyword.get(argument, :action, :store) |> 120 | apply_action(args, key, parsed, parser) |> 121 | check_choices({Dict.get(argument, :choices), key}) 122 | end 123 | 124 | defp check_choices({:ok, args, parsed}, {choices, key}) 125 | when is_list(choices) do 126 | if parsed[key] in choices do 127 | {:ok, args, parsed} 128 | else 129 | {:error, "value for #{key} should be one of #{ 130 | inspect(choices)}, got #{parsed[key]}"} 131 | end 132 | end 133 | defp check_choices(result, _) do 134 | result 135 | end 136 | 137 | defp apply_action(:store, [head | args], key, parsed, _parser) do 138 | {:ok, args, Dict.put(parsed, key, head)} 139 | end 140 | defp apply_action({:store, f}, [head | args], key, parsed, _parser) 141 | when is_function(f) do 142 | {:ok, args, Dict.put(parsed, key, f.(head))} 143 | end 144 | defp apply_action({:store, n}, args, key, parsed, _parser) 145 | when is_number(n) or n in @narg_atoms do 146 | {value, rest} = fetch_nargs(args, n) 147 | {:ok, rest, Dict.put_new(parsed, key, value)} 148 | end 149 | defp apply_action({:store, n, f}, args, key, parsed, _parser) 150 | when is_function(f) and (is_number(n) or n in @narg_atoms) do 151 | {value, rest} = fetch_nargs(args, n) 152 | newvalue = if is_list(value) do 153 | Enum.map(value, f) 154 | else 155 | f.(value) 156 | end 157 | {:ok, rest, Dict.put_new(parsed, key, newvalue)} 158 | end 159 | defp apply_action(:store_true, args, key, parsed, _parser) do 160 | {:ok, args, Dict.put_new(parsed, key, true)} 161 | end 162 | defp apply_action(:store_false, args, key, parsed, _parser) do 163 | {:ok, args, Dict.put_new(parsed, key, false)} 164 | end 165 | defp apply_action({:store_const, const}, args, key, parsed, _parser) do 166 | {:ok, args, Dict.put_new(parsed, key, const)} 167 | end 168 | defp apply_action(:count, args, key, parsed, _parser) do 169 | {:ok, args, Dict.update(parsed, key, 1, &(&1 + 1))} 170 | end 171 | defp apply_action(:help, _, _, _, parser) do 172 | {:message, AP.print_help(parser)} 173 | end 174 | defp apply_action({:version, version}, _, _, _, _) do 175 | {:message, version} 176 | end 177 | 178 | defp unalias_and_apply(<