Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/completion.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Command-Line Auto-Completion

StructKit provides intelligent auto-completion for commands, options, and structure names using static completion scripts generated by [shtab](https://github.com/Iterative/shtab). This approach is reliable across shells and doesn’t require runtime hooks or markers.
StructKit provides intelligent auto-completion for commands, options, and structure names using static completion scripts. Bash, Zsh, and Tcsh scripts are generated by [shtab](https://github.com/Iterative/shtab); Fish scripts are generated by StructKit. This approach is reliable across shells and doesn’t require runtime hooks or markers.

!!! tip "Structure Name Completion"
StructKit completes structure names when using `structkit generate`, showing available structures from both built-in and custom paths.
Expand All @@ -19,11 +19,11 @@ structkit completion install bash
structkit completion install fish
```

You can also generate completion files manually with shtab as shown below.
You can also generate completion files manually as shown below.

## Manual Installation

### 1) Install shtab
### 1) Install shtab (Bash, Zsh, and Tcsh only)

```sh
pip install shtab
Expand Down
4 changes: 1 addition & 3 deletions structkit/commands/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ def _install(self, args):
print("exec zsh")

elif shell == "fish":
print("\n# Install shtab (once, in your environment):")
print("python -m pip install shtab")
print("\n# Generate static fish completion for 'struct':")
print("\n# Generate static fish completion for 'structkit':")
print('mkdir -p ~/.config/fish/completions')
print('structkit --print-completion fish > ~/.config/fish/completions/structkit.fish')
print("\n# Apply now:")
Expand Down
99 changes: 96 additions & 3 deletions structkit/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import argparse
import logging
import os
import shlex
from dotenv import load_dotenv
from structkit.utils import read_config_file, merge_configs
from structkit.commands.generate import GenerateCommand
Expand All @@ -24,6 +25,94 @@
shtab = None

load_dotenv()
SUPPORTED_COMPLETION_SHELLS = ("bash", "zsh", "tcsh", "fish")


def _fish_quote(value):
return shlex.quote(str(value))


def _fish_condition(command_path):
if not command_path:
return "__fish_structkit_using_command"
return "__fish_structkit_using_command " + " ".join(_fish_quote(command) for command in command_path)


def _fish_completion_script(parser):
"""Generate a static Fish completion script from an argparse parser."""
lines = [
"function __fish_structkit_using_command",
" set -l words (commandline -opc)",
" set -e words[1]",
" set -l commands",
" for word in $words",
" if not string match -qr '^-' -- $word",
" set -a commands $word",
" end",
" end",
" test (count $commands) -eq (count $argv)",
" or return 1",
" for index in (seq (count $argv))",
" test \"$commands[$index]\" = \"$argv[$index]\"",
" or return 1",
" end",
"end",
"",
f"complete -c {_fish_quote(parser.prog)} -f",
]

def add_completions(current_parser, command_path=()):
condition = _fish_condition(command_path)
for action in current_parser._actions:
if isinstance(action, argparse._SubParsersAction):
for name, subparser in action.choices.items():
if name not in action._name_parser_map:
continue
help_text = action._name_parser_map[name].description or ""
command = (
f"complete -c {_fish_quote(parser.prog)} -n {_fish_quote(condition)} "
f"-a {_fish_quote(name)}"
)
if help_text:
command += f" -d {_fish_quote(help_text)}"
lines.append(command)
add_completions(subparser, command_path + (name,))
continue

if action.help is argparse.SUPPRESS:
continue

choices = getattr(action, "choices", None)
arguments = ""
if choices:
arguments = " -a " + _fish_quote(" ".join(map(str, choices)))

for option in action.option_strings:
option_type = "-l" if option.startswith("--") else "-s"
command = (
f"complete -c {_fish_quote(parser.prog)} -n {_fish_quote(condition)} "
f"{option_type} {_fish_quote(option.lstrip('-'))}"
)
if action.nargs != 0:
command += " -r"
command += arguments
if action.help:
command += f" -d {_fish_quote(action.help)}"
lines.append(command)

add_completions(parser)
return "\n".join(lines)


class PrintCompletionAction(argparse.Action):
"""Print a completion script for shtab-supported shells or Fish."""

def __call__(self, parser, namespace, values, option_string=None):
if values == "fish":
print(_fish_completion_script(parser))
else:
print(shtab.complete(parser, values))
parser.exit(0)


def get_parser():
Expand Down Expand Up @@ -57,10 +146,14 @@ def get_parser():
from structkit.commands.completion import CompletionCommand
CompletionCommand(subparsers.add_parser('completion', help='Manage shell completions'))

# Add shtab completion printing flags if available
# shtab supports bash, zsh, and tcsh. Fish is generated locally.
if shtab is not None:
# Adds --print-completion and --shell flags
shtab.add_argument_to(parser)
parser.add_argument(
"--print-completion",
choices=SUPPORTED_COMPLETION_SHELLS,
action=PrintCompletionAction,
help="print shell completion script",
)

return parser

Expand Down
18 changes: 18 additions & 0 deletions tests/test_completion_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from unittest.mock import patch

from structkit.commands.completion import CompletionCommand
from structkit.main import get_parser


def make_parser():
Expand Down Expand Up @@ -47,6 +48,7 @@ def test_completion_install_fish_explicit():
assert "Detected shell: fish" in out
assert "structkit --print-completion fish" in out
assert "~/.config/fish/completions/structkit.fish" in out
assert "pip install shtab" not in out


def test_completion_install_auto_detect_zsh():
Expand All @@ -59,3 +61,19 @@ def test_completion_install_auto_detect_zsh():
out = _gather_print_output(mock_print)
assert "Detected shell: zsh" in out
assert "structkit --print-completion zsh" in out


def test_print_completion_fish_generates_a_fish_script(capsys):
parser = get_parser()

try:
parser.parse_args(['--print-completion', 'fish'])
except SystemExit as error:
assert error.code == 0

output = capsys.readouterr().out
assert "function __fish_structkit_using_command" in output
assert "complete -c structkit -f" in output
assert "complete -c structkit -n __fish_structkit_using_command -a generate" in output
assert " -s h -d " in output
assert " -l print-completion -r -a 'bash zsh tcsh fish'" in output
Loading