class Clamp::Completion::BashGenerator

Generates bash shell completion scripts.

Public Class Methods

new(command_class, executable_name) click to toggle source
# File lib/clamp/completion/bash_generator.rb, line 10
def initialize(command_class, executable_name)
  @command_class = command_class
  @executable_name = executable_name
end

Public Instance Methods

generate() click to toggle source
# File lib/clamp/completion/bash_generator.rb, line 15
def generate
  [
    "# Bash completions for #{@executable_name}",
    "# Generated by Clamp",
    "",
    takes_value_function,
    "",
    param_count_function,
    "",
    canonical_function,
    "",
    main_function,
    "",
    "complete -F #{completion_function} #{@executable_name}"
  ].push("").join("\n")
end

Private Instance Methods

build_case(var, indent, assign) { |cmd, has_children| ... } click to toggle source
# File lib/clamp/completion/bash_generator.rb, line 127
def build_case(var, indent, assign)
  entries = {}
  Completion.walk_command_tree(@command_class) do |cmd, path, has_children|
    words = yield(cmd, has_children)
    next if words.nil? || words.empty?

    path_str = path.map { |sub| sub.names.first }.join("::")
    entries[path_str] = words.join(" ")
  end
  lines = ["#{indent}case \"#{var}\" in"]
  entries.each do |path, words|
    pattern = path.empty? ? '""' : "\"#{path}\""
    lines << "#{indent}    #{pattern})"
    lines << "#{indent}        #{assign}($(compgen -W \"#{words}\" -- \"$cur\"))"
    lines << "#{indent}        ;;"
  end
  lines << "#{indent}esac"
  lines.join("\n")
end
build_lookup_function(suffix, entries, verb, default:) click to toggle source
# File lib/clamp/completion/bash_generator.rb, line 169
def build_lookup_function(suffix, entries, verb, default:)
  lines = ["#{completion_function}_#{suffix}() {", '    case "$1" in']
  entries.each do |key, value|
    pattern = key.empty? ? '""' : key
    lines << "        #{pattern}) #{verb} #{value.is_a?(String) ? "\"#{value}\"" : value} ;;"
  end
  lines.push("        *) #{default} ;;", "    esac", "}")
  lines.join("\n")
end
canonical_function() click to toggle source
# File lib/clamp/completion/bash_generator.rb, line 157
def canonical_function
  aliases = {}
  Completion.walk_command_tree(@command_class) do |cmd, _path, has_children|
    next unless has_children

    cmd.recognised_subcommands.each do |sub|
      sub.names.drop(1).each { |name| aliases[name] = sub.names.first }
    end
  end
  build_lookup_function("canonical", aliases, "echo", default: 'echo "$1"')
end
completion_function() click to toggle source
# File lib/clamp/completion/bash_generator.rb, line 34
def completion_function
  "_clamp_complete_#{Completion.encode_name(@executable_name)}"
end
find_subcmd_function() click to toggle source
# File lib/clamp/completion/bash_generator.rb, line 70
def find_subcmd_function
  [
    "#{completion_function}_find_subcmd() {",
    "    local i=1 word subcmd skip",
    "    skip=$(#{completion_function}_param_count \"\")",
    '    while [ "$i" -lt "$COMP_CWORD" ]; do',
    '        word="${COMP_WORDS[$i]}"',
    '        case "$word" in',
    "            -*)",
    "                if #{completion_function}_takes_value \"$word\" \"$subcmd\"; then",
    "                    ((i++))",
    "                fi",
    "                ;;",
    "            *)",
    find_subcmd_match_word,
    "                ;;",
    "        esac",
    "        ((i++))",
    "    done",
    '    echo "$subcmd $skip"',
    "}"
  ].join("\n")
end
find_subcmd_match_word() click to toggle source
# File lib/clamp/completion/bash_generator.rb, line 94
def find_subcmd_match_word
  subcmds = Completion.collect_subcommand_names(@command_class).join("|")
  [
    '                if [ "$skip" -gt 0 ]; then',
    "                    ((skip--))",
    "                else",
    '                    case "$word" in',
    "                        #{subcmds})",
    "                            local canonical=$(#{completion_function}_canonical \"$word\")",
    '                            if [ -z "$subcmd" ]; then',
    '                                subcmd="$canonical"',
    "                            else",
    '                                subcmd="${subcmd}::${canonical}"',
    "                            fi",
    "                            skip=$(#{completion_function}_param_count \"$subcmd\")",
    "                            ;;",
    "                    esac",
    "                fi"
  ].join("\n")
end
main_function() click to toggle source
# File lib/clamp/completion/bash_generator.rb, line 38
def main_function
  [
    "#{completion_function}() {",
    "    local cur prev",
    "    if type _init_completion &>/dev/null; then",
    "        _init_completion",
    "    else",
    '        cur="${COMP_WORDS[COMP_CWORD]}"',
    '        prev="${COMP_WORDS[COMP_CWORD-1]}"',
    "    fi",
    "",
    "    local subcmd_info subcmd params_remaining",
    "    subcmd_info=$(#{completion_function}_find_subcmd)",
    '    subcmd="${subcmd_info%% *}"',
    '    params_remaining="${subcmd_info##* }"',
    "    if #{completion_function}_takes_value \"$prev\" \"$subcmd\"; then",
    "        return",
    "    fi",
    "",
    '    if [[ "$cur" == -* ]]; then',
    options_case("$subcmd"),
    "    fi",
    "",
    '    if [ "$params_remaining" -eq 0 ]; then',
    subcommands_case("$subcmd"),
    "    fi",
    "}",
    "",
    find_subcmd_function
  ].join("\n")
end
options_case(var) click to toggle source
# File lib/clamp/completion/bash_generator.rb, line 115
def options_case(var)
  build_case(var, "    ", "COMPREPLY=") do |cmd, _has_children|
    Completion.visible_options(cmd).flat_map { |o| Completion.expanded_switches(o) }
  end
end
param_count_function() click to toggle source
# File lib/clamp/completion/bash_generator.rb, line 147
def param_count_function
  entries = {}
  Completion.walk_command_tree(@command_class) do |cmd, path, has_children|
    next unless has_children

    entries[path.map { |s| s.names.first }.join("::")] = Completion.required_parameter_count(cmd)
  end
  build_lookup_function("param_count", entries, "echo", default: "echo 0")
end
subcommands_case(var) click to toggle source
# File lib/clamp/completion/bash_generator.rb, line 121
def subcommands_case(var)
  build_case(var, "        ", "COMPREPLY+=") do |cmd, has_children|
    cmd.recognised_subcommands.flat_map(&:names) if has_children
  end
end
takes_value_function() click to toggle source
# File lib/clamp/completion/bash_generator.rb, line 179
def takes_value_function
  entries = {}
  Completion.walk_command_tree(@command_class) do |cmd, path, _has_children|
    path_str = path.map { |sub| sub.names.first }.join("::")
    entries[path_str] = Completion.visible_options(cmd).reject(&:flag?)
                                  .flat_map { |o| Completion.expanded_switches(o) }
  end
  lines = [
    "#{completion_function}_takes_value() {",
    '    local option="$1"',
    '    local subcmd="$2"',
    '    case "$subcmd" in'
  ]
  entries.each do |path, switches|
    next if switches.empty?

    pattern = path.empty? ? '""' : "\"#{path}\""
    lines << "        #{pattern})"
    lines << '            case "$option" in'
    lines << "                #{switches.join('|')}) return 0 ;;"
    lines << "            esac"
    lines << "            ;;"
  end
  lines.push("    esac", "    return 1", "}")
  lines.join("\n")
end