use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use crate::Result;
use crate::cli;
const MARKER: &str = "# semver-sync alias";
const LEGACY_MARKER: &str = "# semver-rs alias";
#[derive(Debug, PartialEq, Eq)]
pub enum Outcome {
Written { path: PathBuf, line: String },
Already { path: PathBuf },
}
pub fn install(args: cli::Alias) -> Result<Outcome> {
let path = match args.file {
Some(path) => path,
None => rc_file()?,
};
let line = format!("alias {}=semver-sync", args.name);
let current = match std::fs::read_to_string(&path) {
Ok(text) => text,
Err(err) if err.kind() == ErrorKind::NotFound => String::new(),
Err(err) => return Err(format!("cannot read {}: {err}", path.display())),
};
let updated = replace_block(¤t, MARKER, &line)
.or_else(|| replace_block(¤t, LEGACY_MARKER, &line))
.unwrap_or_else(|| append_block(¤t, &line));
if updated == current {
return Ok(Outcome::Already { path });
}
std::fs::write(&path, &updated)
.map_err(|err| format!("cannot write {}: {err}", path.display()))?;
Ok(Outcome::Written { path, line })
}
fn rc_file() -> Result<PathBuf> {
let shell = std::env::var("SHELL").unwrap_or_default();
let name = Path::new(&shell)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("this shell");
let home = std::env::var("HOME")
.map_err(|_| String::from("cannot find your home directory; pass --file"))?;
match name {
"zsh" => Ok(Path::new(&home).join(".zshrc")),
"bash" => Ok(Path::new(&home).join(".bashrc")),
other => Err(format!("no known rc file for {other}; pass --file")),
}
}
fn replace_block(current: &str, marker: &str, line: &str) -> Option<String> {
let at = current.find(marker)?;
let after_marker = ¤t[at..];
let marker_end = after_marker
.find('\n')
.map_or(current.len(), |offset| at + offset + 1);
let tail = ¤t[marker_end..];
let alias_end = if tail.starts_with("alias ") {
tail.find('\n')
.map_or(current.len(), |offset| marker_end + offset + 1)
} else {
marker_end
};
let mut updated = String::with_capacity(current.len());
updated.push_str(¤t[..at]);
updated.push_str(&block(line));
updated.push_str(¤t[alias_end..]);
Some(updated)
}
fn append_block(current: &str, line: &str) -> String {
let mut updated = current.to_string();
if !updated.is_empty() && !updated.ends_with('\n') {
updated.push('\n');
}
if !updated.is_empty() {
updated.push('\n');
}
updated.push_str(&block(line));
updated
}
fn block(line: &str) -> String {
format!("{MARKER}\n{line}\n")
}
#[cfg(test)]
mod tests {
use super::*;
fn install_to(path: &Path, name: &str) -> Outcome {
install(cli::Alias {
name: name.to_string(),
file: Some(path.to_path_buf()),
})
.expect("install")
}
#[test]
fn writes_the_alias_into_a_file_that_has_no_block_yet() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join(".zshrc");
std::fs::write(&path, "alias ls=eza\n").expect("write rc");
let outcome = install_to(&path, "ss");
assert!(matches!(outcome, Outcome::Written { .. }));
assert_eq!(
std::fs::read_to_string(&path).expect("read rc"),
"alias ls=eza\n\n# semver-sync alias\nalias ss=semver-sync\n"
);
}
#[test]
fn running_it_twice_changes_nothing() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join(".zshrc");
std::fs::write(&path, "alias ls=eza\n").expect("write rc");
install_to(&path, "ss");
let second = install_to(&path, "ss");
assert_eq!(second, Outcome::Already { path: path.clone() });
assert_eq!(
std::fs::read_to_string(&path).expect("read rc"),
"alias ls=eza\n\n# semver-sync alias\nalias ss=semver-sync\n"
);
}
#[test]
fn another_name_replaces_the_block() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join(".zshrc");
std::fs::write(&path, "# semver-sync alias\nalias ss=semver-sync\n").expect("write rc");
install_to(&path, "ver");
assert_eq!(
std::fs::read_to_string(&path).expect("read rc"),
"# semver-sync alias\nalias ver=semver-sync\n"
);
}
#[test]
fn a_stripped_block_is_filled_again_in_place() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join(".zshrc");
std::fs::write(&path, "top = 1\n# semver-sync alias\nbottom = 2\n").expect("write rc");
install_to(&path, "ss");
assert_eq!(
std::fs::read_to_string(&path).expect("read rc"),
"top = 1\n# semver-sync alias\nalias ss=semver-sync\nbottom = 2\n"
);
}
#[test]
fn a_missing_file_is_created() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join(".bashrc");
install_to(&path, "ss");
assert_eq!(
std::fs::read_to_string(&path).expect("read rc"),
"# semver-sync alias\nalias ss=semver-sync\n"
);
}
#[test]
fn a_pre_rename_block_is_replaced_in_place() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join(".zshrc");
std::fs::write(
&path,
"alias ls=eza\n\n# semver-rs alias\nalias sv=semver-rs\nexport PATH=$PATH\n",
)
.expect("write rc");
install_to(&path, "ss");
assert_eq!(
std::fs::read_to_string(&path).expect("read rc"),
"alias ls=eza\n\n# semver-sync alias\nalias ss=semver-sync\nexport PATH=$PATH\n"
);
}
}