use crate::{file_resolution_check, scoop_version_check};
pub fn init_script() -> &'static str {
concat!(
r#"# scoop shell integration for bash
# Wrapper function for scoop
scoop() {
local command="${1:-}"
case "$command" in
use)
command scoop "$@"
local ret=$?
if [[ $ret -eq 0 ]]; then
shift # remove 'use'
local name=""
for arg in "$@"; do
case "$arg" in
-*) ;; # skip options
*) name="$arg"; break ;;
esac
done
if [[ -n "$name" ]]; then
eval "$(command scoop activate "$name")"
fi
fi
return $ret
;;
activate|deactivate|shell)
# Pass through help/version flags without eval
if [[ "$*" == *--help* ]] || [[ "$*" == *-h* ]] || [[ "$*" == *--version* ]] || [[ "$*" == *-V* ]]; then
command scoop "$@"
else
eval "$(command scoop "$@")"
fi
;;
*)
command scoop "$@"
;;
esac
}
# Auto-activate hook
_scoop_hook() {"#,
scoop_version_check!(bash),
file_resolution_check!(bash),
r#"
}
# Set up PROMPT_COMMAND for auto-activate
if [[ -z "$SCOOP_NO_AUTO" ]]; then
if [[ -z "$PROMPT_COMMAND" ]]; then
PROMPT_COMMAND="_scoop_hook"
else
PROMPT_COMMAND="_scoop_hook;$PROMPT_COMMAND"
fi
fi
# Run hook on startup
_scoop_hook
# Bash completion for scoop
_scoop_complete() {
local cur cmd i
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
# Get subcommand (COMP_WORDS[1])
cmd=""
if [[ ${COMP_CWORD} -ge 1 ]]; then
cmd="${COMP_WORDS[1]}"
fi
# First argument: complete subcommands
if [[ ${COMP_CWORD} -eq 1 ]]; then
COMPREPLY=($(compgen -W "list use create remove info install uninstall doctor init completions activate deactivate shell migrate lang" -- "$cur"))
return
fi
# Option completion (starts with -)
if [[ "$cur" == -* ]]; then
case "$cmd" in
list)
# Special-case `--sort` value completion: when the user
# has typed `--sort=` or `--sort <TAB>`, offer the
# enum values directly instead of the option list.
local prev_word="${COMP_WORDS[COMP_CWORD-1]:-}"
if [[ "$cur" == --sort=* ]]; then
local val="${cur#--sort=}"
COMPREPLY=($(compgen -W "name created last-used" -P "--sort=" -- "$val"))
return 0
fi
if [[ "$prev_word" == "--sort" ]]; then
COMPREPLY=($(compgen -W "name created last-used" -- "$cur"))
return 0
fi
local opts="--pythons --sort --json -q --quiet --no-color --help"
for word in "${COMP_WORDS[@]}"; do
case "$word" in
--pythons) opts="${opts//--pythons }" ;;
--sort|--sort=*) opts="${opts//--sort }" ;;
--json) opts="${opts//--json }" ;;
-q|--quiet) opts="${opts//-q }"; opts="${opts//--quiet }" ;;
--no-color) opts="${opts//--no-color }" ;;
esac
done
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
;;
doctor)
local opts="-v --verbose -q --quiet --json --no-color --help"
# Filter out already used options
for word in "${COMP_WORDS[@]}"; do
case "$word" in
-v|--verbose) opts="${opts//-v }"; opts="${opts//--verbose }" ;;
-q|--quiet) opts="${opts//-q }"; opts="${opts//--quiet }" ;;
--json) opts="${opts//--json }" ;;
--no-color) opts="${opts//--no-color }" ;;
esac
done
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
;;
create)
local opts="--force -q --quiet --no-color --help"
for word in "${COMP_WORDS[@]}"; do
case "$word" in
--force) opts="${opts//--force }" ;;
-q|--quiet) opts="${opts//-q }"; opts="${opts//--quiet }" ;;
--no-color) opts="${opts//--no-color }" ;;
esac
done
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
;;
use)
local opts="--unset --link --global --no-link -q --quiet --no-color --help"
for word in "${COMP_WORDS[@]}"; do
case "$word" in
--unset) opts="${opts//--unset }" ;;
--global) opts="${opts//--global }" ;;
--link|--no-link) opts="${opts//--link }"; opts="${opts//--no-link }" ;;
-q|--quiet) opts="${opts//-q }"; opts="${opts//--quiet }" ;;
--no-color) opts="${opts//--no-color }" ;;
esac
done
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
;;
remove)
local opts="--force -q --quiet --no-color --help"
for word in "${COMP_WORDS[@]}"; do
case "$word" in
--force) opts="${opts//--force }" ;;
-q|--quiet) opts="${opts//-q }"; opts="${opts//--quiet }" ;;
--no-color) opts="${opts//--no-color }" ;;
esac
done
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
;;
install)
local opts="--latest --stable -q --quiet --no-color --help"
for word in "${COMP_WORDS[@]}"; do
case "$word" in
--latest|--stable) opts="${opts//--latest }"; opts="${opts//--stable }" ;;
-q|--quiet) opts="${opts//-q }"; opts="${opts//--quiet }" ;;
--no-color) opts="${opts//--no-color }" ;;
esac
done
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
;;
uninstall)
local opts="-q --quiet --no-color --help"
for word in "${COMP_WORDS[@]}"; do
case "$word" in
-q|--quiet) opts="${opts//-q }"; opts="${opts//--quiet }" ;;
--no-color) opts="${opts//--no-color }" ;;
esac
done
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
;;
info)
local opts="--json --all-packages --no-size -q --quiet --no-color --help"
for word in "${COMP_WORDS[@]}"; do
case "$word" in
--json) opts="${opts//--json }" ;;
--all-packages) opts="${opts//--all-packages }" ;;
--no-size) opts="${opts//--no-size }" ;;
-q|--quiet) opts="${opts//-q }"; opts="${opts//--quiet }" ;;
--no-color) opts="${opts//--no-color }" ;;
esac
done
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
;;
init|completions)
COMPREPLY=($(compgen -W "--help" -- "$cur"))
;;
lang)
local opts="--list --reset --json --help"
for word in "${COMP_WORDS[@]}"; do
case "$word" in
--list) opts="${opts//--list }" ;;
--reset) opts="${opts//--reset }" ;;
--json) opts="${opts//--json }" ;;
esac
done
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
;;
shell)
local opts="--unset -q --quiet --no-color --help"
for word in "${COMP_WORDS[@]}"; do
case "$word" in
--unset) opts="${opts//--unset }" ;;
-q|--quiet) opts="${opts//-q }"; opts="${opts//--quiet }" ;;
--no-color) opts="${opts//--no-color }" ;;
esac
done
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
;;
migrate)
local opts="--help"
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
;;
esac
return
fi
# Argument completion (by subcommand)
case "$cmd" in
use|remove|info|activate|shell)
# Check if env name already provided
local has_arg=false
for ((i=2; i<COMP_CWORD; i++)); do
if [[ "${COMP_WORDS[i]}" != -* ]]; then
has_arg=true
break
fi
done
if [[ "$has_arg" == false ]]; then
COMPREPLY=($(compgen -W "$(command scoop list --bare 2>/dev/null)" -- "$cur"))
fi
;;
uninstall)
COMPREPLY=($(compgen -W "$(command scoop list --pythons --bare 2>/dev/null | sort -u)" -- "$cur"))
;;
init|completions)
COMPREPLY=($(compgen -W "bash zsh fish powershell" -- "$cur"))
;;
create)
# First arg: name, second arg: python version
local arg_count=0
for ((i=2; i<COMP_CWORD; i++)); do
if [[ "${COMP_WORDS[i]}" != -* ]]; then
((arg_count++))
fi
done
if [[ $arg_count -eq 1 ]]; then
# Second positional arg: python version
COMPREPLY=($(compgen -W "$(command scoop list --pythons --bare 2>/dev/null | sort -u)" -- "$cur"))
fi
;;
lang)
# Complete language codes
COMPREPLY=($(compgen -W "en ko ja pt-BR" -- "$cur"))
;;
migrate)
# Complete migrate subcommands
COMPREPLY=($(compgen -W "list all @env" -- "$cur"))
;;
esac
}
complete -o nosort -F _scoop_complete scoop
"#
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(unix)]
fn test_init_script_has_valid_bash_syntax() {
let script = init_script();
let output = std::process::Command::new("bash")
.arg("-n") .arg("-c")
.arg(script)
.output();
match output {
Ok(result) => {
assert!(
result.status.success(),
"Bash script has syntax errors:\n{}",
String::from_utf8_lossy(&result.stderr)
);
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
eprintln!("Skipping bash syntax test: bash not found");
}
Err(e) => panic!("Failed to run bash: {}", e),
}
}
#[test]
fn test_init_script_not_empty() {
let script = init_script();
assert!(!script.is_empty(), "Script should not be empty");
}
#[test]
fn test_init_script_defines_required_functions() {
let script = init_script();
let required_functions = ["scoop()", "_scoop_hook()", "_scoop_complete()"];
for func in required_functions {
assert!(
script.contains(func),
"Script missing required function: {}",
func
);
}
}
#[test]
fn test_init_script_registers_prompt_hook() {
let script = init_script();
assert!(
script.contains("PROMPT_COMMAND"),
"Script must register PROMPT_COMMAND for auto-activation"
);
}
#[test]
fn test_init_script_registers_completion() {
let script = init_script();
assert!(
script.contains("complete -o nosort -F _scoop_complete scoop"),
"Script must register bash completion"
);
}
#[test]
#[cfg(unix)]
fn test_init_script_passes_shellcheck() {
let script = init_script();
let temp_file = tempfile::NamedTempFile::new().unwrap();
std::fs::write(temp_file.path(), script).unwrap();
let output = std::process::Command::new("shellcheck")
.arg("--shell=bash")
.arg("--severity=warning") .arg("--exclude=SC2207")
.arg(temp_file.path())
.output();
match output {
Ok(result) => {
assert!(
result.status.success(),
"shellcheck found issues in bash script:\n{}",
String::from_utf8_lossy(&result.stdout)
);
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
eprintln!(
"Skipping shellcheck test: shellcheck not found (install: brew install shellcheck)"
);
}
Err(e) => panic!("Failed to run shellcheck: {}", e),
}
}
}