defmodule Mix.Tasks.DoIt.Gen.Completion do
use Mix.Task
@shortdoc "Generates shell completion scripts for your DoIt CLI application"
@moduledoc """
Generates shell completion scripts for your DoIt CLI application.
This task helps you generate completion scripts for popular shells and optionally
save them to appropriate locations.
## Usage
mix do_it.gen.completion [options]
## Options
* `--shell` (`-s`) - Shell to generate completion for (bash, fish, zsh)
* `--output` (`-o`) - Output file path (defaults to stdout)
* `--install` (`-i`) - Show installation instructions instead of generating script
* `--main-module` (`-m`) - Main module to use (auto-detected if not specified)
* `--app-name` (`-a`) - Application name (defaults to app name from mix.exs)
## Examples
# Generate bash completion script to stdout
mix do_it.gen.completion --shell bash
# Generate fish completion and save to file
mix do_it.gen.completion --shell fish --output ~/.config/fish/completions/myapp.fish
# Show installation instructions for zsh
mix do_it.gen.completion --shell zsh --install
# Generate for specific main module
mix do_it.gen.completion --shell bash --main-module MyApp.CLI
"""
alias DoIt.Completion
def run(args) do
{opts, _, invalid} =
OptionParser.parse(args,
switches: [
shell: :string,
output: :string,
install: :boolean,
main_module: :string,
app_name: :string,
help: :boolean
],
aliases: [
s: :shell,
o: :output,
i: :install,
m: :main_module,
a: :app_name,
h: :help
]
)
if opts[:help] do
print_help()
:ok
else
if invalid != [] do
Mix.shell().error("Invalid options: #{Enum.join(Enum.map(invalid, &elem(&1, 0)), ", ")}")
print_help()
System.halt(1)
end
shell = opts[:shell] || "bash"
unless shell in ["bash", "fish", "zsh"] do
Mix.shell().error("Unsupported shell: #{shell}. Supported shells: bash, fish, zsh")
System.halt(1)
end
if opts[:install] do
show_installation_instructions(shell, opts)
else
generate_completion_script(shell, opts)
end
end
end
defp generate_completion_script(shell, opts) do
# Ensure the project is compiled
Mix.Task.run("compile")
main_module = get_main_module(opts[:main_module])
app_name = opts[:app_name] || get_app_name()
unless main_module do
Mix.shell().error("Could not determine main module. Please specify with --main-module")
System.halt(1)
end
unless Code.ensure_loaded?(main_module) do
Mix.shell().error("Main module #{main_module} not found or not compiled")
System.halt(1)
end
unless function_exported?(main_module, :command, 0) do
Mix.shell().error("Module #{main_module} does not appear to be a DoIt.MainCommand")
System.halt(1)
end
script =
case shell do
"bash" -> Completion.generate_bash_completion(main_module, app_name)
"fish" -> Completion.generate_fish_completion(main_module, app_name)
"zsh" -> Completion.generate_zsh_completion(main_module, app_name)
end
if output_file = opts[:output] do
output_dir = Path.dirname(output_file)
unless File.exists?(output_dir) do
case Mix.shell().yes?("Directory #{output_dir} does not exist. Create it?") do
true ->
File.mkdir_p!(output_dir)
false ->
Mix.shell().error("Cannot write to #{output_file}")
System.halt(1)
end
end
File.write!(output_file, script)
Mix.shell().info("Generated #{shell} completion script: #{output_file}")
if shell == "fish" do
Mix.shell().info(
"Fish completion is now ready to use (restart your shell or run 'exec fish')"
)
else
Mix.shell().info(
"To enable completion, source this file or add it to your shell's configuration"
)
end
else
IO.puts(script)
end
end
defp show_installation_instructions(shell, opts) do
app_name = opts[:app_name] || get_app_name()
instructions = Completion.get_installation_instructions(app_name, shell)
IO.puts(instructions)
end
defp get_main_module(nil) do
# Try to auto-detect main module from escript configuration
case Mix.Project.config()[:escript] do
nil ->
# Try to find modules that use DoIt.MainCommand
find_main_command_modules() |> List.first()
escript_config ->
escript_config[:main_module]
end
end
defp get_main_module(module_string) when is_binary(module_string) do
try do
String.to_existing_atom("Elixir.#{module_string}")
rescue
ArgumentError ->
try do
String.to_existing_atom(module_string)
rescue
ArgumentError -> nil
end
end
end
defp find_main_command_modules do
# Get all compiled modules and find those using DoIt.MainCommand
case :application.get_key(Mix.Project.config()[:app], :modules) do
{:ok, modules} ->
modules
|> Enum.filter(fn module ->
Code.ensure_loaded?(module) and
function_exported?(module, :command, 0) and
function_exported?(module, :main, 1)
end)
_ ->
[]
end
end
defp get_app_name do
Mix.Project.config()[:app] |> to_string()
end
defp print_help do
IO.puts(@moduledoc)
end
end