feat: add zenodo completion install command#65
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a zenodo completion install subcommand that automates shell completion setup by detecting the user's shell (or accepting a --shell flag), showing what will be modified, and appending the appropriate completion source line to the shell's profile file after user confirmation. This eliminates the manual step of editing shell configuration files that most users skip, making tab completion more accessible.
Changes:
- Added
zenodo completion installsubcommand with shell auto-detection - Implemented shell-specific profile configuration for bash, zsh, fish, and powershell
- Added confirmation prompts before modifying shell configuration files
- Fish shell handled specially by writing completion file directly instead of sourcing
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| fmt.Printf("Completions already installed in %s\n", profilePath) | ||
| return nil | ||
| } | ||
|
|
||
| // Show what we'll do and ask for confirmation. | ||
| fmt.Printf("Shell: %s\n", shell) | ||
| fmt.Printf("File: %s\n", profilePath) | ||
| fmt.Printf("Append: %s\n", sourceLine) | ||
| fmt.Print("\nProceed? [y/N] ") | ||
|
|
||
| reader := bufio.NewReader(os.Stdin) | ||
| answer, _ := reader.ReadString('\n') | ||
| answer = strings.TrimSpace(strings.ToLower(answer)) | ||
| if answer != "y" && answer != "yes" { | ||
| fmt.Println("Cancelled.") |
There was a problem hiding this comment.
Informational messages should be written to stderr for consistency with the rest of the codebase. Other commands consistently use fmt.Fprintf(os.Stderr, ...) for user-facing informational messages (see config.go:50, deposit.go:48, records.go:72). This allows users to redirect actual output separately from status messages.
| fmt.Printf("Completions already installed in %s\n", profilePath) | |
| return nil | |
| } | |
| // Show what we'll do and ask for confirmation. | |
| fmt.Printf("Shell: %s\n", shell) | |
| fmt.Printf("File: %s\n", profilePath) | |
| fmt.Printf("Append: %s\n", sourceLine) | |
| fmt.Print("\nProceed? [y/N] ") | |
| reader := bufio.NewReader(os.Stdin) | |
| answer, _ := reader.ReadString('\n') | |
| answer = strings.TrimSpace(strings.ToLower(answer)) | |
| if answer != "y" && answer != "yes" { | |
| fmt.Println("Cancelled.") | |
| fmt.Fprintf(os.Stderr, "Completions already installed in %s\n", profilePath) | |
| return nil | |
| } | |
| // Show what we'll do and ask for confirmation. | |
| fmt.Fprintf(os.Stderr, "Shell: %s\n", shell) | |
| fmt.Fprintf(os.Stderr, "File: %s\n", profilePath) | |
| fmt.Fprintf(os.Stderr, "Append: %s\n", sourceLine) | |
| fmt.Fprint(os.Stderr, "\nProceed? [y/N] ") | |
| reader := bufio.NewReader(os.Stdin) | |
| answer, _ := reader.ReadString('\n') | |
| answer = strings.TrimSpace(strings.ToLower(answer)) | |
| if answer != "y" && answer != "yes" { | |
| fmt.Fprintln(os.Stderr, "Cancelled.") |
| fmt.Printf("Shell: %s\n", shell) | ||
| fmt.Printf("File: %s\n", profilePath) | ||
| fmt.Printf("Append: %s\n", sourceLine) | ||
| fmt.Print("\nProceed? [y/N] ") |
There was a problem hiding this comment.
The confirmation prompt message should be written to stderr for consistency. See deposit.go:294 where the confirm() helper writes the prompt to stderr. This follows the convention of sending user prompts to stderr while reserving stdout for actual program output.
| fmt.Print("\nProceed? [y/N] ") | |
| fmt.Fprint(os.Stderr, "\nProceed? [y/N] ") |
| answer, _ := reader.ReadString('\n') | ||
| answer = strings.TrimSpace(strings.ToLower(answer)) | ||
| if answer != "y" && answer != "yes" { | ||
| fmt.Println("Cancelled.") |
There was a problem hiding this comment.
The "Cancelled." message should be written to stderr for consistency. Throughout the codebase, status messages like this are written to stderr (see deposit.go:188). This allows users to distinguish program output from status messages.
| fmt.Println("Cancelled.") | |
| fmt.Fprintln(os.Stderr, "Cancelled.") |
| return fmt.Errorf("writing to %s: %w", profilePath, err) | ||
| } | ||
|
|
||
| fmt.Printf("\nDone! Restart your shell or run:\n source %s\n", profilePath) |
There was a problem hiding this comment.
The success message should be written to stderr for consistency. Throughout the codebase, informational messages to the user are written to stderr (see config.go:50, deposit.go:129). This ensures that stdout remains clean for program output that might be piped to other commands.
| fmt.Printf("\nDone! Restart your shell or run:\n source %s\n", profilePath) | |
| fmt.Fprintf(os.Stderr, "\nDone! Restart your shell or run:\n source %s\n", profilePath) |
|
|
||
| switch shell { | ||
| case "bash": | ||
| return filepath.Join(home, ".bashrc"), |
There was a problem hiding this comment.
On macOS, login shells typically source .bash_profile while non-login shells source .bashrc. Many macOS users don't have a .bashrc or it's not sourced by default. Consider checking for .bash_profile on macOS (when runtime.GOOS == "darwin") as the primary location, falling back to .bashrc if it doesn't exist. Alternatively, append to both files if they exist, or provide guidance to users about which file was modified.
| return filepath.Join(home, ".bashrc"), | |
| bashProfile := filepath.Join(home, ".bash_profile") | |
| bashRC := filepath.Join(home, ".bashrc") | |
| // On macOS, login shells typically source .bash_profile, while non-login shells | |
| // source .bashrc. Prefer .bash_profile when it exists, falling back to .bashrc. | |
| if runtime.GOOS == "darwin" { | |
| if _, err := os.Stat(bashProfile); err == nil { | |
| return bashProfile, | |
| `eval "$(zenodo completion bash)"`, | |
| nil | |
| } | |
| // If .bash_profile doesn't exist, fall back to .bashrc (which may or may not exist). | |
| return bashRC, | |
| `eval "$(zenodo completion bash)"`, | |
| nil | |
| } | |
| // Non-macOS: keep using .bashrc as before. | |
| return bashRC, |
| fmt.Printf("Fish completions already installed at %s\n", path) | ||
| return nil | ||
| } | ||
|
|
||
| fmt.Printf("Write fish completions to %s\n", path) | ||
| fmt.Print("Proceed? [y/N] ") | ||
|
|
||
| reader := bufio.NewReader(os.Stdin) | ||
| answer, _ := reader.ReadString('\n') | ||
| answer = strings.TrimSpace(strings.ToLower(answer)) | ||
| if answer != "y" && answer != "yes" { | ||
| fmt.Println("Cancelled.") | ||
| return nil | ||
| } | ||
|
|
||
| // Ensure directory exists. | ||
| if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { | ||
| return fmt.Errorf("creating directory: %w", err) | ||
| } | ||
|
|
||
| f, err := os.Create(path) | ||
| if err != nil { | ||
| return fmt.Errorf("creating %s: %w", path, err) | ||
| } | ||
| defer f.Close() | ||
|
|
||
| if err := rootCmd.GenFishCompletion(f, true); err != nil { | ||
| return fmt.Errorf("generating fish completions: %w", err) | ||
| } | ||
|
|
||
| fmt.Printf("\nDone! Completions will be loaded automatically in new fish sessions.\n") |
There was a problem hiding this comment.
All user-facing messages in this function should be written to stderr for consistency. The codebase consistently uses fmt.Fprintf(os.Stderr, ...) for informational messages (see config.go:50, deposit.go:48, records.go:72).
| for _, ps := range []string{"pwsh", "powershell"} { | ||
| out, err := exec.Command(ps, "-NoProfile", "-Command", "echo $PROFILE").Output() | ||
| if err == nil { | ||
| p := strings.TrimSpace(string(out)) | ||
| if p != "" { | ||
| return p | ||
| } | ||
| } |
There was a problem hiding this comment.
The PowerShell command execution could hang indefinitely if PowerShell has issues. Consider adding a timeout using exec.CommandContext with a context that has a deadline (e.g., 5 seconds). This prevents the command from hanging indefinitely if PowerShell is unresponsive.
|
|
||
| // Check if already installed. | ||
| if alreadyInstalled(profilePath, sourceLine) { | ||
| fmt.Printf("Completions already installed in %s\n", profilePath) |
There was a problem hiding this comment.
The "already installed" message should be written to stderr for consistency. Other informational messages in the codebase are consistently written to stderr (see config.go:50, deposit.go:48).
| fmt.Printf("Completions already installed in %s\n", profilePath) | |
| fmt.Fprintf(os.Stderr, "Completions already installed in %s\n", profilePath) |
| reader := bufio.NewReader(os.Stdin) | ||
| answer, _ := reader.ReadString('\n') | ||
| answer = strings.TrimSpace(strings.ToLower(answer)) | ||
| if answer != "y" && answer != "yes" { | ||
| fmt.Println("Cancelled.") | ||
| return nil | ||
| } |
There was a problem hiding this comment.
Consider extracting this confirmation prompt logic into a reusable helper function to reduce duplication. The same pattern is repeated in installFishCompletion at lines 228-233, and similar logic exists in deposit.go:293-299 (currently commented out). A shared helper would improve maintainability and ensure consistent behavior across commands.
The source line in the shell profile now uses the full path to the executable so it works regardless of PATH or working directory. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users install via Scoop so zenodo is on PATH. The absolute exe path was wrong since it would break when the binary moves. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolves the running executable's absolute path so completions work both during local dev (./zenodo.exe) and after Scoop install. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
zenodo completion installsubcommand that auto-configures shell completions--shellflag)./zenodo.exe) and after Scoop install (zenodo)ELI5
Right now you can press Tab to autocomplete flags, but only if you manually edit your shell config file. This adds a one-time
zenodo completion installcommand that does that setup for you — it figures out your shell, shows you what it'll change, and asks before doing it.How to test
1. Build
2. Install completions
In PowerShell, run:
You should see something like:
Type
yand hit Enter.3. Restart PowerShell
Close the window and open a new one.
4. Try tab completion
Type each of these and press Tab — you should see options autocomplete:
5. Run again — should say already installed
Closes #63
🤖 Generated with Claude Code