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
2 changes: 1 addition & 1 deletion docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ Usage:
struct completion install [bash|zsh|fish]
```

- 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.
- 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.
- This does not modify your shell configuration; it only prints the commands you can copy-paste.

### `init`
Expand Down
141 changes: 44 additions & 97 deletions docs/completion.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Command-Line Auto-Completion

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.
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.

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

## Quick Setup

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

```sh
# Auto-detect current shell and print install steps
Expand All @@ -19,77 +19,48 @@ struct completion install bash
struct completion install fish
```

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

For most users, this simple setup will enable full completion:
## Manual Installation

```sh
# Install (if not already installed)
pip install argcomplete

# Enable completion for current session
eval "$(register-python-argcomplete struct)"

# Make permanent - add to your ~/.zshrc or ~/.bashrc
echo 'eval "$(register-python-argcomplete struct)"' >> ~/.zshrc
```

## Detailed Installation

### 1. Install argcomplete
### 1) Install shtab

```sh
pip install argcomplete
pip install shtab
```

### 2. Enable Global Completion (Optional)
### 2) Generate and install completion for your shell

This step is optional but can be done once per system:
- Zsh

```sh
activate-global-python-argcomplete
```

This command sets up global completion for all Python scripts that use argcomplete.
```sh
mkdir -p ~/.zfunc
struct --print-completion zsh > ~/.zfunc/_struct
# ensure in ~/.zshrc
fpath=(~/.zfunc $fpath)
autoload -U compinit && compinit
exec zsh
```

### 3. Register the Script
- Bash

Add the following line to your shell's configuration file:
```sh
mkdir -p ~/.local/share/bash-completion/completions
struct --print-completion bash > ~/.local/share/bash-completion/completions/struct
source ~/.bashrc
```

**For Bash** (`.bashrc` or `.bash_profile`):
- Fish

```sh
eval "$(register-python-argcomplete struct)"
```

**For Zsh** (`.zshrc`):

```sh
eval "$(register-python-argcomplete struct)"
```

**For Fish** (`.config/fish/config.fish`):

```fish
register-python-argcomplete --shell fish struct | source
```

### 4. Reload Your Shell

```sh
# For Bash
source ~/.bashrc

# For Zsh
source ~/.zshrc

# For Fish
source ~/.config/fish/config.fish
```
```sh
mkdir -p ~/.config/fish/completions
struct --print-completion fish > ~/.config/fish/completions/struct.fish
fish -c 'source ~/.config/fish/completions/struct.fish'
```

## Usage

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

### Command Completion
```sh
Expand Down Expand Up @@ -132,12 +103,7 @@ struct generate --log <Tab>

### Per-Project Completion

If you only want completion for specific projects, you can add completion to your project's virtual environment activation script:

```sh
# In your .venv/bin/activate file, add:
eval "$(register-python-argcomplete struct)"
```
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.

### Custom Completion

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

### Completion Not Working

1. **Check argcomplete installation**:

```sh
python -c "import argcomplete; print('OK')"
```

2. **Verify global activation**:
1. Verify shtab is installed in the environment you’re using:

```sh
activate-global-python-argcomplete --user
python -c "import shtab; print('OK')"
```

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

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

### Slow Completion

If completion is slow, you can enable caching:

```sh
export ARGCOMPLETE_USE_TEMPFILES=1
```

Add this to your shell configuration file for persistent caching.
4. Restart your shell (or run `exec zsh`/`source ~/.bashrc`).

### Debug Completion

Enable debug mode to troubleshoot completion issues:

```sh
export _ARGCOMPLETE_DEBUG=1
struct <Tab>
```
For shell-specific debugging, check that the generated file contains the struct completion function and is in the correct directory for your shell.

## Platform-Specific Notes

### macOS

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

```sh
# Using Homebrew
brew install bash-completion

# Then add to ~/.bash_profile:
# bash profile
[[ -r "/usr/local/etc/profile.d/bash_completion.sh" ]] && . "/usr/local/etc/profile.d/bash_completion.sh"
```

### Windows

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

### Docker

Expand Down
6 changes: 1 addition & 5 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ pip install git+https://github.com/httpdss/struct.git
```

!!! tip "Enable Auto-Completion"
After installation, enable command-line auto-completion for better productivity:
```sh
eval "$(register-python-argcomplete struct)"
```
For permanent setup, see the [Command-Line Completion](completion.md) guide.
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.

## From Source

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ openai
python-dotenv
jinja2
PyGithub
argcomplete
shtab
colorlog
boto3
google-cloud
Expand Down
39 changes: 22 additions & 17 deletions struct_module/commands/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
class CompletionCommand(Command):
def __init__(self, parser):
super().__init__(parser)
parser.description = "Manage CLI shell completions for struct (argcomplete)"
parser.description = "Manage CLI shell completions for struct (shtab-generated)"
sub = parser.add_subparsers(dest="action")

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

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

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

elif shell == "fish":
print("\n# One-time dependency (if not installed):")
print("python -m pip install argcomplete")
print("\n# Install fish completion file for 'struct':")
print("\n# Install shtab (once, in your environment):")
print("python -m pip install shtab")
print("\n# Generate static fish completion for 'struct':")
print('mkdir -p ~/.config/fish/completions')
print('register-python-argcomplete --shell fish struct > ~/.config/fish/completions/struct.fish')
print('struct --print-completion fish > ~/.config/fish/completions/struct.fish')
print("\n# Apply now:")
print("fish -c 'source ~/.config/fish/completions/struct.fish'")

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

print("\nTip: If 'register-python-argcomplete' is not found, try:\n python -m argcomplete.shellintegration <shell>")
print("\nTip: You can also print completion directly via: struct --print-completion <shell>")
49 changes: 44 additions & 5 deletions struct_module/commands/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
class GenerateCommand(Command):
def __init__(self, parser):
super().__init__(parser)
parser.description = "Generate the project structure from a YAML configuration file"
structure_arg = parser.add_argument('structure_definition', type=str, help='Path to the YAML configuration file')
structure_arg.completer = structures_completer
parser.add_argument('base_path', type=str, help='Base path where the structure will be created')
Expand All @@ -30,6 +31,33 @@ def __init__(self, parser):
choices=['console', 'file'], default='file', help='Output mode')
parser.set_defaults(func=self.execute)

def _parse_template_vars(self, vars_str):
"""Parse a comma-separated KEY=VALUE string into a dict safely.
- Ignores empty tokens and trailing commas
- Supports values containing '=' by splitting only on the first '='
- Logs and skips malformed entries without raising
"""
result = {}
if not vars_str:
return result
# Normalize by removing accidental leading/trailing commas and whitespace
tokens = [t.strip() for t in vars_str.strip(', ').split(',')]
for token in tokens:
if not token:
continue
if '=' not in token:
# Skip malformed item but warn
self.logger.warning(f"Skipping malformed template var (no '='): '{token}'")
continue
key, value = token.split('=', 1)
key = key.strip()
value = value
if not key:
self.logger.warning(f"Skipping template var with empty key: '{token}'")
continue
result[key] = value
return result

def _deep_merge_dicts(self, dict1, dict2):
"""
Deep merge two dictionaries, with dict2 values overriding dict1 values.
Expand Down Expand Up @@ -146,7 +174,8 @@ def _create_structure(self, args, mappings=None, summary=None, print_summary=Tru
if config is None:
return summary if summary is not None else None

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

if args.vars:
merged_vars = args.vars + "," + merged_vars
# Merge parent args.vars safely without introducing trailing commas
if getattr(args, 'vars', None):
parts = []
parent_vars = args.vars.strip().strip(',')
if parent_vars:
parts.append(parent_vars)
if merged_vars:
parts.append(merged_vars)
merged_vars = ",".join(parts)

# If nothing to merge, keep None to avoid accidental truthiness with empty string
merged_vars = merged_vars if merged_vars else None

if isinstance(content['struct'], str):
self._create_structure({
Expand Down Expand Up @@ -345,8 +384,8 @@ def _create_structure(self, args, mappings=None, summary=None, print_summary=Tru
self.logger.info(f" ✅ Created: {summary['created']}")
self.logger.info(f" ✅ Updated: {summary['updated']}")
self.logger.info(f" 📝 Appended: {summary['appended']}")
self.logger.info(f" ⏭️ Skipped: {summary['skipped']}")
self.logger.info(f" 🗄️ Backed up: {summary['backed_up']}")
self.logger.info(f" ⏭️ Skipped: {summary['skipped']}")
self.logger.info(f" 🗄️ Backed up: {summary['backed_up']}")
self.logger.info(f" 🔁 Renamed: {summary['renamed']}")
self.logger.info(f" 📁 Folders created: {summary['folders']}")
if args.dry_run:
Expand Down
1 change: 1 addition & 0 deletions struct_module/commands/generate_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
class GenerateSchemaCommand(Command):
def __init__(self, parser):
super().__init__(parser)
parser.description = "Generate JSON schema for available structures"
parser.add_argument('-s', '--structures-path', type=str, help='Path to structure definitions')
parser.add_argument('-o', '--output', type=str, help='Output file path for the schema (default: stdout)')
parser.set_defaults(func=self.execute)
Expand Down
Loading
Loading