Packages
fnord
0.6.5
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
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