coliru

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

commit c654f6f8b1ab8033d096b4d2500b56511139e2b0
parent 15622e294abc79e212c237a8f39f117b9b9a1770
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Sun,  7 Jul 2024 13:05:30 -0700

Implement --list-tags flag

Diffstat:
MREADME.md | 1+
Msrc/cli.rs | 46+++++++++++++++++++++++++++++++++++++---------
Msrc/core.rs | 21+++++++++++++--------
Msrc/manifest.rs | 40++++++++++++++++++++++++++++++++++++++++
Mtests/basic.rs | 27+++++++++++++++++++++++++--
5 files changed, 116 insertions(+), 19 deletions(-)

diff --git a/README.md b/README.md @@ -36,6 +36,7 @@ coliru manifest.yml --tag-rules tag1 tag2,tag3 ^tag4 Some other helpful options include: - `--help`, `-h`: Print full help information +- `--list-tags`, `-l`: List the tags in the manifest and quit without installing - `--dry-run`, `-n`: Do a trial run without any permanent changes - `--copy`: Interpret link commands as copy commands - `--host <HOST>`: Install dotfiles on another machine over SSH diff --git a/src/cli.rs b/src/cli.rs @@ -1,9 +1,11 @@ //! The coliru command line interface +use anyhow::{Context, Result}; use colored::{Colorize, control::set_override}; use clap::{Parser, ColorChoice}; use std::path::Path; -use super::core::execute_manifest_file; +use super::core::{install_manifest, list_tags}; +use super::manifest::parse_manifest_file; /// CLI about description const HELP_ABOUT: &str = "A minimal, flexible, dotfile installer"; @@ -11,10 +13,16 @@ const HELP_ABOUT: &str = "A minimal, flexible, dotfile installer"; /// CLI examples to be appended to help output const HELP_EXAMPLES: &str = "\ Examples: - # Install dotfiles from manifest.yml with tags matching A && (B || C) && !D + # List tags in manifest + coliru manifest.yml --list-tags + + # Preview installation steps with tags matching A && (B || C) && !D + coliru manifest.yml --tag-rules A B,C ^D --dry-run + + # Install dotfiles on local machine coliru manifest.yml --tag-rules A B,C ^D - # Install dotfiles from manifest.yml to user@hostname over SSH + # Install dotfiles to user@hostname over SSH coliru manifest.yml --tag-rules A B,C ^D --host user@hostname"; /// Arguments to the coliru CLI @@ -29,6 +37,10 @@ struct Args { #[arg(short, long, value_name="RULE", num_args=0..)] pub tag_rules: Vec<String>, + /// List available tags and quit without installing + #[arg(short, long)] + pub list_tags: bool, + /// Do a trial run without any permanent changes #[arg(short = 'n', long)] pub dry_run: bool, @@ -49,13 +61,8 @@ struct Args { /// Runs the coliru CLI pub fn run() { let args = Args::parse(); - let manifest_path = Path::new(&args.manifest); - if args.no_color { - set_override(false); - } - match execute_manifest_file(&manifest_path, args.tag_rules, &args.host, - args.dry_run, args.copy) { + match run_args(args) { Err(why) => { eprintln!("{} {:#}", "Error:".bold().red(), why); std::process::exit(2); @@ -65,3 +72,24 @@ pub fn run() { }, } } + +/// Runs the coliru CLI according to a set of arguments +/// +/// Returns an Err if a critical occurs, Ok(true) if minor errors occurred, and +/// Ok(false) if no errors occurred. +fn run_args(args: Args) -> Result<bool> { + if args.no_color { + set_override(false); + } + + let manifest = parse_manifest_file(Path::new(&args.manifest)) + .context("Failed to parse manifest")?; + + if args.list_tags { + list_tags(manifest); + Ok(false) + } else { + install_manifest(manifest, args.tag_rules, &args.host, args.dry_run, + args.copy) + } +} diff --git a/src/core.rs b/src/core.rs @@ -1,10 +1,10 @@ -//! Manifest execution functions +//! Core manifest operation functions use anyhow::{Context, Result}; use colored::{Colorize, ColoredString}; use std::env::set_current_dir; use std::path::Path; -use super::manifest::{CopyLinkOptions, RunOptions, parse_manifest_file}; +use super::manifest::{Manifest, CopyLinkOptions, RunOptions, get_tags}; use super::tags::tags_match; use super::local::{copy_file, link_file, run_command}; use super::ssh::{resolve_path, send_command, send_staged_files, stage_file}; @@ -37,15 +37,20 @@ fn handle_error(result: Result<()>) -> bool { false } -/// Executes the steps in a coliru manifest file according to a set of tag rules +/// Prints the available tags in a manifest +pub fn list_tags(manifest: Manifest) { + for tag in get_tags(manifest) { + println!("{}", tag); + } +} + +/// Executes the steps in a coliru manifest according to a set of tag rules /// -/// Returns an Err if a critical err occurs and returns a bool indicating +/// Returns an Err if a critical error occurs and returns a bool indicating /// whether any minor errors occurred otherwise -pub fn execute_manifest_file(path: &Path, tag_rules: Vec<String>, host: &str, - dry_run: bool, copy: bool) -> Result<bool> { +pub fn install_manifest(manifest: Manifest, tag_rules: Vec<String>, host: &str, + dry_run: bool, copy: bool) -> Result<bool> { - let manifest = parse_manifest_file(path) - .context("Failed to parse manifest")?; let temp_dir = tempdir().context("Failed to create temporary directory")?; set_current_dir(manifest.base_dir) .context("Failed to set working directory")?; diff --git a/src/manifest.rs b/src/manifest.rs @@ -3,6 +3,7 @@ use anyhow::Result; use serde::Deserialize; use serde_yaml; +use std::collections::HashSet; use std::fs::read_to_string; use std::path::{Path, PathBuf}; @@ -88,6 +89,21 @@ pub fn parse_manifest_file(path: &Path) -> Result<Manifest> { }) } +/// Returns a sorted, de-duplicated vector of all tags in a manifest +pub fn get_tags(manifest: Manifest) -> Vec<String> { + let mut tag_set: HashSet<String> = HashSet::new(); + + for step in manifest.steps { + for tag in step.tags { + tag_set.insert(tag); + } + } + + let mut tags: Vec<String> = tag_set.iter().map(|s| s.to_owned()).collect(); + tags.sort(); + tags +} + #[cfg(test)] mod tests { use super::*; @@ -196,4 +212,28 @@ mod tests { assert_eq!(actual.is_ok(), true); assert_eq!(actual.unwrap(), expected); } + + #[test] + fn get_tags_basic() { + let manifest_path = Path::new("examples/test/manifest.yml"); + let manifest = parse_manifest_file(manifest_path).unwrap(); + let expected = vec![ + String::from("linux"), + String::from("macos"), + String::from("windows"), + ]; + let actual = get_tags(manifest); + assert_eq!(actual, expected); + } + + #[test] + fn get_tags_empty() { + let manifest = Manifest { + steps: vec![], + base_dir: PathBuf::from("examples/test/empty.yml"), + }; + let expected: Vec<String> = vec![]; + let actual = get_tags(manifest); + assert_eq!(actual, expected); + } } diff --git a/tests/basic.rs b/tests/basic.rs @@ -19,6 +19,7 @@ Arguments: Options: -t, --tag-rules [<RULE>...] The set of tag rules to enforce + -l, --list-tags List available tags and quit without installing -n, --dry-run Do a trial run without any permanent changes --host <HOST> Install dotfiles on another machine over SSH --copy Interpret link commands as copy commands @@ -27,10 +28,16 @@ Options: -V, --version Print version Examples: - # Install dotfiles from manifest.yml with tags matching A && (B || C) && !D + # List tags in manifest + coliru manifest.yml --list-tags + + # Preview installation steps with tags matching A && (B || C) && !D + coliru manifest.yml --tag-rules A B,C ^D --dry-run + + # Install dotfiles on local machine coliru manifest.yml --tag-rules A B,C ^D - # Install dotfiles from manifest.yml to user@hostname over SSH + # Install dotfiles to user@hostname over SSH coliru manifest.yml --tag-rules A B,C ^D --host user@hostname "); let (stdout, stderr, exitcode) = run_command(&mut cmd); @@ -167,3 +174,19 @@ fn test_basic_absolute_manifest() { assert_eq!(foo_exists, true); assert_eq!(log_exists, false); } + +#[test] +fn test_basic_list_tags() { + let (_dirs, mut cmd) = setup_e2e_local("test_basic_list_tags"); + cmd.args(["manifest.yml", "--list-tags", "-t", "windows"]); + + let expected = "\ +linux +macos +windows +"; + let (stdout, stderr, exitcode) = run_command(&mut cmd); + assert_eq!(&stderr, ""); + assert_eq!(&stdout, expected); + assert_eq!(exitcode, Some(0)); +}