-module(hoist). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/hoist.gleam"). -export([new_flag/1, with_long_alias/2, with_short_alias/2, as_toggle/1, as_count/1, validate_flag_specs/1, parse_with_hook/4, parse/2]). -export_type([parse_error/1, flag_kind/0, flag_spec/0, validated_flag_specs/0, flag_spec_validation_error/0, flag/0, args/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. -type parse_error(DRV) :: {unknown_flag, binary()} | {value_not_provided, binary()} | {value_not_supported, binary(), binary()} | {custom_error, DRV}. -type flag_kind() :: value_kind | toggle_kind | count_kind. -opaque flag_spec() :: {flag_spec, binary(), gleam@set:set(binary()), gleam@set:set(binary()), flag_kind()}. -opaque validated_flag_specs() :: {validated_flag_specs, gleam@dict:dict(binary(), flag_spec()), gleam@dict:dict(binary(), flag_spec())}. -type flag_spec_validation_error() :: {empty_name, flag_spec()} | {invalid_name_or_alias, binary(), flag_spec()} | {invalid_short_alias, binary(), flag_spec()}. -type flag() :: {value_flag, binary(), binary()} | {toggle_flag, binary()} | {count_flag, binary(), integer()}. -type args() :: {args, list(binary()), list(flag())}. -file("src/hoist.gleam", 19). -spec replace_error_flag_name(parse_error(DRW), binary()) -> parse_error(DRW). replace_error_flag_name(Error, Flag_name) -> case Error of {custom_error, _} -> Error; {unknown_flag, _} -> Error; {value_not_provided, _} -> {value_not_provided, Flag_name}; {value_not_supported, _, Given} -> {value_not_supported, Flag_name, Given} end. -file("src/hoist.gleam", 56). ?DOC(" Creates a new flag with the given long name, e.g. `--name`. Case sensitive.\n"). -spec new_flag(binary()) -> flag_spec(). new_flag(Name) -> {flag_spec, Name, gleam@set:new(), gleam@set:new(), value_kind}. -file("src/hoist.gleam", 64). ?DOC( " Adds a long alias for a command, e.g. `--name, --first-name`.\n" "\n" " A flag can have multiple long aliases. These are stored in a\n" " set and will be deduplicated. Case sensitive.\n" ). -spec with_long_alias(flag_spec(), binary()) -> flag_spec(). with_long_alias(Flag, Alias) -> {flag_spec, erlang:element(2, Flag), gleam@set:insert(erlang:element(3, Flag), Alias), erlang:element(4, Flag), erlang:element(5, Flag)}. -file("src/hoist.gleam", 72). ?DOC( " Adds a short alias for a command, e.g. `--name, -n`.\n" "\n" " A flag can have multiple short aliases. These are stored in a\n" " set and will be deduplicated. Case sensitive.\n" ). -spec with_short_alias(flag_spec(), binary()) -> flag_spec(). with_short_alias(Flag, Short) -> {flag_spec, erlang:element(2, Flag), erlang:element(3, Flag), gleam@set:insert(erlang:element(4, Flag), Short), erlang:element(5, Flag)}. -file("src/hoist.gleam", 79). ?DOC( " Designates this flag as a toggle-kind flag. Generally used to enable\n" " or disable an option, e.g. `--colour`, `--no-colour`. Does not\n" " receive a value.\n" ). -spec as_toggle(flag_spec()) -> flag_spec(). as_toggle(Flag) -> {flag_spec, erlang:element(2, Flag), erlang:element(3, Flag), erlang:element(4, Flag), toggle_kind}. -file("src/hoist.gleam", 85). ?DOC( " Designates this flag as a count-kind flag. Often used for verbosity levels,\n" " e.g. `-vvv` would have a count of 3.\n" ). -spec as_count(flag_spec()) -> flag_spec(). as_count(Flag) -> {flag_spec, erlang:element(2, Flag), erlang:element(3, Flag), erlang:element(4, Flag), count_kind}. -file("src/hoist.gleam", 96). -spec flag_name_validation_regex() -> gleam@regexp:regexp(). flag_name_validation_regex() -> Pattern@1 = case gleam@regexp:compile( <<"^[a-zA-Z0-9][a-zA-Z0-9_-]*$"/utf8>>, {options, false, false} ) of {ok, Pattern} -> Pattern; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"hoist"/utf8>>, function => <<"flag_name_validation_regex"/utf8>>, line => 97, value => _assert_fail, start => 2862, 'end' => 3015, pattern_start => 2873, pattern_end => 2884}) end, Pattern@1. -file("src/hoist.gleam", 106). -spec validate_single_flag_spec(flag_spec(), gleam@regexp:regexp()) -> {ok, flag_spec()} | {error, flag_spec_validation_error()}. validate_single_flag_spec(Flag_spec, Validation_regex) -> All_long_names = [erlang:element(2, Flag_spec) | gleam@set:to_list(erlang:element(3, Flag_spec))], Long_name_validation_result = gleam@list:try_each( All_long_names, fun(Name) -> case Name of <<""/utf8>> -> {error, {empty_name, Flag_spec}}; Name@1 -> case gleam@regexp:check(Validation_regex, Name@1) of true -> {ok, nil}; false -> {error, {invalid_name_or_alias, Name@1, Flag_spec}} end end end ), gleam@result:'try'( Long_name_validation_result, fun(_) -> Short_name_validation_result = gleam@list:try_each( gleam@set:to_list(erlang:element(4, Flag_spec)), fun(Short) -> case string:length(Short) of 0 -> {error, {empty_name, Flag_spec}}; 1 -> case gleam@regexp:check(Validation_regex, Short) of true -> {ok, nil}; false -> {error, {invalid_short_alias, Short, Flag_spec}} end; _ -> {error, {invalid_short_alias, Short, Flag_spec}} end end ), gleam@result:'try'( Short_name_validation_result, fun(_) -> {ok, Flag_spec} end ) end ). -file("src/hoist.gleam", 156). ?DOC( " Validates flag specs.\n" "\n" " Ensures that every defined long flag and its aliases:\n" " - Have at least a one character name\n" " - Start with a letter or number\n" " - Only contain letters, numbers, hyphens and underscores\n" "\n" " Ensures that every defined short flag:\n" " - Has exactly one character\n" " - Is a letter or number\n" ). -spec validate_flag_specs(list(flag_spec())) -> {ok, validated_flag_specs()} | {error, list(flag_spec_validation_error())}. validate_flag_specs(Flags) -> {_, Flag_validation_errors} = begin _pipe = gleam@list:map( Flags, fun(_capture) -> validate_single_flag_spec( _capture, flag_name_validation_regex() ) end ), gleam@result:partition(_pipe) end, case Flag_validation_errors of [] -> {Long_flags, Short_flags} = gleam@list:fold( Flags, {maps:new(), maps:new()}, fun(Dicts, Flag) -> {Lf, Stl} = Dicts, Lf@1 = gleam@dict:insert(Lf, erlang:element(2, Flag), Flag), Lf@2 = gleam@set:fold( erlang:element(3, Flag), Lf@1, fun(Curr, Alias) -> gleam@dict:insert(Curr, Alias, Flag) end ), Stl@1 = gleam@set:fold( erlang:element(4, Flag), Stl, fun(Curr@1, Short) -> gleam@dict:insert(Curr@1, Short, Flag) end ), {Lf@2, Stl@1} end ), {ok, {validated_flag_specs, Long_flags, Short_flags}}; Errors -> {error, Errors} end. -file("src/hoist.gleam", 546). -spec upsert_flag(flag(), list(flag())) -> list(flag()). upsert_flag(New_flag, Flags) -> _pipe = gleam@list:filter( Flags, fun(Input) -> erlang:element(2, Input) /= erlang:element(2, New_flag) end ), lists:append(_pipe, [New_flag]). -file("src/hoist.gleam", 551). -spec upsert_count_flag(flag_spec(), list(flag())) -> list(flag()). upsert_count_flag(Flag_spec, Flags) -> Existing_flag_count_result = gleam@list:find_map( Flags, fun(Input) -> case Input of {count_flag, N, Count} when N =:= erlang:element(2, Flag_spec) -> {ok, Count}; _ -> {error, nil} end end ), Count@2 = case Existing_flag_count_result of {ok, Count@1} -> Count@1 + 1; {error, _} -> 1 end, upsert_flag({count_flag, erlang:element(2, Flag_spec), Count@2}, Flags). -file("src/hoist.gleam", 318). -spec handle_positional( binary(), list(binary()), validated_flag_specs(), args(), DTE, fun((DTE, binary(), args(), validated_flag_specs()) -> {ok, {DTE, validated_flag_specs()}} | {error, DTF}) ) -> {DTE, {ok, args()} | {error, parse_error(DTF)}}. handle_positional( New_arg, Remaining_input, Flag_specs, State, Hook_state, Parse_hook ) -> New_args = {args, lists:append(erlang:element(2, State), [New_arg]), erlang:element(3, State)}, case Parse_hook(Hook_state, New_arg, New_args, Flag_specs) of {error, Error} -> {Hook_state, {error, {custom_error, Error}}}; {ok, {New_hook_state, New_flag_specs}} -> do_parse( Remaining_input, New_flag_specs, {ok, New_args}, New_hook_state, Parse_hook ) end. -file("src/hoist.gleam", 249). -spec do_parse( list(binary()), validated_flag_specs(), {ok, args()} | {error, parse_error(DST)}, DSX, fun((DSX, binary(), args(), validated_flag_specs()) -> {ok, {DSX, validated_flag_specs()}} | {error, DST}) ) -> {DSX, {ok, args()} | {error, parse_error(DST)}}. do_parse(Remaining_input, Flag_specs, State, Hook_state, Parse_hook) -> case State of {error, Error} -> {Hook_state, {error, Error}}; {ok, State@1} -> case Remaining_input of [] -> {Hook_state, {ok, State@1}}; [<<"--"/utf8>> | Rest] -> Arguments = lists:append(erlang:element(2, State@1), Rest), {Hook_state, {ok, {args, Arguments, erlang:element(3, State@1)}}}; [<<"--"/utf8, Flag_name/binary>> | Rest@1] -> handle_long_flag( Flag_name, Rest@1, Flag_specs, State@1, Hook_state, Parse_hook ); [<<"-"/utf8>> = Arg | Rest@2] -> handle_positional( Arg, Rest@2, Flag_specs, State@1, Hook_state, Parse_hook ); [<<"-"/utf8, Flag_names/binary>> | Rest@3] -> handle_short_flag( Flag_names, Rest@3, Flag_specs, State@1, Hook_state, Parse_hook ); [Arg@1 | Rest@4] -> handle_positional( Arg@1, Rest@4, Flag_specs, State@1, Hook_state, Parse_hook ) end end. -file("src/hoist.gleam", 238). ?DOC( " Similar to [`parse`](#parse) but allows passing a custom hook that gets called\n" " after a positional argument is parsed.\n" "\n" " The hook accepts the current hook state, the most recent positional argument,\n" " the currently parsed arguments and flags, and the available flag specs.\n" "\n" " The hook can return a new set of flags to be used for parsing the next argument,\n" " along with a new state value.\n" "\n" " Useful when the flags available for parsing depend on the value of a previous\n" " positional argument.\n" ). -spec parse_with_hook( list(binary()), validated_flag_specs(), DSL, fun((DSL, binary(), args(), validated_flag_specs()) -> {ok, {DSL, validated_flag_specs()}} | {error, DSM}) ) -> {DSL, {ok, args()} | {error, parse_error(DSM)}}. parse_with_hook(Input, Flags, Hook_state, Parse_hook) -> State = {args, [], []}, do_parse(Input, Flags, {ok, State}, Hook_state, Parse_hook). -file("src/hoist.gleam", 215). ?DOC( " Parses positional args and flags from a list of args.\n" "\n" " Note: this function does not sanitise input in any way. If you require\n" " sanitisation, e.g. removal of whitespace-only arguments, you must handle\n" " that yourself.\n" ). -spec parse(list(binary()), validated_flag_specs()) -> {ok, args()} | {error, parse_error(any())}. parse(Input, Flags) -> {_, Outcome} = parse_with_hook( Input, Flags, nil, fun(State, _, _, Flags@1) -> {ok, {State, Flags@1}} end ), Outcome. -file("src/hoist.gleam", 343). -spec handle_long_flag( binary(), list(binary()), validated_flag_specs(), args(), DTM, fun((DTM, binary(), args(), validated_flag_specs()) -> {ok, {DTM, validated_flag_specs()}} | {error, DTN}) ) -> {DTM, {ok, args()} | {error, parse_error(DTN)}}. handle_long_flag( Flag_name, Remaining_input, Flag_specs, State, Hook_state, Parse_hook ) -> {Parsed_flag_name, Has_equals_value, Remaining_input@1} = case gleam@string:split_once( Flag_name, <<"="/utf8>> ) of {ok, {Name, Value}} -> {Name, true, [Value | Remaining_input]}; {error, _} -> {Flag_name, false, Remaining_input} end, case gleam_stdlib:map_get(erlang:element(2, Flag_specs), Parsed_flag_name) of {error, _} -> {Hook_state, {error, {unknown_flag, Parsed_flag_name}}}; {ok, Flag_spec} -> case {erlang:element(5, Flag_spec), Has_equals_value} of {value_kind, _} -> case Remaining_input@1 of [Value@1 | Rest] -> do_parse( Rest, Flag_specs, {ok, {args, erlang:element(2, State), lists:append( erlang:element(3, State), [{value_flag, erlang:element(2, Flag_spec), Value@1}] )}}, Hook_state, Parse_hook ); [] -> {Hook_state, {error, {value_not_provided, Parsed_flag_name}}} end; {toggle_kind, true} -> Given = gleam@result:unwrap( gleam@list:first(Remaining_input@1), <<""/utf8>> ), Parse_error = {value_not_supported, Parsed_flag_name, Given}, {Hook_state, {error, Parse_error}}; {count_kind, true} -> Given@1 = gleam@result:unwrap( gleam@list:first(Remaining_input@1), <<""/utf8>> ), Parse_error@1 = {value_not_supported, Parsed_flag_name, Given@1}, {Hook_state, {error, Parse_error@1}}; {toggle_kind, false} -> Flag = {toggle_flag, erlang:element(2, Flag_spec)}, Flags = upsert_flag(Flag, erlang:element(3, State)), do_parse( Remaining_input@1, Flag_specs, {ok, {args, erlang:element(2, State), Flags}}, Hook_state, Parse_hook ); {count_kind, false} -> Flags@1 = upsert_count_flag( Flag_spec, erlang:element(3, State) ), do_parse( Remaining_input@1, Flag_specs, {ok, {args, erlang:element(2, State), Flags@1}}, Hook_state, Parse_hook ) end end. -file("src/hoist.gleam", 434). -spec handle_short_flag( binary(), list(binary()), validated_flag_specs(), args(), DTU, fun((DTU, binary(), args(), validated_flag_specs()) -> {ok, {DTU, validated_flag_specs()}} | {error, DTV}) ) -> {DTU, {ok, args()} | {error, parse_error(DTV)}}. handle_short_flag( Flag_names, Remaining_input, Flag_specs, State, Hook_state, Parse_hook ) -> Graphemes = gleam@string:to_graphemes(Flag_names), case Graphemes of [] -> do_parse( Remaining_input, Flag_specs, {ok, State}, Hook_state, Parse_hook ); [Short_flag | Rest_flags] -> case gleam_stdlib:map_get(erlang:element(3, Flag_specs), Short_flag) of {error, _} -> {Hook_state, {error, {unknown_flag, Short_flag}}}; {ok, Flag} -> case Rest_flags of [] -> {Hook_state@1, Result} = handle_long_flag( erlang:element(2, Flag), Remaining_input, Flag_specs, State, Hook_state, Parse_hook ), Result@1 = gleam@result:map_error( Result, fun(_capture) -> replace_error_flag_name( _capture, Short_flag ) end ), {Hook_state@1, Result@1}; [<<"="/utf8>> | Value_graphemes] -> Value = erlang:list_to_binary(Value_graphemes), case erlang:element(5, Flag) of toggle_kind -> Error = {value_not_supported, Short_flag, Value}, {Hook_state, {error, Error}}; count_kind -> Error = {value_not_supported, Short_flag, Value}, {Hook_state, {error, Error}}; value_kind -> Flag@1 = {value_flag, erlang:element(2, Flag), Value}, Flags = lists:append( erlang:element(3, State), [Flag@1] ), do_parse( Remaining_input, Flag_specs, {ok, {args, erlang:element(2, State), Flags}}, Hook_state, Parse_hook ) end; Rest_flags@1 -> case erlang:element(5, Flag) of value_kind -> Flag@2 = {value_flag, erlang:element(2, Flag), erlang:list_to_binary(Rest_flags@1)}, Flags@1 = upsert_flag( Flag@2, erlang:element(3, State) ), do_parse( Remaining_input, Flag_specs, {ok, {args, erlang:element(2, State), Flags@1}}, Hook_state, Parse_hook ); toggle_kind -> Flag@3 = {toggle_flag, erlang:element(2, Flag)}, Flags@2 = upsert_flag( Flag@3, erlang:element(3, State) ), handle_short_flag( erlang:list_to_binary(Rest_flags@1), Remaining_input, Flag_specs, {args, erlang:element(2, State), Flags@2}, Hook_state, Parse_hook ); count_kind -> handle_short_flag( erlang:list_to_binary(Rest_flags@1), Remaining_input, Flag_specs, {args, erlang:element(2, State), upsert_count_flag( Flag, erlang:element(3, State) )}, Hook_state, Parse_hook ) end end end end.