Packages

fnord

0.6.3

AI code archaeology

Current section

Files

Jump to
fnord lib shell completion bash.ex
Raw

lib/shell/completion/bash.ex

defmodule Shell.Completion.Bash do
@moduledoc """
Bash completion script generator. This module should not be used directly.
Instead, use the `Shell.Completion` module to generate completion scripts.
"""
alias Shell.Completion, as: API
@spec generate_bash_script(API.command_spec()) :: String.t()
def generate_bash_script(spec) when is_map(spec) do
case validate_command_spec(spec) do
:ok ->
tables =
build_tables(
spec,
spec.name,
%{subcommands: %{}, options: %{}, option_args: %{}, argument: %{}}
)
generate_bash_script_from_tables(spec.name, tables)
error ->
error
end
end
@spec validate_command_spec(API.command_spec()) :: :ok | {:error, String.t()}
def validate_command_spec(spec) when is_map(spec) do
if Map.has_key?(spec, :name) do
:ok
else
{:error, "Command spec must have a :name key"}
end
end
@spec build_tables(map(), String.t(), map()) :: map()
defp build_tables(spec, current_path, tables) do
tables =
update_in(tables, [:subcommands, current_path], fn existing ->
existing || []
end)
tables =
update_in(tables, [:options, current_path], fn existing ->
existing || []
end)
tables = put_in(tables, [:argument, current_path], nil)
tables =
Enum.reduce(Map.get(spec, :options, []), tables, fn opt, acc ->
acc =
update_in(acc, [:options, current_path], fn list -> (list || []) ++ [opt[:name]] end)
if Map.has_key?(opt, :from) do
takes_argument = Map.get(opt, :takes_argument, true)
if takes_argument do
put_in(
acc,
[:option_args, "#{current_path}:#{opt[:name]}"],
completion_source_to_bash(opt[:from])
)
else
acc
end
else
acc
end
end)
args = Map.get(spec, :arguments, [])
tables =
case args do
[arg | _] ->
put_in(tables, [:argument, current_path], completion_source_to_bash(arg[:from]))
_ ->
tables
end
tables =
Enum.reduce(Map.get(spec, :subcommands, []), tables, fn sub, acc ->
acc =
update_in(acc, [:subcommands, current_path], fn list ->
(list || []) ++ [sub[:name]]
end)
new_path = current_path <> ":" <> sub[:name]
acc = put_in(acc, [:subcommands, new_path], [])
acc = put_in(acc, [:options, new_path], [])
acc = put_in(acc, [:argument, new_path], nil)
build_tables(sub, new_path, acc)
end)
tables
end
@spec completion_source_to_bash(API.completion_source()) :: String.t()
defp completion_source_to_bash({:choices, choices}) when is_list(choices) do
"choices:" <> Enum.join(choices, " ")
end
defp completion_source_to_bash({:command, cmd}) when is_binary(cmd) do
"cmd:" <> cmd
end
defp completion_source_to_bash(:files), do: "files"
defp completion_source_to_bash(:directories), do: "directories"
defp completion_source_to_bash(fun) when is_function(fun, 1) do
raise "Custom function completions are not supported in bash"
end
@spec generate_bash_script_from_tables(String.t(), map()) :: String.t()
defp generate_bash_script_from_tables(command_name, tables) do
header =
"#!/usr/bin/env bash\n" <>
"# Bash completion script for #{command_name}\n" <>
"declare -A _sc_subcommands\n" <>
"declare -A _sc_options\n" <>
"declare -A _sc_option_args\n" <>
"declare -A _sc_argument\n\n"
subcommands_lines =
tables.subcommands
|> Enum.map(fn {path, subs} ->
~s(_sc_subcommands["#{path}"]="#{Enum.join(subs, " ")}")
end)
|> Enum.join("\n")
options_lines =
tables.options
|> Enum.map(fn {path, opts} ->
~s(_sc_options["#{path}"]="#{Enum.join(opts, " ")}")
end)
|> Enum.join("\n")
option_args_lines =
tables.option_args
|> Enum.map(fn {key, spec} ->
~s(_sc_option_args["#{key}"]="#{spec}")
end)
|> Enum.join("\n")
argument_lines =
tables.argument
|> Enum.filter(fn {_path, arg} -> not is_nil(arg) end)
|> Enum.map(fn {path, arg} ->
~s(_sc_argument["#{path}"]="#{arg}")
end)
|> Enum.join("\n")
function_body = """
_#{command_name}_completion() {
local cur prev words cword path
if type _get_comp_words_by_ref >/dev/null 2>&1; then
_get_comp_words_by_ref -n : cur prev words cword
else
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
words=("${COMP_WORDS[@]}")
cword="${COMP_CWORD}"
fi
# Determine current path
path="${words[0]}"
for ((i=1; i < cword; i++)); do
token="${words[i]}"
if [[ "$token" == -* ]]; then
continue
fi
local subs="${_sc_subcommands[$path]}"
if [[ -n "$subs" ]]; then
for sub in $subs; do
if [[ "$token" == "$sub" ]]; then
path="${path}:$token"
break
fi
done
fi
done
# Check if previous token is an option expecting an argument.
if [[ "$prev" == -* ]]; then
local key="${path}:${prev}"
if [[ -n "${_sc_option_args[$key]}" ]]; then
local spec="${_sc_option_args[$key]}"
if [[ "$spec" == choices:* ]]; then
local choices="${spec#choices:}"
COMPREPLY=( $(compgen -W "$choices" -- "$cur") )
return 0
elif [[ "$spec" == "files" ]]; then
COMPREPLY=( $(compgen -f -- "$cur") )
return 0
elif [[ "$spec" == "directories" ]]; then
COMPREPLY=( $(compgen -d -- "$cur") )
return 0
elif [[ "$spec" == cmd:* ]]; then
local cmd="${spec#cmd:}"
local choices
choices=$(eval "$cmd")
COMPREPLY=( $(compgen -W "$choices" -- "$cur") )
return 0
fi
fi
fi
# If current token starts with '-', complete options.
if [[ "$cur" == -* ]]; then
local opts="${_sc_options[$path]}"
COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
return 0
fi
# Otherwise, complete subcommands if any.
local subs="${_sc_subcommands[$path]}"
if [[ -n "$subs" ]]; then
COMPREPLY=( $(compgen -W "$subs" -- "$cur") )
return 0
fi
# Otherwise, complete argument if defined.
local arg="${_sc_argument[$path]}"
if [[ -n "$arg" ]]; then
if [[ "$arg" == choices:* ]]; then
local choices="${arg#choices:}"
COMPREPLY=( $(compgen -W "$choices" -- "$cur") )
return 0
elif [[ "$arg" == "files" ]]; then
COMPREPLY=( $(compgen -f -- "$cur") )
return 0
elif [[ "$arg" == "directories" ]]; then
COMPREPLY=( $(compgen -d -- "$cur") )
return 0
elif [[ "$arg" == cmd:* ]]; then
local cmd="${arg#cmd:}"
local choices
choices=$(eval "$cmd")
COMPREPLY=( $(compgen -W "$choices" -- "$cur") )
return 0
fi
fi
return 0
}
complete -F _#{command_name}_completion #{command_name}
"""
[
header,
subcommands_lines,
options_lines,
option_args_lines,
argument_lines,
"\n",
function_body
]
|> Enum.filter(&(&1 != ""))
|> Enum.join("\n")
end
end