coliru

A minimal, flexible, dotfile installer
git clone https://git.ashermorgan.net/coliru/
Log | Files | Refs | README

commit 3b4d891b5c407796aa9e413132db07323440551b
parent 6df7de115843504ddfb52829dce36f79ab11edea
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Sat,  6 Jul 2024 12:19:06 -0700

Fix CLI output for relative SSH destinations

Previously host:~/.coliru/foo would be printed as host:foo, and transfer
messages weren't printed at all for run commands.

Diffstat:
Msrc/core.rs | 90++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msrc/ssh.rs | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mtests/ssh.rs | 7+++++++
3 files changed, 111 insertions(+), 48 deletions(-)

diff --git a/src/core.rs b/src/core.rs @@ -5,10 +5,27 @@ use std::path::Path; use super::manifest::{CopyLinkOptions, RunOptions, parse_manifest_file}; use super::tags::tags_match; use super::local::{copy_file, link_file, run_command}; -use super::ssh::{send_command, send_staged_files, stage_file}; +use super::ssh::{resolve_path, send_command, send_staged_files, stage_file}; use tempfile::tempdir; -/// Execute the steps in a coliru manifest file according to a set of tag rules +/// The base directory for SSH installs, relative to the home directory +const SSH_INSTALL_DIR: &str = ".coliru"; + +/// Performs a dry-run check inside of a loop +/// +/// Will print `(DRY RUN)` and then continue to next loop iteration if `dry_run` +/// evaluates to `true`. +macro_rules! check_dry_run { + ($dry_run:expr) => { + if $dry_run { + println!(" (DRY RUN)"); + continue; + } + println!(""); + } +} + +/// Executes the steps in a coliru manifest file according to a set of tag rules pub fn execute_manifest_file(path: &Path, tag_rules: Vec<String>, host: &str, dry_run: bool, copy: bool) { @@ -50,29 +67,32 @@ pub fn execute_manifest_file(path: &Path, tag_rules: Vec<String>, host: &str, } } -/// Execute a set of copy commands +/// Executes a set of copy commands fn execute_copies(copies: &[CopyLinkOptions], host: &str, staging_dir: &Path, dry_run: bool, step_str: &str) { for copy in copies { - if host == "" { - print!("{} Copy {} to {}", step_str, copy.src, copy.dst); + // Resolve relative dst paths if installing over SSH + let _dst = if host != "" { + resolve_path(&copy.dst, &format!("~/{}", SSH_INSTALL_DIR)) } else { - print!("{} Copy {} to {}:{}", step_str, copy.src, host, copy.dst); - } + copy.dst.clone() + }; - if dry_run { - println!(" (DRY RUN)"); - continue; + print!("{} Copy {} to ", step_str, copy.src); + if host != "" { + print!("{}:", host); } - println!(""); + print!("{}", _dst); + + check_dry_run!(dry_run); if host == "" { - if let Err(why) = copy_file(&copy.src, &copy.dst) { + if let Err(why) = copy_file(&copy.src, &_dst) { eprintln!(" Error: {}", why); } } else { - if let Err(why) = stage_file(&copy.src, &copy.dst, staging_dir) { + if let Err(why) = stage_file(&copy.src, &_dst, staging_dir) { eprintln!(" Error: {}", why); } } @@ -85,16 +105,12 @@ fn execute_copies(copies: &[CopyLinkOptions], host: &str, staging_dir: &Path, } } -/// Execute a set of link commands +/// Executes a set of link commands fn execute_links(links: &[CopyLinkOptions], dry_run: bool, step_str: &str) { for link in links { print!("{} Link {} to {}", step_str, link.src, link.dst); - if dry_run { - println!(" (DRY RUN)"); - continue; - } - println!(""); + check_dry_run!(dry_run); if let Err(why) = link_file(&link.src, &link.dst) { eprintln!(" Error: {}", why); @@ -102,43 +118,37 @@ fn execute_links(links: &[CopyLinkOptions], dry_run: bool, step_str: &str) { } } -/// Execute a set of run commands +/// Executes a set of run commands fn execute_runs(runs: &[RunOptions], tag_rules: &[String], host: &str, staging_dir: &Path, dry_run: bool, step_str: &str) { - if !dry_run && host != "" { - for run in runs { - if let Err(why) = stage_file(&run.src, &run.src, staging_dir) { - eprintln!("Error: {}", why); - } - } + if host != "" { + // Copy scripts to remote machine + let run_copies: Vec<CopyLinkOptions> = runs.iter().map(|x| { + CopyLinkOptions { src: x.src.clone(), dst: x.src.clone() } + }).collect(); - if let Err(why) = send_staged_files(staging_dir, host) { - eprintln!("Error: {}", why); - } + execute_copies(&run_copies, host, staging_dir, dry_run, step_str); } for run in runs { - let postfix = run.postfix.replace("$COLIRU_RULES", &tag_rules.join(" ")); + let postfix = run.postfix.replace("$COLIRU_RULES", + &tag_rules.join(" ")); let cmd = format!("{} {} {}", run.prefix, run.src, postfix); - if host == "" { - print!("{} Run {}", step_str, cmd); - } else { - print!("{} Run {} on {}", step_str, cmd, host); - } - if dry_run { - println!(" (DRY RUN)"); - continue; + print!("{} Run {}", step_str, cmd); + if host != "" { + print!(" on {}", host); } - println!(""); + + check_dry_run!(dry_run); if host == "" { if let Err(why) = run_command(&cmd) { eprintln!(" Error: {}", why); } } else { - let ssh_cmd = format!("cd .coliru && {}", &cmd); + let ssh_cmd = format!("cd {} && {}", SSH_INSTALL_DIR, &cmd); if let Err(why) = send_command(&ssh_cmd, host) { eprintln!(" Error: {}", why); } diff --git a/src/ssh.rs b/src/ssh.rs @@ -17,28 +17,44 @@ use std::path::{MAIN_SEPARATOR_STR, Path, PathBuf}; use std::process::Command; use super::local::copy_file; +/// Makes a relative path absolute according to a certain base directory +/// +/// Paths begining with tildes are interpreted as absolute paths. +/// +/// ``` +/// assert_eq!(resolve_path("dir1/foo", "~/dir2"), "~/dir2/dir1/foo"); +/// assert_eq!(resolve_path("/dir1/foo", "~/dir2"), "/dir1/foo"); +/// assert_eq!(resolve_path("~/dir1/foo", "~/dir2"), "~/dir1/foo"); +/// ``` +pub fn resolve_path(src: &str, dir: &str) -> String { + if !src.starts_with("~") && Path::new(src).is_relative() { + return format!("{dir}/{src}") + } + src.to_owned() +} + /// Copies a file to an SCP staging directory /// -/// Tildes will be expanded to the remote user's home directory. Relative paths -/// are interpreted relative to `~/.coliru`. +/// Tildes are expanded and relative paths are interpreted relative to the +/// remote user's home directory. /// /// ``` -/// // Prepare to transfer foo to ~/foo, bar to /bar, and baz to ~/.coliru/baz +/// // Prepare to transfer foo to ~/foo, bar to /bar, and baz to ~/baz /// let staging_dir = Path::new("/tmp/staging"); /// stage_file("foo", "~/foo", staging_dir); /// stage_file("bar", "/bar", staging_dir); /// stage_file("baz", "baz", staging_dir); /// ``` +pub fn stage_file(src: &str, dst: &str, staging_dir: &Path) -> Result<(), + String> { -pub fn stage_file(src: &str, dst: &str, staging_dir: &Path) -> Result<(), String> { // Staging directories are used to copy multiple files at once while // automatically creating missing directories on the remote machine. The // example code above produces the following staging directory layout: // // /tmp/staging/ // ├── home/ - // │   ├── .coliru - // │   │   └── baz + // │   ├── baz // │   └── foo // └── root/ //    └── bar @@ -54,7 +70,7 @@ pub fn stage_file(src: &str, dst: &str, staging_dir: &Path) -> Result<(), String .into(); // Resolve relative paths to home staging directory: - _dst = home_dir.join(".coliru").join(_dst); + _dst = home_dir.join(_dst); // Resolve other absolute paths to root staging directory: if !_dst.starts_with(home_dir) { @@ -158,6 +174,36 @@ mod tests { use std::fs; #[test] + fn test_resolve_path_relative() { + let result = resolve_path("dir1/foo", "~/dir2"); + + assert_eq!(result, "~/dir2/dir1/foo"); + } + + #[test] + fn test_resolve_path_tilde() { + let result = resolve_path("~/dir1/foo", "~/dir2"); + + assert_eq!(result, "~/dir1/foo"); + } + + #[test] + #[cfg(target_family = "unix")] + fn test_resolve_path_absolute() { + let result = resolve_path("/dir1/foo", "~/dir2"); + + assert_eq!(result, "/dir1/foo"); + } + + #[test] + #[cfg(target_family = "windows")] + fn test_resolve_path_absolute() { + let result = resolve_path("C:\\dir1\\foo", "~/dir2"); + + assert_eq!(result, "C:\\dir1\\foo"); + } + + #[test] fn test_stage_file_tilde() { let tmp = setup_integration("test_stage_file_tilde"); @@ -180,7 +226,7 @@ mod tests { let src = tmp.local.join("foo"); let dst = "dir/bar"; - let dst_real = tmp.local.join("home").join(".coliru").join("dir") + let dst_real = tmp.local.join("home").join("dir") .join("bar"); let staging = &tmp.local; write_file(&src, "contents of foo"); diff --git a/tests/ssh.rs b/tests/ssh.rs @@ -15,6 +15,7 @@ fn test_ssh_standard() { [1/3] Copy gitconfig to {SSH_HOST}:~/test_ssh_standard/.gitconfig [2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_standard/.bashrc [2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_standard/.vimrc +[2/3] Copy test_ssh_standard/script.sh to {SSH_HOST}:~/.coliru/test_ssh_standard/script.sh [2/3] Run sh test_ssh_standard/script.sh arg1 linux on {SSH_HOST} script.sh called with arg1 linux "); @@ -43,6 +44,7 @@ fn test_ssh_run_alternate_tag_rules_1() { let expected = format!("\ [2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_run_alternate_tag_rules_1/.bashrc [2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_run_alternate_tag_rules_1/.vimrc +[2/3] Copy test_ssh_run_alternate_tag_rules_1/script.sh to {SSH_HOST}:~/.coliru/test_ssh_run_alternate_tag_rules_1/script.sh [2/3] Run sh test_ssh_run_alternate_tag_rules_1/script.sh arg1 linux ^windows on {SSH_HOST} script.sh called with arg1 linux ^windows "); @@ -72,6 +74,7 @@ fn test_ssh_run_alternate_tag_rules_2() { [1/3] Copy gitconfig to {SSH_HOST}:~/test_ssh_run_alternate_tag_rules_2/.gitconfig [2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_run_alternate_tag_rules_2/.bashrc [2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_run_alternate_tag_rules_2/.vimrc +[2/3] Copy test_ssh_run_alternate_tag_rules_2/script.sh to {SSH_HOST}:~/.coliru/test_ssh_run_alternate_tag_rules_2/script.sh [2/3] Run sh test_ssh_run_alternate_tag_rules_2/script.sh arg1 macos on {SSH_HOST} script.sh called with arg1 macos "); @@ -100,6 +103,7 @@ fn test_ssh_dry_run() { [1/3] Copy gitconfig to {SSH_HOST}:~/test_ssh_dry_run/.gitconfig (DRY RUN) [2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_dry_run/.bashrc (DRY RUN) [2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_dry_run/.vimrc (DRY RUN) +[2/3] Copy test_ssh_dry_run/script.sh to {SSH_HOST}:~/.coliru/test_ssh_dry_run/script.sh (DRY RUN) [2/3] Run sh test_ssh_dry_run/script.sh arg1 linux on {SSH_HOST} (DRY RUN) "); assert_eq!(&stderr_to_string(&mut cmd), ""); @@ -128,6 +132,7 @@ fn test_ssh_copy() { [1/3] Copy gitconfig to {SSH_HOST}:~/test_ssh_copy/.gitconfig [2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_copy/.bashrc [2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_copy/.vimrc +[2/3] Copy test_ssh_copy/script.sh to {SSH_HOST}:~/.coliru/test_ssh_copy/script.sh [2/3] Run sh test_ssh_copy/script.sh arg1 linux on {SSH_HOST} script.sh called with arg1 linux "); @@ -158,6 +163,7 @@ fn test_ssh_run_failure() { [1/3] Copy gitconfig to {SSH_HOST}:~/test_ssh_run_failure/.gitconfig [2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_run_failure/.bashrc [2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_run_failure/.vimrc +[2/3] Copy test_ssh_run_failure/script.sh to {SSH_HOST}:~/.coliru/test_ssh_run_failure/script.sh [2/3] Run sh test_ssh_run_failure/script.sh arg1 linux on {SSH_HOST} "); let expected_stderr = " Error: SSH exited with exit status: 1\n"; @@ -186,6 +192,7 @@ fn test_ssh_missing_file() { [1/3] Copy gitconfig to {SSH_HOST}:~/test_ssh_missing_file/.gitconfig [2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_missing_file/.bashrc [2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_missing_file/.vimrc +[2/3] Copy test_ssh_missing_file/script.sh to {SSH_HOST}:~/.coliru/test_ssh_missing_file/script.sh [2/3] Run sh test_ssh_missing_file/script.sh arg1 linux on {SSH_HOST} script.sh called with arg1 linux ");