├── rebar3 ├── .gitignore ├── src ├── Makefile ├── cli.app.src ├── sample2_cli.erl ├── cli.erl ├── sample_cli.erl ├── cli_main.erl ├── cli_opt.erl ├── cli_debug.erl ├── cli_tests.erl ├── cli_help.erl └── cli_parser.erl ├── bin ├── sample ├── sample2 ├── sample.py ├── sample2.py └── test ├── rebar.config ├── Makefile ├── LICENSE └── README.md /rebar3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gar1t/erlang-cli/HEAD/rebar3 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /rebar.lock 3 | /ebin 4 | /.rebar3 5 | *.beam 6 | *.pyc 7 | erl_crash.dump 8 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | home = $(realpath .)/.. 2 | 3 | _default: 4 | cd $(home); make 5 | 6 | %: 7 | cd $(home); make $@ 8 | -------------------------------------------------------------------------------- /bin/sample: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%! -pa build/default/lib/cli/ebin 3 | 4 | main(Args) -> 5 | sample_cli:main(Args). 6 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %%-*-erlang-*- 2 | 3 | {base_dir, "build"}. 4 | {erl_opts, [warnings_as_errors]}. 5 | {compiler_source_format, relative}. 6 | -------------------------------------------------------------------------------- /bin/sample2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%-*-erlang-*- 3 | %%! -pa build/default/lib/cli/ebin 4 | 5 | main(Args) -> 6 | sample2_cli:main(Args). 7 | -------------------------------------------------------------------------------- /src/cli.app.src: -------------------------------------------------------------------------------- 1 | %%% -*-erlang-*- 2 | {application, cli, 3 | [{description, "Command line interface utils"}, 4 | {vsn, "0.0.0"}, 5 | {modules, []}, 6 | {registered, []}, 7 | {applications, [kernel, stdlib]}, 8 | {env, []} 9 | ]}. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ebin = build/default/lib/cli/ebin/ 2 | 3 | compile: 4 | ./rebar3 compile 5 | 6 | clean: 7 | rm -rf build ebin rebar.lock .rebar3 8 | 9 | test: compile 10 | bin/test $(TESTS) 11 | 12 | readme: 13 | multimarkdown README.md > /tmp/erlang-cli-readme.html 14 | -------------------------------------------------------------------------------- /bin/sample.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import argparse 4 | 5 | parser = argparse.ArgumentParser() 6 | parser.add_argument('msg', help="message to print", nargs='?', metavar='MSG') 7 | parser.add_argument('-C', '--caps', action='store_true', 8 | help="print message in caps") 9 | parser.add_argument('-X', '--x-factor', metavar='MYSTERY', 10 | nargs='?', 11 | help="the X factor - no one really knows " 12 | "what this option is for") 13 | print parser.parse_args() 14 | -------------------------------------------------------------------------------- /bin/sample2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import click 4 | 5 | @click.command() 6 | @click.option('--message', '-M', help="message to print") 7 | @click.option('-X', 8 | help="the X factor - no one really knows what this " 9 | "value is for; use at your own risk!") 10 | @click.option('--this-is-a-long-option', 11 | metavar='SUPER11', 12 | help="some super weird long variable name, no idea what it " 13 | "can mean") 14 | def hello(): 15 | click.echo('Hello World!') 16 | 17 | if __name__ == '__main__': 18 | hello() 19 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%-*-erlang-*- 3 | %%! -pa build/default/lib/cli/ebin 4 | 5 | main(Args) -> 6 | try 7 | run_tests(Args) 8 | catch 9 | _:Err -> 10 | print_error(Err, erlang:get_stacktrace()), 11 | erlang:halt(1) 12 | end. 13 | 14 | run_tests([]) -> 15 | cli_tests:run(); 16 | run_tests(Tests) -> 17 | lists:foreach(fun cli_tests:run/1, Tests). 18 | 19 | print_error(Err, Stack) -> 20 | io:format("ERROR~n"), 21 | print_file_and_line(Err, Stack), 22 | io:format("~p~n", [Stack]). 23 | 24 | print_file_and_line(Err, Stack) -> 25 | case last_file_info(Stack) of 26 | {File, Line} -> 27 | io:format("~s:~b: ~99999p~n", [shorten_file(File), Line, Err]); 28 | error -> 29 | io:format("~99999p~n", [Err]) 30 | end. 31 | 32 | last_file_info([{_, _, _, Attrs}|Rest]) -> 33 | case proplists:get_value(file, Attrs) of 34 | undefined -> last_file_info(Rest); 35 | File -> {File, proplists:get_value(line, Attrs)} 36 | end; 37 | last_file_info([]) -> 38 | error. 39 | 40 | shorten_file(File) -> 41 | re:replace(File, "^.*/build/default/lib/cli/", ""). 42 | -------------------------------------------------------------------------------- /src/sample2_cli.erl: -------------------------------------------------------------------------------- 1 | -module(sample2_cli). 2 | 3 | -export([main/1]). 4 | 5 | -define(default_message, "Ay Chiwawa!"). 6 | 7 | main(Args) -> 8 | Code = cli:main(Args, parser(), fun handle_cmd/1), 9 | erlang:halt(Code). 10 | 11 | parser() -> 12 | cli:command_parser( 13 | "sample2", 14 | "[OPTION]... COMMAND [ARG]...", 15 | "Sample CLI that supports commands yo.\n", 16 | [{force, "-F, --force", "confirm risky operations", [flag]}], 17 | [{"list", "list directory contents", list_parser()}, 18 | {"del", "delete directory contents", del_parser()} 19 | ], 20 | [{version, "1.0"}]). 21 | 22 | list_parser() -> 23 | cli:parser( 24 | "sample2 list", 25 | "[OPTION]... [DIR]...", 26 | "List DIRs or current dir if not specified.", 27 | []). 28 | 29 | del_parser() -> 30 | cli:parser( 31 | "sample2 del", 32 | "[OPTION]... [DIR]...", 33 | "Delete DIRs or current dir if not specified. Requires '--force'.", 34 | []). 35 | 36 | handle_cmd({"list", Opts, Args}) -> 37 | handle_list(Opts, Args); 38 | handle_cmd({"del", Opts, Args}) -> 39 | handle_del(Opts, Args). 40 | 41 | handle_list(Opts, Args) -> 42 | io:format("TODO: list ~p ~p~n", [Opts, Args]). 43 | 44 | handle_del(Opts, Args) -> 45 | io:format("TODO: del ~p ~p~n", [Opts, Args]). 46 | -------------------------------------------------------------------------------- /src/cli.erl: -------------------------------------------------------------------------------- 1 | -module(cli). 2 | 3 | -export([parser/4, parser/5, command_parser/5, command_parser/6, 4 | parse_args/2, print_help/1, print_version/1, print_error/2, 5 | print_usage_error/1, main/3, main_error/1, main_error/2]). 6 | 7 | -define(default_exit_code, 2). 8 | 9 | parser(Prog, Usage, Desc, OptionSpec) -> 10 | parser(Prog, Usage, Desc, OptionSpec, []). 11 | 12 | parser(Prog, Usage, Desc, OptionSpec, Config) -> 13 | cli_parser:new( 14 | Prog, 15 | [{usage, Usage}, 16 | {desc, Desc}, 17 | {options, parser_opts(OptionSpec)} 18 | |Config]). 19 | 20 | command_parser(Prog, Usage, Desc, OptionSpec, Commands) -> 21 | parser(Prog, Usage, Desc, OptionSpec, [{commands, Commands}]). 22 | 23 | command_parser(Prog, Usage, Desc, OptionSpec, Commands, Config) -> 24 | parser(Prog, Usage, Desc, OptionSpec, [{commands, Commands}|Config]). 25 | 26 | parser_opts(Specs) -> 27 | [parser_opt(Spec) || Spec <- Specs]. 28 | 29 | parser_opt({Key, Name}) -> 30 | cli_opt:new(Key, [{name, Name}]); 31 | parser_opt({Key, Name, Desc}) -> 32 | cli_opt:new(Key, [{name, Name}, {desc, Desc}]); 33 | parser_opt({Key, Name, Desc, Opts}) -> 34 | cli_opt:new(Key, [{name, Name}, {desc, Desc}|Opts]). 35 | 36 | parse_args(Args, Parser) -> 37 | cli_parser:parse_args(Args, Parser). 38 | 39 | print_help(Parser) -> 40 | cli_help:print_help(Parser). 41 | 42 | print_version(Parser) -> 43 | cli_help:print_version(Parser). 44 | 45 | print_error(Err, Parser) -> 46 | cli_help:print_error(Err, Parser). 47 | 48 | print_usage_error(Parser) -> 49 | cli_help:print_usage_error(Parser). 50 | 51 | main(Args, Parser, HandleParsed) -> 52 | cli_main:main(Args, Parser, HandleParsed). 53 | 54 | main_error(Msg) -> 55 | cli_main:main_error(Msg). 56 | 57 | main_error(ExitCode, Msg) -> 58 | cli_main:main_error(ExitCode, Msg). 59 | -------------------------------------------------------------------------------- /src/sample_cli.erl: -------------------------------------------------------------------------------- 1 | -module(sample_cli). 2 | 3 | -export([main/1]). 4 | 5 | main(Args) -> 6 | Parser = sample_parser(), 7 | handle_parsed(cli:parse_args(Args, Parser)). 8 | 9 | handle_parsed({{ok, print_help}, P}) -> 10 | cli:print_help(P); 11 | handle_parsed({{ok, print_version}, P}) -> 12 | cli:print_version(P); 13 | handle_parsed({{ok, Parsed}, _P}) -> 14 | handle_args(Parsed); 15 | handle_parsed({{error, Err}, P}) -> 16 | cli:print_error_and_halt(Err, P). 17 | 18 | sample_parser() -> 19 | cli:parser( 20 | "sample", 21 | "[OPTION]... [MSG]\n[OPTION]... WHOOPPEEE", 22 | "A sample CLI using erlang-cli.\n" 23 | "\n" 24 | "This program illustrates various capabilities of erlang-cli. It's " 25 | "sweet. You'll know this when you try it.", 26 | [{caps, "-C, --caps", "print message in caps", [flag]}, 27 | {x, "-X", "the X factor", [flag]}, 28 | {y, "-Y", "the Y factor", [{metavar, "FACTOR"}]}, 29 | {z, "-Z, --zed", "the Z factor", [no_arg]}, 30 | {some_long_option, "-L, --super-long-option", 31 | "this is some really long option - not sure what to make " 32 | "of it really; super cool, or super weird?"}, 33 | {maybe_value_option, "-M, --maybe-value", 34 | "you can specify a value here or not - up to you!", 35 | [optional_arg, {metavar, "MYSTERY"}] 36 | } 37 | ], 38 | [{version, 39 | "1.0\n" 40 | "Copyright (C) 2016 Garrett Smith\n" 41 | "License GPLv3+: GNU GPL version 3 or later " 42 | ". This is free software: " 43 | "you are free to change and redistribute it. There is NO WARRANTY, " 44 | "to the extent permitted by law.\n" 45 | "\n" 46 | "Written by Garrett Smith." 47 | } 48 | ]). 49 | 50 | handle_args({Opts, Args}) -> 51 | io:format("Options: ~p~n", [Opts]), 52 | io:format("Args: ~p~n", [Args]). 53 | -------------------------------------------------------------------------------- /src/cli_main.erl: -------------------------------------------------------------------------------- 1 | -module(cli_main). 2 | 3 | -export([main/3, main_error/1, main_error/2]). 4 | 5 | -define(ok_exit_code, 0). 6 | -define(error_exit_code, 1). 7 | 8 | main(Args, Parser, Handler) -> 9 | handle_main_parse_args(cli_parser:parse_args(Args, Parser), Handler). 10 | 11 | handle_main_parse_args({{ok, print_help}, P}, _) -> 12 | print_help(P); 13 | handle_main_parse_args({{ok, print_version}, P}, _) -> 14 | print_version(P); 15 | handle_main_parse_args({{ok, Parsed}, P}, Handle) -> 16 | handle_parsed(Parsed, Handle, P); 17 | handle_main_parse_args({{error, Err}, P}, _) -> 18 | print_error(Err, P). 19 | 20 | print_help(P) -> 21 | cli_help:print_help(P), 22 | ?ok_exit_code. 23 | 24 | print_version(P) -> 25 | cli_help:print_version(P), 26 | ?ok_exit_code. 27 | 28 | print_error(Err, P) -> 29 | cli_help:print_error(Err, P), 30 | ?error_exit_code. 31 | 32 | handle_parsed(Parsed, Handler, P) -> 33 | Result = (catch call_handler(Handler, Parsed)), 34 | maybe_print_error(Result, P), 35 | to_exit_code(Result). 36 | 37 | call_handler(F, Args) when is_function(F) -> 38 | F(Args); 39 | call_handler({M, F, A}, Args) -> 40 | apply(M, F, [Args|A]). 41 | 42 | -define(is_msg(X), is_list(X) orelse is_binary(X)). 43 | 44 | maybe_print_error({error, {N, Msg}}, P) when is_integer(N), ?is_msg(Msg) -> 45 | cli_help:print_error(Msg, P); 46 | maybe_print_error({error, Msg}, P) when ?is_msg(Msg) -> 47 | cli_help:print_error(Msg, P); 48 | maybe_print_error({'EXIT', Err}, P) -> 49 | cli_help:print_error(format_exception(Err), P); 50 | maybe_print_error(_, _) -> 51 | ok. 52 | 53 | format_exception(Err) -> 54 | io_lib:format("internal error~n~p", [Err]). 55 | 56 | to_exit_code(ok) -> ?ok_exit_code; 57 | to_exit_code({ok, N}) when is_integer(N) -> N; 58 | to_exit_code(error) -> ?error_exit_code; 59 | to_exit_code({error, N}) when is_integer(N) -> N; 60 | to_exit_code({error, {N, _Msg}}) when is_integer(N) -> N; 61 | to_exit_code({error, _Msg}) -> ?error_exit_code; 62 | to_exit_code({'EXIT', _}) -> ?error_exit_code. 63 | 64 | main_error(ExitCode) when is_integer(ExitCode) -> 65 | throw({error, ExitCode}); 66 | main_error(Msg) -> 67 | throw({error, {?error_exit_code, Msg}}). 68 | 69 | main_error(ExitCode, Msg) -> 70 | throw({error, {ExitCode, Msg}}). 71 | -------------------------------------------------------------------------------- /src/cli_opt.erl: -------------------------------------------------------------------------------- 1 | -module(cli_opt). 2 | 3 | -export([new/2]). 4 | 5 | -export([key/1, desc/1, has_arg/1, short/1, long/1, metavar/1, 6 | visible/1]). 7 | 8 | -export([int_val/4]). 9 | 10 | -record(opt, {key, desc, has_arg, short, long, metavar, visible}). 11 | 12 | %% =================================================================== 13 | %% New 14 | %% =================================================================== 15 | 16 | new(Key, Opts) -> 17 | {Short, Long} = short_long_from_opts(Opts, Key), 18 | #opt{ 19 | key=Key, 20 | desc=desc_from_opts(Opts), 21 | has_arg=has_arg_from_opts(Opts), 22 | short=Short, 23 | long=Long, 24 | metavar=metavar_from_opts(Opts), 25 | visible=visible_from_opts(Opts) 26 | }. 27 | 28 | desc_from_opts(Opts) -> 29 | proplists:get_value(desc, Opts, ""). 30 | 31 | short_long_from_opts(Opts, Key) -> 32 | short_long_from_name(name_from_opts(Opts, Key)). 33 | 34 | name_from_opts(Opts, Key) -> 35 | Default = fun() -> long_opt_from_key(Key) end, 36 | opt_val(name, Opts, Default). 37 | 38 | long_opt_from_key(Key) -> 39 | "--" ++ replace(atom_to_list(Key), "_", "-"). 40 | 41 | short_long_from_name(Name) -> 42 | Pattern = "^(?:(-.))?(?:, )?(?:(--.+))?$", 43 | case re:run(Name, Pattern, [{capture, all_but_first, list}]) of 44 | {match, ["", Long]} -> {undefined, Long}; 45 | {match, [Short, Long]} -> {Short, Long}; 46 | {match, [Short]} -> {Short, undefined}; 47 | nomatch -> error({bad_option_name, Name}) 48 | end. 49 | 50 | has_arg_from_opts(Opts) -> 51 | Default = fun() -> default_has_arg(Opts) end, 52 | opt_val(has_arg, Opts, Default). 53 | 54 | default_has_arg(Opts) -> 55 | apply_boolopt_map( 56 | [{flag, no}, 57 | {no_arg, no}, 58 | {optional_arg, optional}, 59 | {'_', yes}], 60 | Opts). 61 | 62 | metavar_from_opts(Opts) -> 63 | Default = fun() -> default_metavar(Opts) end, 64 | opt_val(metavar, Opts, Default). 65 | 66 | default_metavar(Opts) -> 67 | apply_boolopt_map( 68 | [{flag, undefined}, 69 | {no_arg, undefined}, 70 | {'_', "VALUE"}], 71 | Opts). 72 | 73 | visible_from_opts(Opts) -> 74 | not proplists:get_bool(hidden, Opts). 75 | 76 | %% =================================================================== 77 | %% Attrs 78 | %% =================================================================== 79 | 80 | key(#opt{key=Key}) -> Key. 81 | 82 | desc(#opt{desc=Desc}) -> Desc. 83 | 84 | has_arg(#opt{has_arg=HasArg}) -> HasArg. 85 | 86 | short(#opt{short=Short}) -> Short. 87 | 88 | long(#opt{long=Long}) -> Long. 89 | 90 | metavar(#opt{metavar=Metavar}) -> Metavar. 91 | 92 | visible(#opt{visible=Visible}) -> Visible. 93 | 94 | %% =================================================================== 95 | %% Converters 96 | %% =================================================================== 97 | 98 | int_val(Key, Opts, Default, ErrMsg) -> 99 | case proplists:get_value(Key, Opts) of 100 | undefined -> Default; 101 | Str -> str_to_int(Str, ErrMsg) 102 | end. 103 | 104 | str_to_int(Str, ErrMsg) -> 105 | try 106 | list_to_integer(Str) 107 | catch 108 | error:badarg -> throw({error, ErrMsg}) 109 | end. 110 | 111 | %% =================================================================== 112 | %% Helpers 113 | %% =================================================================== 114 | 115 | opt_val(Key, Opts, Default) -> 116 | case proplists:get_value(Key, Opts, '$undefined') of 117 | '$undefined' when is_function(Default) -> Default(); 118 | '$undefined' -> Default; 119 | Val -> Val 120 | end. 121 | 122 | apply_boolopt_map([{'_', Result}|_], _Opts) -> Result; 123 | apply_boolopt_map([{Opt, Result}|Rest], Opts) -> 124 | case proplists:get_bool(Opt, Opts) of 125 | true -> Result; 126 | false -> apply_boolopt_map(Rest, Opts) 127 | end. 128 | 129 | replace(Str, Replace, With) -> 130 | re:replace(Str, Replace, With, [{return, list}]). 131 | -------------------------------------------------------------------------------- /src/cli_debug.erl: -------------------------------------------------------------------------------- 1 | -module(cli_debug). 2 | 3 | -export([trace_module/1, 4 | trace_module/2, 5 | trace_function/2, 6 | trace_function/3, 7 | trace_messages/1, 8 | trace_messages/2, 9 | stop_tracing/0, 10 | init_trace_from_env/1]). 11 | 12 | %%%=================================================================== 13 | %%% API 14 | %%%=================================================================== 15 | 16 | trace_module(Module) -> 17 | trace_module(Module, []). 18 | 19 | trace_module(Module, Opts) -> 20 | start_tracer(Opts), 21 | dbg_tpl(Module, Opts), 22 | dbg_calls(). 23 | 24 | trace_function(Module, Function) -> 25 | trace_function(Module, Function, []). 26 | 27 | trace_function(Module, Function, Opts) -> 28 | start_tracer(Opts), 29 | dbg_tpl(Module, Function, Opts), 30 | dbg_calls(). 31 | 32 | trace_messages(Process) -> 33 | trace_messages(Process, []). 34 | 35 | trace_messages(Process, Opts) -> 36 | start_tracer(Opts), 37 | dbg_messages(Process, Opts). 38 | 39 | stop_tracing() -> 40 | dbg:stop_clear(). 41 | 42 | %%%=================================================================== 43 | %%% Init trace from env 44 | %%%=================================================================== 45 | 46 | init_trace_from_env(false) -> 47 | ok; 48 | init_trace_from_env(Env) -> 49 | lists:foreach(fun apply_trace/1, parse_trace_env(Env)). 50 | 51 | parse_trace_env(undefined) -> 52 | []; 53 | parse_trace_env(Str) -> 54 | [trace_spec(Token) || Token <- string:tokens(Str, ",")]. 55 | 56 | trace_spec(Str) -> 57 | [list_to_atom(Token) || Token <- string:tokens(Str, ":")]. 58 | 59 | apply_trace([M]) -> trace_module(M); 60 | apply_trace([M, F]) -> trace_function(M, F). 61 | 62 | %%%=================================================================== 63 | %%% dbg wrappers 64 | %%%=================================================================== 65 | 66 | dbg_tpl(Module, Opts) -> 67 | handle_dbg_tpl(dbg:tpl(Module, match_spec(Opts))). 68 | 69 | dbg_tpl(Module, {Function, Arity}, Opts) -> 70 | handle_dbg_tpl(dbg:tpl(Module, Function, Arity, match_spec(Opts))); 71 | dbg_tpl(Module, Function, Opts) -> 72 | handle_dbg_tpl(dbg:tpl(Module, Function, match_spec(Opts))). 73 | 74 | handle_dbg_tpl({ok, _}) -> ok; 75 | handle_dbg_tpl({error, Err}) -> error(Err). 76 | 77 | dbg_calls() -> 78 | handle_dbg_p(dbg:p(all, c)). 79 | 80 | handle_dbg_p({ok, _}) -> ok; 81 | handle_dbg_p({error, Err}) -> error(Err). 82 | 83 | dbg_messages(Process, Opts) -> 84 | handle_dbg_p(dbg:p(Process, trace_flags(Opts))). 85 | 86 | trace_flags(Opts) -> 87 | message_flags(Opts, process_event_flags(Opts, [])). 88 | 89 | message_flags(Opts, Acc) -> 90 | case proplists:get_value(send_only, Opts, false) of 91 | true -> [s|Acc]; 92 | false -> 93 | case proplists:get_value(receive_only, Opts, false) of 94 | true -> [r|Acc]; 95 | false -> [m|Acc] 96 | end 97 | end. 98 | 99 | process_event_flags(Opts, Acc) -> 100 | case proplists:get_value(process_events, Opts, false) of 101 | true -> [p|Acc]; 102 | false -> Acc 103 | end. 104 | 105 | %%%=================================================================== 106 | %%% tracer 107 | %%%=================================================================== 108 | 109 | start_tracer(Opts) -> 110 | start_dbg(), 111 | handle_tracer(dbg:tracer(process, tracer(Opts))). 112 | 113 | tracer(Opts) -> 114 | Pattern = pattern_match_spec(proplists:get_value(pattern, Opts)), 115 | Out = trace_device(proplists:get_value(file, Opts)), 116 | {fun(Msg, []) -> maybe_trace(Msg, Pattern, Out), [] end, []}. 117 | 118 | pattern_match_spec(undefined) -> undefined; 119 | pattern_match_spec(Pattern) -> 120 | ets:match_spec_compile([{Pattern, [], ['$_']}]). 121 | 122 | trace_device(undefined) -> standard_io; 123 | trace_device(File) when is_list(File) -> 124 | handle_file_open(file:open(File, [append])). 125 | 126 | handle_file_open({ok, Out}) -> Out; 127 | handle_file_open({error, Err}) -> error({trace_file, Err}). 128 | 129 | handle_tracer({ok, _Pid}) -> ok; 130 | handle_tracer({error, already_started}) -> ok. 131 | 132 | start_dbg() -> 133 | handle_dbg_start(catch(dbg:start())). 134 | 135 | handle_dbg_start({ok, _Pid}) -> ok; 136 | handle_dbg_start({'EXIT', {{case_clause, Pid}, _}}) 137 | when is_pid(Pid) -> ok. 138 | 139 | maybe_trace(Msg, undefined, Out) -> 140 | trace(Msg, Out); 141 | maybe_trace({_, _, return_from, _, _}=Msg, _Pattern, Out) -> 142 | trace(Msg, Out); 143 | maybe_trace(Msg, Pattern, Out) -> 144 | handle_pattern_match(apply_pattern(Pattern, Msg), Msg, Out). 145 | 146 | apply_pattern(Pattern, Msg) -> 147 | ets:match_spec_run([msg_content(Msg)], Pattern). 148 | 149 | msg_content({trace, _, call, {_, _, Args}}) -> Args; 150 | msg_content({trace, _, return_from, _, Val}) -> Val; 151 | msg_content({trace, _, send, Msg, _}) -> Msg; 152 | msg_content({trace, _, 'receive', Msg}) -> Msg; 153 | msg_content(Other) -> Other. 154 | 155 | handle_pattern_match([_], Msg, Out) -> trace(Msg, Out); 156 | handle_pattern_match([], _Msg, _Out) -> not_traced. 157 | 158 | trace(Msg, Out) -> 159 | {Format, Data} = format_msg(Msg), 160 | io:format(Out, Format, Data). 161 | 162 | format_msg({trace, Pid, call, {M, F, A}}) -> 163 | {"~n=TRACE CALL==== ~s ===~n~p -> ~s:~s/~p~n~p~n", 164 | [timestamp(), Pid, M, F, length(A), A]}; 165 | format_msg({trace, Pid, return_from, {M, F, Arity}, Val}) -> 166 | {"~n=TRACE RETURN==== ~s ===~n~p <- ~s:~s/~p~n~p~n", 167 | [timestamp(), Pid, M, F, Arity, Val]}; 168 | format_msg({trace, Pid, send, Msg, Dest}) -> 169 | {"~n=TRACE SEND==== ~s ===~n~p -> ~p~n~p~n", 170 | [timestamp(), Pid, Dest, Msg]}; 171 | format_msg({trace, Pid, 'receive', Msg}) -> 172 | {"~n=TRACE RECEIVE==== ~s ===~n~p~n~p~n", [timestamp(), Pid, Msg]}; 173 | format_msg(Other) -> 174 | HR = hr(), 175 | {"~s~nTRACE:~n~s~n ~p~n~n", [HR, HR, Other]}. 176 | 177 | timestamp() -> 178 | {{Y, M, D}, {H, Min, S}} = erlang:localtime(), 179 | io_lib:format("~p-~s-~p::~p:~p:~p", [D, month(M), Y, H, Min, S]). 180 | 181 | month(1) -> "Jan"; 182 | month(2) -> "Feb"; 183 | month(3) -> "Mar"; 184 | month(4) -> "Apr"; 185 | month(5) -> "May"; 186 | month(6) -> "Jun"; 187 | month(7) -> "Jul"; 188 | month(8) -> "Aug"; 189 | month(9) -> "Sep"; 190 | month(10) -> "Oct"; 191 | month(11) -> "Nov"; 192 | month(12) -> "Dec". 193 | 194 | hr() -> 195 | case io:columns() of 196 | {ok, N} -> binary:copy(<<"-">>, N - 2); 197 | {error, enotsup} -> binary:copy(<<"-">>, 78) 198 | end. 199 | 200 | %%%=================================================================== 201 | %%% Match spec support 202 | %%%=================================================================== 203 | 204 | match_spec(Opts) -> 205 | case proplists:get_value(no_return, Opts, false) of 206 | true -> 207 | [{'_', [], []}]; 208 | false -> 209 | [{'_', [], [{return_trace}]}] 210 | end. 211 | -------------------------------------------------------------------------------- /src/cli_tests.erl: -------------------------------------------------------------------------------- 1 | -module(cli_tests). 2 | 3 | -compile([nowarn_unused_function, export_all]). 4 | 5 | %% =================================================================== 6 | %% Run 7 | %% =================================================================== 8 | 9 | run() -> 10 | cli_debug:init_trace_from_env(os:getenv("TRACE")), 11 | test_cli_opt(), 12 | test_parse_args(), 13 | test_parse_pos_args(), 14 | test_opt_convert(), 15 | test_help(). 16 | 17 | run(Test) -> 18 | cli_debug:init_trace_from_env(os:getenv("TRACE")), 19 | F = list_to_atom("test_" ++ Test), 20 | ?MODULE:F(). 21 | 22 | %% =================================================================== 23 | %% CLI opt 24 | %% =================================================================== 25 | 26 | test_cli_opt() -> 27 | io:format("cli_opt: "), 28 | 29 | Opt = fun(Key, Opts) -> opt_props(cli_opt:new(Key, Opts)) end, 30 | 31 | #{key := foo, 32 | desc := "", 33 | has_arg := yes, 34 | short := undefined, 35 | long := "--foo", 36 | metavar := "VALUE"} = Opt(foo, []), 37 | 38 | #{desc := "foo option"} = Opt(foo, [{desc, "foo option"}]), 39 | 40 | #{has_arg := yes} = Opt(bar, []), 41 | #{has_arg := no} = Opt(bar, [flag]), 42 | #{has_arg := no} = Opt(bar, [no_arg]), 43 | #{has_arg := optional} = Opt(bar, [optional_arg]), 44 | 45 | #{short := undefined, long := "--baz"} = Opt(baz, []), 46 | #{short := "-Z", long := "--baz"} = Opt(baz, [{name, "-Z, --baz"}]), 47 | #{short := "-Z", long := undefined} = Opt(baz, [{name, "-Z"}]), 48 | #{short := undefined, long := "--baz"} = Opt(baz, [{name, "--baz"}]), 49 | 50 | #{metavar := "VALUE"} = Opt(foo, []), 51 | #{metavar := undefined} = Opt(foo, [flag]), 52 | #{metavar := undefined} = Opt(foo, [no_arg]), 53 | #{metavar := "FOO"} = Opt(foo, [{metavar, "FOO"}]), 54 | 55 | io:format("OK~n"). 56 | 57 | opt_props(O) -> 58 | #{key => cli_opt:key(O), 59 | desc => cli_opt:desc(O), 60 | has_arg => cli_opt:has_arg(O), 61 | short => cli_opt:short(O), 62 | long => cli_opt:long(O), 63 | metavar => cli_opt:metavar(O)}. 64 | 65 | %% =================================================================== 66 | %% Parse args 67 | %% =================================================================== 68 | 69 | test_parse_args() -> 70 | io:format("parse_args: "), 71 | 72 | P = fun(Args, OptSpec) -> element(1, parse_args(Args, OptSpec)) end, 73 | 74 | %% Null parser (NP) 75 | NP = fun(Args) -> P(Args, []) end, 76 | {ok, {[], []}} = NP([]), 77 | 78 | %% Unspecified options 79 | {ok, {[], []}} = P([], [{foo, "-F"}, {bar, "-B"}]), 80 | 81 | %% Positional args with single foo option (Foo) 82 | Foo = fun(Args) -> P(Args, [{foo, "-F"}]) end, 83 | {ok, {[], ["foo", "bar"]}} = Foo(["foo", "bar"]), 84 | {ok, {[], ["-"]}} = Foo(["-"]), 85 | {ok, {[{foo, "foo"}], ["bar"]}} = Foo(["-F", "foo", "bar"]), 86 | {ok, {[], ["-F", "foo"]}} = Foo(["--", "-F", "foo"]), 87 | 88 | %% Option with required arg (Req) 89 | Req = fun(Args) -> P(Args, [{foo, "-F, --foo"}]) end, 90 | {error, {missing_opt_arg, foo, "--foo"}} = Req(["--foo"]), 91 | {ok, {[{foo, "123"}], []}} = Req(["--foo", "123"]), 92 | {ok, {[{foo, "123"}], []}} = Req(["--foo=123"]), 93 | {error, {missing_opt_arg, foo, "-F"}} = Req(["-F"]), 94 | {ok, {[{foo, "123"}], []}} = Req(["-F", "123"]), 95 | {ok, {[{foo, "123"}], []}} = Req(["-F123"]), 96 | 97 | %% Option with no arg (NoArg) 98 | NoArg = fun(Args) -> P(Args, [{foo, "-F, --foo", "", [no_arg]}]) end, 99 | {ok, {[foo], []}} = NoArg(["--foo"]), 100 | {ok, {[foo], ["123"]}} = NoArg(["--foo", "123"]), 101 | {error, {unexpected_opt_arg, foo, "--foo"}} = NoArg(["--foo=123"]), 102 | {ok, {[foo], []}} = NoArg(["-F"]), 103 | {ok, {[foo], ["123"]}} = NoArg(["-F", "123"]), 104 | {error, {unknown_opt, "-1"}} = NoArg(["-F123"]), 105 | 106 | %% Option with optional arg (Opt) 107 | Opt = fun(Args) -> P(Args, [{foo, "-F, --foo", "", [optional_arg]}]) end, 108 | {ok, {[{foo, ""}], []}} = Opt(["--foo"]), 109 | {ok, {[{foo, "123"}], []}} = Opt(["--foo", "123"]), 110 | {ok, {[{foo, "123"}], []}} = Opt(["--foo=123"]), 111 | {ok, {[{foo, ""}], []}} = Opt(["-F"]), 112 | {ok, {[{foo, "123"}], []}} = Opt(["-F", "123"]), 113 | {ok, {[{foo, "123"}], []}} = Opt(["-F123"]), 114 | 115 | %% Built-in --help option 116 | {ok, print_help} = NP(["--help"]), 117 | {ok, print_help} = NP(["--help", "-F"]), 118 | {error, {unknown_opt, "-F"}} = NP(["-F", "--help"]), 119 | 120 | %% Built-in --version option (only when version is defined) (VP) 121 | {error, {unknown_opt, "--version"}} = NP(["--version"]), 122 | VP = fun(Args) -> element(1, parse_args(Args, [], [{version, "1.0"}])) end, 123 | {ok, print_version} = VP(["--version"]), 124 | {ok, print_version} = VP(["--version", "-F"]), 125 | {error, {unknown_opt, "-F"}} = VP(["-F", "--version"]), 126 | 127 | %% Kitchen sink (KS) 128 | KSOpts = 129 | [{flag, "-F, --flag", "", [flag]}, 130 | {required, "--req", "", []}, 131 | {optional, "-O", "", [optional_arg]}], 132 | KS = fun(Args) -> P(Args, KSOpts) end, 133 | {ok, {[flag, {required, "abc"}, {optional, "arg1"}], ["arg2"]}} = 134 | KS(["-F", "--req", "abc", "-O", "arg1", "arg2"]), 135 | {error, {missing_opt_arg, required, "--req"}} = 136 | KS(["-F", "--req", "-O", "arg1", "arg2"]), 137 | {ok, {[flag, {required, "abc"}, {optional, ""}], ["arg1", "arg2"]}} = 138 | KS(["-F", "--req", "abc", "-O", "--", "arg1", "arg2"]), 139 | 140 | %% Multiple values 141 | {ok, {[flag, flag, flag, {optional, "foo"}, {optional, "bar"}], []}} = 142 | KS(["-FFF", "-Ofoo", "-Obar"]), 143 | 144 | io:format("OK~n"). 145 | 146 | parse_args(Args, OptSpec) -> 147 | parse_args(Args, OptSpec, []). 148 | 149 | parse_args(Args, OptSpec, Props) -> 150 | Parser = cli:parser("p", "", "", OptSpec, Props), 151 | cli:parse_args(Args, Parser). 152 | 153 | %% =================================================================== 154 | %% Parse pos args 155 | %% =================================================================== 156 | 157 | test_parse_pos_args() -> 158 | io:format("parse_pos_args: "), 159 | 160 | P = fun(Args, PosArgs) -> 161 | element(1, parse_args(Args, [], [{pos_args, PosArgs}])) 162 | end, 163 | 164 | {ok, {[], []}} = P([], any), 165 | {ok, {[], ["a"]}} = P(["a"], any), 166 | {ok, {[], ["a", "b"]}} = P(["a", "b"], any), 167 | 168 | {ok, {[], ["a"]}} = P(["a"], 1), 169 | {error, missing_pos_arg} = P([], 1), 170 | {error, {unexpected_pos_arg, "b"}} = P(["a", "b"], 1), 171 | 172 | {ok, {[], ["a", "b"]}} = P(["a", "b"], 2), 173 | {error, missing_pos_arg} = P([], 2), 174 | {error, missing_pos_arg} = P(["a"], 2), 175 | {error, {unexpected_pos_arg, "c"}} = P(["a", "b", "c"], 2), 176 | 177 | {ok, {[], ["a"]}} = P(["a"], {1, 2}), 178 | {ok, {[], ["a", "b"]}} = P(["a", "b"], {1, 2}), 179 | {error, missing_pos_arg} = P([], {1, 2}), 180 | {error, {unexpected_pos_arg, "c"}} = P(["a", "b", "c"], {1, 2}), 181 | 182 | {ok, {[], []}} = P([], {any, 2}), 183 | {ok, {[], ["a"]}} = P(["a"], {any, 2}), 184 | {ok, {[], ["a", "b"]}} = P(["a", "b"], {any, 2}), 185 | {error, {unexpected_pos_arg, "c"}} = P(["a", "b", "c"], {any, 2}), 186 | 187 | {ok, {[], ["a"]}} = P(["a"], {1, any}), 188 | {ok, {[], ["a", "b"]}} = P(["a", "b"], {1, any}), 189 | {ok, {[], ["a", "b", "c"]}} = P(["a", "b", "c"], {1, any}), 190 | {error, missing_pos_arg} = P([], {1, any}), 191 | 192 | io:format("OK~n"). 193 | 194 | %% =================================================================== 195 | %% Opt convert 196 | %% =================================================================== 197 | 198 | test_opt_convert() -> 199 | io:format("opt_convert: "), 200 | 201 | 1 = cli_opt:int_val(i, [{i, "1"}], 2, "i must be a number"), 202 | 2 = cli_opt:int_val(i, [], 2, "i must be a number"), 203 | {error, "i must be a number"} 204 | = (catch cli_opt:int_val(i, [{i, "a"}], 2, "i must be a number")), 205 | 206 | io:format("OK~n"). 207 | 208 | %% =================================================================== 209 | %% Help 210 | %% =================================================================== 211 | 212 | test_help() -> 213 | io:format("help: "), 214 | 215 | P = 216 | cli:parser( 217 | "CMD", 218 | "[OPTION]... ARG1 [Arg2]", 219 | "Line one of description.\n" 220 | "\n" 221 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque " 222 | "facilisis mauris vel aliquet mollis.\n" 223 | "\n" 224 | "!! this line will not be formatted", 225 | [{opt1, "-O, --opt1", "an option", []}, 226 | {opt2, "--opt2", "another option", [{metavar, "VAL2"}]}, 227 | {flag, "-F, --flag", "a flag", [flag]}]), 228 | 229 | Buf = iobuf_new(), 230 | <<>> = iobuf_data(Buf), 231 | 232 | cli_help:print_help(Buf, P, [{page_width, 72}, {opt_desc_col, 26}]), 233 | 234 | <<"Usage: CMD [OPTION]... ARG1 [Arg2]\n" 235 | "Line one of description.\n" 236 | "\n" 237 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque\n" 238 | "facilisis mauris vel aliquet mollis.\n" 239 | "\n" 240 | " this line will not be formatted\n" 241 | "\n" 242 | "Options:\n" 243 | " -O, --opt1=VALUE an option\n" 244 | " --opt2=VAL2 another option\n" 245 | " -F, --flag a flag\n" 246 | " --help print this help and exit\n">> 247 | = iobuf_data(Buf), 248 | 249 | io:format("OK~n"). 250 | 251 | iobuf_new() -> 252 | spawn(fun() -> iobuf_loop([]) end). 253 | 254 | iobuf_loop(Data) -> 255 | receive 256 | {data, From} -> 257 | From ! {data, self(), lists:reverse(Data)}, 258 | iobuf_loop(Data); 259 | {io_request, From, Ref, {put_chars, unicode, M, F, A}} -> 260 | Chars = apply(M, F, A), 261 | From ! {io_reply, Ref, ok}, 262 | iobuf_loop([Chars|Data]); 263 | close -> 264 | ok; 265 | Other -> 266 | error({iobuf_msg, Other}) 267 | end. 268 | 269 | iobuf_data(Buf) -> 270 | Buf ! {data, self()}, 271 | receive 272 | {data, Buf, Data} -> iolist_to_binary(Data) 273 | end. 274 | -------------------------------------------------------------------------------- /src/cli_help.erl: -------------------------------------------------------------------------------- 1 | -module(cli_help). 2 | 3 | -export([print_help/1, print_help/2, print_help/3, 4 | print_version/1, print_version/2, print_version/3, 5 | print_error/2, print_error/3, 6 | print_usage_error/1, print_usage_error/2]). 7 | 8 | -export([format_opt_name/1]). 9 | 10 | -define(default_page_width, 79). 11 | -define(default_opt_desc_col, 30). 12 | 13 | -record(fmt, {page_width, opt_desc_col}). 14 | 15 | %% =================================================================== 16 | %% Print help 17 | %% =================================================================== 18 | 19 | print_help(Parser) -> 20 | print_help(standard_error, Parser, []). 21 | 22 | print_help(Device, Parser) -> 23 | print_help(Device, Parser, []). 24 | 25 | print_help(Device, Parser, Opts) -> 26 | Fmt = init_fmt(Opts), 27 | print_usage(Device, Parser), 28 | print_program_desc(Device, Parser, Fmt), 29 | maybe_print_commands( 30 | cli_parser:is_command_parser(Parser), 31 | Device, Parser, Fmt), 32 | print_options(Device, Parser, Fmt). 33 | 34 | init_fmt(Opts) -> 35 | Opt = fun(Name, Default) -> proplists:get_value(Name, Opts, Default) end, 36 | #fmt{ 37 | page_width=Opt(page_width, ?default_page_width), 38 | opt_desc_col=Opt(opt_desc_col, ?default_opt_desc_col) 39 | }. 40 | 41 | %% ------------------------------------------------------------------- 42 | %% Usage 43 | %% ------------------------------------------------------------------- 44 | 45 | print_usage(Device, Parser) -> 46 | UsageLines = usage_lines(Parser), 47 | Prog = cli_parser:prog(Parser), 48 | print_usage_lines(Device, UsageLines, Prog, first). 49 | 50 | usage_lines(Parser) -> 51 | split_lines(cli_parser:usage(Parser)). 52 | 53 | print_usage_lines(Device, [Line|Rest], Prog, LineType) -> 54 | Prefix = usage_line_prefix(LineType), 55 | io:format(Device, "~s ~s ~s~n", [Prefix, Prog, Line]), 56 | print_usage_lines(Device, Rest, Prog, more); 57 | print_usage_lines(_Device, [], _Prog, _LineType) -> 58 | ok. 59 | 60 | usage_line_prefix(first) -> "Usage:"; 61 | usage_line_prefix(more) -> " or:". 62 | 63 | %% ------------------------------------------------------------------- 64 | %% Program desc 65 | %% ------------------------------------------------------------------- 66 | 67 | print_program_desc(Device, Parser, Fmt) -> 68 | io:format(Device, "~s~n", [formatted_program_desc(Parser, Fmt)]). 69 | 70 | formatted_program_desc(Parser, #fmt{page_width=Width}) -> 71 | Pars = split_lines(program_desc(Parser)), 72 | prettypr:format(pars_doc(Pars), Width, Width). 73 | 74 | program_desc(Parser) -> 75 | case cli_parser:desc(Parser) of 76 | undefined -> ""; 77 | Desc -> Desc 78 | end. 79 | 80 | %% ------------------------------------------------------------------- 81 | %% Commands 82 | %% ------------------------------------------------------------------- 83 | 84 | maybe_print_commands(true, Device, Parser, Fmt) -> 85 | io:format(Device, "Commands:~n", []), 86 | lists:foreach( 87 | fun(Cmd) -> print_visible_command(Cmd, Device, Fmt) end, 88 | cli_parser:commands(Parser)), 89 | io:format(Device, "~n", []); 90 | maybe_print_commands(false, _Device, _Parser, _Fmt) -> 91 | ok. 92 | 93 | print_visible_command({Name, Help, CmdParser}, Device, Fmt) -> 94 | case cli_parser:visible(CmdParser) of 95 | true -> print_command(Device, Name, Help, Fmt); 96 | false -> ok 97 | end. 98 | 99 | print_command(Device, Name, Help, Fmt) -> 100 | print_opt_name_with_padding(Device, format_command_name(Name), Fmt), 101 | print_opt_desc(Device, Help, Fmt). 102 | 103 | format_command_name(Name) -> 104 | io_lib:format(" ~s", [Name]). 105 | 106 | %% ------------------------------------------------------------------- 107 | %% Options 108 | %% ------------------------------------------------------------------- 109 | 110 | print_options(Device, Parser, Fmt) -> 111 | io:format(Device, "Options:~n", []), 112 | print_parser_opts(Device, cli_parser:options(Parser), Fmt), 113 | print_help_and_version_opts(Device, Parser). 114 | 115 | print_parser_opts(Device, Opts, Fmt) -> 116 | lists:foreach(fun(Opt) -> print_visible_opt(Opt, Device, Fmt) end, Opts). 117 | 118 | print_visible_opt(Opt, Device, Fmt) -> 119 | case cli_opt:visible(Opt) of 120 | true -> print_opt(Device, Opt, Fmt); 121 | false -> ok 122 | end. 123 | 124 | print_opt(Device, Opt, Fmt) -> 125 | print_opt_name_with_padding(Device, format_opt_name(Opt), Fmt), 126 | print_opt_desc(Device, format_opt_desc(Opt, Fmt), Fmt). 127 | 128 | format_opt_name(Opt) -> 129 | Short = cli_opt:short(Opt), 130 | Long = cli_opt:long(Opt), 131 | Meta = {cli_opt:has_arg(Opt), cli_opt:metavar(Opt)}, 132 | io_lib:format( 133 | "~s~s~s", 134 | [opt_short(Short, Long, Meta), 135 | opt_short_long_delim(Short, Long), 136 | opt_long(Long, Meta)]). 137 | 138 | opt_short(undefined, _, _) -> 139 | " "; 140 | opt_short(Short, undefined, {no, _}) -> 141 | io_lib:format(" ~s", [Short]); 142 | opt_short(Short, undefined, {yes, Metavar}) -> 143 | io_lib:format(" ~s ~s", [Short, Metavar]); 144 | opt_short(Short, undefined, {optional, Metavar}) -> 145 | io_lib:format(" ~s [~s]", [Short, Metavar]); 146 | opt_short(Short, _, _) -> 147 | io_lib:format(" ~s", [Short]). 148 | 149 | opt_short_long_delim(undefined, _Long) -> " "; 150 | opt_short_long_delim(_Short, undefined) -> ""; 151 | opt_short_long_delim(_Short, _Long) -> ", ". 152 | 153 | opt_long(undefined, _) -> 154 | ""; 155 | opt_long(Long, {no, _}) -> 156 | Long; 157 | opt_long(Long, {yes, Metavar}) -> 158 | io_lib:format("~s=~s", [Long, Metavar]); 159 | opt_long(Long, {optional, Metavar}) -> 160 | io_lib:format("~s[=~s]", [Long, Metavar]). 161 | 162 | print_opt_name_with_padding(Device, FormattedName, Fmt) -> 163 | io:format(Device, FormattedName, []), 164 | pad_to_opt_desc(Device, FormattedName, Fmt). 165 | 166 | pad_to_opt_desc(Device, FormattedName, #fmt{opt_desc_col=StartCol}) -> 167 | case StartCol - iolist_size(FormattedName) of 168 | Line1Padding when Line1Padding >= 0 -> 169 | io:format(Device, string:copies(" ", Line1Padding), []); 170 | _ -> 171 | io:format(Device, "~n", []), 172 | io:format(Device, string:copies(" ", StartCol), []) 173 | end. 174 | 175 | format_opt_desc(Opt, #fmt{page_width=Page, opt_desc_col=ColStart}) -> 176 | Desc = cli_opt:desc(Opt), 177 | Width = Page - ColStart, 178 | prettypr:format(prettypr:text_par(Desc), Width). 179 | 180 | print_opt_desc(Device, Desc, Fmt) -> 181 | [Line1|Rest] = split_lines(Desc), 182 | io:format(Device, Line1, []), 183 | io:format(Device, "~n", []), 184 | print_indented_opt_lines(Device, Fmt, Rest). 185 | 186 | print_indented_opt_lines(Device, #fmt{opt_desc_col=Padding}=Fmt, [Line|Rest]) -> 187 | io:format(Device, string:copies(" ", Padding), []), 188 | io:format(Device, Line, []), 189 | io:format(Device, "~n", []), 190 | print_indented_opt_lines(Device, Fmt, Rest); 191 | print_indented_opt_lines(_Device, _Help, []) -> 192 | ok. 193 | 194 | print_help_and_version_opts(Device, Parser) -> 195 | print_help_opt(Device), 196 | maybe_print_version_opt(has_version(Parser), Device). 197 | 198 | print_help_opt(Device) -> 199 | io:format( 200 | Device, " --help print this help and exit~n", []). 201 | 202 | has_version(Parser) -> cli_parser:version(Parser) /= undefined. 203 | 204 | maybe_print_version_opt(true, Device) -> 205 | io:format( 206 | Device, " --version print version information and exit~n", []); 207 | maybe_print_version_opt(false, _) -> 208 | ok. 209 | 210 | %% =================================================================== 211 | %% Print version 212 | %% =================================================================== 213 | 214 | print_version(Parser) -> 215 | print_version(standard_error, Parser, []). 216 | 217 | print_version(Device, Parser) -> 218 | print_version(Device, Parser, []). 219 | 220 | print_version(Device, Parser, Opts) -> 221 | Prog = cli_parser:prog(Parser), 222 | {Version, Extra} = split_parser_version(Parser), 223 | print_program_and_version(Device, Prog, Version), 224 | print_version_extra(Device, Extra, init_fmt(Opts)). 225 | 226 | split_parser_version(Parser) -> 227 | [Version|Extra] = split_lines(cli_parser:version(Parser)), 228 | {Version, Extra}. 229 | 230 | print_program_and_version(Device, Prog, Version) -> 231 | io:format(Device, "~s ~s~n", [Prog, Version]). 232 | 233 | print_version_extra(_Device, [], _Fmt) -> ok; 234 | print_version_extra(Device, Extra, Fmt) -> 235 | io:format(Device, formatted_version_extra(Extra, Fmt), []). 236 | 237 | formatted_version_extra(Pars, #fmt{page_width=Width}) -> 238 | prettypr:format(pars_doc(Pars), Width, Width). 239 | 240 | %% =================================================================== 241 | %% Print error 242 | %% =================================================================== 243 | 244 | print_error(Err, Parser) -> 245 | print_error(standard_error, Err, Parser). 246 | 247 | print_error(Device, Err, Parser) -> 248 | {SuggestHelp, Msg} = format_error_msg(Err), 249 | print_prog_msg(Device, Parser, Msg), 250 | maybe_print_help_suggestion(SuggestHelp, Device, Parser). 251 | 252 | print_prog_msg(Device, Parser, Msg) -> 253 | Prog = cli_parser:prog(Parser), 254 | io:format(Device, "~s: ~s~n", [Prog, Msg]). 255 | 256 | maybe_print_help_suggestion(true, Device, Parser) -> 257 | Prog = cli_parser:prog(Parser), 258 | io:format(Device, "Try '~s --help' for more information.~n", [Prog]); 259 | maybe_print_help_suggestion(false, _Device, _Parser) -> 260 | ok. 261 | 262 | format_error_msg({unknown_opt, Name}) -> 263 | {true, io_lib:format("unrecognized option '~s'", [Name])}; 264 | format_error_msg({missing_opt_arg, _Key, Name}) -> 265 | {true, io_lib:format("option '~s' requires an argument", [Name])}; 266 | format_error_msg({unexpected_opt_arg, _Key, Name}) -> 267 | {true, io_lib:format("option '~s' doesn't allow an argument", [Name])}; 268 | format_error_msg({unknown_command, Name}) -> 269 | {true, io_lib:format("unrecognized command '~s'", [Name])}; 270 | format_error_msg(missing_command) -> 271 | {true, "this program requires a command"}; 272 | format_error_msg({unexpected_pos_arg, Arg}) -> 273 | {true, io_lib:format("unexpected argument '~s'", [Arg])}; 274 | format_error_msg(missing_pos_arg) -> 275 | {true, "missing one or more arguments"}; 276 | format_error_msg(Msg) when is_list(Msg); is_binary(Msg) -> 277 | {false, Msg}. 278 | 279 | %% =================================================================== 280 | %% Print usage error 281 | %% =================================================================== 282 | 283 | print_usage_error(Parser) -> 284 | print_usage_error(standard_error, Parser). 285 | 286 | print_usage_error(Device, Parser) -> 287 | Prog = cli_parser:prog(Parser), 288 | print_usage(Device, Parser), 289 | io:format(Device, "Try '~s --help' for more information.~n", [Prog]). 290 | 291 | %% =================================================================== 292 | %% Helpers 293 | %% =================================================================== 294 | 295 | split_lines(Str) -> 296 | re:split(Str, "\n", [{return, list}]). 297 | 298 | pars_doc(Pars) -> 299 | prettypr:par([prettypr:break(text_par_or_empty(Par)) || Par <- Pars]). 300 | 301 | text_par_or_empty("") -> prettypr:empty(); 302 | text_par_or_empty("!!" ++ Text) -> prettypr:text(Text); 303 | text_par_or_empty(Text) -> prettypr:text_par(Text). 304 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/cli_parser.erl: -------------------------------------------------------------------------------- 1 | -module(cli_parser). 2 | 3 | -export([new/2, prog/1, usage/1, version/1, desc/1, options/1, 4 | commands/1, is_command_parser/1, visible/1, parse_args/2]). 5 | 6 | -record(parser, {prog, usage, version, desc, opts, cmds, pos_args, visible}). 7 | -record(ps, {p, opts, mode}). % parse state (ps) 8 | -record(lo, {k, s, l, arg}). % lookup option (lo) 9 | 10 | %% =================================================================== 11 | %% New 12 | %% =================================================================== 13 | 14 | new(Prog, Opts) -> 15 | #parser{ 16 | prog=Prog, 17 | usage=proplists:get_value(usage, Opts), 18 | version=proplists:get_value(version, Opts), 19 | desc=proplists:get_value(desc, Opts), 20 | opts=proplists:get_value(options, Opts, []), 21 | cmds=proplists:get_value(commands, Opts, []), 22 | pos_args=proplists:get_value(pos_args, Opts, any), 23 | visible=not proplists:get_bool(hidden, Opts)}. 24 | 25 | %% =================================================================== 26 | %% Attrs 27 | %% =================================================================== 28 | 29 | prog(#parser{prog=Prog}) -> Prog. 30 | 31 | usage(#parser{usage=Usage}) -> Usage. 32 | 33 | version(#parser{version=Version}) -> Version. 34 | 35 | desc(#parser{desc=Desc}) -> Desc. 36 | 37 | options(#parser{opts=Opts}) -> Opts. 38 | 39 | commands(#parser{cmds=Cmds}) -> Cmds. 40 | 41 | is_command_parser(#parser{cmds=Cmds}) -> length(Cmds) > 0. 42 | 43 | visible(#parser{visible=Visible}) -> Visible. 44 | 45 | %% =================================================================== 46 | %% Parse args 47 | %% =================================================================== 48 | 49 | parse_args(Args, RootParser) -> 50 | Tokens = tokenize(Args), 51 | {ParseResult, ActiveParser} = parse_tokens(Tokens, RootParser), 52 | finalize(ParseResult, ActiveParser). 53 | 54 | %% ------------------------------------------------------------------- 55 | %% Tokenize 56 | %% ------------------------------------------------------------------- 57 | 58 | tokenize(Args) -> 59 | acc_tokens(Args, has_opts, []). 60 | 61 | acc_tokens([Arg|Rest], all_args, Acc) -> 62 | acc_tokens(Rest, all_args, [{arg, Arg}|Acc]); 63 | acc_tokens(["--"|Rest], has_opts, Acc) -> 64 | acc_tokens(Rest, all_args, [argsep|Acc]); 65 | acc_tokens(["--"++Long|Rest], has_opts, Acc) -> 66 | acc_tokens(Rest, has_opts, [long_opt(Long)|Acc]); 67 | acc_tokens(["-"++Short|Rest], has_opts, Acc) when length(Short) > 0 -> 68 | acc_tokens(Rest, has_opts, [short_opt(Short)|Acc]); 69 | acc_tokens([Arg|Rest], has_opts, Acc) -> 70 | acc_tokens(Rest, has_opts, [{arg, Arg}|Acc]); 71 | acc_tokens([], _, Acc) -> 72 | lists:reverse(Acc). 73 | 74 | long_opt(Opt) -> 75 | case re:split(Opt, "=", [{return, list}, {parts, 2}]) of 76 | [Name] -> {long, Name, undefined}; 77 | [Name, Val] -> {long, Name, Val} 78 | end. 79 | 80 | short_opt([Char]) -> {short, [Char], undefined}; 81 | short_opt([Char|MoreChars]) -> {short, [Char], MoreChars}. 82 | 83 | %% ------------------------------------------------------------------- 84 | %% Parse 85 | %% ------------------------------------------------------------------- 86 | 87 | parse_tokens(Tokens, Parser) -> 88 | case is_command_parser(Parser) of 89 | true -> parse_command_tokens(Tokens, Parser); 90 | false -> parse_all_tokens(Tokens, Parser) 91 | end. 92 | 93 | parse_command_tokens(Tokens, Parser) -> 94 | PS = parse_state(Parser, to_pos), 95 | handle_parse_command_tokens(parse_tokens_acc(Tokens, PS, []), PS). 96 | 97 | handle_parse_command_tokens({ok, {Cmd, RestTokens, Parsed}}, PS) -> 98 | try_sub_parse_tokens( 99 | find_sub_parser(Cmd, PS), 100 | Cmd, RestTokens, Parsed, PS); 101 | handle_parse_command_tokens({ok, _Parsed}, #ps{p=Parser}) -> 102 | {{error, missing_command}, Parser}; 103 | handle_parse_command_tokens({error, Err}, #ps{p=Parser}) -> 104 | {{error, Err}, Parser}. 105 | 106 | find_sub_parser(Cmd, #ps{p=#parser{cmds=Cmds}}) -> 107 | case lists:keyfind(Cmd, 1, Cmds) of 108 | {_, _, Parser} -> {ok, Parser}; 109 | false -> error 110 | end. 111 | 112 | try_sub_parse_tokens({ok, Parser}, Cmd, RestTokens, Parsed, _PS0) -> 113 | PS = parse_state(Parser, all), 114 | SubParseResult = sub_parse_tokens(Cmd, RestTokens, Parsed, PS), 115 | {SubParseResult, Parser}; 116 | try_sub_parse_tokens(error, Cmd, _RestTokens, _Parsed, #ps{p=Parser}) -> 117 | SubParseResult = {error, {unknown_command, Cmd}}, 118 | {SubParseResult, Parser}. 119 | 120 | sub_parse_tokens(Cmd, Tokens, Parsed, PS) -> 121 | handle_sub_parse_tokens(parse_tokens_acc(Tokens, PS, Parsed), Cmd). 122 | 123 | handle_sub_parse_tokens({ok, Parsed}, Cmd) -> 124 | {ok, {Cmd, Parsed}}; 125 | handle_sub_parse_tokens({error, Err}, _Cmd) -> 126 | {error, Err}. 127 | 128 | parse_all_tokens(Tokens, Parser) -> 129 | PS = parse_state(Parser, all), 130 | ParseResult = parse_tokens_acc(Tokens, PS, []), 131 | {ParseResult, Parser}. 132 | 133 | parse_tokens_acc([], _PS, Acc) -> 134 | {ok, lists:reverse(Acc)}; 135 | parse_tokens_acc(Tokens, #ps{opts=Opts}=PS, Acc) -> 136 | handle_try_parse(try_parse(Tokens, Opts), PS, Acc). 137 | 138 | handle_try_parse({ok, {{pos, Arg}, NextTokens}}, #ps{mode=to_pos}, Acc) -> 139 | {ok, {Arg, NextTokens, lists:reverse(Acc)}}; 140 | handle_try_parse({ok, {Parsed, NextTokens}}, PS, Acc) -> 141 | parse_tokens_acc(NextTokens, PS, [Parsed|Acc]); 142 | handle_try_parse({ok, NextTokens}, PS, Acc) -> 143 | parse_tokens_acc(NextTokens, PS, Acc); 144 | handle_try_parse({error, Err}, _PS, _Acc) -> 145 | {error, Err}. 146 | 147 | try_parse(Tokens, [LookupOpt|RestLookup]) -> 148 | handle_opt(opt(Tokens, LookupOpt), Tokens, RestLookup); 149 | try_parse([{arg, Arg}|RestTokens], []) -> 150 | {ok, {{pos, Arg}, RestTokens}}; 151 | try_parse([OptToken|_], []) -> 152 | {error, {unknown_opt, opt_token_str(OptToken)}}. 153 | 154 | handle_opt(nomatch, Tokens, Lookup) -> 155 | try_parse(Tokens, Lookup); 156 | handle_opt({ok, {Opt, NextTokens}}, _, _) -> 157 | {ok, {{opt, Opt}, NextTokens}}; 158 | handle_opt({skipped, NextTokens}, _, _) -> 159 | {ok, NextTokens}; 160 | handle_opt({error, Err}, _, _) -> 161 | {error, Err}. 162 | 163 | opt([{long, Name, Val}|Rest], #lo{l=Name}=LO) -> 164 | long_opt({Name, Val}, Rest, LO); 165 | opt([{short, Name, MoreChars}|Rest], #lo{s=Name}=LO) -> 166 | short_opt({Name, MoreChars}, Rest, LO); 167 | opt([argsep|Rest], _) -> 168 | {skipped, Rest}; 169 | opt(_, _) -> 170 | nomatch. 171 | 172 | long_opt(Token, Rest, #lo{arg=yes, k=Key}) -> 173 | long_opt_required_arg(Token, Rest, Key); 174 | long_opt(Token, Rest, #lo{arg=no, k=Key}) -> 175 | long_opt_no_arg(Token, Rest, Key); 176 | long_opt(Token, Rest, #lo{arg=opt, k=Key}) -> 177 | long_opt_maybe_arg(Token, Rest, Key). 178 | 179 | long_opt_required_arg({_Name, Val}, Rest, Key) when Val /= undefined -> 180 | {ok, {{Key, Val}, Rest}}; 181 | long_opt_required_arg({_Name, undefined}, [{arg, Val}|Rest], Key) -> 182 | {ok, {{Key, Val}, Rest}}; 183 | long_opt_required_arg({Name, undefined}, _Rest, Key) -> 184 | {error, {missing_opt_arg, Key, opt_token_str({long, Name})}}; 185 | long_opt_required_arg(_Token, _Rest, _Key) -> 186 | nomatch. 187 | 188 | long_opt_no_arg({_Name, undefined}, Rest, Key) -> 189 | {ok, {{Key, undefined}, Rest}}; 190 | long_opt_no_arg({Name, _Val}, _Rest, Key) -> 191 | {error, {unexpected_opt_arg, Key, opt_token_str({long, Name})}}; 192 | long_opt_no_arg(_Token, _Rest, _Key) -> 193 | nomatch. 194 | 195 | long_opt_maybe_arg({_Name, Val}, Rest, Key) when Val /= undefined -> 196 | {ok, {{Key, Val}, Rest}}; 197 | long_opt_maybe_arg({_Name, undefined}, [{arg, Val}|Rest], Key) -> 198 | {ok, {{Key, Val}, Rest}}; 199 | long_opt_maybe_arg({_Name, undefined}, Rest, Key) -> 200 | {ok, {{Key, ""}, Rest}}; 201 | long_opt_maybe_arg(_Token, _Rest, _Key) -> 202 | nomatch. 203 | 204 | short_opt(Token, Rest, #lo{arg=yes, k=Key}) -> 205 | short_opt_required_arg(Token, Rest, Key); 206 | short_opt(Token, Rest, #lo{arg=no, k=Key}) -> 207 | short_opt_no_arg(Token, Rest, Key); 208 | short_opt(Token, Rest, #lo{arg=opt, k=Key}) -> 209 | short_opt_maybe_arg(Token, Rest, Key). 210 | 211 | short_opt_required_arg({_Name, Val}, Rest, Key) when Val /= undefined -> 212 | {ok, {{Key, Val}, Rest}}; 213 | short_opt_required_arg({_Name, undefined}, [{arg, Val}|Rest], Key) -> 214 | {ok, {{Key, Val}, Rest}}; 215 | short_opt_required_arg({Name, undefined}, _Rest, Key) -> 216 | {error, {missing_opt_arg, Key, opt_token_str({short, Name})}}; 217 | short_opt_required_arg(_Token, _Rest, _Key) -> 218 | nomatch. 219 | 220 | short_opt_no_arg({_Name, MoreChars}, Rest, Key) when MoreChars /= undefined -> 221 | {ok, {{Key, undefined}, apply_more_short_chars(MoreChars, Rest)}}; 222 | short_opt_no_arg({_Name, undefined}, Rest, Key) -> 223 | {ok, {{Key, undefined}, Rest}}; 224 | short_opt_no_arg(_Token, _Rest, _Key) -> 225 | nomatch. 226 | 227 | apply_more_short_chars([Char], Tokens) -> 228 | [{short, [Char], undefined}|Tokens]; 229 | apply_more_short_chars([Char|Rest], Tokens) -> 230 | [{short, [Char], Rest}|Tokens]. 231 | 232 | short_opt_maybe_arg({_Name, Val}, Rest, Key) when Val /= undefined -> 233 | {ok, {{Key, Val}, Rest}}; 234 | short_opt_maybe_arg({_Name, undefined}, [{arg, Val}|Rest], Key) -> 235 | {ok, {{Key, Val}, Rest}}; 236 | short_opt_maybe_arg({_Name, undefined}, Rest, Key) -> 237 | {ok, {{Key, ""}, Rest}}; 238 | short_opt_maybe_arg(_Token, _Rest, _Key) -> 239 | nomatch. 240 | 241 | opt_token_str({long, Name}) -> "--" ++ Name; 242 | opt_token_str({long, Name, _}) -> "--" ++ Name; 243 | opt_token_str({short, Name}) -> "-" ++ Name; 244 | opt_token_str({short, Name, _}) -> "-" ++ Name. 245 | 246 | %% ------------------------------------------------------------------- 247 | %% Parse state 248 | %% ------------------------------------------------------------------- 249 | 250 | parse_state(Parser, Mode) -> 251 | #ps{p=Parser, opts=opts_lookup(Parser), mode=Mode}. 252 | 253 | opts_lookup(#parser{opts=Opts}) -> 254 | [lookup_opt(Opt) || Opt <- Opts]. 255 | 256 | lookup_opt(Opt) -> 257 | #lo{ 258 | k=cli_opt:key(Opt), 259 | s=strip_short(cli_opt:short(Opt)), 260 | l=strip_long(cli_opt:long(Opt)), 261 | arg=lo_arg(Opt) 262 | }. 263 | 264 | strip_short(undefined) -> undefined; 265 | strip_short("-"++Short) -> Short. 266 | 267 | strip_long(undefined) -> undefined; 268 | strip_long("--"++Long) -> Long. 269 | 270 | lo_arg(Opt) -> 271 | case cli_opt:has_arg(Opt) of 272 | yes -> yes; 273 | optional -> opt; 274 | no -> no 275 | end. 276 | 277 | %% ------------------------------------------------------------------- 278 | %% Finalize 279 | %% ------------------------------------------------------------------- 280 | 281 | finalize({ok, {Cmd, Parsed}}, Parser) -> 282 | {Opts, PosArgs} = finalize_acc(Parsed, [], []), 283 | handle_validate_pos_args( 284 | validate_pos_args(PosArgs, Parser), 285 | {Cmd, Opts, PosArgs}, Parser); 286 | 287 | finalize({ok, Parsed}, Parser) -> 288 | {Opts, PosArgs} = finalize_acc(Parsed, [], []), 289 | handle_validate_pos_args( 290 | validate_pos_args(PosArgs, Parser), 291 | {Opts, PosArgs}, Parser); 292 | finalize({error, {unknown_opt, "--help"}}, Parser) -> 293 | handle_unknown_help_opt(Parser); 294 | finalize({error, {unknown_opt, "--version"}=Err}, Parser) -> 295 | handle_unknown_version_opt(Parser, Err); 296 | finalize({error, Err}, Parser) -> 297 | {{error, Err}, Parser}. 298 | 299 | validate_pos_args(Args, #parser{pos_args=Spec}) -> 300 | check_arg_len(Args, Spec). 301 | 302 | check_arg_len(_, any) -> ok; 303 | check_arg_len([], 0) -> ok; 304 | check_arg_len([], {0, _}) -> ok; 305 | check_arg_len([], {any, _}) -> ok; 306 | check_arg_len([Arg|_], 0) -> {error, {unexpected_pos_arg, Arg}}; 307 | check_arg_len([Arg|_], {_, 0}) -> {error, {unexpected_pos_arg, Arg}}; 308 | check_arg_len([], N) when N > 0 -> {error, missing_pos_arg}; 309 | check_arg_len([], {N, _}) when N > 0 -> {error, missing_pos_arg}; 310 | check_arg_len([_|Rest], N) -> check_arg_len(Rest, decr_arg_pos(N)). 311 | 312 | decr_arg_pos(any) -> any; 313 | decr_arg_pos(N) when is_integer(N), N > 0 -> N - 1; 314 | decr_arg_pos(N) when is_integer(N) -> N; 315 | decr_arg_pos({N, M}) -> {decr_arg_pos(N), decr_arg_pos(M)}. 316 | 317 | handle_validate_pos_args(ok, ParseResult, Parser) -> 318 | {{ok, ParseResult}, Parser}; 319 | handle_validate_pos_args({error, Err}, _ParseResult, Parser) -> 320 | {{error, Err}, Parser}. 321 | 322 | handle_unknown_help_opt(Parser) -> 323 | {{ok, print_help}, Parser}. 324 | 325 | handle_unknown_version_opt(#parser{version=undefined}=Parser, Err) -> 326 | {{error, Err}, Parser}; 327 | handle_unknown_version_opt(Parser, _Err) -> 328 | {{ok, print_version}, Parser}. 329 | 330 | finalize_acc([{opt, {Key, undefined}}|Rest], Opts, PosArgs) -> 331 | finalize_acc(Rest, [Key|Opts], PosArgs); 332 | finalize_acc([{opt, {Key, Val}}|Rest], Opts, PosArgs) -> 333 | finalize_acc(Rest, [{Key, Val}|Opts], PosArgs); 334 | finalize_acc([{pos, Arg}|Rest], Opts, PosArgs) -> 335 | finalize_acc(Rest, Opts, [Arg|PosArgs]); 336 | finalize_acc([], Opts, PosArgs) -> 337 | {lists:reverse(Opts), lists:reverse(PosArgs)}. 338 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Goals 6 | 7 | - Support for full featured command line interfaces (CLIs) in Erlang 8 | - Trivially embeddable library, not a framework 9 | - Consistent with mainstream POSIX utility conventions 10 | - Support for commands with sub-parsers 11 | 12 | # Design notes 13 | 14 | The following is a collection of design notes inspired by various 15 | publicly available sources, which are mentioned where applicable. 16 | 17 | ## Notes from The Open Group Base Specifications Issue 7 18 | 19 | See the 20 | [Utility Conventions](http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12) 21 | chapter. 22 | 23 | Note that *12.2 Utility Syntax Guidelines* in the above documentation 24 | provides a number of guidelines. These are supported by *erlang-cli* 25 | (i.e. they are not prohibited either technically or by convention) but 26 | they are not strictly enforced. The one exception is guideline 9: "All 27 | options should precede operands on the command line" which is enforced 28 | currently (see notes below) 29 | 30 | Options are always listed before positional arguments. This conforms 31 | to the Utility Argument Syntax. However, it diverges from Python's 32 | argparse, which allows options to be specified after positional 33 | arguments. This decision is opinionated and controversial as it's 34 | technically possible to differentiate between positional arguments and 35 | options. For the time being however it's made for simplicity and 36 | conformance to the POSIX guidelines. 37 | 38 | This rule will have to be relaxed in the case of commands, which 39 | represent sub-programs and should have their own set of options and 40 | positional arguments. 41 | 42 | Options may have either short forms, long forms, or both short and 43 | long forms. Options that do not have arguments may be specified 44 | together using their short form as a single argument. 45 | 46 | The POSIX guidelines provides for two methods of showing command 47 | usage. The first lists options together, as in: 48 | 49 | utility_name [-abcDxyz][-p arg][operand] 50 | 51 | The second uses a catch all `[options]` placeholder, as in: 52 | 53 | utility_name [options][operands] 54 | 55 | As a simplifying step *erlang-cli* will use the second form for 56 | options, relying on the full help content below the usage 57 | prologue. This is also controversial, especially for very simple 58 | programs. It may be desirable to parameterize this behavior. 59 | 60 | Option arguments and positional arguments that are optional (i.e. are 61 | not required) are specified in usage or help content using surrounding 62 | square brackets '[' and ']'. 63 | 64 | Ellipses "..." are used to indicate that the preceding option or 65 | positional argument may be repeated. 66 | 67 | ## Argument types and validation 68 | 69 | If an option argument or positional argument can be converted to a 70 | number, it will be. Both integers and floats will be supported. A 71 | parser however may modify this behavior to force values to be parsed 72 | as text. 73 | 74 | *erlang-cli* should support a reasonably rich set of validation rules 75 | to ensure that parsed values may be used directly without further 76 | validation. This must include ultimately the ability to apply a 77 | function to raw input to both validate and generate values as needed. 78 | 79 | Usage and help text must be wrapped where possible. Wrapping rules are 80 | yet to be defined. 81 | 82 | ## API conventions 83 | 84 | *erlang-cli* makes use of internal types but does not expose those to 85 | users. 86 | 87 | Required arguments are specified as the initial arguments for a 88 | function. Optional arguments are either specified by using functions 89 | with higher arity (i.e. they accept more arguments) or as proplists. 90 | 91 | Results follow the Erlang conventions of using tagged two-tuples for 92 | both success and error results, or untagged values and exceptions. 93 | 94 | Optional arguments lists are specified using proplists rather than maps. 95 | 96 | Resulting lists of parsed and validated arguments use proplists rather 97 | than maps. 98 | 99 | ## Usage and help 100 | 101 | *erlang-cli* will provide reasonable default formatting for usage and 102 | help text. As stated in goals, *erlang-cli* will produce output that 103 | is consistent with POSIX usage/synopsis guidelines. 104 | 105 | *erlang-cli* will support custom usage and help formatting and content 106 | where reasonable. 107 | 108 | Some of the POSIX utilities used to establish formatting guidelines include: 109 | 110 | - ls 111 | - cp 112 | - sed 113 | - grep 114 | - mkdir 115 | - tar 116 | 117 | The observations below generally hold true for these utilities. 118 | 119 | Help text starts with single line usage examples: 120 | 121 | Usage: 122 | 123 | The word usage is always capitalized. 124 | 125 | Some utilities list multiple usage patterns. This is arguably clearer 126 | than providing complex syntax for argument configurations in a single 127 | usage example. It's not clear how multiple usage examples might be 128 | implemented in *erlang-cli*. 129 | 130 | Immediately following the usage line is usually a single line 131 | describing the command. The single line description is a complete 132 | sentence - i.e. ends with a period. Additional descriptive paragraphs 133 | may follow, though it seems by convention that the "front matter" of 134 | utility help is kept to a minimum and detailed help is provided at the 135 | end of the options list. 136 | 137 | Help text appears to always be wrap at column 79. 138 | 139 | After the command description, there's a list of options. 140 | 141 | Options are listed with their short and/or long form, followed by 142 | spaces up to column 30, where the option description begins. If an 143 | option description needs to wrap, it is indented on the next line at 144 | column 30. 145 | 146 | Option short and long forms are formatted as follows: 147 | 148 | ' ' [short_form] [', '] [long_form] 149 | 150 | where at least one of `short_form` or `long_form` appears and `', '` 151 | appears when both `short_form` and `long_form` appear. 152 | 153 | It's common to see two options, which appear at the end of the list of 154 | options and only in long form: 155 | 156 | --help display this help and exit 157 | --version output version information and exit 158 | 159 | None of these utilities support commands. 160 | 161 | Commands however may be supported in a separate list using a similar 162 | format to options. 163 | 164 | ## Formatting option help 165 | 166 | There are a few schemes in practice for formatting option help. 167 | 168 | Nearly all commands represent an option using the grammar listed 169 | above. E.g. '-f/--force' would appear as: 170 | 171 | -f, --force 172 | 173 | The help text formatting however differs across commands. 174 | 175 | Some formats maintain a left margin starting at col 31. E.g. 176 | 177 | -f, --force do not prompt before overwriting 178 | 179 | 01234567890123456789012345678901234567890123456789012345678901234567890123456789 180 | 0 1 2 3 4 5 6 7 181 | 182 | Long lines (past col 79) are wrapped, either at col 31 or col 33 183 | (i.e. a two space indent on subsequent lines). E.g. 184 | 185 | -u, --update move only when the SOURCE file is newer 186 | than the destination file or when the 187 | destination file is missing 188 | 189 | If an option name would extend into the help text, the help text is 190 | shifted accordingly to ensure two spaces between the itself and the 191 | name. E.g. 192 | 193 | -t, --target-directory=DIRECTORY move all SOURCE arguments into DIRECTORY 194 | 195 | Some commands use column alignment that's different from 30, though 196 | they are less common. 197 | 198 | Some commands, e.g. sed, display help text on a new line following the 199 | option name. 200 | 201 | In the interest of consistency with the majority of standard POSIX 202 | commands, we will adopt these rules: 203 | 204 | - Options names are displayed using the standard '-h, --help' style 205 | - Option help text will begin on the same line as the name starting at 206 | column COL 207 | - Long option help text will be wrapped to subsequent lines as needed 208 | with each line starting at column COL 209 | 210 | COL may be 31 or some lower number. Ideally it would be configurable. 211 | 212 | Subsequent lines of help text will not be indented by two spaces, as 213 | is a common practice for commands. This added level of indentation 214 | doesn't seem to help readability while only adding irregularity. 215 | 216 | Here's an example from `tar`: 217 | 218 | -k, --keep-old-files don't replace existing files when extracting, 219 | treat them as errors 220 | --keep-directory-symlink preserve existing symlinks to directories when 221 | extracting 222 | --keep-newer-files don't replace existing files that are newer than 223 | their archive copies 224 | 225 | 01234567890123456789012345678901234567890123456789012345678901234567890123456789 226 | 0 1 2 3 4 5 6 7 227 | 228 | ## Help for positional arguments 229 | 230 | The help output for the most utilities does not include specific help 231 | for positional arguments. Instead, positional arguments are 232 | described in general help text. 233 | 234 | This doesn't make a lot of sense I think - if options have help text I 235 | think so too should positional arguments. 236 | 237 | Python's argparser provides help text for each positional argument. 238 | 239 | Python's Click framework has this to say: 240 | 241 | > Arguments cannot be documented this way. This is to follow the 242 | > general convention of Unix tools of using arguments for only the 243 | > most necessary things and to document them in the introduction text 244 | > by referring to them by name. 245 | 246 | Their observation is correct, however it seems odd to deliberately 247 | remove a feature that would appear to provide additional information. 248 | 249 | I'm tempted to provide argument help text under an "Arguments" heading 250 | (or possibly no heading at all), but let users optionally hide that 251 | section to conform with the POSIX convention (note I haven't seen this 252 | documented explicitly, but it's certainly the convention). 253 | 254 | For example: 255 | 256 | Usage: hello [OPTION]... MSG 257 | Prints a message to the console. 258 | 259 | MSG message to print 260 | 261 | Options: 262 | 263 | -C, --caps print message in caps 264 | 265 | Alternatively, to conform to the POSIX convention, the parser can be 266 | created with a `hide_arg_help` flag, in which case help text like this 267 | can be generated: 268 | 269 | Usage: hello [OPTION]... MSG 270 | Prints MSG to the console. 271 | 272 | Options: 273 | 274 | -C, --caps print message in caps 275 | 276 | This is arguably better -- it's shorter and more natural. And indeed 277 | if the list of arguments is short, as it should be, this form of 278 | documentation is superior. 279 | 280 | We can decide which default behavior is preferable and name the parser 281 | option accordingly (i.e. show_arg_help vs hide_arg_help). 282 | 283 | ## Help text width 284 | 285 | The standard in all of the above utilities is to wrap text at 286 | col 79. No attempt is made to wrap according to the terminal width. 287 | 288 | Here are some reasons why this is a reasonable approach: 289 | 290 | - Getting the correct terminal width is not always trivial - it 291 | introduces moving parts that can fail 292 | - Anyone using a terminal width of less than 80 will be suffering anyway 293 | - Text wrapping past 79 is harder to read 294 | 295 | ## Argument names 296 | 297 | Rather than infer an argument name, `erlang-cli` will require the name 298 | up front when defining the argument. If the argument is an option, 299 | sensible defaults will be used for the option name. This shifts the 300 | emphasis away from the user interface to the data structure. 301 | 302 | This is the simplest argument definition: 303 | 304 | cli:arg(foo, "a positional arg") 305 | 306 | An option may be specified using additional argument options: 307 | 308 | cli:arg(bar, "an option", [option]) 309 | 310 | Alternatively: 311 | 312 | cli:option(bar, "an option") 313 | 314 | By default an option may be specified using its long form, which is 315 | `"--" + NAME`, where `NAME` is the argument atom converted to a 316 | string, substituting underscores (`_`) with hyphens (`-`). 317 | 318 | In the example above, the value for `bar` may be specified using 319 | `--bar VALUE` or `--bar=VALUE`. 320 | 321 | Option names may be explicitly provided using the `name` option: 322 | 323 | cli:arg(bar, "an option", [option, {name, "-b, --bar"}]) 324 | cli:option(bar, "an option", [{name, "-b, --bar"}) 325 | 326 | or alternatively with a two-tuple for `option`: 327 | 328 | cli:arg(bar, "an option", [{option, "-b, --bar"}]) 329 | 330 | ## Optional vs required values 331 | 332 | Positional arguments may be required or not. In one respect, an 333 | optional positional argument could be considered an option. It may 334 | however be more natural to use a positional. For example, a missing 335 | FILE argument is often a signal to read from standard input. 336 | 337 | An option may in turn have an optional value. Or, in other words, the 338 | option will use a sensible default value if one isn't provided by the 339 | user. 340 | 341 | The question: how do we indicate whether or not an argument value is 342 | required or optional? 343 | 344 | One of the problems with 'required' and 'optional' is that the concept 345 | collides with a similar distinction presented by 'positional argument' 346 | and 'option'. A positional argument is generally a value that is 347 | expected from the user, which an option is not. 348 | 349 | What we're talking about here is the nature of the _value_ I think. 350 | 351 | For a positional argument we should presume that the user _must_ 352 | specify a value, unless otherwise specified. For example, this 353 | positional argument should be required: 354 | 355 | cli:arg(file, "file to read; use '-' for stdin") 356 | 357 | However, if a default is provided, we can use that if the user omits 358 | the value: 359 | 360 | cli:arg(file, "file to read; use '-' to for stdin (default)", 361 | [{default, "-"}]) 362 | 363 | In the first case, help text would be something like this: 364 | 365 | Usage: myprog FILE 366 | 367 | In the second case: 368 | 369 | Usage: myprog [FILE] 370 | 371 | What about options? I think the same scheme applies. Here's an option 372 | that expects a value: 373 | 374 | cli:arg(file, "file to read", [option]) 375 | 376 | and one whose value may be omitted: 377 | 378 | cli:arg(file, "file to read", [option, {default, "-"}]) 379 | 380 | The help for the first: 381 | 382 | --file=VALUE file to read 383 | 384 | and for the second: 385 | 386 | --file[=VALUE] file to read 387 | 388 | Flags then become syntactic sugar for bool options with default of 389 | false. So these are equivalent: 390 | 391 | cli:arg(caps, "print in caps", [flag]) 392 | 393 | cli:arg(caps, "print in caps", [option, {type, bool}, {default, false}]) 394 | 395 | UPDATE: This approach isn't going to work for obvious reasons. Options 396 | may not be specified and therefore a default value suggests the value 397 | for the arg when the option is not provided. It only works for 398 | positional arguments. 399 | 400 | "Value argument" can be used however. These values apply: 401 | 402 | - optional 403 | - required 404 | - none 405 | 406 | This is similar to `narg` from Python's argparser, but applies to zero 407 | or one arguments rather than zero or more. 408 | 409 | ## Data validation 410 | 411 | I'm inclined to not validate or otherwise interpret values and simply 412 | parsed the arguments and provide values as strings. If an option 413 | doesn't have a value, it appears in the list of options without a 414 | value (as is the standard for Erlang proplists). 415 | 416 | ## Commands and subparsers 417 | 418 | NOTE: Support for commands will be added once the base/root 419 | functionality is implemented. 420 | 421 | Each parser must support optional *commands*, each of which is 422 | associated with another parser. This allows a single command line 423 | utility to perform multiple actions, each corresponding to a command 424 | name. 425 | 426 | Commands may not be nested. This is controversial, but is a 427 | simplifying decision for now. Developers should consider using command 428 | name spaces, separating tier with hyphens. For example, if a nested 429 | command might be this: 430 | 431 | mycli foo list [OPTION]... [ARG]... 432 | mycli foo create [OPTION]... [ARG]... 433 | mycli foo delete [OPTION]... [ARG]... 434 | 435 | Consider a flattened command structure: 436 | 437 | mycli foo-list [OPTION]... [ARG]... 438 | mycli foo-create [OPTION]... [ARG]... 439 | mycli foo-delete [OPTION]... [ARG]... 440 | 441 | Common options and position arguments may be duplicated as needed 442 | using macros, variables, functions - or any combination thereof! 443 | 444 | # Roadmap ideas 445 | 446 | - Commands 447 | - Provide default argument values using environment variables 448 | - User prompts 449 | - Autocomplete 450 | - Manpage integration 451 | - Plain text documentation (i.e. storing help text in a formatted 452 | plain text file) 453 | - Option sections (see `tar --help`) 454 | 455 | # Best practices 456 | 457 | Command and option help should not be capitalized, end with period, or 458 | otherwise be treated as complete sentences. They are short descriptive 459 | blurbs. Use semi-colons to add additional blurb. 460 | 461 | # Glossary 462 | 463 | **Argument** 464 | : An element in the array of arguments passed to a program 465 | 466 | **Command** 467 | : An single positional argument that represent a command to execute 468 | and indicates that a sub-parser should be used handle subsequent 469 | arguments 470 | 471 | **Option** 472 | : An argument consisting of one or two leading hyphen characters 473 | followed by letters or digits 474 | 475 | **Option Argument** 476 | : An argument immediately following an option 477 | 478 | **Parser** 479 | : A representation of command line parser that can be used to process 480 | raw command line arguments 481 | 482 | **Positional Argument** 483 | : An argument that follows the last option or option argument 484 | 485 | **Required Argument** 486 | : A positional argument that must be provided 487 | 488 | **Required Option Argument** 489 | : An option argument that must be provided 490 | 491 | **Short Form Option** 492 | : An option consisting of a single leading hyphen followed by a single 493 | character or digit 494 | 495 | **Long Form Option** 496 | : An option consisting of two leading hyphens followed by one or more 497 | characters or digits 498 | 499 | # To Do 500 | 501 | - `--help` and `--version` options are hard coded, and this isn't 502 | good - provide a flag associated with the parser to disable 503 | either/both (maybe) 504 | 505 | - There's no built-in indication that "COMMAND --help" is available; 506 | the user can provide that cue in the usage, but it's an added burden 507 | for something that's always available 508 | 509 | - Using prettypr:text_par removes all spacing from help text. This is 510 | right in some cases but we need to support an explicit "don't 511 | reformat" symbol, maybe appearing at the start of a par - in which 512 | case we use prettpr:text, which preserves formatting. 513 | 514 | - The ronn format form man pages is inherently better for help 515 | text. It'd be nice to interface with external markdown files in an 516 | elegant way. 517 | --------------------------------------------------------------------------------