Skip to content

Commit

Permalink
add CLI parser
Browse files Browse the repository at this point in the history
  • Loading branch information
zekroTJA committed Apr 27, 2023
1 parent f69637c commit 4ad46bd
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 111 deletions.
5 changes: 4 additions & 1 deletion fw/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
[package]
name = "fw"
version = "0.1.0"
version = "1.0.0"
edition = "2021"
authors = ["Ringo Hoffmann <contact@zekro.de>"]
description = "A very simple tool to watch files and execute commands on transitions."

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1.0.70"
clap = { version = "4.2.4", features = ["derive"] }
directories = "5.0.0"
env_logger = "0.10.0"
figment = { version = "0.10.8", features = ["yaml", "toml", "json"] }
Expand Down
20 changes: 18 additions & 2 deletions fw/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
use std::{collections::HashMap, ops::Deref, path::Path};

use crate::util::{split_cmd_trimmed, transtion_to_string};
use anyhow::Result;
use directories::ProjectDirs;
use figment::{
providers::{Format, Json, Toml, Yaml},
Figment,
Figment, Provider,
};
use fwatch::Transition;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct Config {
pub check_interval_ms: Option<u64>,
pub actions: Vec<Action>,
pub actions: HashMap<String, Action>,
}

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -62,6 +64,20 @@ impl Config {
.merge(Json::file("fw.json"))
.extract()?)
}

pub fn from_file<T: AsRef<Path>>(path: T) -> Result<Self> {
let ext = path.as_ref().extension().unwrap_or_default();
let mut figment = Figment::new();

figment = match ext.to_string_lossy().deref() {
"yml" | "yaml" => figment.merge(Yaml::file(path)),
"toml" => figment.merge(Toml::file(path)),
"json" => figment.merge(Json::file(path)),
_ => anyhow::bail!("invalid config file type"),
};

Ok(figment.extract()?)
}
}

impl Target {
Expand Down
130 changes: 22 additions & 108 deletions fw/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
mod config;
mod util;
mod watcher;

use crate::config::{Command, Config};
use crate::util::transtion_to_string;
use crate::config::Config;
use clap::Parser;
use env_logger::Env;
use fwatch::{BasicTarget, Transition, Watcher};
use log::{debug, error, info};
use std::ffi::OsStr;
use std::str;
use std::{process::Stdio, time::Duration};
use tokio::{process, time};
use log::{debug, error};
use watcher::watch;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cmd {
pub actions: Option<Vec<String>>,
#[arg(short, long)]
pub config: Option<String>,
}

#[tokio::main]
async fn main() {
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();

let cfg = match Config::init() {
let cmd = Cmd::parse();

let res = match cmd.config {
Some(cfg) => Config::from_file(cfg),
None => Config::init(),
};

let cfg = match res {
Ok(v) => v,
Err(err) => {
error!("config initialization failed: {err}");
Expand All @@ -25,103 +37,5 @@ async fn main() {

debug!("Parsed config: {cfg:#?}");

let sleep_interval = Duration::from_millis(cfg.check_interval_ms.unwrap_or(1000));

let mut watcher: Watcher<BasicTarget> = Watcher::new();

cfg.actions
.iter()
.flat_map(|action| &action.targets)
.for_each(|target| watcher.add_target(BasicTarget::new(target.path())));

info!("Watching targets ...");
loop {
for (index, transition) in watcher
.watch()
.into_iter()
.enumerate()
.filter(|(_, transition)| !matches!(transition, Transition::None))
{
let Some(path) = watcher.get_path(index) else {
error!("could not get path for index {index}");
continue;
};

for action in &cfg.actions {
for target in &action.targets {
if target.path() != path.to_string_lossy()
|| !target.matches_transition(transition)
{
debug!("Change not tracked: {:?} -> {:?}", &path, &transition);
continue;
}

info!("Change detected: {:?} -> {:?}", &path, &transition);

let cmds = action.commands.clone();
let envmap = [
("FW_PATH", format!("{}", path.clone().to_string_lossy())),
(
"FW_TRANSITION",
transtion_to_string(&transition).to_string(),
),
];
tokio::spawn(async move {
execute_commands(&cmds, envmap).await;
});
}
}
}

time::sleep(sleep_interval).await;
}
}

async fn execute_commands<E, K, V>(cmds: &[Command], env: E)
where
E: IntoIterator<Item = (K, V)> + Clone,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
for cmd in cmds {
let args = cmd.split_command();
if args.is_empty() {
continue;
}

let ex = args[0];
let args = &args[1..];

let mut exec = process::Command::new(ex);
exec.args(args).current_dir(cmd.cwd()).envs(env.clone());

if cmd.is_async() {
match exec.spawn() {
Err(err) => {
error!("Command execution failed ({}): {}", cmd.command(), err);
return;
}
Ok(v) => {
info!("Command spawend ({}): ID: {:?}", cmd.command(), v.id());
}
}
} else {
match exec
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.output()
.await
{
Err(err) => {
error!("Command execution failed ({}): {}", cmd.command(), err);
return;
}
Ok(out) => info!(
"Command executed ({}): {}",
cmd.command(),
str::from_utf8(out.stdout.as_slice()).unwrap_or_default()
),
}
}
}
watch(&cfg, cmd.actions).await;
}
121 changes: 121 additions & 0 deletions fw/src/watcher.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use crate::config::{Command, Config};
use crate::util::transtion_to_string;
use fwatch::{BasicTarget, Transition, Watcher};
use log::{debug, error, info};
use std::ffi::OsStr;
use std::str;
use std::{process::Stdio, time::Duration};
use tokio::{process, time};

pub async fn watch(cfg: &Config, filter: Option<Vec<String>>) {
let sleep_interval = Duration::from_millis(cfg.check_interval_ms.unwrap_or(1000));

let mut watcher: Watcher<BasicTarget> = Watcher::new();

let actions: Vec<(_, _)> = cfg
.actions
.iter()
.filter(|(name, _)| filter.is_none() || filter.as_ref().unwrap().contains(name))
.collect();

if actions.is_empty() {
error!("No actions to watch matching the passed action names");
return;
}

actions
.iter()
.flat_map(|(_, action)| &action.targets)
.for_each(|target| watcher.add_target(BasicTarget::new(target.path())));

info!("Watching targets ...");
loop {
for (index, transition) in watcher
.watch()
.into_iter()
.enumerate()
.filter(|(_, transition)| !matches!(transition, Transition::None))
{
let Some(path) = watcher.get_path(index) else {
error!("could not get path for index {index}");
continue;
};

for (_, action) in &actions {
for target in &action.targets {
if target.path() != path.to_string_lossy()
|| !target.matches_transition(transition)
{
debug!("Change not tracked: {:?} -> {:?}", &path, &transition);
continue;
}

info!("Change detected: {:?} -> {:?}", &path, &transition);

let cmds = action.commands.clone();
let envmap = [
("FW_PATH", format!("{}", path.clone().to_string_lossy())),
(
"FW_TRANSITION",
transtion_to_string(&transition).to_string(),
),
];
tokio::spawn(async move {
execute_commands(&cmds, envmap).await;
});
}
}
}

time::sleep(sleep_interval).await;
}
}

async fn execute_commands<E, K, V>(cmds: &[Command], env: E)
where
E: IntoIterator<Item = (K, V)> + Clone,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
for cmd in cmds {
let args = cmd.split_command();
if args.is_empty() {
continue;
}

let ex = args[0];
let args = &args[1..];

let mut exec = process::Command::new(ex);
exec.args(args).current_dir(cmd.cwd()).envs(env.clone());

if cmd.is_async() {
match exec.spawn() {
Err(err) => {
error!("Command execution failed ({}): {}", cmd.command(), err);
return;
}
Ok(v) => {
info!("Command spawend ({}): ID: {:?}", cmd.command(), v.id());
}
}
} else {
match exec
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.output()
.await
{
Err(err) => {
error!("Command execution failed ({}): {}", cmd.command(), err);
return;
}
Ok(out) => info!(
"Command executed ({}): {}",
cmd.command(),
str::from_utf8(out.stdout.as_slice()).unwrap_or_default()
),
}
}
}
}

0 comments on commit 4ad46bd

Please sign in to comment.