diff --git a/bashly.gemspec b/bashly.gemspec index 3023e3b4..902ddfa3 100644 --- a/bashly.gemspec +++ b/bashly.gemspec @@ -16,7 +16,7 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 3.2' s.add_dependency 'colsole', '~> 1.0' - s.add_dependency 'completely', '~> 0.8.0.rc6' + s.add_dependency 'completely', '~> 0.8.0' s.add_dependency 'gtx', '~> 0.1.1' s.add_dependency 'listen', '~> 3.9' s.add_dependency 'lp', '~> 0.2.0' @@ -24,11 +24,6 @@ Gem::Specification.new do |s| s.add_dependency 'requires', '~> 1.1' s.add_dependency 'tty-markdown', '~> 0.7.2' - # Missing sub-dependencies - # logger: required and not bundled by `listen` 3.9.0 - # ref: https://github.com/guard/listen/issues/591 - s.add_dependency 'logger', '~> 1.7' - s.metadata = { 'bug_tracker_uri' => 'https://github.com/bashly-framework/bashly/issues', 'changelog_uri' => 'https://github.com/bashly-framework/bashly/blob/master/CHANGELOG.md', diff --git a/lib/bashly/completions/bashly-completions.bash b/lib/bashly/completions/bashly-completions.bash index 68835c9f..0f85d38c 100644 --- a/lib/bashly/completions/bashly-completions.bash +++ b/lib/bashly/completions/bashly-completions.bash @@ -4,121 +4,136 @@ # completely (https://github.com/bashly-framework/completely) # Modifying it manually is not recommended -_bashly_completions_route_flag_expects_value() { +_bashly_completions_node_flag_state() { case "$1:$2" in - 4:--env|4:-e) return 0 ;; - 4:--wrap|4:-r) return 0 ;; - 5:--source|5:-s) return 0 ;; - 8:--show|8:-s) return 0 ;; + 0:--help|0:-h) return 0 ;; + 0:--version|0:-v) return 0 ;; + 1:--help|1:-h) return 0 ;; + 1:--minimal|1:-m) return 0 ;; + 2:--help|2:-h) return 0 ;; + 3:--help|3:-h) return 0 ;; + 3:--verbose|3:-v) return 0 ;; + 4:--env|4:-e) return 2 ;; + 4:--wrap|4:-r) return 2 ;; + 4:--help|4:-h) return 0 ;; + 4:--force|4:-f) return 0 ;; + 4:--quiet|4:-q) return 0 ;; + 4:--upgrade|4:-u) return 0 ;; + 4:--watch|4:-w) return 0 ;; + 5:--source|5:-s) return 2 ;; + 5:--help|5:-h) return 0 ;; + 5:--force|5:-f) return 0 ;; + 5:--list|5:-l) return 0 ;; + 6:--help|6:-h) return 0 ;; + 6:--index|6:-i) return 0 ;; + 7:--help|7:-h) return 0 ;; + 7:--install|7:-i) return 0 ;; + 7:--uninstall|7:-u) return 0 ;; + 8:--show|8:-s) return 2 ;; + 8:--help|8:-h) return 0 ;; + 8:--watch|8:-w) return 0 ;; + 8:--list|8:-l) return 0 ;; + 8:--about|8:-a) return 0 ;; + 9:--help|9:-h) return 0 ;; esac return 1 } -_bashly_completions_resolve_route() { - route_id= - route_word_count=-1 - route_has_positionals=0 - positional_index=0 - if (( ${#non_options[@]} >= 0 )) && - (( 0 > route_word_count )) - then - route_id=0 - route_word_count=0 - route_has_positionals=0 - positional_index=$((${#non_options[@]} - 0)) - fi - - if (( ${#non_options[@]} >= 1 )) && - (( 1 > route_word_count )) && - [[ "${non_options[0]}" == "init" || "${non_options[0]}" == "i" ]] - then - route_id=1 - route_word_count=1 - route_has_positionals=0 - positional_index=$((${#non_options[@]} - 1)) - fi - - if (( ${#non_options[@]} >= 1 )) && - (( 1 > route_word_count )) && - [[ "${non_options[0]}" == "preview" || "${non_options[0]}" == "p" ]] - then - route_id=2 - route_word_count=1 - route_has_positionals=0 - positional_index=$((${#non_options[@]} - 1)) - fi - - if (( ${#non_options[@]} >= 1 )) && - (( 1 > route_word_count )) && - [[ "${non_options[0]}" == "validate" || "${non_options[0]}" == "v" ]] - then - route_id=3 - route_word_count=1 - route_has_positionals=0 - positional_index=$((${#non_options[@]} - 1)) - fi - - if (( ${#non_options[@]} >= 1 )) && - (( 1 > route_word_count )) && - [[ "${non_options[0]}" == "generate" || "${non_options[0]}" == "build" || "${non_options[0]}" == "g" ]] - then - route_id=4 - route_word_count=1 - route_has_positionals=0 - positional_index=$((${#non_options[@]} - 1)) - fi - - if (( ${#non_options[@]} >= 1 )) && - (( 1 > route_word_count )) && - [[ "${non_options[0]}" == "add" || "${non_options[0]}" == "a" ]] - then - route_id=5 - route_word_count=1 - route_has_positionals=1 - positional_index=$((${#non_options[@]} - 1)) - fi - - if (( ${#non_options[@]} >= 1 )) && - (( 1 > route_word_count )) && - [[ "${non_options[0]}" == "doc" ]] - then - route_id=6 - route_word_count=1 - route_has_positionals=1 - positional_index=$((${#non_options[@]} - 1)) - fi +_bashly_completions_option_seen() { + local completed_option option_name + for completed_option in "${completed_options[@]}"; do + for option_name in "$@"; do + [[ "$completed_option" == "$option_name" ]] && return 0 + done + done - if (( ${#non_options[@]} >= 1 )) && - (( 1 > route_word_count )) && - [[ "${non_options[0]}" == "completions" || "${non_options[0]}" == "c" ]] - then - route_id=7 - route_word_count=1 - route_has_positionals=0 - positional_index=$((${#non_options[@]} - 1)) - fi + return 1 +} - if (( ${#non_options[@]} >= 1 )) && - (( 1 > route_word_count )) && - [[ "${non_options[0]}" == "render" ]] - then - route_id=8 - route_word_count=1 - route_has_positionals=1 - positional_index=$((${#non_options[@]} - 1)) - fi +_bashly_completions_resolve_node() { + node_id=0 + node_word_count=0 + positional_index=0 - if (( ${#non_options[@]} >= 1 )) && - (( 1 > route_word_count )) && - [[ "${non_options[0]}" == "shell" || "${non_options[0]}" == "s" ]] - then - route_id=9 - route_word_count=1 - route_has_positionals=0 - positional_index=$((${#non_options[@]} - 1)) - fi + local word + for word in "${non_options[@]}"; do + case "$node_id:$word" in + 0:init) + node_id=1 + node_word_count=1 + ;; + 0:i) + node_id=1 + node_word_count=1 + ;; + 0:preview) + node_id=2 + node_word_count=1 + ;; + 0:p) + node_id=2 + node_word_count=1 + ;; + 0:validate) + node_id=3 + node_word_count=1 + ;; + 0:v) + node_id=3 + node_word_count=1 + ;; + 0:generate) + node_id=4 + node_word_count=1 + ;; + 0:build) + node_id=4 + node_word_count=1 + ;; + 0:g) + node_id=4 + node_word_count=1 + ;; + 0:add) + node_id=5 + node_word_count=1 + ;; + 0:a) + node_id=5 + node_word_count=1 + ;; + 0:doc) + node_id=6 + node_word_count=1 + ;; + 0:completions) + node_id=7 + node_word_count=1 + ;; + 0:c) + node_id=7 + node_word_count=1 + ;; + 0:render) + node_id=8 + node_word_count=1 + ;; + 0:shell) + node_id=9 + node_word_count=1 + ;; + 0:s) + node_id=9 + node_word_count=1 + ;; + *) + break + ;; + esac + done + positional_index=$((${#non_options[@]} - node_word_count)) } _bashly_completions() { @@ -135,11 +150,12 @@ _bashly_completions() { local non_options=() local completed_options=() - local route_id= - local route_word_count=-1 - local route_has_positionals=0 + local node_id= + local node_word_count=-1 local positional_index=0 - _bashly_completions_resolve_route + local invalid_completion=0 + local flag_state=0 + _bashly_completions_resolve_node local skip_next=0 for word in "${completed[@]}"; do @@ -149,25 +165,28 @@ _bashly_completions() { fi if [[ "${word:0:1}" == "-" ]]; then + _bashly_completions_node_flag_state "$node_id" "$word" + flag_state=$? + if (( flag_state == 1 )); then + invalid_completion=1 + break + fi + completed_options+=("$word") - if _bashly_completions_route_flag_expects_value "$route_id" "$word"; then + if (( flag_state == 2 )); then skip_next=1 fi continue fi non_options+=("$word") - _bashly_completions_resolve_route + _bashly_completions_resolve_node done COMPREPLY=() + (( invalid_completion )) && return - if [[ -z "$route_id" ]] || { (( route_word_count == 0 )) && (( !route_has_positionals )) && [[ "${cur:0:1}" != "-" ]]; }; then - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "init i preview p validate v generate build g add a doc completions c render shell s" -- "$cur") - return - fi - - case "$route_id:$prev" in + case "$node_id:$prev" in 4:--env|4:-e) while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "development production" -- "$cur") return @@ -183,329 +202,106 @@ _bashly_completions() { ;; esac + if [[ "${cur:0:1}" != "-" ]] && (( positional_index == 0 )); then + case "$node_id" in + 0) + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "init i preview p validate v generate build g add a doc completions c render shell s" -- "$cur") + return + ;; + esac + fi + if [[ "${cur:0:1}" == "-" ]]; then - case "$route_id" in + case "$node_id" in 0) local words=() - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --help|-h) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--help" "-h") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --version|-v) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--version" "-v") - fi + _bashly_completions_option_seen "--help" "-h" || words+=("--help" "-h") + _bashly_completions_option_seen "--version" "-v" || words+=("--version" "-v") while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") return ;; 1) local words=() - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --help|-h) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--help" "-h") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --minimal|-m) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--minimal" "-m") - fi + _bashly_completions_option_seen "--help" "-h" || words+=("--help" "-h") + _bashly_completions_option_seen "--minimal" "-m" || words+=("--minimal" "-m") while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") return ;; 2) local words=() - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --help|-h) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--help" "-h") - fi + _bashly_completions_option_seen "--help" "-h" || words+=("--help" "-h") while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") return ;; 3) local words=() - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --help|-h) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--help" "-h") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --verbose|-v) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--verbose" "-v") - fi + _bashly_completions_option_seen "--help" "-h" || words+=("--help" "-h") + _bashly_completions_option_seen "--verbose" "-v" || words+=("--verbose" "-v") while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") return ;; 4) local words=() - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --help|-h) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--help" "-h") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --env|-e) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--env" "-e") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --force|-f) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--force" "-f") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --quiet|-q) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--quiet" "-q") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --upgrade|-u) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--upgrade" "-u") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --watch|-w) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--watch" "-w") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --wrap|-r) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--wrap" "-r") - fi + _bashly_completions_option_seen "--help" "-h" || words+=("--help" "-h") + _bashly_completions_option_seen "--env" "-e" || words+=("--env" "-e") + _bashly_completions_option_seen "--force" "-f" || words+=("--force" "-f") + _bashly_completions_option_seen "--quiet" "-q" || words+=("--quiet" "-q") + _bashly_completions_option_seen "--upgrade" "-u" || words+=("--upgrade" "-u") + _bashly_completions_option_seen "--watch" "-w" || words+=("--watch" "-w") + _bashly_completions_option_seen "--wrap" "-r" || words+=("--wrap" "-r") while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") return ;; 5) local words=() - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --help|-h) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--help" "-h") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --force|-f) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--force" "-f") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --list|-l) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--list" "-l") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --source|-s) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--source" "-s") - fi + _bashly_completions_option_seen "--help" "-h" || words+=("--help" "-h") + _bashly_completions_option_seen "--force" "-f" || words+=("--force" "-f") + _bashly_completions_option_seen "--list" "-l" || words+=("--list" "-l") + _bashly_completions_option_seen "--source" "-s" || words+=("--source" "-s") while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") return ;; 6) local words=() - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --help|-h) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--help" "-h") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --index|-i) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--index" "-i") - fi + _bashly_completions_option_seen "--help" "-h" || words+=("--help" "-h") + _bashly_completions_option_seen "--index" "-i" || words+=("--index" "-i") while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") return ;; 7) local words=() - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --help|-h) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--help" "-h") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --install|-i) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--install" "-i") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --uninstall|-u) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--uninstall" "-u") - fi + _bashly_completions_option_seen "--help" "-h" || words+=("--help" "-h") + _bashly_completions_option_seen "--install" "-i" || words+=("--install" "-i") + _bashly_completions_option_seen "--uninstall" "-u" || words+=("--uninstall" "-u") while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") return ;; 8) local words=() - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --help|-h) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--help" "-h") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --watch|-w) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--watch" "-w") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --show|-s) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--show" "-s") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --list|-l) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--list" "-l") - fi - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --about|-a) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--about" "-a") - fi + _bashly_completions_option_seen "--help" "-h" || words+=("--help" "-h") + _bashly_completions_option_seen "--watch" "-w" || words+=("--watch" "-w") + _bashly_completions_option_seen "--show" "-s" || words+=("--show" "-s") + _bashly_completions_option_seen "--list" "-l" || words+=("--list" "-l") + _bashly_completions_option_seen "--about" "-a" || words+=("--about" "-a") while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") return ;; 9) local words=() - local option_seen=0 - for completed_option in "${completed_options[@]}"; do - case "$completed_option" in - --help|-h) option_seen=1 ;; - esac - done - if ((!option_seen)); then - words+=("--help" "-h") - fi + _bashly_completions_option_seen "--help" "-h" || words+=("--help" "-h") while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") return ;; esac fi - case "$route_id:$positional_index" in + case "$node_id:$positional_index" in 5:0) while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "colors completions completions_script completions_yaml config help hooks ini lib render_markdown render_markdown_github render_mandoc settings stacktrace strings validations yaml" -- "$cur") return ;; 6:0) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "arg arg.allowed arg.completions arg.default arg.help arg.name arg.repeatable arg.required arg.validate command command.alias command.args command.catch_all command.commands command.completions command.default command.dependencies command.environment_variables command.examples command.expose command.extensible command.filename command.filters command.flags command.footer command.function command.group command.help command.help_header_override command.name command.private command.variables command.version environment_variable environment_variable.default environment_variable.help environment_variable.name environment_variable.private environment_variable.required environment_variable.validate flag flag.allowed flag.arg flag.completions flag.conflicts flag.default flag.help flag.long flag.needs flag.private flag.repeatable flag.required flag.short flag.unique flag.validate variable variable.name variable.value" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "arg arg.allowed arg.completions arg.default arg.help arg.name arg.repeatable arg.required arg.validate command command.alias command.args command.catch_all command.commands command.completions command.default command.dependencies command.environment_variables command.examples command.expose command.extensible command.filename command.filters command.flags command.footer command.function command.group command.help command.help_header_override command.name command.private command.variables command.version environment_variable environment_variable.default environment_variable.help environment_variable.name environment_variable.private environment_variable.required environment_variable.validate flag flag.alias flag.allowed flag.arg flag.completions flag.conflicts flag.default flag.help flag.long flag.needs flag.private flag.repeatable flag.required flag.short flag.unique flag.validate variable variable.name variable.value" -- "$cur") return ;; 8:0) diff --git a/lib/bashly/completions/completely.yaml b/lib/bashly/completions/completely.yaml index 6f2f2f74..08d0697e 100644 --- a/lib/bashly/completions/completely.yaml +++ b/lib/bashly/completions/completely.yaml @@ -118,6 +118,7 @@ tokens: - environment_variable.required - environment_variable.validate - flag + - flag.alias - flag.allowed - flag.arg - flag.completions diff --git a/lib/bashly/config_validator.rb b/lib/bashly/config_validator.rb index a68f44f6..1b4481db 100644 --- a/lib/bashly/config_validator.rb +++ b/lib/bashly/config_validator.rb @@ -126,6 +126,7 @@ def assert_flag(key, value) assert_optional_string "#{key}.short", value['short'] assert_optional_string "#{key}.help", value['help'] assert_optional_string "#{key}.arg", value['arg'] + assert_string_or_array "#{key}.alias", value['alias'] assert_string_or_array "#{key}.default", value['default'] assert_string_or_array "#{key}.validate", value['validate'] @@ -139,6 +140,10 @@ def assert_flag(key, value) assert value['long'].match(/^--[a-zA-Z0-9_-]+$/), "#{key}.long must be in the form of '--name'" if value['long'] assert value['short'].match(/^-[a-zA-Z0-9]$/), "#{key}.short must be in the form of '-n'" if value['short'] + Array(value['alias']).each_with_index do |flag_alias, index| + alias_key = value['alias'].is_a?(Array) ? "#{key}.alias[#{index}]" : "#{key}.alias" + assert_flag_alias alias_key, flag_alias + end refute value['arg'].match(/^-/), "#{key}.arg must not start with '-'" if value['arg'] refute value['required'] && value['default'], "#{key} cannot have both nub`required` and nub`default`" @@ -165,6 +170,13 @@ def assert_flag(key, value) end end + def assert_flag_alias(key, value) + assert( + value.match(/^--[a-zA-Z0-9_-]+$/) || value.match(/^-[a-zA-Z0-9]$/), + "#{key} must be in the form of '--name' or '-n'" + ) + end + def assert_env_var(key, value) assert_hash key, value, keys: Script::EnvironmentVariable.option_keys assert_string "#{key}.name", value['name'] @@ -222,8 +234,7 @@ def assert_command(key, value) assert_array "#{key}.variables", value['variables'], of: :var assert_uniq "#{key}.commands", value['commands'], %w[name alias] - assert_uniq "#{key}.flags", value['flags'], 'long' - assert_uniq "#{key}.flags", value['flags'], 'short' + assert_uniq "#{key}.flags", value['flags'], %w[long short alias] assert_uniq "#{key}.args", value['args'], 'name' if value['function'] diff --git a/lib/bashly/docs/flag.yml b/lib/bashly/docs/flag.yml index b444e440..f7178f5c 100644 --- a/lib/bashly/docs/flag.yml +++ b/lib/bashly/docs/flag.yml @@ -13,6 +13,17 @@ flag: help: Repository user name required: true +flag.alias: + help: Specify one or more aliases for this flag. Each alias must include the `--` or `-` prefix. + url: https://bashly.dev/configuration/flag/#alias + example: |- + flags: + - long: --container + short: -c + alias: [--pod, -p] + arg: name + help: Container name + flag.allowed: help: Specify a list of allowed values. Can be used in conjunction with `default` and `required`, and must be accompanied by `arg`. url: https://bashly.dev/configuration/flag/#allowed @@ -205,4 +216,4 @@ flag.validate: - long: --config arg: path help: Load configuration from a file - validate: file_exists \ No newline at end of file + validate: file_exists diff --git a/lib/bashly/script/flag.rb b/lib/bashly/script/flag.rb index 19fca03f..7b26c3cb 100644 --- a/lib/bashly/script/flag.rb +++ b/lib/bashly/script/flag.rb @@ -10,20 +10,20 @@ class Flag < Base class << self def option_keys @option_keys ||= %i[ - allowed arg completions conflicts default help long needs + alias allowed arg completions conflicts default help long needs private repeatable required short unique validate ] end end + def alt + return [] unless options['alias'] + + options['alias'].is_a?(String) ? [options['alias']] : options['alias'] + end + def aliases - if long && short - [long, short] - elsif long - [long] - else - [short] - end + primary_aliases + alt end def default_string @@ -47,6 +47,12 @@ def usage_string(extended: false) result << strings[:repeatable] if repeatable && extended result.join ' ' end + + private + + def primary_aliases + [long, short].compact + end end end end diff --git a/schemas/bashly.json b/schemas/bashly.json index b233fea2..fef4250e 100644 --- a/schemas/bashly.json +++ b/schemas/bashly.json @@ -176,6 +176,32 @@ "-s" ] }, + "alias": { + "title": "alias", + "description": "One or more aliases for the current flag\nhttps://bashly.dev/configuration/flag/#alias", + "oneOf": [ + { + "type": "string", + "pattern": "^(--[a-zA-Z0-9_-]+|-[a-zA-Z0-9])$", + "examples": [ + "--pod" + ] + }, + { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "description": "An alias of the current flag", + "type": "string", + "pattern": "^(--[a-zA-Z0-9_-]+|-[a-zA-Z0-9])$", + "examples": [ + "-p" + ] + } + } + ] + }, "help": { "title": "help", "description": "A description of the current flag\nhttps://bashly.dev/configuration/flag/#help", diff --git a/spec/approvals/cli/doc/full b/spec/approvals/cli/doc/full index 62463761..eb4baebc 100644 --- a/spec/approvals/cli/doc/full +++ b/spec/approvals/cli/doc/full @@ -626,6 +626,20 @@ flag See https://bashly.dev/configuration/flag/ +flag.alias + + Specify one or more aliases for this flag. Each alias must include the -- or + - prefix. + + flags: + - long: --container + short: -c + alias: [--pod, -p] + arg: name + help: Container name + + See https://bashly.dev/configuration/flag/#alias + flag.allowed Specify a list of allowed values. Can be used in conjunction with default diff --git a/spec/approvals/cli/doc/index b/spec/approvals/cli/doc/index index bcfdb059..d381f3a9 100644 --- a/spec/approvals/cli/doc/index +++ b/spec/approvals/cli/doc/index @@ -39,6 +39,7 @@ environment_variable.private environment_variable.required environment_variable.validate flag +flag.alias flag.allowed flag.arg flag.completions diff --git a/spec/approvals/validations/flag_alias b/spec/approvals/validations/flag_alias new file mode 100644 index 00000000..590763f2 --- /dev/null +++ b/spec/approvals/validations/flag_alias @@ -0,0 +1 @@ +# \ No newline at end of file diff --git a/spec/approvals/validations/flag_alias_array b/spec/approvals/validations/flag_alias_array new file mode 100644 index 00000000..8420c15e --- /dev/null +++ b/spec/approvals/validations/flag_alias_array @@ -0,0 +1 @@ +# \ No newline at end of file diff --git a/spec/approvals/validations/flag_alias_format b/spec/approvals/validations/flag_alias_format new file mode 100644 index 00000000..2e3d50a1 --- /dev/null +++ b/spec/approvals/validations/flag_alias_format @@ -0,0 +1 @@ +# \ No newline at end of file diff --git a/spec/approvals/validations/non_uniq_flags_aliases b/spec/approvals/validations/non_uniq_flags_aliases new file mode 100644 index 00000000..0fc06d38 --- /dev/null +++ b/spec/approvals/validations/non_uniq_flags_aliases @@ -0,0 +1 @@ +# \ No newline at end of file diff --git a/spec/approvals/validations/non_uniq_flags_longs b/spec/approvals/validations/non_uniq_flags_longs index a2f38a60..87a7d082 100644 --- a/spec/approvals/validations/non_uniq_flags_longs +++ b/spec/approvals/validations/non_uniq_flags_longs @@ -1 +1 @@ -# \ No newline at end of file +# \ No newline at end of file diff --git a/spec/approvals/validations/non_uniq_flags_shorts b/spec/approvals/validations/non_uniq_flags_shorts index 0a94213e..55ce687e 100644 --- a/spec/approvals/validations/non_uniq_flags_shorts +++ b/spec/approvals/validations/non_uniq_flags_shorts @@ -1 +1 @@ -# \ No newline at end of file +# \ No newline at end of file diff --git a/spec/bashly/completion_builder_spec.rb b/spec/bashly/completion_builder_spec.rb index 93132063..c66a255a 100644 --- a/spec/bashly/completion_builder_spec.rb +++ b/spec/bashly/completion_builder_spec.rb @@ -1,8 +1,6 @@ describe CompletionBuilder do - fixtures = load_fixture('completion_builder') - describe '#call' do - fixtures.each do |fixture, options| + load_fixture('completion_builder').each do |fixture, options| context "with :#{fixture}" do let(:command) { Script::Command.new options['command'] } let(:builder) do @@ -18,6 +16,7 @@ end context 'with a default command' do + let(:fixtures) { load_fixture('completion_builder') } let(:command) { Script::Command.new fixtures[:default_command]['command'] } let(:data) { described_class.new(command).call } diff --git a/spec/bashly/concerns/completions_command_spec.rb b/spec/bashly/concerns/completions_command_spec.rb index 17b4f741..93e030c4 100644 --- a/spec/bashly/concerns/completions_command_spec.rb +++ b/spec/bashly/concerns/completions_command_spec.rb @@ -3,6 +3,7 @@ let(:fixtures) { load_fixture('script/commands') } let(:fixture) { :completions_simple } + let(:completion_generator) { instance_double Completely::Completions } describe '#completion_data' do it 'returns a data structure for completely' do @@ -11,17 +12,16 @@ end describe '#completion_function' do - it 'returns the generated bash completion script wrapped in a function' do - expected_data = subject.completion_data - completion_generator = instance_double Completely::Completions - - expect(Completely::Completions).to receive(:new) - .with(expected_data) + before do + allow(Completely::Completions).to receive(:new) + .with(subject.completion_data) .and_return completion_generator - expect(completion_generator).to receive(:wrapper_function) + allow(completion_generator).to receive(:wrapper_function) .with('custom_name') .and_return 'wrapped completion script' + end + it 'returns the generated bash completion script wrapped in a function' do expect(subject.completion_function('custom_name')) .to eq 'wrapped completion script' end @@ -38,16 +38,15 @@ end describe '#completion_script' do - it 'returns the generated bash completion script' do - expected_data = subject.completion_data - completion_generator = instance_double Completely::Completions - - expect(Completely::Completions).to receive(:new) - .with(expected_data) + before do + allow(Completely::Completions).to receive(:new) + .with(subject.completion_data) .and_return completion_generator - expect(completion_generator).to receive(:script) + allow(completion_generator).to receive(:script) .and_return 'completion script' + end + it 'returns the generated bash completion script' do expect(subject.completion_script) .to eq 'completion script' end diff --git a/spec/bashly/concerns/completions_flag_spec.rb b/spec/bashly/concerns/completions_flag_spec.rb index dcb71b9d..cea6cb2e 100644 --- a/spec/bashly/concerns/completions_flag_spec.rb +++ b/spec/bashly/concerns/completions_flag_spec.rb @@ -28,4 +28,14 @@ end end end + + describe '#completion_option_entry' do + context 'when the flag has aliases' do + let(:fixture) { :aliases } + + it 'includes all aliases in the option entry' do + expect(subject.completion_option_entry('name')).to eq '--container|-c|--pod|-p ' + end + end + end end diff --git a/spec/bashly/libraries/completions_function_spec.rb b/spec/bashly/libraries/completions_function_spec.rb index d0ad572f..56c1e8f5 100644 --- a/spec/bashly/libraries/completions_function_spec.rb +++ b/spec/bashly/libraries/completions_function_spec.rb @@ -6,12 +6,16 @@ before { reset_tmp_dir example: 'minimal' } describe '#files' do - it 'returns an array with a single hash' do + it 'returns one file at the default path' do files = subject.files - content = files.first[:content] expect(files.size).to eq 1 expect(files.first[:path]).to eq "#{Settings.source_dir}/lib/send_completions.sh" + end + + it 'returns the completions function content' do + content = subject.files.first[:content] + expect(content).to include '## [@bashly-upgrade completions send_completions]' expect(content).to include 'send_completions() {' expect(content).to include 'generated by' diff --git a/spec/bashly/libraries/completions_script_spec.rb b/spec/bashly/libraries/completions_script_spec.rb index dec4c737..8cc867a4 100644 --- a/spec/bashly/libraries/completions_script_spec.rb +++ b/spec/bashly/libraries/completions_script_spec.rb @@ -6,12 +6,16 @@ before { reset_tmp_dir example: 'minimal' } describe '#files' do - it 'returns an array with a single hash' do + it 'returns one file at the default path' do files = subject.files - content = files.first[:content] expect(files.size).to eq 1 expect(files.first[:path]).to eq "#{Settings.target_dir}/completions.bash" + end + + it 'returns the completions script content' do + content = subject.files.first[:content] + expect(content).to include '# download completion' expect(content).to include 'generated by' expect(content).to include 'completely' diff --git a/spec/bashly/script/flag_spec.rb b/spec/bashly/script/flag_spec.rb index bce2f523..100ca4f1 100644 --- a/spec/bashly/script/flag_spec.rb +++ b/spec/bashly/script/flag_spec.rb @@ -38,6 +38,22 @@ expect(subject.aliases).to eq ['-s'] end end + + context 'with aliases' do + let(:fixture) { :aliases } + + it 'returns an array with the long, short and alias values' do + expect(subject.aliases).to eq ['--container', '-c', '--pod', '-p'] + end + end + + context 'with one alias' do + let(:fixture) { :alias_string } + + it 'returns an array with the long and alias values' do + expect(subject.aliases).to eq ['--container', '--pod'] + end + end end describe '#default_string' do @@ -95,6 +111,14 @@ expect(subject.usage_string).to eq '--help, -h' end + context 'with aliases' do + let(:fixture) { :aliases } + + it 'includes all aliases inline' do + expect(subject.usage_string).to eq '--container, -c, --pod, -p NAME' + end + end + context 'with extended: true' do context 'when the flag is optional' do it 'returns the same string as it does without extended' do @@ -119,4 +143,17 @@ end end end + + describe '#render' do + context 'with aliases' do + let(:fixture) { :aliases } + + it 'matches every alias and assigns to the canonical flag name' do + result = subject.render(:case) + + expect(result).to include '--container | -c | --pod | -p)' + expect(result).to include "args['--container']=\"$2\"" + end + end + end end diff --git a/spec/fixtures/script/flags.yml b/spec/fixtures/script/flags.yml index 1346ab31..4c8a831f 100644 --- a/spec/fixtures/script/flags.yml +++ b/spec/fixtures/script/flags.yml @@ -3,6 +3,16 @@ short: -h help: show this help +:aliases: + long: --container + short: -c + alias: [--pod, -p] + arg: name + +:alias_string: + long: --container + alias: --pod + :long_only: long: --long diff --git a/spec/fixtures/script/validations.yml b/spec/fixtures/script/validations.yml index 80655b90..6a6bb818 100644 --- a/spec/fixtures/script/validations.yml +++ b/spec/fixtures/script/validations.yml @@ -257,6 +257,27 @@ - long: --user arg: --name +:flag_alias: + name: invalid + help: flag.alias should be a string or array of strings + flags: + - long: --user + alias: 1 + +:flag_alias_array: + name: invalid + help: flag.alias array values should be strings + flags: + - long: --user + alias: [--person, 1] + +:flag_alias_format: + name: invalid + help: flag.alias values should be valid flag names + flags: + - long: --user + alias: user + :flag_allowed_and_completions: name: invalid help: flag with both allowed and completions @@ -419,6 +440,15 @@ flags: - short: -f - short: -f + +:non_uniq_flags_aliases: + name: invalid + help: flag aliases conflict with other flag names + flags: + - long: --container + alias: [--pod, -p] + - long: --podman + short: -p :non_uniq_arg_names: name: invalid diff --git a/support/schema/bashly.yml b/support/schema/bashly.yml index 699832dd..ccc130f8 100644 --- a/support/schema/bashly.yml +++ b/support/schema/bashly.yml @@ -148,6 +148,25 @@ definitions: pattern: ^-[^-] examples: - -s + alias: + title: alias + description: |- + One or more aliases for the current flag + https://bashly.dev/configuration/flag/#alias + oneOf: + - type: string + pattern: ^(--[a-zA-Z0-9_-]+|-[a-zA-Z0-9])$ + examples: + - --pod + - type: array + minItems: 1 + uniqueItems: true + items: + description: An alias of the current flag + type: string + pattern: ^(--[a-zA-Z0-9_-]+|-[a-zA-Z0-9])$ + examples: + - -p help: title: help description: |-