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
46 changes: 46 additions & 0 deletions src/app/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,22 @@ impl OutputArgs {
self.eval.is_some() && self.format.is_none()
}

/// Fails when the shell statements of `command` would only be printed on a terminal:
/// without the shell integration and without an explicit --format, they would show
/// secret values on screen without changing the shell.
pub fn check_destination(&self, command: &str, stdout_is_terminal: bool) -> anyhow::Result<()> {
if stdout_is_terminal && self.eval.is_none() && self.format.is_none() {
let (shell, rc) = match self.export_format() {
ExportFormat::Bash => ("bash", "~/.bashrc"),
_ => ("zsh", "~/.zshrc"),
};
anyhow::bail!(
"`{command}` changes your shell through the shell integration, which isn't active here; add `eval \"$(keysafe init {shell})\"` to {rc}, or run `eval \"$(keysafe {command})\"`"
);
}
Ok(())
}

/// Get the export format: the explicit one, the integration's shell, or $SHELL detection.
pub fn export_format(&self) -> ExportFormat {
self.format
Expand Down Expand Up @@ -853,6 +869,36 @@ mod tests {
assert!(!args.is_eval());
}

#[test]
fn statements_are_not_printed_on_a_terminal_without_the_integration() {
let plain = OutputArgs {
format: None,
eval: None,
..Default::default()
};
let err = plain
.check_destination("load", true)
.unwrap_err()
.to_string();
assert!(
err.starts_with("`load` changes your shell through the shell integration"),
"{err}"
);

// Piped or evaluated, under the integration, or with an explicit format: printed
assert!(plain.check_destination("load", false).is_ok());
let integrated = OutputArgs {
eval: Some(Shell::Zsh),
..Default::default()
};
assert!(integrated.check_destination("load", true).is_ok());
let explicit = OutputArgs {
format: Some(ExportFormat::Zsh),
..Default::default()
};
assert!(explicit.check_destination("unload", true).is_ok());
}

#[test]
fn export_format_follows_shell_integration() {
let args = OutputArgs {
Expand Down
6 changes: 5 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ mod log;
mod vault;

use std::fs::File;
use std::io::Write;
use std::io::{IsTerminal, Write};
use std::os::fd::FromRawFd;
use std::os::unix::process::ExitStatusExt;
use std::path::PathBuf;
Expand Down Expand Up @@ -71,6 +71,8 @@ fn main() -> ExitCode {
fn run(program: Program) -> Result<ExitCode> {
match program.command {
ProgramCommand::Load(args) => {
args.output
.check_destination("load", std::io::stdout().is_terminal())?;
let writer = statements(&args.output);
let loader = loader(&args.parent);
let agent = Box::new(Agent::new());
Expand All @@ -82,6 +84,8 @@ fn run(program: Program) -> Result<ExitCode> {
command.execute(&args)?
}
ProgramCommand::Unload(args) => {
args.output
.check_destination("unload", std::io::stdout().is_terminal())?;
let writer = statements(&args.output);
let cache = cache(&args.parent);
let agent = Box::new(Agent::new());
Expand Down
Loading