Skip to content
Open
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
70 changes: 62 additions & 8 deletions pkg/cmd/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/DopplerHQ/cli/pkg/utils"
Expand Down Expand Up @@ -73,6 +74,8 @@ var completionInstallCmd = &cobra.Command{
var buf bytes.Buffer
var path string
var name string
// Track if using fallback path (requires manual fpath configuration)
var usingFallbackPath bool

if utils.IsWindows() {
utils.HandleError(errors.New("Completion files are not supported on Windows. You can use completion files with Windows Subsystem for Linux (WSL)."))
Expand All @@ -94,15 +97,31 @@ var completionInstallCmd = &cobra.Command{
}
// zsh completions file start with an underscore
name = "_doppler"
path = "/usr/local/share/zsh/site-functions"
// Try standard path first, fall back to user directory if not writable
standardPath := "/usr/local/share/zsh/site-functions"
if isDirectoryWritable(standardPath) {
path = standardPath
} else {
// Fall back to XDG_DATA_HOME/doppler or ~/.local/share/doppler
dataHome := os.Getenv("XDG_DATA_HOME")
if dataHome == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
utils.HandleError(err, "Unable to determine home directory")
}
dataHome = fmt.Sprintf("%s/.local/share", homeDir)
}
path = fmt.Sprintf("%s/doppler/zsh/completions", dataHome)
usingFallbackPath = true
}
} else {
utils.HandleError(errors.New("Your shell is not supported"))
}

// create directory if it doesn't exist
// create directory and intermediate directories if they don't exist
if !utils.Exists(path) {
// using 755 to mimic expected /etc/ perms
err := os.Mkdir(path, 0755) // #nosec G301
err := os.MkdirAll(path, 0755) // #nosec G301
if err != nil {
utils.HandleError(err, "Unable to write completion file")
}
Expand All @@ -114,12 +133,18 @@ var completionInstallCmd = &cobra.Command{
utils.HandleError(err, "Unable to write completion file")
}

utils.Print("Your shell has been configured for Doppler CLI completions! Restart your shell to apply.")
utils.Print("Completions have been installed. Restart your shell to apply.")
utils.Print("")
if utils.IsMacOS() {
utils.Print("Note: The homebrew 'bash-completion' package is required for completions to work. See https://docs.brew.sh/Shell-Completion for more info.")
} else {
utils.Print("Note: The 'bash-completion' package is required for completions to work. See https://github.com/scop/bash-completion for more info.")
if strings.HasSuffix(shell, "/zsh") && usingFallbackPath {
utils.Print("To enable completions, add the following to your ~/.zshrc:")
utils.Print("")
utils.Print(fmt.Sprintf(" fpath=(%s $fpath)", path))
} else if strings.HasSuffix(shell, "/bash") {
if utils.IsMacOS() {
utils.Print("Note: The homebrew 'bash-completion' package is required for completions to work. See https://docs.brew.sh/Shell-Completion for more info.")
} else {
utils.Print("Note: The 'bash-completion' package is required for completions to work. See https://github.com/scop/bash-completion for more info.")
}
}
},
}
Expand All @@ -141,6 +166,35 @@ func getShell(args []string) string {
return shell
}

func isDirectoryWritable(path string) bool {
// If directory exists, check if it's writable
if info, err := os.Stat(path); err == nil {
if !info.IsDir() {
return false
}
// Try to create a temp file to test write access
testFile := fmt.Sprintf("%s/.doppler_write_test", path)
if f, err := os.Create(testFile); err == nil {
f.Close()
os.Remove(testFile)
return true
}
return false
}

// Directory doesn't exist, check if parent is writable
parent := filepath.Dir(path)
if info, err := os.Stat(parent); err == nil && info.IsDir() {
testFile := fmt.Sprintf("%s/.doppler_write_test", parent)
if f, err := os.Create(testFile); err == nil {
f.Close()
os.Remove(testFile)
return true
}
}
return false
}

func init() {
rootCmd.AddCommand(completionCmd)
completionCmd.AddCommand(completionInstallCmd)
Expand Down
1 change: 1 addition & 0 deletions tests/e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export DOPPLER_ENABLE_VERSION_CHECK=false
"$DIR/e2e/global-flags.sh"
"$DIR/e2e/update.sh"
"$DIR/e2e/flags.sh"
"$DIR/e2e/completion-install.sh"

echo -e "\nAll tests completed successfully!"
exit 0
114 changes: 114 additions & 0 deletions tests/e2e/completion-install.sh
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This file isn't added to tests/e2e.sh, so it won't be run in CI.

Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/bin/bash

# E2E tests for zsh completion installation

set -euo pipefail

TEST_NAME="completion install"
TEMP_HOME=""

cleanup() {
exit_code=$?
if [ "$exit_code" -ne 0 ]; then
echo "ERROR: '$TEST_NAME' tests failed during execution"
fi
# Clean up temp directories
if [ -n "$TEMP_HOME" ] && [ -d "$TEMP_HOME" ]; then
rm -rf "$TEMP_HOME"
fi
exit "$exit_code"
}
trap cleanup EXIT
trap cleanup INT

beforeAll() {
echo "INFO: Executing '$TEST_NAME' tests"
}

beforeEach() {
# Clean up any previous temp home
if [ -n "$TEMP_HOME" ] && [ -d "$TEMP_HOME" ]; then
rm -rf "$TEMP_HOME"
fi
TEMP_HOME=$(mktemp -d)
}

afterAll() {
echo "INFO: Completed '$TEST_NAME' tests"
if [ -n "$TEMP_HOME" ] && [ -d "$TEMP_HOME" ]; then
rm -rf "$TEMP_HOME"
fi
}

error() {
message=$1
echo "ERROR: $message"
exit 1
}

beforeAll

# Test 1 - zsh uses fallback path when standard path is not writable
beforeEach
echo "Testing: zsh fallback to XDG_DATA_HOME/doppler/zsh/completions"
HOME="$TEMP_HOME" SHELL="/bin/zsh" "$DOPPLER_BINARY" completion install --silent

# Verify completion file was created in XDG-compliant directory (defaults to ~/.local/share)
[[ -f "$TEMP_HOME/.local/share/doppler/zsh/completions/_doppler" ]] || error "completion file not created at ~/.local/share/doppler/zsh/completions/_doppler"

echo "PASS: zsh fallback path"

# Test 2 - prints fpath instructions when using fallback path
beforeEach
echo "Testing: prints fpath instructions"
output=$(HOME="$TEMP_HOME" SHELL="/bin/zsh" "$DOPPLER_BINARY" completion install 2>&1)

# Verify instructions are printed
echo "$output" | grep -q "fpath=" || error "fpath instructions not printed"
echo "$output" | grep -q "doppler/zsh/completions" || error "doppler path not in instructions"

echo "PASS: prints fpath instructions"

# Test 3 - bash completions still work (regression test)
beforeEach
echo "Testing: bash completions"
HOME="$TEMP_HOME" SHELL="/bin/bash" "$DOPPLER_BINARY" completion install --silent || true

# The important thing is that it doesn't crash or try to use zsh paths
# Bash will fail on most systems due to /etc or /usr/local/etc permissions
echo "PASS: bash completions (no crash)"

# Test 4 - completion file contains valid zsh completion code
beforeEach
echo "Testing: completion file content"
HOME="$TEMP_HOME" SHELL="/bin/zsh" "$DOPPLER_BINARY" completion install --silent

# Verify the completion file contains expected zsh completion directives
grep -q "compdef" "$TEMP_HOME/.local/share/doppler/zsh/completions/_doppler" || grep -q "#compdef" "$TEMP_HOME/.local/share/doppler/zsh/completions/_doppler" || error "completion file doesn't contain valid zsh completion directives"

echo "PASS: completion file content"

# Test 5 - directory structure is created correctly
beforeEach
echo "Testing: directory structure"
HOME="$TEMP_HOME" SHELL="/bin/zsh" "$DOPPLER_BINARY" completion install --silent

# Verify the full directory structure exists
[[ -d "$TEMP_HOME/.local/share/doppler" ]] || error "~/.local/share/doppler directory not created"
[[ -d "$TEMP_HOME/.local/share/doppler/zsh" ]] || error "~/.local/share/doppler/zsh directory not created"
[[ -d "$TEMP_HOME/.local/share/doppler/zsh/completions" ]] || error "~/.local/share/doppler/zsh/completions directory not created"

echo "PASS: directory structure"

# Test 6 - respects XDG_DATA_HOME when set
beforeEach
echo "Testing: XDG_DATA_HOME is respected"
mkdir -p "$TEMP_HOME/custom-data"
HOME="$TEMP_HOME" XDG_DATA_HOME="$TEMP_HOME/custom-data" SHELL="/bin/zsh" "$DOPPLER_BINARY" completion install --silent

# Verify completion file was created in custom XDG_DATA_HOME
[[ -f "$TEMP_HOME/custom-data/doppler/zsh/completions/_doppler" ]] || error "completion file not created in custom XDG_DATA_HOME"

echo "PASS: XDG_DATA_HOME is respected"

afterAll
Loading