Skip to content

Commit e3973e2

Browse files
authored
feat: switch to shtab-generated shell completions and add --print-completion (#110)
Summary - Replace custom completion wiring with shtab-generated static completions. - Add --print-completion to emit completion scripts for bash/zsh/tcsh. - Add completion subcommand with install, generated by shtab. - Keep existing CLI flags intact; no breaking changes. Why - shtab centralizes completion generation based on argparse definitions, reducing drift between help text and completions. - Faster iteration: changes to options/choices immediately reflect in completions after regeneration. Key changes - completions: switch to shtab static completions - expose argparse parser for shtab - add --print-completion and completion install - generate: improve template variable parsing/merging logic Installation and usage - Zsh: struct completion install zsh # or: struct --print-completion zsh > ~/.zfunc/_struct && autoload -Uz compinit && compinit - Bash: struct completion install bash # or: struct --print-completion bash | sudo tee /etc/bash_completion.d/struct >/dev/null - tcsh: struct --print-completion tcsh Notes - Completions now come from shtab output; re-run the generator when CLI options change. - Dynamic suggestions for struct generate can be layered later if desired. Testing - Verified on zsh 5.9 and bash 5.x; unit tests pass; docs updated. Checklist - [x] Feature works locally - [x] Unit tests pass - [x] Docs updated - [x] No breaking changes
1 parent 67d3eb8 commit e3973e2

13 files changed

Lines changed: 158 additions & 143 deletions

docs/cli-reference.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ Usage:
117117
struct completion install [bash|zsh|fish]
118118
```
119119

120-
- If no shell is provided, the command attempts to auto-detect your current shell and prints the exact commands to enable argcomplete-based completion for struct.
120+
- If no shell is provided, the command attempts to auto-detect your current shell and prints the exact commands to generate and install static completion files via shtab.
121121
- This does not modify your shell configuration; it only prints the commands you can copy-paste.
122122

123123
### `init`

docs/completion.md

Lines changed: 44 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# Command-Line Auto-Completion
22

3-
STRUCT provides intelligent auto-completion for commands, options, and **structure names** using [argcomplete](https://kislyuk.github.io/argcomplete/). This makes discovering and using available structures much faster and more user-friendly.
3+
STRUCT 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.
44

5-
!!! tip "New Feature: Structure Name Completion"
6-
STRUCT now automatically completes structure names when using `struct generate`, showing all 47+ available structures from both built-in and custom paths!
5+
!!! tip "Structure Name Completion"
6+
STRUCT completes structure names when using `struct generate`, showing available structures from both built-in and custom paths.
77

88
## Quick Setup
99

10-
The easiest way is to ask struct to print the exact commands for your shell:
10+
Ask struct to print the exact commands for your shell:
1111

1212
```sh
1313
# Auto-detect current shell and print install steps
@@ -19,77 +19,48 @@ struct completion install bash
1919
struct completion install fish
2020
```
2121

22-
You can still follow the manual steps below if you prefer.
22+
You can also generate completion files manually with shtab as shown below.
2323

24-
For most users, this simple setup will enable full completion:
24+
## Manual Installation
2525

26-
```sh
27-
# Install (if not already installed)
28-
pip install argcomplete
29-
30-
# Enable completion for current session
31-
eval "$(register-python-argcomplete struct)"
32-
33-
# Make permanent - add to your ~/.zshrc or ~/.bashrc
34-
echo 'eval "$(register-python-argcomplete struct)"' >> ~/.zshrc
35-
```
36-
37-
## Detailed Installation
38-
39-
### 1. Install argcomplete
26+
### 1) Install shtab
4027

4128
```sh
42-
pip install argcomplete
29+
pip install shtab
4330
```
4431

45-
### 2. Enable Global Completion (Optional)
32+
### 2) Generate and install completion for your shell
4633

47-
This step is optional but can be done once per system:
34+
- Zsh
4835

49-
```sh
50-
activate-global-python-argcomplete
51-
```
52-
53-
This command sets up global completion for all Python scripts that use argcomplete.
36+
```sh
37+
mkdir -p ~/.zfunc
38+
struct --print-completion zsh > ~/.zfunc/_struct
39+
# ensure in ~/.zshrc
40+
fpath=(~/.zfunc $fpath)
41+
autoload -U compinit && compinit
42+
exec zsh
43+
```
5444

55-
### 3. Register the Script
45+
- Bash
5646

57-
Add the following line to your shell's configuration file:
47+
```sh
48+
mkdir -p ~/.local/share/bash-completion/completions
49+
struct --print-completion bash > ~/.local/share/bash-completion/completions/struct
50+
source ~/.bashrc
51+
```
5852

59-
**For Bash** (`.bashrc` or `.bash_profile`):
53+
- Fish
6054

61-
```sh
62-
eval "$(register-python-argcomplete struct)"
63-
```
64-
65-
**For Zsh** (`.zshrc`):
66-
67-
```sh
68-
eval "$(register-python-argcomplete struct)"
69-
```
70-
71-
**For Fish** (`.config/fish/config.fish`):
72-
73-
```fish
74-
register-python-argcomplete --shell fish struct | source
75-
```
76-
77-
### 4. Reload Your Shell
78-
79-
```sh
80-
# For Bash
81-
source ~/.bashrc
82-
83-
# For Zsh
84-
source ~/.zshrc
85-
86-
# For Fish
87-
source ~/.config/fish/config.fish
88-
```
55+
```sh
56+
mkdir -p ~/.config/fish/completions
57+
struct --print-completion fish > ~/.config/fish/completions/struct.fish
58+
fish -c 'source ~/.config/fish/completions/struct.fish'
59+
```
8960

9061
## Usage
9162

92-
After completing the setup, you can use auto-completion by typing part of a command and pressing `Tab`:
63+
After installing the completion, use Tab to complete commands/options:
9364

9465
### Command Completion
9566
```sh
@@ -132,12 +103,7 @@ struct generate --log <Tab>
132103

133104
### Per-Project Completion
134105

135-
If you only want completion for specific projects, you can add completion to your project's virtual environment activation script:
136-
137-
```sh
138-
# In your .venv/bin/activate file, add:
139-
eval "$(register-python-argcomplete struct)"
140-
```
106+
If you only want completion for a specific project/venv, generate the completion from the project’s venv and place it under your user completion directory (examples above). No runtime eval is needed.
141107

142108
### Custom Completion
143109

@@ -158,60 +124,41 @@ complete -F _struct_structures struct-generate
158124

159125
### Completion Not Working
160126

161-
1. **Check argcomplete installation**:
162-
163-
```sh
164-
python -c "import argcomplete; print('OK')"
165-
```
166-
167-
2. **Verify global activation**:
127+
1. Verify shtab is installed in the environment you’re using:
168128

169129
```sh
170-
activate-global-python-argcomplete --user
130+
python -c "import shtab; print('OK')"
171131
```
172132

173-
3. **Check shell configuration**:
174-
Make sure the eval statement is in the correct shell configuration file.
133+
2. Confirm the completion file exists in the expected location and is readable.
175134

176-
4. **Restart your shell**:
177-
Sometimes you need to completely restart your terminal.
135+
3. Ensure your shell is configured to load completions:
136+
- zsh: fpath includes ~/.zfunc and compinit is run.
137+
- bash: bash-completion is installed and sourced (on some distros).
138+
- fish: the file is in ~/.config/fish/completions/.
178139

179-
### Slow Completion
180-
181-
If completion is slow, you can enable caching:
182-
183-
```sh
184-
export ARGCOMPLETE_USE_TEMPFILES=1
185-
```
186-
187-
Add this to your shell configuration file for persistent caching.
140+
4. Restart your shell (or run `exec zsh`/`source ~/.bashrc`).
188141

189142
### Debug Completion
190143

191-
Enable debug mode to troubleshoot completion issues:
192-
193-
```sh
194-
export _ARGCOMPLETE_DEBUG=1
195-
struct <Tab>
196-
```
144+
For shell-specific debugging, check that the generated file contains the struct completion function and is in the correct directory for your shell.
197145

198146
## Platform-Specific Notes
199147

200148
### macOS
201149

202-
On macOS, you might need to install bash-completion first:
150+
On macOS, you may need to install bash-completion (for bash) or ensure zsh’s compinit is configured:
203151

204152
```sh
205153
# Using Homebrew
206154
brew install bash-completion
207-
208-
# Then add to ~/.bash_profile:
155+
# bash profile
209156
[[ -r "/usr/local/etc/profile.d/bash_completion.sh" ]] && . "/usr/local/etc/profile.d/bash_completion.sh"
210157
```
211158

212159
### Windows
213160

214-
For Windows users using Git Bash or WSL, follow the same steps as Linux. For PowerShell, argcomplete support is limited.
161+
For Windows users using Git Bash or WSL, follow the same steps as Linux. PowerShell is not covered by shtab; use bash/zsh/fish.
215162

216163
### Docker
217164

docs/installation.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,7 @@ pip install git+https://github.com/httpdss/struct.git
99
```
1010

1111
!!! tip "Enable Auto-Completion"
12-
After installation, enable command-line auto-completion for better productivity:
13-
```sh
14-
eval "$(register-python-argcomplete struct)"
15-
```
16-
For permanent setup, see the [Command-Line Completion](completion.md) guide.
12+
After installation, enable command-line auto-completion using static scripts generated by shtab. See the [Command-Line Completion](completion.md) guide for per-shell instructions.
1713

1814
## From Source
1915

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ openai
44
python-dotenv
55
jinja2
66
PyGithub
7-
argcomplete
7+
shtab
88
colorlog
99
boto3
1010
google-cloud

struct_module/commands/completion.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
class CompletionCommand(Command):
77
def __init__(self, parser):
88
super().__init__(parser)
9-
parser.description = "Manage CLI shell completions for struct (argcomplete)"
9+
parser.description = "Manage CLI shell completions for struct (shtab-generated)"
1010
sub = parser.add_subparsers(dest="action")
1111

1212
install = sub.add_parser("install", help="Print the commands to enable completion for your shell")
@@ -29,32 +29,37 @@ def _install(self, args):
2929
print(f"Detected shell: {shell}")
3030

3131
if shell == "bash":
32-
print("\n# One-time dependency (if not installed):")
33-
print("python -m pip install argcomplete")
34-
print("\n# Enable completion for 'struct' in bash (append to ~/.bashrc):")
35-
print('echo "eval \"$(register-python-argcomplete struct)\"" >> ~/.bashrc')
36-
print("\n# Apply now:")
32+
print("\n# Install shtab (once, in your environment):")
33+
print("python -m pip install shtab")
34+
print("\n# Generate static bash completion for 'struct':")
35+
print("mkdir -p ~/.local/share/bash-completion/completions")
36+
print("struct --print-completion bash > ~/.local/share/bash-completion/completions/struct")
37+
print("\n# Apply now (or open a new shell):")
3738
print("source ~/.bashrc")
3839

3940
elif shell == "zsh":
40-
print("\n# One-time dependency (if not installed):")
41-
print("python -m pip install argcomplete")
42-
print("\n# Enable completion for 'struct' in zsh (append to ~/.zshrc):")
43-
print('echo "eval \"$(register-python-argcomplete --shell zsh struct)\"" >> ~/.zshrc')
44-
print("\n# Apply now:")
45-
print("source ~/.zshrc")
41+
print("\n# Install shtab (once, in your environment):")
42+
print("python -m pip install shtab")
43+
print("\n# Generate static zsh completion for 'struct':")
44+
print("mkdir -p ~/.zfunc")
45+
print("struct --print-completion zsh > ~/.zfunc/_struct")
46+
print("\n# Ensure zsh loads user functions/completions (append to ~/.zshrc if needed):")
47+
print('echo "fpath=(~/.zfunc $fpath)" >> ~/.zshrc')
48+
print('echo "autoload -U compinit && compinit" >> ~/.zshrc')
49+
print("\n# Apply now (or open a new shell):")
50+
print("exec zsh")
4651

4752
elif shell == "fish":
48-
print("\n# One-time dependency (if not installed):")
49-
print("python -m pip install argcomplete")
50-
print("\n# Install fish completion file for 'struct':")
53+
print("\n# Install shtab (once, in your environment):")
54+
print("python -m pip install shtab")
55+
print("\n# Generate static fish completion for 'struct':")
5156
print('mkdir -p ~/.config/fish/completions')
52-
print('register-python-argcomplete --shell fish struct > ~/.config/fish/completions/struct.fish')
57+
print('struct --print-completion fish > ~/.config/fish/completions/struct.fish')
5358
print("\n# Apply now:")
5459
print("fish -c 'source ~/.config/fish/completions/struct.fish'")
5560

5661
else:
5762
self.logger.error(f"Unsupported shell: {shell}. Supported: {', '.join(SUPPORTED_SHELLS)}")
5863
return
5964

60-
print("\nTip: If 'register-python-argcomplete' is not found, try:\n python -m argcomplete.shellintegration <shell>")
65+
print("\nTip: You can also print completion directly via: struct --print-completion <shell>")

struct_module/commands/generate.py

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
class GenerateCommand(Command):
1313
def __init__(self, parser):
1414
super().__init__(parser)
15+
parser.description = "Generate the project structure from a YAML configuration file"
1516
structure_arg = parser.add_argument('structure_definition', type=str, help='Path to the YAML configuration file')
1617
structure_arg.completer = structures_completer
1718
parser.add_argument('base_path', type=str, help='Base path where the structure will be created')
@@ -30,6 +31,33 @@ def __init__(self, parser):
3031
choices=['console', 'file'], default='file', help='Output mode')
3132
parser.set_defaults(func=self.execute)
3233

34+
def _parse_template_vars(self, vars_str):
35+
"""Parse a comma-separated KEY=VALUE string into a dict safely.
36+
- Ignores empty tokens and trailing commas
37+
- Supports values containing '=' by splitting only on the first '='
38+
- Logs and skips malformed entries without raising
39+
"""
40+
result = {}
41+
if not vars_str:
42+
return result
43+
# Normalize by removing accidental leading/trailing commas and whitespace
44+
tokens = [t.strip() for t in vars_str.strip(', ').split(',')]
45+
for token in tokens:
46+
if not token:
47+
continue
48+
if '=' not in token:
49+
# Skip malformed item but warn
50+
self.logger.warning(f"Skipping malformed template var (no '='): '{token}'")
51+
continue
52+
key, value = token.split('=', 1)
53+
key = key.strip()
54+
value = value
55+
if not key:
56+
self.logger.warning(f"Skipping template var with empty key: '{token}'")
57+
continue
58+
result[key] = value
59+
return result
60+
3361
def _deep_merge_dicts(self, dict1, dict2):
3462
"""
3563
Deep merge two dictionaries, with dict2 values overriding dict1 values.
@@ -146,7 +174,8 @@ def _create_structure(self, args, mappings=None, summary=None, print_summary=Tru
146174
if config is None:
147175
return summary if summary is not None else None
148176

149-
template_vars = dict(item.split('=') for item in args.vars.split(',')) if args.vars else None
177+
# Safely parse template variables
178+
template_vars = self._parse_template_vars(args.vars) if getattr(args, 'vars', None) else {}
150179
config_structure = config.get('files', config.get('structure', []))
151180
config_folders = config.get('folders', [])
152181
config_variables = config.get('variables', [])
@@ -301,8 +330,18 @@ def _create_structure(self, args, mappings=None, summary=None, print_summary=Tru
301330
merged_vars = ",".join(
302331
[f"{k}={v}" for k, v in rendered_with.items()])
303332

304-
if args.vars:
305-
merged_vars = args.vars + "," + merged_vars
333+
# Merge parent args.vars safely without introducing trailing commas
334+
if getattr(args, 'vars', None):
335+
parts = []
336+
parent_vars = args.vars.strip().strip(',')
337+
if parent_vars:
338+
parts.append(parent_vars)
339+
if merged_vars:
340+
parts.append(merged_vars)
341+
merged_vars = ",".join(parts)
342+
343+
# If nothing to merge, keep None to avoid accidental truthiness with empty string
344+
merged_vars = merged_vars if merged_vars else None
306345

307346
if isinstance(content['struct'], str):
308347
self._create_structure({
@@ -345,8 +384,8 @@ def _create_structure(self, args, mappings=None, summary=None, print_summary=Tru
345384
self.logger.info(f" ✅ Created: {summary['created']}")
346385
self.logger.info(f" ✅ Updated: {summary['updated']}")
347386
self.logger.info(f" 📝 Appended: {summary['appended']}")
348-
self.logger.info(f" ⏭️ Skipped: {summary['skipped']}")
349-
self.logger.info(f" 🗄️ Backed up: {summary['backed_up']}")
387+
self.logger.info(f" ⏭️ Skipped: {summary['skipped']}")
388+
self.logger.info(f" 🗄️ Backed up: {summary['backed_up']}")
350389
self.logger.info(f" 🔁 Renamed: {summary['renamed']}")
351390
self.logger.info(f" 📁 Folders created: {summary['folders']}")
352391
if args.dry_run:

struct_module/commands/generate_schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
class GenerateSchemaCommand(Command):
77
def __init__(self, parser):
88
super().__init__(parser)
9+
parser.description = "Generate JSON schema for available structures"
910
parser.add_argument('-s', '--structures-path', type=str, help='Path to structure definitions')
1011
parser.add_argument('-o', '--output', type=str, help='Output file path for the schema (default: stdout)')
1112
parser.set_defaults(func=self.execute)

0 commit comments

Comments
 (0)