use crate::cli::{InitShellArgs, SupportedShell};
use crate::commands::cd::CD_OUT_ENV;
pub async fn run(args: InitShellArgs) -> anyhow::Result<()> {
let name = &args.name;
let script = match args.shell {
SupportedShell::Powershell => powershell_wrapper(name),
SupportedShell::Bash | SupportedShell::Zsh => posix_wrapper(name),
SupportedShell::Fish => fish_wrapper(name),
};
print!("{script}");
Ok(())
}
fn posix_wrapper(name: &str) -> String {
format!(
r#"{name}() {{
local tmp dest status
tmp=$(mktemp) || return 1
{env}="$tmp" shoka cd "$@"
status=$?
if [ "$status" -eq 0 ]; then
dest=$(cat "$tmp")
fi
rm -f "$tmp"
[ "$status" -eq 0 ] && [ -n "$dest" ] && cd -- "$dest"
return $status
}}
"#,
env = CD_OUT_ENV,
)
}
fn fish_wrapper(name: &str) -> String {
format!(
r#"function {name}
set -l tmp (mktemp); or return 1
{env}=$tmp shoka cd $argv
set -l status $status
set -l dest ""
if test $status -eq 0
set dest (cat $tmp)
end
rm -f $tmp
if test $status -eq 0; and test -n "$dest"
cd -- $dest
end
return $status
end
"#,
env = CD_OUT_ENV,
)
}
fn powershell_wrapper(name: &str) -> String {
format!(
r#"function {name} {{
$tmp = New-TemporaryFile
try {{
$env:{env} = $tmp.FullName
shoka cd @args
$code = $LASTEXITCODE
if ($code -eq 0) {{
$dest = Get-Content -LiteralPath $tmp.FullName -Raw
if ($dest) {{ Set-Location -LiteralPath $dest.TrimEnd() }}
}}
$global:LASTEXITCODE = $code
}} finally {{
Remove-Item -LiteralPath $tmp.FullName -Force -ErrorAction SilentlyContinue
Remove-Item Env:{env} -ErrorAction SilentlyContinue
}}
}}
"#,
env = CD_OUT_ENV,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::SupportedShell;
fn args(shell: SupportedShell, name: &str) -> InitShellArgs {
InitShellArgs {
shell,
name: name.into(),
}
}
fn rendered(shell: SupportedShell, name: &str) -> String {
match shell {
SupportedShell::Powershell => powershell_wrapper(name),
SupportedShell::Bash | SupportedShell::Zsh => posix_wrapper(name),
SupportedShell::Fish => fish_wrapper(name),
}
}
#[test]
fn posix_wrapper_uses_sidechannel_env_var() {
let body = rendered(SupportedShell::Bash, "s");
assert!(body.contains("s()"), "function name should be `s`: {body}");
assert!(
body.contains("SHOKA_CD_OUT"),
"wrapper must set the sidechannel env var: {body}"
);
assert!(
!body.contains("$(shoka cd"),
"wrapper must not capture stdout via command substitution: {body}"
);
}
#[test]
fn fish_wrapper_uses_sidechannel_env_var() {
let body = rendered(SupportedShell::Fish, "s");
assert!(body.contains("function s"));
assert!(body.contains("SHOKA_CD_OUT"));
assert!(
!body.contains("(shoka cd"),
"fish wrapper must not capture stdout via subshell: {body}"
);
}
#[test]
fn powershell_wrapper_uses_sidechannel_env_var() {
let body = rendered(SupportedShell::Powershell, "s");
assert!(body.contains("function s"));
assert!(body.contains("SHOKA_CD_OUT"));
assert!(body.contains("finally"), "wrapper missing cleanup: {body}");
}
#[test]
fn run_emits_wrapper_for_each_supported_shell() {
let rt = tokio::runtime::Runtime::new().unwrap();
for shell in [
SupportedShell::Bash,
SupportedShell::Zsh,
SupportedShell::Fish,
SupportedShell::Powershell,
] {
rt.block_on(run(args(shell, "s"))).unwrap();
}
}
}