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