Skip to content

Commit

Permalink
Add support for batch execution of command
Browse files Browse the repository at this point in the history
  • Loading branch information
kimsnj committed Nov 11, 2018
1 parent 32ec7af commit 721a0b6
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 42 deletions.
22 changes: 22 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,16 @@ pub fn build_app() -> App<'static, 'static> {
.value_terminator(";")
.value_name("cmd"),
)
.arg(
arg("exec-batch")
.long("exec-batch")
.short("X")
.min_values(1)
.allow_hyphen_values(true)
.value_terminator(";")
.value_name("cmd")
.conflicts_with("exec"),
)
.arg(
arg("exclude")
.long("exclude")
Expand Down Expand Up @@ -277,6 +287,18 @@ fn usage() -> HashMap<&'static str, Help> {
'{//}': parent directory\n \
'{.}': path without file extension\n \
'{/.}': basename without file extension");
doc!(h, "exec-batch"
, "Execute a command with all search results at once"
, "Execute a command with all search results at once.\n\
All arguments following --exec-batch are taken to be arguments to the command until the \
argument ';' is encountered.\n\
A single occurence of the following placeholders is authorized and substituted by the paths derived from the \
search results before the command is executed:\n \
'{}': path\n \
'{/}': basename\n \
'{//}': parent directory\n \
'{.}': path without file extension\n \
'{/.}': basename without file extension");
doc!(h, "exclude"
, "Exclude entries that match the given glob pattern"
, "Exclude files/directories that match the given glob pattern. This overrides any \
Expand Down
4 changes: 2 additions & 2 deletions src/exec/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
use std::io;
use std::io::Write;
use std::process::Command;
use std::sync::{Arc, Mutex};
use std::sync::Mutex;

/// Executes a command.
pub fn execute_command(mut cmd: Command, out_perm: Arc<Mutex<()>>) {
pub fn execute_command(mut cmd: Command, out_perm: &Mutex<()>) {
// Spawn the supplied command.
let output = cmd.output();

Expand Down
13 changes: 13 additions & 0 deletions src/exec/job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,16 @@ pub fn job(
cmd.generate_and_execute(&value, Arc::clone(&out_perm));
}
}

pub fn batch(rx: Receiver<WorkerResult>, cmd: &CommandTemplate, show_filesystem_errors: bool) {
let paths = rx.iter().filter_map(|value| match value {
WorkerResult::Entry(val) => Some(val),
WorkerResult::Error(err) => {
if show_filesystem_errors {
print_error!("{}", err);
}
None
}
});
cmd.generate_and_execute_batch(paths);
}
119 changes: 110 additions & 9 deletions src/exec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,58 @@ mod job;
mod token;

use std::borrow::Cow;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Arc, Mutex};

use regex::Regex;

use self::command::execute_command;
use self::input::{basename, dirname, remove_extension};
pub use self::job::job;
pub use self::job::{job, batch};
use self::token::Token;

/// Execution mode of the command
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ExecutionMode {
/// Command is executed for each path found
OneByOne,
/// Command is run for a batch of results at once
Batch,
}

/// Represents a template that is utilized to generate command strings.
///
/// The template is meant to be coupled with an input in order to generate a command. The
/// `generate_and_execute()` method will be used to generate a command and execute it.
#[derive(Debug, Clone, PartialEq)]
pub struct CommandTemplate {
args: Vec<ArgumentTemplate>,
mode: ExecutionMode,
}

impl CommandTemplate {
pub fn new<I, S>(input: I) -> CommandTemplate
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
Self::build(input, ExecutionMode::OneByOne)
}

pub fn new_batch<I, S>(input: I) -> Result<CommandTemplate, &'static str>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let cmd = Self::build(input, ExecutionMode::Batch);
if cmd.tokens_number() > 1 {
return Err("Only one placeholder allowed for batch commands");
}
Ok(cmd)
}

fn build<I, S>(input: I, mode: ExecutionMode) -> CommandTemplate
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
Expand Down Expand Up @@ -91,26 +121,64 @@ impl CommandTemplate {
args.push(ArgumentTemplate::Tokens(vec![Token::Placeholder]));
}

CommandTemplate { args }
CommandTemplate { args, mode }
}

fn tokens_number(&self) -> usize {
self.args.iter().filter(|arg| arg.has_tokens()).count()
}

fn prepare_path(input: &Path) -> String {
input
.strip_prefix(".")
.unwrap_or(input)
.to_string_lossy()
.into_owned()
}

/// Generates and executes a command.
///
/// Using the internal `args` field, and a supplied `input` variable, a `Command` will be
/// build. Once all arguments have been processed, the command is executed.
pub fn generate_and_execute(&self, input: &Path, out_perm: Arc<Mutex<()>>) {
let input = input
.strip_prefix(".")
.unwrap_or(input)
.to_string_lossy()
.into_owned();
let input = Self::prepare_path(input);

let mut cmd = Command::new(self.args[0].generate(&input).as_ref());
for arg in &self.args[1..] {
cmd.arg(arg.generate(&input).as_ref());
}

execute_command(cmd, out_perm)
execute_command(cmd, &out_perm)
}

pub fn is_batch(&self) -> bool {
self.mode == ExecutionMode::Batch
}

pub fn generate_and_execute_batch<I>(&self, paths: I)
where
I: Iterator<Item = PathBuf>,
{
let mut cmd = Command::new(self.args[0].generate("").as_ref());
let mut paths = paths.map(|p| Self::prepare_path(&p));
let mut has_path = false;

for arg in &self.args[1..] {
if arg.has_tokens() {
// A single `Tokens` is expected
// So we can directy consume the iterator once and for all
for path in &mut paths {
cmd.arg(arg.generate(&path).as_ref());
has_path = true;
}
} else {
cmd.arg(arg.generate("").as_ref());
}
}

if has_path {
execute_command(cmd, &Mutex::new(()));
}
}
}

Expand All @@ -125,6 +193,14 @@ enum ArgumentTemplate {
}

impl ArgumentTemplate {
pub fn has_tokens(&self) -> bool {
if let ArgumentTemplate::Tokens(_) = self {
true
} else {
false
}
}

pub fn generate<'a>(&'a self, path: &str) -> Cow<'a, str> {
use self::Token::*;

Expand Down Expand Up @@ -162,6 +238,7 @@ mod tests {
ArgumentTemplate::Text("${SHELL}:".into()),
ArgumentTemplate::Tokens(vec![Token::Placeholder]),
],
mode: ExecutionMode::OneByOne,
}
);
}
Expand All @@ -175,6 +252,7 @@ mod tests {
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::Tokens(vec![Token::NoExt]),
],
mode: ExecutionMode::OneByOne,
}
);
}
Expand All @@ -188,6 +266,7 @@ mod tests {
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::Tokens(vec![Token::Basename]),
],
mode: ExecutionMode::OneByOne,
}
);
}
Expand All @@ -201,6 +280,7 @@ mod tests {
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::Tokens(vec![Token::Parent]),
],
mode: ExecutionMode::OneByOne,
}
);
}
Expand All @@ -214,6 +294,7 @@ mod tests {
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::Tokens(vec![Token::BasenameNoExt]),
],
mode: ExecutionMode::OneByOne,
}
);
}
Expand All @@ -231,7 +312,27 @@ mod tests {
Token::Text(".ext".into())
]),
],
mode: ExecutionMode::OneByOne,
}
);
}

#[test]
fn tokens_single_batch() {
assert_eq!(
CommandTemplate::new_batch(&["echo", "{.}"]).unwrap(),
CommandTemplate {
args: vec![
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::Tokens(vec![Token::NoExt]),
],
mode: ExecutionMode::Batch,
}
);
}

#[test]
fn tokens_multiple_batch() {
assert!(CommandTemplate::new_batch(&["echo", "{.}", "{}"]).is_err());
}
}
11 changes: 10 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,16 @@ fn main() {
None
};

let command = matches.values_of("exec").map(CommandTemplate::new);
let command = matches
.values_of("exec")
.map(CommandTemplate::new)
.or_else(|| {
matches.values_of("exec-batch").map(|m| {
CommandTemplate::new_batch(m).unwrap_or_else(|e| {
print_error_and_exit!("{}", e);
})
})
});

let size_limits: Vec<SizeFilter> = matches
.values_of("size")
Expand Down
48 changes: 26 additions & 22 deletions src/walk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,34 +126,38 @@ pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<FdOptions>) {
let receiver_thread = thread::spawn(move || {
// This will be set to `Some` if the `--exec` argument was supplied.
if let Some(ref cmd) = rx_config.command {
let shared_rx = Arc::new(Mutex::new(rx));
if cmd.is_batch() {
exec::batch(rx, cmd, show_filesystem_errors);
} else {
let shared_rx = Arc::new(Mutex::new(rx));

let out_perm = Arc::new(Mutex::new(()));
let out_perm = Arc::new(Mutex::new(()));

// TODO: the following line is a workaround to replace the `unsafe` block that was
// previously used here to avoid the (unnecessary?) cloning of the command. The
// `unsafe` block caused problems on some platforms (SIGILL instructions on Linux) and
// therefore had to be removed.
let cmd = Arc::new(cmd.clone());
// TODO: the following line is a workaround to replace the `unsafe` block that was
// previously used here to avoid the (unnecessary?) cloning of the command. The
// `unsafe` block caused problems on some platforms (SIGILL instructions on Linux) and
// therefore had to be removed.
let cmd = Arc::new(cmd.clone());

// Each spawned job will store it's thread handle in here.
let mut handles = Vec::with_capacity(threads);
for _ in 0..threads {
let rx = Arc::clone(&shared_rx);
let cmd = Arc::clone(&cmd);
let out_perm = Arc::clone(&out_perm);
// Each spawned job will store it's thread handle in here.
let mut handles = Vec::with_capacity(threads);
for _ in 0..threads {
let rx = Arc::clone(&shared_rx);
let cmd = Arc::clone(&cmd);
let out_perm = Arc::clone(&out_perm);

// Spawn a job thread that will listen for and execute inputs.
let handle =
thread::spawn(move || exec::job(rx, cmd, out_perm, show_filesystem_errors));
// Spawn a job thread that will listen for and execute inputs.
let handle =
thread::spawn(move || exec::job(rx, cmd, out_perm, show_filesystem_errors));

// Push the handle of the spawned thread into the vector for later joining.
handles.push(handle);
}
// Push the handle of the spawned thread into the vector for later joining.
handles.push(handle);
}

// Wait for all threads to exit before exiting the program.
for h in handles {
h.join().unwrap();
// Wait for all threads to exit before exiting the program.
for h in handles {
h.join().unwrap();
}
}
} else {
let start = time::Instant::now();
Expand Down
Loading

0 comments on commit 721a0b6

Please sign in to comment.