use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Shell {
Bash,
Zsh,
Fish,
}
impl Shell {
pub fn from_name(name: &str) -> Option<Self> {
match name.trim().to_ascii_lowercase().as_str() {
"bash" => Some(Shell::Bash),
"zsh" => Some(Shell::Zsh),
"fish" => Some(Shell::Fish),
_ => None,
}
}
pub const fn as_str(self) -> &'static str {
match self {
Shell::Bash => "bash",
Shell::Zsh => "zsh",
Shell::Fish => "fish",
}
}
pub fn default_history_path(self) -> Option<PathBuf> {
let home = home_dir()?;
Some(match self {
Shell::Bash => home.join(".bash_history"),
Shell::Zsh => home.join(".zsh_history"),
Shell::Fish => {
if let Some(xdg) = std::env::var_os("XDG_DATA_HOME") {
PathBuf::from(xdg).join("fish").join("fish_history")
} else {
home.join(".local")
.join("share")
.join("fish")
.join("fish_history")
}
}
})
}
}
fn home_dir() -> Option<PathBuf> {
std::env::var_os("HOME").map(PathBuf::from)
}
pub fn detect_shell(input: &str) -> Shell {
for line in input
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.take(10)
{
if line.starts_with("- cmd:") {
return Shell::Fish;
}
if looks_like_zsh_extended(line) {
return Shell::Zsh;
}
}
Shell::Bash
}
fn looks_like_zsh_extended(line: &str) -> bool {
crate::parsers::zsh_history_parser::parse_zsh_extended(line).is_some()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shell_from_name() {
assert_eq!(Shell::from_name("bash"), Some(Shell::Bash));
assert_eq!(Shell::from_name("ZSH"), Some(Shell::Zsh));
assert_eq!(Shell::from_name("Fish"), Some(Shell::Fish));
assert_eq!(Shell::from_name("powershell"), None);
}
#[test]
fn shell_as_str() {
assert_eq!(Shell::Bash.as_str(), "bash");
assert_eq!(Shell::Zsh.as_str(), "zsh");
assert_eq!(Shell::Fish.as_str(), "fish");
}
#[test]
fn bash_default_path() {
let path = Shell::Bash.default_history_path().unwrap();
assert_eq!(path.file_name().unwrap(), ".bash_history");
}
#[test]
fn zsh_default_path() {
let path = Shell::Zsh.default_history_path().unwrap();
assert_eq!(path.file_name().unwrap(), ".zsh_history");
}
#[test]
fn fish_default_path() {
let path = Shell::Fish.default_history_path().unwrap();
assert_eq!(path.file_name().unwrap(), "fish_history");
}
#[test]
fn detect_bash() {
assert_eq!(detect_shell("ls\ngit status\ncd .."), Shell::Bash);
}
#[test]
fn detect_zsh_extended() {
let input = ": 1577836800:0;ls\n: 1577836801:0;git status\n";
assert_eq!(detect_shell(input), Shell::Zsh);
}
#[test]
fn detect_fish() {
let input = "- cmd: ls\n when: 1577836800\n- cmd: git status\n when: 1577836801\n";
assert_eq!(detect_shell(input), Shell::Fish);
}
#[test]
fn detect_empty_defaults_to_bash() {
assert_eq!(detect_shell(""), Shell::Bash);
}
#[test]
fn zsh_extended_detection_helper() {
assert!(looks_like_zsh_extended(": 1577836800:0;git push"));
assert!(looks_like_zsh_extended(":1234567890:0;ls"));
assert!(!looks_like_zsh_extended("git push"));
assert!(!looks_like_zsh_extended("- cmd: ls"));
assert!(!looks_like_zsh_extended(":not zsh"));
}
#[test]
fn detect_mixed_falls_to_first_signal() {
let input = "ls\n- cmd: git status\n when: 1\n";
assert_eq!(detect_shell(input), Shell::Fish);
}
}