JustBash.FlagParser (JustBash v0.4.0)

View Source

A shared module for parsing command-line flags in bash commands.

Supports:

  • Boolean flags: -a, -l, -v
  • Combined flags: -la (equivalent to -l -a)
  • Value flags: -n 10, -d "," (flag that takes next argument)
  • Getopt clusters ending in a value flag: -nk2 is -n -k 2
  • Stop parsing at --
  • --help, answered from the spec itself

A flag the spec does not describe is an error, never an operand. Demoting it turned sort -Q file into a read of a file named -Q, which sort reports as nothing at all: empty stdout, empty stderr, exit 0. Callers get {:error, {:unknown_flag, flag}} and report it with format_error/3.

Usage

# Define your flag spec
spec = %{
  boolean: [:a, :l, :v, :r],
  value: [:n, :d],
  integer: [:n],
  value_labels: %{n: "number of lines"},
  defaults: %{a: false, l: false, v: false, r: false, n: 10, d: nil}
}

# Parse arguments
case FlagParser.parse(args, spec) do
  {:ok, flags, rest} -> ...
  :help -> FlagParser.help("ls", spec)
  {:error, reason} -> FlagParser.format_error("ls", reason, @usage)
end

Flag Specification

  • :boolean - List of single-character atoms for boolean flags
  • :value - List of single-character atoms for flags that take a value
  • :defaults - Map of default values for all flags
  • :aliases - Map of flag string to atom, for spellings that are not the atom itself ("R" => :r, "-recursive" => :r)
  • :multi_value - Value flags that accumulate a list instead of overwriting
  • :integer - Value flags whose value is a count. Only these are converted; everything else stays a string, so sort -t 1 is the delimiter "1"
  • :value_labels - What an :integer flag counts, as GNU names it in invalid number of lines: 'abc'. Required for every :integer flag
  • :usage - The synopsis line help/2 prints, defaulting to "<command> [OPTION]..."

Summary

Functions

Render a parse/2 error the way GNU coreutils words it, followed by usage.

The usage Try '<command> --help' for more information. promises.

Parse command-line arguments according to the given flag specification.

Types

error()

@type error() ::
  {:unknown_flag, String.t()}
  | {:missing_value, String.t()}
  | {:invalid_value, String.t(), String.t()}

flag_spec()

@type flag_spec() :: %{
  :boolean => [atom()],
  :value => [atom()],
  :defaults => map(),
  optional(:aliases) => map(),
  optional(:multi_value) => [atom()],
  optional(:integer) => [atom()],
  optional(:value_labels) => %{required(atom()) => String.t()},
  optional(:usage) => String.t()
}

parse_result()

@type parse_result() :: {:ok, map(), [String.t()]} | :help | {:error, error()}

Functions

format_error(command, arg, usage)

@spec format_error(String.t(), error(), String.t()) :: String.t()

Render a parse/2 error the way GNU coreutils words it, followed by usage.

A short option is named by its character and a long option in full, because that is the unit that was rejected — -laQ is a bad Q, and invalid option -- '-' for --nope identifies nothing.

Examples

iex> FlagParser.format_error("sort", {:unknown_flag, "Q"}, "")
"sort: invalid option -- 'Q'\n"

iex> FlagParser.format_error("sort", {:unknown_flag, "--nope"}, "")
"sort: unrecognized option '--nope'\n"

iex> FlagParser.format_error("head", {:invalid_value, "number of lines", "abc"}, "")
"head: invalid number of lines: 'abc'\n"

help(command, spec)

@spec help(String.t(), flag_spec()) :: String.t()

The usage Try '<command> --help' for more information. promises.

Rendered from the spec, so it lists exactly the flags the parser accepts and cannot drift from them.

Examples

iex> spec = %{boolean: [:a], value: [:n], defaults: %{a: false, n: nil}}
iex> FlagParser.help("demo", spec)
"Usage: demo [OPTION]...\nOptions this shell implements:\n  -a\n  -n VALUE\n"

parse(args, spec)

@spec parse([String.t()], flag_spec()) :: parse_result()

Parse command-line arguments according to the given flag specification.

Returns {:ok, flags, remaining_args} where:

  • flags is a map containing all flag values
  • remaining_args is a list of non-flag arguments

Returns :help for --help, and {:error, reason} for the first argument that is flag-shaped but not in the spec, for a value flag with nothing after it, and for a value declared :integer that is not a number.

Examples

iex> spec = %{boolean: [:a, :l], value: [:n], integer: [:n], value_labels: %{n: "number of lines"}, defaults: %{a: false, l: false, n: 10}}
iex> FlagParser.parse(["-a", "-n", "5", "file.txt"], spec)
{:ok, %{a: true, l: false, n: 5}, ["file.txt"]}

iex> spec = %{boolean: [:a, :l], value: [], defaults: %{a: false, l: false}}
iex> FlagParser.parse(["-al", "dir"], spec)
{:ok, %{a: true, l: true}, ["dir"]}

iex> spec = %{boolean: [:a, :l], value: [], defaults: %{a: false, l: false}}
iex> FlagParser.parse(["-Q", "dir"], spec)
{:error, {:unknown_flag, "Q"}}