Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for batch execution of command #360

Merged
merged 2 commits into from
Nov 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

WorkerResult::Entry(val) => Some(val),
WorkerResult::Error(err) => {
if show_filesystem_errors {
print_error!("{}", err);
}
None
}
});
cmd.generate_and_execute_batch(paths);
}
127 changes: 117 additions & 10 deletions src/exec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,61 @@ mod job;
mod token;

use std::borrow::Cow;
use std::path::Path;
use std::process::Command;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
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::{batch, job};
use self::token::Token;

/// Execution mode of the command
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ExecutionMode {
/// Command is executed for each search result
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.number_of_tokens() > 1 {
return Err("Only one placeholder allowed for batch commands");
}
if cmd.args[0].has_tokens() {
return Err("First argument of exec-batch is expected to be a fixed executable");
}
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 +124,68 @@ impl CommandTemplate {
args.push(ArgumentTemplate::Tokens(vec![Token::Placeholder]));
}

CommandTemplate { args }
CommandTemplate { args, mode }
}

fn number_of_tokens(&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 in_batch_mode(&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());
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That generate("") call looks a bit weird. Do we expect the first argument to contain any tokens?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this is what causes the panic you mention for `fd -X "echo {}". Will have a look.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the latest update, the first argument is not expected to have a token (checked at new_batch), so input to generate is known not to be used.

cmd.stdin(Stdio::inherit());
cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit());

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());
}
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we add the following lines here:

cmd.stdin(Stdio::inherit());
cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit());

we can use --exec-batch with interactive terminal commands such as vim.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's indeed much better with these lines added.

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

Expand All @@ -125,6 +200,13 @@ enum ArgumentTemplate {
}

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

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

Expand Down Expand Up @@ -162,6 +244,7 @@ mod tests {
ArgumentTemplate::Text("${SHELL}:".into()),
ArgumentTemplate::Tokens(vec![Token::Placeholder]),
],
mode: ExecutionMode::OneByOne,
}
);
}
Expand All @@ -175,6 +258,7 @@ mod tests {
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::Tokens(vec![Token::NoExt]),
],
mode: ExecutionMode::OneByOne,
}
);
}
Expand All @@ -188,6 +272,7 @@ mod tests {
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::Tokens(vec![Token::Basename]),
],
mode: ExecutionMode::OneByOne,
}
);
}
Expand All @@ -201,6 +286,7 @@ mod tests {
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::Tokens(vec![Token::Parent]),
],
mode: ExecutionMode::OneByOne,
}
);
}
Expand All @@ -214,6 +300,7 @@ mod tests {
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::Tokens(vec![Token::BasenameNoExt]),
],
mode: ExecutionMode::OneByOne,
}
);
}
Expand All @@ -231,7 +318,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.in_batch_mode() {
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