174 lines
4.8 KiB
Erlang
174 lines
4.8 KiB
Erlang
% @doc gap = graduated argument parser
|
|
%
|
|
% This is "graduated" with respect to degree of opinionation. You can use as
|
|
% much or as little of this library as you want.
|
|
%
|
|
% However, more and more advanced functionality (e.g. automated help messages
|
|
% for the user) require stronger and stronger assumptions about your program's
|
|
% argument structure.
|
|
%
|
|
% I chose assumptions that are true for the majority of UNIX programs.
|
|
%
|
|
% Tiers
|
|
%
|
|
% 1. tokens/1: play around with the xg-tokens escript to see
|
|
-module(gap).
|
|
|
|
-export_type([
|
|
token/0
|
|
]).
|
|
|
|
-export([
|
|
tokens/1,
|
|
parse_stem/2
|
|
]).
|
|
|
|
|
|
%% tokenizing
|
|
|
|
-type token() :: {'--', Token :: string()}
|
|
| {'-', Token :: [char()]}
|
|
| stdin
|
|
| {naked, Token :: string()}.
|
|
|
|
|
|
-spec tokens(Args) -> Tokens
|
|
when Args :: [string()],
|
|
Tokens :: [token()].
|
|
% @doc
|
|
% Assumptions:
|
|
% 1. -- means "remaining args are verbatim"
|
|
% 2. empty - maps to atom 'stdin'
|
|
% 3. -xyz is equivalent to -x -y -z
|
|
%
|
|
% The only opinionation here is in the treatment of a naked "--" arg:
|
|
%
|
|
% tokens(["-foo", "-", "--bar", "--", "--baz", "-" "quux"]) ->
|
|
% [{'-', $f},
|
|
% {'-', $o},
|
|
% {'-', $o},
|
|
% stdin,
|
|
% {'--', "bar"},
|
|
% {naked, "--baz"},
|
|
% {naked, "-"},
|
|
% {naked, "quux"}].
|
|
|
|
% "--" -> parse rest verbatim
|
|
tokens(["--" | Rest]) ->
|
|
Nekkid = fun(Token) -> {naked, Token} end,
|
|
lists:map(Nekkid, Rest);
|
|
% "--option" -> normal
|
|
tokens(["--" ++ Token | Rest]) ->
|
|
[{'--', Token} | tokens(Rest)];
|
|
% "-" -> stdin
|
|
tokens(["-" | Rest]) ->
|
|
[stdin | tokens(Rest)];
|
|
% "-xyz" -> {'-', $x}, {'-', $y}, {'-', $z}]
|
|
tokens(["-" ++ Chars | Rest]) ->
|
|
Optize = fun(Char) -> {'-', Char} end,
|
|
Tokens = lists:map(Optize, Chars),
|
|
Tokens ++ tokens(Rest);
|
|
tokens([Token | Rest]) ->
|
|
[{naked, Token} | tokens(Rest)];
|
|
tokens([]) ->
|
|
[].
|
|
|
|
|
|
%% parsing inputs
|
|
|
|
-type aritee() :: non_neg_integer() | infinity.
|
|
-type spec_stem() :: {spec_stem, OptionSpecs :: [spec_opt()],
|
|
LeafSpecs :: [spec_leaf()]}.
|
|
-type spec_opt() :: {spec_opt, LongName :: string(),
|
|
ShortName :: char(),
|
|
Arity :: aritee()}.
|
|
-type spec_leaf() :: spec_args()
|
|
| spec_cmd().
|
|
-type spec_args() :: {spec_args, Arity :: aritee()}.
|
|
-type spec_cmd() :: {spec_cmd, Name :: string(),
|
|
Subtree :: spec_stem()}.
|
|
|
|
|
|
%% parsing outputs
|
|
|
|
-type stem() :: {stem, Opts :: [opt()],
|
|
MaybeLeaf :: none | {value, leaf()}}.
|
|
-type opt() :: {opt, LongName :: string(),
|
|
Args :: args()}.
|
|
-type leaf() :: args()
|
|
| cmd().
|
|
-type args() :: {args, [token()]}.
|
|
-type cmd() :: {cmd, Name :: string(),
|
|
Subtree :: stem()}.
|
|
|
|
|
|
-spec parse_stem(Spec, Tokens) -> {Stem, Rest}
|
|
when Spec :: spec_stem(),
|
|
Tokens :: [token()],
|
|
Stem :: stem(),
|
|
Rest :: Tokens.
|
|
% @doc
|
|
% This is the beginning of opinionation
|
|
%
|
|
% Assumes your program's command syntax is
|
|
%
|
|
% Stem :: rootcmd GlobalOptions SubTree
|
|
%
|
|
% where
|
|
% GlobalOptions :: -one --or -more --global --options
|
|
% SubTree :: Args | SubCmd
|
|
% Args :: ["specified", "number", "of", "string arguments"]
|
|
% SubCmd ::
|
|
|
|
parse_stem({spec_stem, OptSpecs, SubtreeSpec}, Args0) ->
|
|
{Opts, Args1} = parse_opts(OptSpecs, Args0),
|
|
{Subtree, Args2} = parse_subtree(SubtreeSpec, Args1),
|
|
{{tree, Opts, Subtree}, Args2}.
|
|
|
|
|
|
-spec parse_opts(OptSpecs, Tokens) -> {Opts, Rest}
|
|
when OptSpecs :: [spec_opt()],
|
|
Tokens :: [token()],
|
|
Opts :: [opt()],
|
|
Rest :: Tokens.
|
|
|
|
parse_opts(Specs, Tokens) ->
|
|
parse_opts(Specs, Tokens, []).
|
|
|
|
|
|
%% parse options
|
|
%% out of specs, move on
|
|
|
|
|
|
% match --foo -> consume args
|
|
match_opt({spec_opt, LongName, _, Aritee}, [{'--', LongName} | Rest0]) ->
|
|
case match_args(Aritee, Rest0) of
|
|
{match, OptArgs, Rest1} -> {match, {opt, LongName, OptArgs}, Rest1};
|
|
Fail -> Fail
|
|
end;
|
|
% match -f -> consume args
|
|
match_opt({spec_opt, LongName, ShortName, Aritee}, [{'-', ShortName} | Rest0]) ->
|
|
case match_args(Aritee, Rest0) of
|
|
{match, OptArgs, Rest1} -> {match, {opt, LongName, OptArgs}, Rest1};
|
|
Fail -> Fail
|
|
end;
|
|
% not a match
|
|
match_opt(_, Tokens) ->
|
|
{fail, Tokens}.
|
|
|
|
|
|
match_args(infinity, Rest0) ->
|
|
{match, {args, Rest0}, []};
|
|
match_args(N, [Head | Rest0]) when is_integer(N), N >= 1 ->
|
|
case match_args(N-1, Rest0) of
|
|
{match, {args, TailArgs}, Rest1} -> {match, {args, [Head | TailArgs]}, Rest1};
|
|
Fail -> Fail
|
|
end;
|
|
match_args(0, List) ->
|
|
{match, {args, []}, List};
|
|
match_args(_, List) ->
|
|
{fail, List}.
|
|
|
|
|
|
parse_subtree(_, _) -> error(nyi).
|