use serde_json::{Value, json};
pub fn acceptable(path: &str) -> bool {
!matches!(name(path).as_str(), "fish" | "nu")
}
pub fn name(path: &str) -> String {
path.rsplit('/').next().unwrap_or(path).to_ascii_lowercase()
}
pub fn login(path: &str) -> bool {
matches!(
name(path).as_str(),
"bash" | "dash" | "fish" | "ksh" | "sh" | "zsh"
)
}
fn raw_shells() -> Vec<String> {
let text = std::fs::read_to_string("/etc/shells").unwrap_or_default();
let mut out: Vec<String> = Vec::new();
for line in text.lines() {
let t = line.trim();
if t.is_empty() || t.starts_with('#') {
continue;
}
if !out.iter().any(|s| s == t) {
out.push(t.to_string());
}
}
if out.is_empty() {
out = vec!["/bin/bash".into(), "/bin/zsh".into(), "/bin/sh".into()];
}
out
}
pub fn list() -> Vec<Value> {
raw_shells()
.into_iter()
.map(|path| {
json!({
"path": path,
"name": name(&path),
"acceptable": acceptable(&path),
})
})
.collect()
}
pub fn preferred(cwd_root: &str) -> String {
if let Ok(shell) = std::env::var("SHELL")
&& !shell.is_empty()
&& std::path::Path::new(&shell).is_file()
&& acceptable(&shell)
{
return shell;
}
let _ = cwd_root;
let bash = "/bin/bash";
if std::path::Path::new(bash).is_file() {
return bash.to_string();
}
"/bin/sh".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn names_and_acceptability() {
assert_eq!(name("/usr/bin/bash"), "bash");
assert_eq!(name("ZSH"), "zsh");
assert!(acceptable("/bin/bash"));
assert!(!acceptable("/usr/bin/fish"));
assert!(!acceptable("/usr/bin/nu"));
assert!(login("/bin/zsh"));
assert!(login("/usr/bin/fish"));
assert!(!login("/usr/bin/git-shell"));
assert!(!login("/bin/rbash"));
}
#[test]
fn list_has_shape_and_dedup() {
let l = list();
assert!(!l.is_empty());
for item in &l {
assert!(item["path"].is_string());
assert!(item["name"].is_string());
assert!(item["acceptable"].is_boolean());
assert_eq!(
item["name"].as_str().unwrap(),
name(item["path"].as_str().unwrap())
);
}
let mut paths: Vec<&str> = l.iter().map(|v| v["path"].as_str().unwrap()).collect();
let n = paths.len();
paths.sort();
paths.dedup();
assert_eq!(paths.len(), n, "duplicate shell paths in /pty/shells");
}
}