"""Shell-specific completion script generators.
This module provides modular completion script generation for different shells,
replacing the large hardcoded strings with template-based generation.
"""
from abc import ABC, abstractmethod
from typing import Any
[docs]
class CompletionGenerator(ABC):
"""Base class for shell completion generators."""
[docs]
def __init__(self, command_name: str = "xraylabtool"):
self.command_name = command_name
[docs]
@abstractmethod
def generate(
self, commands: dict[str, dict[str, Any]], global_options: list[str]
) -> str:
"""Generate completion script for this shell."""
pass
@property
@abstractmethod
def shell_name(self) -> str:
"""Name of the shell this generator supports."""
pass
@property
@abstractmethod
def file_extension(self) -> str:
"""File extension for completion scripts."""
pass
[docs]
class BashCompletionGenerator(CompletionGenerator):
"""Generates Bash completion scripts."""
@property
def shell_name(self) -> str:
return "bash"
@property
def file_extension(self) -> str:
return ""
[docs]
def generate(
self, commands: dict[str, dict[str, Any]], global_options: list[str]
) -> str:
"""Generate Bash completion script."""
template = self._get_template()
# Generate command list
command_list = " ".join(commands.keys())
# Generate global options
global_opts = " ".join(global_options)
# Generate command-specific completion logic
command_completions = self._generate_command_completions(commands)
return template.format(
command_name=self.command_name,
commands=command_list,
global_options=global_opts,
command_completions=command_completions,
)
def _get_template(self) -> str:
"""Get the Bash completion template."""
return """#!/bin/bash
# {command_name} shell completion for Bash
# Generated automatically by XRayLabTool completion system
_{command_name}_complete() {{
local cur prev opts
COMPREPLY=()
cur="${{COMP_WORDS[COMP_CWORD]}}"
# Safely get previous word
if [[ ${{COMP_CWORD}} -gt 0 ]]; then
prev="${{COMP_WORDS[COMP_CWORD-1]}}"
else
prev=""
fi
# Main commands
local commands="{commands}"
# Global options
local global_opts="{global_options}"
# If we're at the first argument level (command selection)
if [[ ${{COMP_CWORD}} -eq 1 ]]; then
COMPREPLY=( $(compgen -W "${{commands}} ${{global_opts}}" -- "${{cur}}") )
return 0
fi
# Get the command
local command=""
if [[ ${{#COMP_WORDS[@]}} -gt 1 ]]; then
command="${{COMP_WORDS[1]}}"
fi
{command_completions}
# Default fallback
COMPREPLY=( $(compgen -W "${{global_opts}}" -- "${{cur}}") )
}}
# Register completion
complete -F _{command_name}_complete {command_name}
"""
def _generate_command_completions(self, commands: dict[str, dict[str, Any]]) -> str:
"""Generate command-specific completion logic."""
completions = []
for cmd_name, cmd_info in commands.items():
options = cmd_info.get("options", [])
cmd_info.get("arguments", [])
if "subcommands" in cmd_info:
# Handle commands with subcommands (like completion)
subcommands = cmd_info.get("subcommands", {})
subcmd_list = " ".join(subcommands.keys())
completion_logic = f""" # {cmd_name} command with subcommands
if [[ "${{command}}" == "{cmd_name}" ]]; then
local {cmd_name}_opts="{" ".join(options)}"
local {cmd_name}_subcommands="{subcmd_list}"
# If we have 3+ words, check for subcommand completion
if [[ ${{#COMP_WORDS[@]}} -gt 2 ]]; then
local subcommand="${{COMP_WORDS[2]}}"
case "${{subcommand}}" in"""
for subcmd_name, subcmd_info in subcommands.items():
subcmd_options = subcmd_info.get("options", [])
completion_logic += f"""
{subcmd_name})
local {subcmd_name}_opts="{" ".join(subcmd_options)}"
COMPREPLY=( $(compgen -W "${{{subcmd_name}_opts}}" -- "${{cur}}") )
return 0
;;"""
completion_logic += """
esac
fi
# Complete subcommands if at the right position
if [[ ${COMP_CWORD} -eq 2 ]]; then
COMPREPLY=( $(compgen -W "${{cmd_name}_subcommands} ${{cmd_name}_opts}" -- "${cur}") )
else
COMPREPLY=( $(compgen -W "${{cmd_name}_opts}" -- "${cur}") )
fi
return 0
fi
"""
else:
# Handle regular commands
completion_logic = f""" # {cmd_name} command
if [[ "${{command}}" == "{cmd_name}" ]]; then
local {cmd_name}_opts="{" ".join(options)}"
# Handle file completions for specific options
case "${{prev}}" in
--output|-o|--input|-i|--file|-f)
COMPREPLY=( $(compgen -f -- "${{cur}}") )
return 0
;;
--format)
COMPREPLY=( $(compgen -W "json yaml csv excel html pdf" -- "${{cur}}") )
return 0
;;
esac
COMPREPLY=( $(compgen -W "${{{cmd_name}_opts}} ${{global_opts}}" -- "${{cur}}") )
return 0
fi
"""
completions.append(completion_logic)
return "\n".join(completions)
[docs]
class ZshCompletionGenerator(CompletionGenerator):
"""Generates native Zsh completion scripts."""
@property
def shell_name(self) -> str:
return "zsh"
@property
def file_extension(self) -> str:
return ""
[docs]
def generate(
self, commands: dict[str, dict[str, Any]], global_options: list[str]
) -> str:
"""Generate native Zsh completion script."""
template = self._get_template()
# Generate command definitions
command_data = self._generate_command_definitions(commands, global_options)
command_definitions, command_args = command_data.split("||")
return template.format(
command_name=self.command_name,
command_definitions=command_definitions,
command_args=command_args,
)
def _get_template(self) -> str:
"""Get the Zsh completion template."""
return """#compdef {command_name}
# {command_name} shell completion for Zsh
# Generated automatically by XRayLabTool completion system
# Load zsh completion system if not already loaded
if ! command -v _arguments >/dev/null 2>&1; then
autoload -U compinit && compinit
fi
_{command_name}() {{
local context state line
_arguments -C \\
'(-h --help){{-h,--help}}[Show help message]' \\
'(-v --verbose){{-v,--verbose}}[Enable verbose output]' \\
'(--version)--version[Show version information]' \\
'1: :->command' \\
'*:: :->args'
case $state in
command)
local commands
commands=(
{command_definitions}
)
_describe 'commands' commands
;;
args)
case $words[1] in
{command_args}
esac
;;
esac
}}
# Register the completion function
compdef _{command_name} {command_name}
"""
def _generate_command_definitions(
self, commands: dict[str, dict[str, Any]], global_options: list[str]
) -> str:
"""Generate Zsh command definitions."""
definitions = []
command_args = []
for cmd_name, cmd_info in commands.items():
description = cmd_info.get("description", f"Run {cmd_name} command")
definitions.append(f' "{cmd_name}:{description}"')
# Check if this command has subcommands
if "subcommands" in cmd_info:
# Handle nested subcommands (like completion)
cmd_args = f""" {cmd_name})
_arguments \\"""
# Add main command options
main_options = cmd_info.get("options", [])
for option in main_options:
if option.startswith("--"):
opt_name = option.replace("--", "").replace("-", " ")
cmd_args += f"""
'{option}[{opt_name.title()}]' \\"""
elif option.startswith("-"):
opt_name = option.replace("-", "")
cmd_args += f"""
'{option}[{opt_name.upper()}]' \\"""
cmd_args += """
'1: :->subcommand' \\
'*:: :->subargs'
case $state in
subcommand)
local subcommands
subcommands=("""
# Add subcommand definitions
subcommands = cmd_info.get("subcommands", {})
for subcmd_name, subcmd_info in subcommands.items():
subcmd_desc = subcmd_info.get(
"description", f"{subcmd_name} subcommand"
)
cmd_args += f"""
"{subcmd_name}:{subcmd_desc}\""""
cmd_args += """
)
_describe 'subcommands' subcommands
;;
subargs)
case $words[1] in"""
# Add subcommand argument handling
for subcmd_name, subcmd_info in subcommands.items():
subcmd_options = subcmd_info.get("options", [])
cmd_args += f"""
{subcmd_name})
_arguments \\"""
for option in subcmd_options:
if option.startswith("--"):
opt_name = option.replace("--", "").replace("-", " ")
cmd_args += f"""
'{option}[{opt_name.title()}]' \\"""
elif option.startswith("-"):
opt_name = option.replace("-", "")
cmd_args += f"""
'{option}[{opt_name.upper()}]' \\"""
cmd_args = cmd_args.rstrip(" \\")
cmd_args += """
;;"""
cmd_args += """
esac
;;
esac
;;"""
else:
# Handle regular commands
options = cmd_info.get("options", [])
cmd_args = f""" {cmd_name})
_arguments \\"""
for option in options:
if option.startswith("--"):
opt_name = option.replace("--", "").replace("-", " ")
cmd_args += f"""
'{option}[{opt_name.title()}]' \\"""
elif option.startswith("-"):
opt_name = option.replace("-", "")
cmd_args += f"""
'{option}[{opt_name.upper()}]' \\"""
# Add global options
for option in global_options:
if option.startswith("--"):
opt_name = option.replace("--", "").replace("-", " ")
cmd_args += f"""
'{option}[{opt_name.title()}]' \\"""
cmd_args = cmd_args.rstrip(" \\")
cmd_args += """
;;"""
command_args.append(cmd_args)
definitions_str = "\n".join(definitions)
command_args_str = "\n".join(command_args)
# Return properly formatted string for template
return f"{definitions_str}||{command_args_str}"
[docs]
class FishCompletionGenerator(CompletionGenerator):
"""Generates Fish shell completion scripts."""
@property
def shell_name(self) -> str:
return "fish"
@property
def file_extension(self) -> str:
return ".fish"
[docs]
def generate(
self, commands: dict[str, dict[str, Any]], global_options: list[str]
) -> str:
"""Generate Fish completion script."""
template = self._get_template()
# Generate command completions
command_completions = self._generate_command_completions(
commands, global_options
)
return template.format(
command_name=self.command_name,
command_completions=command_completions,
)
def _get_template(self) -> str:
"""Get the Fish completion template."""
return """# {command_name} shell completion for Fish
# Generated automatically by XRayLabTool completion system
{command_completions}
"""
def _generate_command_completions(
self, commands: dict[str, dict[str, Any]], global_options: list[str]
) -> str:
"""Generate Fish command completions."""
completions = []
# Global options
for option in global_options:
if option.startswith("--"):
opt_name = option.replace("--", "").replace("-", " ")
completions.append(
f"complete -c {self.command_name} -l {option[2:]} -d"
f" '{opt_name.title()}'"
)
elif option.startswith("-") and len(option) == 2:
completions.append(
f"complete -c {self.command_name} -s {option[1]} -d 'Short option'"
)
# Command completions
for cmd_name, cmd_info in commands.items():
description = cmd_info.get("description", f"Run {cmd_name} command")
completions.append(
f"complete -c {self.command_name} -f -n '__fish_use_subcommand' -a"
f" '{cmd_name}' -d '{description}'"
)
if "subcommands" in cmd_info:
# Handle commands with subcommands (like completion)
subcommands = cmd_info.get("subcommands", {})
# Add main command options
main_options = cmd_info.get("options", [])
for option in main_options:
if option.startswith("--"):
opt_name = option.replace("--", "").replace("-", " ")
completions.append(
f"complete -c {self.command_name} -f -n"
f" '__fish_seen_subcommand_from {cmd_name}' -l {option[2:]} -d"
f" '{opt_name.title()}'"
)
elif option.startswith("-") and len(option) == 2:
completions.append(
f"complete -c {self.command_name} -f -n"
f" '__fish_seen_subcommand_from {cmd_name}' -s {option[1]} -d"
" 'Short option'"
)
# Add subcommand completions
for subcmd_name, subcmd_info in subcommands.items():
subcmd_desc = subcmd_info.get(
"description", f"{subcmd_name} subcommand"
)
completions.append(
f"complete -c {self.command_name} -f -n"
f" '__fish_seen_subcommand_from {cmd_name}' -a"
f" '{subcmd_name}' -d '{subcmd_desc}'"
)
# Add subcommand options
subcmd_options = subcmd_info.get("options", [])
for option in subcmd_options:
if option.startswith("--"):
opt_name = option.replace("--", "").replace("-", " ")
completions.append(
f"complete -c {self.command_name} -f -n"
f" '__fish_seen_subcommand_from {cmd_name}' -n"
f" 'test (count (commandline -opc)) -ge 3; and contains -- {subcmd_name} (commandline -opc)' -l {option[2:]} -d"
f" '{opt_name.title()}'"
)
elif option.startswith("-") and len(option) == 2:
completions.append(
f"complete -c {self.command_name} -f -n"
f" '__fish_seen_subcommand_from {cmd_name}' -n"
f" 'test (count (commandline -opc)) -ge 3; and contains -- {subcmd_name} (commandline -opc)' -s {option[1]} -d"
" 'Short option'"
)
else:
# Handle regular commands
options = cmd_info.get("options", [])
for option in options:
if option.startswith("--"):
opt_name = option.replace("--", "").replace("-", " ")
completions.append(
f"complete -c {self.command_name} -f -n"
f" '__fish_seen_subcommand_from {cmd_name}' -l {option[2:]} -d"
f" '{opt_name.title()}'"
)
elif option.startswith("-") and len(option) == 2:
completions.append(
f"complete -c {self.command_name} -f -n"
f" '__fish_seen_subcommand_from {cmd_name}' -s {option[1]} -d"
" 'Short option'"
)
return "\n".join(completions)
[docs]
class PowerShellCompletionGenerator(CompletionGenerator):
"""Generates PowerShell completion scripts."""
@property
def shell_name(self) -> str:
return "powershell"
@property
def file_extension(self) -> str:
return ".ps1"
[docs]
def generate(
self, commands: dict[str, dict[str, Any]], global_options: list[str]
) -> str:
"""Generate PowerShell completion script."""
template = self._get_template()
# Generate command cases
command_cases = self._generate_command_cases(commands)
# Generate global options
global_opts = ", ".join(f'"{opt}"' for opt in global_options)
return template.format(
command_name=self.command_name,
command_cases=command_cases,
global_options=global_opts,
)
def _get_template(self) -> str:
"""Get the PowerShell completion template."""
return """# {command_name} shell completion for PowerShell
# Generated automatically by XRayLabTool completion system
Register-ArgumentCompleter -Native -CommandName {command_name} -ScriptBlock {{
param($commandName, $wordToComplete, $cursorPosition)
$command = $wordToComplete
$words = $command -split '\\s+'
# Remove empty elements
$words = $words | Where-Object {{ $_ -ne '' }}
# Global options
$globalOptions = @({global_options})
if ($words.Count -le 1) {{
# Complete main commands and global options
$commands = @('calc', 'batch', 'compare', 'convert', 'formula', 'atomic', 'bragg', 'list', 'completion')
$completions = $commands + $globalOptions
$completions | Where-Object {{ $_ -like "$wordToComplete*" }}
return
}}
$subcommand = $words[1]
switch ($subcommand) {{
{command_cases}
default {{
$globalOptions | Where-Object {{ $_ -like "$wordToComplete*" }}
}}
}}
}}
"""
def _generate_command_cases(self, commands: dict[str, dict[str, Any]]) -> str:
"""Generate PowerShell command case statements."""
cases = []
for cmd_name, cmd_info in commands.items():
options = cmd_info.get("options", [])
if "subcommands" in cmd_info:
# Handle commands with subcommands (like completion)
subcommands = cmd_info.get("subcommands", {})
subcmd_list = ", ".join(f'"{subcmd}"' for subcmd in subcommands.keys())
option_list = ", ".join(f'"{opt}"' for opt in options)
case = f""" '{cmd_name}' {{
if ($words.Count -eq 2) {{
# Complete subcommands and main options
$subcommands = @({subcmd_list})
$options = @({option_list})
$completions = $subcommands + $options
$completions | Where-Object {{ $_ -like "$wordToComplete*" }}
}} elseif ($words.Count -gt 2) {{
# Handle subcommand options
$subcommand = $words[2]
switch ($subcommand) {{"""
for subcmd_name, subcmd_info in subcommands.items():
subcmd_options = subcmd_info.get("options", [])
subcmd_option_list = ", ".join(f'"{opt}"' for opt in subcmd_options)
case += f"""
'{subcmd_name}' {{
$subOptions = @({subcmd_option_list})
$subOptions | Where-Object {{ $_ -like "$wordToComplete*" }}
}}"""
case += """
default {
$globalOptions | Where-Object { $_ -like "$wordToComplete*" }
}
}
}
}"""
else:
# Handle regular commands
option_list = ", ".join(f'"{opt}"' for opt in options)
case = f""" '{cmd_name}' {{
$options = @({option_list})
$options | Where-Object {{ $_ -like "$wordToComplete*" }}
}}"""
cases.append(case)
return "\n".join(cases)
[docs]
class CompletionManager:
"""Manages completion script generation for all supported shells."""
[docs]
def __init__(self) -> None:
self.generators = {
"bash": BashCompletionGenerator(),
"zsh": ZshCompletionGenerator(),
"fish": FishCompletionGenerator(),
"powershell": PowerShellCompletionGenerator(),
}
[docs]
def get_supported_shells(self) -> list[str]:
"""Get list of supported shell types."""
return list(self.generators.keys())
[docs]
def generate_completion(
self, shell: str, commands: dict[str, dict[str, Any]], global_options: list[str]
) -> str:
"""Generate completion script for specified shell."""
if shell not in self.generators:
raise ValueError(f"Unsupported shell: {shell}")
generator = self.generators[shell]
return generator.generate(commands, global_options)
[docs]
def get_filename(self, shell: str, command_name: str = "xraylabtool") -> str:
"""Get the appropriate filename for a completion script."""
if shell not in self.generators:
raise ValueError(f"Unsupported shell: {shell}")
generator = self.generators[shell]
return f"{command_name}{generator.file_extension}"
[docs]
def get_global_options() -> list[str]:
"""Get global options available for all commands."""
return ["--help", "-h", "--version", "--verbose", "-v", "--config", "--log-level"]