coliru

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

commit 79ea5ca604b1d361d8ec3c47881b10fe08863d3b
parent 6bf55414c191dbdd1655c817701a0b9d02512325
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Thu,  4 Jul 2024 20:55:20 -0700

Improve documentation

Diffstat:
Msrc/cli.rs | 4+++-
Msrc/core.rs | 2++
Msrc/local.rs | 36++++++++++++++++++++++++++++++------
Msrc/main.rs | 6++++--
Msrc/manifest.rs | 26++++++++++++++++++++++++++
Msrc/ssh.rs | 73++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/tags.rs | 9++-------
Mtests/basic.rs | 7+++----
Dtests/common/mod.rs | 166-------------------------------------------------------------------------------
Mtests/local.rs | 7+++----
Mtests/ssh.rs | 7+++----
Atests/test_utils/mod.rs | 207+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
12 files changed, 345 insertions(+), 205 deletions(-)

diff --git a/src/cli.rs b/src/cli.rs @@ -1,8 +1,10 @@ +//! The coliru command line interface + use clap::{Parser, ColorChoice}; use std::path::Path; use super::core::execute_manifest_file; -/// Stores arguments to the coliru CLI +/// Arguments to the coliru CLI #[derive(Parser, Debug)] #[command(version, color=ColorChoice::Never, about="A minimal, flexible, dotfile installer")] diff --git a/src/core.rs b/src/core.rs @@ -1,3 +1,5 @@ +//! Manifest execution functions + use std::env::set_current_dir; use std::path::Path; use super::manifest::{CopyLinkOptions, RunOptions, parse_manifest_file}; diff --git a/src/local.rs b/src/local.rs @@ -1,3 +1,11 @@ +//! Local dotfile installation utilities +//! +//! ``` +//! copy_file("foo", "~/foo"); +//! link_file("bar", "~/bar"); +//! run_command("echo 'Hello world'"); +//! ``` + use shellexpand::tilde; use std::io; use std::fs; @@ -6,10 +14,14 @@ use std::os::unix::fs::symlink; use std::path::{PathBuf, absolute}; use std::process::Command; -/// Copies the contents of a local file to another local file. +/// Copies the contents of a file to another file /// /// Tildes are expanded if present and the destination file is overwritten if /// necessary. +/// +/// ``` +/// copy_file("foo", "~/foo"); +/// ``` pub fn copy_file(src: &str, dst: &str) -> io::Result<()> { if absolute(src)? == absolute(dst)? { return Ok(()); } let _dst = prepare_path(dst)?; @@ -17,10 +29,14 @@ pub fn copy_file(src: &str, dst: &str) -> io::Result<()> { Ok(()) } -/// Creates a symbolic link to a local file. +/// Creates a symbolic link to a file /// /// Tildes are expanded if present and the destination file is overwritten if /// necessary. On non-Unix platforms, a hard link will be created instead. +/// +/// ``` +/// link_file("bar", "~/bar"); +/// ``` #[cfg(target_family = "unix")] pub fn link_file(src: &str, dst: &str) -> io::Result<()> { if absolute(src)? == absolute(dst)? { return Ok(()); } @@ -36,8 +52,12 @@ pub fn link_file(src: &str, dst: &str) -> io::Result<()> { Ok(()) } -/// Creates the parent directories of a path and return the path with tildes -/// expanded. +/// Creates the parent directories of a path, deletes the file if it exists, and +/// returns the path with tildes expanded +/// +/// ``` +/// prepare_path("~/foo"); +/// ``` fn prepare_path(path: &str) -> io::Result<PathBuf> { let _dst: PathBuf = (&tilde(path).to_mut()).into(); if let Some(_path) = _dst.parent() { @@ -50,7 +70,11 @@ fn prepare_path(path: &str) -> io::Result<PathBuf> { Ok(_dst) } -/// Executes a local command using sh on Unix and cmd on Windows +/// Executes a command using `sh` on Unix and `cmd` on Windows +/// +/// ``` +/// run_command("echo 'Hello world'"); +/// ``` pub fn run_command(command: &str) -> Result<(), String> { let status; @@ -75,7 +99,7 @@ pub fn run_command(command: &str) -> Result<(), String> #[cfg(test)] mod tests { use super::*; - use crate::common::{setup_integration, write_file}; + use crate::test_utils::{setup_integration, write_file}; #[test] fn test_copy_file_create_dirs() { diff --git a/src/main.rs b/src/main.rs @@ -1,3 +1,5 @@ +//! A minimal, flexible, dotfile installer + mod cli; mod core; mod local; @@ -6,8 +8,8 @@ mod ssh; mod tags; #[cfg(test)] -#[path = "../tests/common/mod.rs"] -mod common; // Re-use e2e test utils for integration tests +#[path = "../tests/test_utils/mod.rs"] +mod test_utils; // Re-use E2E test utils for integration tests fn main() { cli::run(); diff --git a/src/manifest.rs b/src/manifest.rs @@ -1,52 +1,78 @@ +//! Coliru manifest parsing + use serde::Deserialize; use serde_yaml; use std::fs::read_to_string; use std::path::{Path, PathBuf}; +/// The options for a copy or link command #[derive(Debug, PartialEq, Deserialize)] pub struct CopyLinkOptions { + /// The source file (relative to the parent manifest file) pub src: String, + + /// The destination path (relative to the parent manifest file) pub dst: String, } +/// The options for a run command #[derive(Debug, PartialEq, Deserialize)] pub struct RunOptions { + /// The location of the script (relative to the parent manifest file) pub src: String, + /// The optional shell command prefix #[serde(default)] pub prefix: String, + /// The optional shell command postfix #[serde(default)] pub postfix: String, } +/// A manifest step #[derive(Debug, PartialEq, Deserialize)] pub struct Step { + /// The step's copy commands #[serde(default)] pub copy: Vec<CopyLinkOptions>, + /// The step's link commands #[serde(default)] pub link: Vec<CopyLinkOptions>, + /// The step's run commands #[serde(default)] pub run: Vec<RunOptions>, + /// The step's tags #[serde(default)] pub tags: Vec<String>, } +/// A coliru manifest as it appears in a file, without the base_dir property #[derive(Debug, PartialEq, Deserialize)] struct RawManifest { + + /// The manifest steps steps: Vec<Step>, } +/// A parsed coliru manifest #[derive(Debug, PartialEq)] pub struct Manifest { + /// The manifest steps pub steps: Vec<Step>, + + /// The parent directory of the manifest file pub base_dir: PathBuf, } /// Parse a coliru YAML manifest file +/// +/// ``` +/// let manifest = parse_manifest_file(Path::new("manifest.yml"))?; +/// ``` pub fn parse_manifest_file(path: &Path) -> Result<Manifest, String> { let raw_str = read_to_string(path).map_err(|why| why.to_string())?; let raw_manifest = serde_yaml::from_str::<RawManifest>(&raw_str) diff --git a/src/ssh.rs b/src/ssh.rs @@ -1,16 +1,48 @@ +//! Remote dotfile installation utilities +//! +//! To send files to a remote machine via SCP, first stage them using +//! [`stage_file`], then transfer them using [`send_staged_files`]. +//! +//! ``` +//! let staging_dir = Path::new("/tmp/staging"); +//! let host = "user@hostname"; +//! stage_file("foo.sh", "~/foo.sh", staging_dir); +//! send_staged_files(staging_dir, host); +//! send_command("bash ~/foo.sh", host); +//! ``` + use shellexpand::tilde_with_context; use std::fs::{read_dir, remove_dir_all}; use std::path::{MAIN_SEPARATOR_STR, Path, PathBuf}; use std::process::Command; use super::local::copy_file; -/// Copy a file to an SCP staging directory +/// 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`. /// -/// The destination directory structure will be recreated in the staging -/// directory under either the home or root subdirectories. This staging system -/// allows missing directories to be created automatically on the remote -/// machine. +/// ``` +/// // Prepare to transfer foo to ~/foo, bar to /bar, and baz to ~/.coliru/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> { + // 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 + // │   └── foo + // └── root/ + //    └── bar + let home_dir = staging_dir.join("home"); let root_dir = staging_dir.join("root"); let get_home_dir = || { @@ -41,7 +73,15 @@ pub fn stage_file(src: &str, dst: &str, staging_dir: &Path) -> Result<(), String .map_err(|why| why.to_string()) } -/// Transfer the files in an SCP staging directory to a remote machine +/// Transfers the files in an SCP staging directory to a remote machine +/// +/// `host` may be an SSH alias or a string in the form `user@hostname`. Use +/// [`stage_file`] to produce a staging directory. The contents of the staging +/// directory are deleted after they are successfully transferred. +/// +/// ``` +/// send_staged_files(Path::new("/tmp/staging"), "user@hostname"); +/// ``` pub fn send_staged_files(staging_dir: &Path, host: &str) -> Result<(), String> { let home_dir = staging_dir.join("home"); if home_dir.exists() { @@ -56,8 +96,14 @@ pub fn send_staged_files(staging_dir: &Path, host: &str) -> Result<(), String> { Ok(()) } -/// Copy a directory to another machine via SCP and merge it with a destination -/// directory +/// Copies a directory to another machine via SCP and merges it with a +/// destination directory +/// +/// `host` may be an SSH alias or a string in the form `user@hostname`. +/// +/// ``` +/// send_dir("new_home", "~/", "user@hostname"); +/// ``` fn send_dir(src: &str, dst: &str, host: &str) -> Result<(), String> { // To avoid the source directory being copied as a subdirectory of the // destination directory, we must send the contents of the directory @@ -80,8 +126,13 @@ fn send_dir(src: &str, dst: &str, host: &str) -> Result<(), String> { Ok(()) } -/// Execute a command on another machine via SSH -#[allow(dead_code)] +/// Executes a command on another machine via SSH +/// +/// `host` may be an SSH alias or a string in the form `user@hostname`. +/// +/// ``` +/// send_command("echo 'Hello World'"); +/// ``` pub fn send_command(command: &str, host: &str) -> Result<(), String> { let mut cmd = Command::new("ssh"); if host == "test@localhost" { @@ -102,7 +153,7 @@ mod tests { #![allow(unused_imports)] use super::*; - use crate::common::{SSH_HOST, read_file, setup_integration, write_file}; + use crate::test_utils::{SSH_HOST, read_file, setup_integration, write_file}; use std::fs; diff --git a/src/tags.rs b/src/tags.rs @@ -1,12 +1,7 @@ +//! Tag rule matching functionality + /// Checks if a list of tags matches a list of tag rules /// -/// Rules and tags are both specified as strings. Each rule contains one or more -/// desired tags separated by `,`. A list of tags matches a list of rules if -/// each rule contains at least one tag that appears in the list of tags. The -/// results of rules prefixed with `^` will be negated. Any list of tags will -/// match an empty list of tag rules. An empty list of tags will only match an -/// empty list of tag rules. -/// /// ``` /// let rules = ["linux,macos", "system", "^work"]; /// let tags_1 = ["macos", "system", "user"]; diff --git a/tests/basic.rs b/tests/basic.rs @@ -1,9 +1,8 @@ -/// End to end tests that do not test general CLI behavior rather than specific -/// installation behavior +//! End to end tests that test general, non-installation, CLI behavior -mod common; +mod test_utils; -use common::*; +use test_utils::*; use std::env::consts::EXE_SUFFIX; #[test] diff --git a/tests/common/mod.rs b/tests/common/mod.rs @@ -1,166 +0,0 @@ -#![allow(dead_code)] - -use std::env; -use std::fs; -use std::io::Write; -use std::path::{Path, PathBuf}; -use std::process::Command; - -/// The SSH test server -pub const SSH_HOST: &str = "test@localhost"; // TODO: add explicit port - -/// A set of temporary directories that are automatically deleted when the value -/// is dropped -pub struct TempDirs { - /// A temporary directory that is located at or in $HOME on Unix - pub home: PathBuf, - - /// A temporary directory that is located at or under the CWD - pub local: PathBuf, - - /// A temporary directory that is mounted to the SSH server under $HOME - pub ssh: PathBuf, - - /// A temporary directory that is mounted to the SSH server under ~/.coliru - pub ssh_cwd: PathBuf, -} -impl Drop for TempDirs { - fn drop(&mut self) { - fs::remove_dir_all(&self.home).unwrap(); - fs::remove_dir_all(&self.local).unwrap(); - fs::remove_dir_all(&self.ssh).unwrap(); - fs::remove_dir_all(&self.ssh_cwd).unwrap(); - } -} -impl TempDirs { - fn new(name: &str) -> TempDirs { - // The CWD of the current process is always the repository root - let dir = env::current_dir().unwrap().join("tests").join(".temp"); - - let home = dir.join("home").join(name); - let local = dir.join("local").join(name); - let ssh = dir.join("ssh").join(name); - let ssh_cwd = dir.join("ssh").join(".coliru").join(name); - - assert_eq!(home.exists(), false); - assert_eq!(local.exists(), false); - assert_eq!(ssh.exists(), false); - assert_eq!(ssh_cwd.exists(), false); - - fs::create_dir_all(&home).unwrap(); - fs::create_dir_all(&local).unwrap(); - fs::create_dir_all(&ssh).unwrap(); - fs::create_dir_all(&ssh_cwd).unwrap(); - - TempDirs { home, local, ssh, ssh_cwd } - } -} - -/// Initializes temporary directories for integration tests -/// -/// On Unix, $HOME is set to the parent directory of the home temporary -/// directory, which is the same for all integration tests. This prevents issues -/// when tests are run in multiple threads. -pub fn setup_integration(name: &str) -> TempDirs { - let dirs = TempDirs::new(name); - if cfg!(target_family = "unix") { - env::set_var("HOME", dirs.home.parent().unwrap()); - } - dirs -} - -/// Initializes temporary directories and a coliru Command for e2e tests -/// -/// The Command's CWD is set to the local temporary directory, and on Unix, the -/// Command's $HOME variable is set to the home temporary directory. -fn setup_e2e(name: &str) -> (TempDirs, Command) { - let dirs = TempDirs::new(name); - - let exe = env::current_exe().unwrap().parent().unwrap().to_path_buf() - .join(format!("../coliru{}", env::consts::EXE_SUFFIX)); - let mut cmd = Command::new(exe); - cmd.current_dir(&dirs.local); - if cfg!(target_family = "unix") { - cmd.env("HOME", &dirs.home); - } - - (dirs, cmd) -} - -/// Initializes temporary directories and a coliru Command for local e2e tests -/// -/// A test dotfile repo is copied to the local folder, to be installed to $HOME -/// on Unix and CWD on Windows. -pub fn setup_e2e_local(name: &str) -> (TempDirs, Command) { - let (dirs, cmd) = setup_e2e(name); - - // It's difficult to mock $HOME on Windows, so install dotfiles in CWD - let home_dir = if cfg!(target_family = "unix") { "~/" } else { "" }; - copy_manifest(&dirs.local, home_dir, ""); - - (dirs, cmd) -} - -/// Initializes temporary directories and a coliru Command for ssh e2e tests -/// -/// A test dotfile repo is copied to the local folder, to be installed to -/// ~/test_name/ with scripts copied to ~/.coliru/test_name/, and the --host -/// argument is set to the test SSH server. -pub fn setup_e2e_ssh(name: &str) -> (TempDirs, Command) { - let (dirs, mut cmd) = setup_e2e(name); - cmd.args(["--host", SSH_HOST]); - - // Replace ~/ and scripts/ with custom directory to isolate SSH tests - copy_manifest(&dirs.local, &format!("~/{name}/"), &format!("{name}/")); - - (dirs, cmd) -} - -/// Create a basic manifest file and its associated dotfiles in a directory -/// -/// All occurrences of the string "~/" and "scripts/" (e.g. in manifest.yml and -/// scripts/*) will be replaced with the value of home_dir and script_dir -/// respectively to ensures that dotfiles are isolated across tests when -/// necessary. -fn copy_manifest(dir: &Path, home_dir: &str, script_dir: &str) { - let examples = env::current_exe().unwrap().parent().unwrap().to_path_buf() - .join("../../../examples/test"); - - let copy_file = |path: &str| { - let mut contents = read_file(&examples.join(path)); - contents = contents.replace("~/", home_dir); - contents = contents.replace("scripts/", script_dir); - let dst = path.replace("scripts/", script_dir); - write_file(&dir.join(dst), &contents); - }; - - copy_file("manifest.yml"); - fs::create_dir_all(&dir.join(script_dir)).unwrap(); - copy_file("scripts/script.bat"); - copy_file("scripts/script.sh"); - copy_file("bashrc"); - copy_file("gitconfig"); - copy_file("vimrc"); - -} - -/// Writes a string to a file, overwriting it if it already exists. -pub fn write_file(path: &Path, contents: &str) { - let mut file = fs::File::create(path).unwrap(); - file.write_all(contents.as_bytes()).unwrap(); -} - -/// Reads the contents of a file into a string. -pub fn read_file(path: &Path) -> String { - fs::read_to_string(path).unwrap() -} - -/// Returns the stdout of a command as a String. -pub fn stdout_to_string(cmd: &mut Command) -> String { - String::from_utf8_lossy(&cmd.output().unwrap().stdout).into_owned() -} - -/// Returns the stderr of a command as a String. -pub fn stderr_to_string(cmd: &mut Command) -> String { - String::from_utf8_lossy(&cmd.output().unwrap().stderr).into_owned() -} diff --git a/tests/local.rs b/tests/local.rs @@ -1,9 +1,8 @@ -/// End to end tests that test specific installation behavior on the local file -/// system +//! End to end tests that test installation behavior on the local file system -mod common; +mod test_utils; -use common::*; +use test_utils::*; use std::fs::remove_file; #[test] diff --git a/tests/ssh.rs b/tests/ssh.rs @@ -1,9 +1,8 @@ -/// End to end tests that test specific installation behavior on a remote -/// machine via SSH +//! End to end tests that test installation behavior on a remote machine via SSH -mod common; +mod test_utils; -use common::*; +use test_utils::*; use std::fs::remove_file; #[test] diff --git a/tests/test_utils/mod.rs b/tests/test_utils/mod.rs @@ -0,0 +1,207 @@ +//! Coliru testing utilities +//! +//! These utilities create and manage resources used in integration and +//! end-to-end tests, including temporary directories, processes running coliru +//! commands, and test dotfile repositories. There are also functions for basic +//! I/O and capturing command output. All temporary directories are located +//! under the `.temp` directory and are unique to each test according to name. + +#![allow(dead_code)] + +use std::env; +use std::fs; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// The SSH test server +pub const SSH_HOST: &str = "test@localhost"; // TODO: add explicit port + +/// A set of temporary directories that are automatically deleted when the value +/// is dropped +pub struct TempDirs { + /// A temporary directory that is located at or in `~/` on Unix + pub home: PathBuf, + + /// A temporary directory that is located at or under the current working + /// directory + pub local: PathBuf, + + /// A temporary directory that is mounted to the SSH server under `~/` + pub ssh: PathBuf, + + /// A temporary directory that is mounted to the SSH server under + /// `~/.coliru` + pub ssh_cwd: PathBuf, +} +impl Drop for TempDirs { + fn drop(&mut self) { + fs::remove_dir_all(&self.home).unwrap(); + fs::remove_dir_all(&self.local).unwrap(); + fs::remove_dir_all(&self.ssh).unwrap(); + fs::remove_dir_all(&self.ssh_cwd).unwrap(); + } +} +impl TempDirs { + /// Creates a new set of temporary directories with a certain name + /// + /// ``` + /// let dirs = TempDirs::new("test_foo"); + /// ``` + fn new(name: &str) -> TempDirs { + // The working directory of the main process is always the repository + // root + let dir = env::current_dir().unwrap().join("tests").join(".temp"); + + let home = dir.join("home").join(name); + let local = dir.join("local").join(name); + let ssh = dir.join("ssh").join(name); + let ssh_cwd = dir.join("ssh").join(".coliru").join(name); + + assert_eq!(home.exists(), false); + assert_eq!(local.exists(), false); + assert_eq!(ssh.exists(), false); + assert_eq!(ssh_cwd.exists(), false); + + fs::create_dir_all(&home).unwrap(); + fs::create_dir_all(&local).unwrap(); + fs::create_dir_all(&ssh).unwrap(); + fs::create_dir_all(&ssh_cwd).unwrap(); + + TempDirs { home, local, ssh, ssh_cwd } + } +} + +/// Initializes temporary directories for integration tests +/// +/// On Unix, `$HOME` is set to the parent directory of the home temporary +/// directory, which is the same for all integration tests. This prevents issues +/// when tests are run in multiple threads. +/// +/// ``` +/// let dirs = setup_integration("test_foo"); +/// ``` +pub fn setup_integration(name: &str) -> TempDirs { + let dirs = TempDirs::new(name); + if cfg!(target_family = "unix") { + env::set_var("HOME", dirs.home.parent().unwrap()); + } + dirs +} + +/// Initializes temporary directories and a coliru Command for E2E tests +/// +/// The Command's working directory is set to the local temporary directory, and +/// on Unix, the Command's `$HOME` variable is set to the home temporary +/// directory. +/// +/// ``` +/// let (dirs, cmd) = setup_e2e("test_foo"); +/// ``` +fn setup_e2e(name: &str) -> (TempDirs, Command) { + let dirs = TempDirs::new(name); + + let exe = env::current_exe().unwrap().parent().unwrap().to_path_buf() + .join(format!("../coliru{}", env::consts::EXE_SUFFIX)); + let mut cmd = Command::new(exe); + cmd.current_dir(&dirs.local); + if cfg!(target_family = "unix") { + cmd.env("HOME", &dirs.home); + } + + (dirs, cmd) +} + +/// Initializes temporary directories and a coliru Command for local E2E tests +/// +/// A test dotfile repository is copied to the working directory (mapped to the +/// `local` temporary directory), to be installed to the home directory on Unix +/// (mapped to the `home` temporary directory) and the current working directory +/// on Windows (mapped to the `local` temporary directory). +/// +/// ``` +/// let (dirs, cmd) = setup_e2e_local("test_foo"); +/// ``` +pub fn setup_e2e_local(name: &str) -> (TempDirs, Command) { + let (dirs, cmd) = setup_e2e(name); + + // It's difficult to mock $HOME on Windows, so install dotfiles in CWD + let home_dir = if cfg!(target_family = "unix") { "~/" } else { "" }; + copy_manifest(&dirs.local, home_dir, ""); + + (dirs, cmd) +} + +/// Initializes temporary directories and a coliru Command for SSH E2E tests +/// +/// A test dotfile repository is copied to the working directory (mapped to the +/// `local` temporary directory), to be installed over SSH to `~/test_name/` +/// (mapped to the `ssh` temporary directory), with scripts copied to +/// `~/.coliru/test_name/` (mapped to the `ssh_cwd` temporary directory). The +/// `--host` flag is set to the test SSH server. +/// +/// ``` +/// let (dirs, cmd) = setup_e2e_ssh("test_foo"); +/// ``` +pub fn setup_e2e_ssh(name: &str) -> (TempDirs, Command) { + let (dirs, mut cmd) = setup_e2e(name); + cmd.args(["--host", SSH_HOST]); + + // Replace ~/ and scripts/ with custom directory to isolate SSH tests + copy_manifest(&dirs.local, &format!("~/{name}/"), &format!("{name}/")); + + (dirs, cmd) +} + +/// Initializes a basic dotfiles repository in a directory +/// +/// The dotfiles from `examples/test/` are used as a starting template. All +/// occurrences of `~/` and `scripts/` are replaced with `home_dir` and +/// `script_dir`, respectively, to allow dotfiles to be isolated across tests +/// when necessary. +/// +/// ``` +/// copy_manifest(&Path::new("/tmp/dotfiles/"), "~/", "scripts/"); +/// ``` +fn copy_manifest(dir: &Path, home_dir: &str, script_dir: &str) { + let examples = env::current_exe().unwrap().parent().unwrap().to_path_buf() + .join("../../../examples/test"); + + let copy_file = |path: &str| { + let mut contents = read_file(&examples.join(path)); + contents = contents.replace("~/", home_dir); + contents = contents.replace("scripts/", script_dir); + let dst = path.replace("scripts/", script_dir); + write_file(&dir.join(dst), &contents); + }; + + copy_file("manifest.yml"); + fs::create_dir_all(&dir.join(script_dir)).unwrap(); + copy_file("scripts/script.bat"); + copy_file("scripts/script.sh"); + copy_file("bashrc"); + copy_file("gitconfig"); + copy_file("vimrc"); + +} + +/// Writes a string to a file, overwriting it if it already exists +pub fn write_file(path: &Path, contents: &str) { + let mut file = fs::File::create(path).unwrap(); + file.write_all(contents.as_bytes()).unwrap(); +} + +/// Reads the contents of a file into a string +pub fn read_file(path: &Path) -> String { + fs::read_to_string(path).unwrap() +} + +/// Returns the stdout of a command as a String +pub fn stdout_to_string(cmd: &mut Command) -> String { + String::from_utf8_lossy(&cmd.output().unwrap().stdout).into_owned() +} + +/// Returns the stderr of a command as a String +pub fn stderr_to_string(cmd: &mut Command) -> String { + String::from_utf8_lossy(&cmd.output().unwrap().stderr).into_owned() +}