Remove shell with --exec

This commit is contained in:
Matthias Reitinger 2017-11-03 01:39:03 +01:00 committed by David Peter
parent 1a6c92c475
commit 18709b1ede
8 changed files with 219 additions and 311 deletions

7
Cargo.lock generated
View File

@ -13,7 +13,6 @@ dependencies = [
"num_cpus 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"regex-syntax 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"shell-escape 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
@ -228,11 +227,6 @@ dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "shell-escape"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "strsim"
version = "0.6.0"
@ -365,7 +359,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1731164734096285ec2a5ec7fea5248ae2f5485b3feeb0115af4fda2183b2d1b"
"checksum regex-syntax 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ad890a5eef7953f55427c50575c680c42841653abd2b028b68cd223d157f62db"
"checksum same-file 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d931a44fdaa43b8637009e7632a02adc4f2b2e0733c08caa4cf00e8da4a117a7"
"checksum shell-escape 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "dd5cc96481d54583947bfe88bf30c23d53f883c6cd0145368b69989d97b84ef8"
"checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694"
"checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6"
"checksum term_size 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2b6b55df3198cc93372e85dd2ed817f0e38ce8cc0f22eb32391bfad9c4bf209"

View File

@ -40,7 +40,6 @@ lazy_static = "0.2.9"
num_cpus = "1.6.2"
regex = "0.2"
regex-syntax = "0.4"
shell-escape = "0.1.3"
[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
libc = "0.2"

View File

@ -85,7 +85,9 @@ pub fn build_app() -> App<'static, 'static> {
arg("exec")
.long("exec")
.short("x")
.takes_value(true)
.multiple(true)
.allow_hyphen_values(true)
.value_terminator(";")
.value_name("cmd"),
)
.arg(

View File

@ -7,228 +7,159 @@
// according to those terms.
use std::path::MAIN_SEPARATOR;
use std::borrow::Cow;
use shell_escape::escape;
/// Removes the parent component of the path
pub fn basename(path: &str) -> &str {
let mut index = 0;
for (id, character) in path.char_indices() {
if character == MAIN_SEPARATOR {
index = id;
}
}
/// A builder for efficiently generating input strings.
///
/// After choosing your required specs, the `get()` method will escape special characters found
/// in the input. Allocations will only occur if special characters are found that need to be
/// escaped.
pub struct Input<'a> {
data: &'a str,
// FIXME: On Windows, should return what for C:file.txt D:file.txt and \\server\share ?
if index != 0 {
return &path[index + 1..];
}
path
}
impl<'a> Input<'a> {
/// Creates a new `Input` structure, which provides access to command-building
/// primitives, such as `basename()` and `dirname()`.
pub fn new(data: &'a str) -> Input<'a> {
Input { data }
/// Removes the extension from the path
pub fn remove_extension(path: &str) -> &str {
let mut has_dir = false;
let mut dir_index = 0;
let mut ext_index = 0;
for (id, character) in path.char_indices() {
if character == MAIN_SEPARATOR {
has_dir = true;
dir_index = id;
}
if character == '.' {
ext_index = id;
}
}
/// Removes the parent component of the path
pub fn basename(&'a mut self) -> &'a mut Self {
let mut index = 0;
for (id, character) in self.data.char_indices() {
if character == MAIN_SEPARATOR {
index = id;
}
}
// FIXME: On Windows, should return what for C:file.txt D:file.txt and \\server\share ?
if index != 0 {
self.data = &self.data[index + 1..]
}
self
// Account for hidden files and directories
if ext_index != 0 && (!has_dir || dir_index + 2 <= ext_index) {
return &path[0..ext_index];
}
/// Removes the extension from the path
pub fn remove_extension(&'a mut self) -> &'a mut Self {
let mut has_dir = false;
let mut dir_index = 0;
let mut ext_index = 0;
path
}
for (id, character) in self.data.char_indices() {
if character == MAIN_SEPARATOR {
has_dir = true;
dir_index = id;
}
if character == '.' {
ext_index = id;
}
/// Removes the basename from the path.
pub fn dirname(path: &str) -> &str {
let mut has_dir = false;
let mut index = 0;
for (id, character) in path.char_indices() {
if character == MAIN_SEPARATOR {
has_dir = true;
index = id;
}
// Account for hidden files and directories
if ext_index != 0 && (!has_dir || dir_index + 2 <= ext_index) {
self.data = &self.data[0..ext_index];
}
self
}
/// Removes the basename from the path.
pub fn dirname(&'a mut self) -> &'a mut Self {
let mut has_dir = false;
let mut index = 0;
for (id, character) in self.data.char_indices() {
if character == MAIN_SEPARATOR {
has_dir = true;
index = id;
}
}
// FIXME: On Windows, return what for C:file.txt D:file.txt and \\server\share ?
self.data = if !has_dir {
"."
} else if index == 0 {
&self.data[..1]
} else {
&self.data[0..index]
};
self
}
pub fn get(&'a self) -> Cow<'a, str> {
escape(Cow::Borrowed(self.data))
}
#[cfg(test)]
fn get_private(&'a self) -> Cow<'a, str> {
Cow::Borrowed(self.data)
// FIXME: On Windows, return what for C:file.txt D:file.txt and \\server\share ?
if !has_dir {
"."
} else if index == 0 {
&path[..1]
} else {
&path[0..index]
}
}
#[cfg(test)]
mod tests {
use super::{MAIN_SEPARATOR, Input};
use super::{MAIN_SEPARATOR, basename, dirname, remove_extension};
fn correct(input: &str) -> String {
let mut sep = String::new();
sep.push(MAIN_SEPARATOR);
input.replace('/', &sep)
input.replace('/', &MAIN_SEPARATOR.to_string())
}
#[test]
fn path_remove_ext_simple() {
assert_eq!(
&Input::new("foo.txt").remove_extension().get_private(),
"foo"
);
assert_eq!(remove_extension("foo.txt"), "foo");
}
#[test]
fn path_remove_ext_dir() {
assert_eq!(
&Input::new(&correct("dir/foo.txt"))
.remove_extension()
.get_private(),
&correct("dir/foo")
remove_extension(&correct("dir/foo.txt")),
correct("dir/foo")
);
}
#[test]
fn path_hidden() {
assert_eq!(&Input::new(".foo").remove_extension().get_private(), ".foo")
assert_eq!(remove_extension(".foo"), ".foo")
}
#[test]
fn path_remove_ext_utf8() {
assert_eq!(
&Input::new("💖.txt").remove_extension().get_private(),
"💖"
);
assert_eq!(remove_extension("💖.txt"), "💖");
}
#[test]
fn path_remove_ext_empty() {
assert_eq!(&Input::new("").remove_extension().get_private(), "");
assert_eq!(remove_extension(""), "");
}
#[test]
fn path_basename_simple() {
assert_eq!(&Input::new("foo.txt").basename().get_private(), "foo.txt");
assert_eq!(basename("foo.txt"), "foo.txt");
}
#[test]
fn path_basename_no_ext() {
assert_eq!(
&Input::new("foo.txt")
.basename()
.remove_extension()
.get_private(),
"foo"
);
assert_eq!(remove_extension(basename("foo.txt")), "foo");
}
#[test]
fn path_basename_dir() {
assert_eq!(
&Input::new(&correct("dir/foo.txt")).basename().get_private(),
"foo.txt"
);
assert_eq!(basename(&correct("dir/foo.txt")), "foo.txt");
}
#[test]
fn path_basename_empty() {
assert_eq!(&Input::new("").basename().get_private(), "");
assert_eq!(basename(""), "");
}
#[test]
fn path_basename_utf8() {
assert_eq!(
&Input::new(&correct("💖/foo.txt"))
.basename()
.get_private(),
"foo.txt"
);
assert_eq!(
&Input::new(&correct("dir/💖.txt"))
.basename()
.get_private(),
"💖.txt"
);
assert_eq!(basename(&correct("💖/foo.txt")), "foo.txt");
assert_eq!(basename(&correct("dir/💖.txt")), "💖.txt");
}
#[test]
fn path_dirname_simple() {
assert_eq!(&Input::new("foo.txt").dirname().get_private(), ".");
assert_eq!(dirname("foo.txt"), ".");
}
#[test]
fn path_dirname_dir() {
assert_eq!(
&Input::new(&correct("dir/foo.txt")).dirname().get_private(),
"dir"
);
assert_eq!(dirname(&correct("dir/foo.txt")), "dir");
}
#[test]
fn path_dirname_utf8() {
assert_eq!(
&Input::new(&correct("💖/foo.txt")).dirname().get_private(),
"💖"
);
assert_eq!(
&Input::new(&correct("dir/💖.txt")).dirname().get_private(),
"dir"
);
assert_eq!(dirname(&correct("💖/foo.txt")), "💖");
assert_eq!(dirname(&correct("dir/💖.txt")), "dir");
}
#[test]
fn path_dirname_empty() {
assert_eq!(&Input::new("").dirname().get_private(), ".");
assert_eq!(dirname(""), ".");
}
#[test]
fn path_dirname_root() {
#[cfg(windows)]
assert_eq!(&Input::new("C:\\").dirname().get_private(), "C:");
assert_eq!(dirname("C:\\"), "C:");
#[cfg(windows)]
assert_eq!(&Input::new("\\").dirname().get_private(), "\\");
assert_eq!(dirname("\\"), "\\");
#[cfg(not(windows))]
assert_eq!(&Input::new("/").dirname().get_private(), "/");
assert_eq!(dirname("/"), "/");
}
}

View File

@ -20,9 +20,6 @@ pub fn job(
cmd: Arc<TokenizedCommand>,
out_perm: Arc<Mutex<()>>,
) {
// A string buffer that will be re-used in each iteration.
let buffer = &mut String::with_capacity(256);
loop {
// Create a lock on the shared receiver for this thread.
let lock = rx.lock().unwrap();
@ -36,9 +33,7 @@ pub fn job(
// Drop the lock so that other threads can read from the the receiver.
drop(lock);
// Generate a command to store within the buffer, and execute the command.
// Note that the `then_execute()` method will clear the buffer for us.
cmd.generate(buffer, &value, Arc::clone(&out_perm))
.then_execute();
// Generate a command and execute it.
cmd.generate(&value, Arc::clone(&out_perm)).then_execute();
}
}

View File

@ -12,163 +12,167 @@ mod token;
mod job;
mod input;
use std::borrow::Cow;
use std::path::Path;
use std::sync::{Arc, Mutex};
// use self::paths::{basename, dirname, remove_extension};
use self::input::Input;
use regex::Regex;
use self::input::{basename, dirname, remove_extension};
use self::ticket::CommandTicket;
use self::token::Token;
pub use self::job::job;
/// Signifies that a placeholder token was found
const PLACE: u8 = 1;
/// Signifies that the '{' character was found.
const OPEN: u8 = 2;
/// Contains a collection of `Token`'s that are utilized to generate command strings.
/// Contains a collection of `TokenizedArgument`s that are utilized to generate command strings.
///
/// The tokens are a represntation of the supplied command template, and are meant to be coupled
/// with an input in order to generate a command. The `generate()` method will be used to
/// generate a command and obtain a ticket for executing that command.
/// The arguments are a representation of the supplied command template, and are meant to be coupled
/// with an input in order to generate a command. The `generate()` method will be used to generate
/// a command and obtain a ticket for executing that command.
#[derive(Debug, Clone, PartialEq)]
pub struct TokenizedCommand {
pub tokens: Vec<Token>,
args: Vec<TokenizedArgument>,
}
/// Represents a single command argument.
///
/// The argument is either a collection of `Token`s including at least one placeholder variant,
/// or a fixed text.
#[derive(Clone, Debug, PartialEq)]
enum TokenizedArgument {
Tokens(Vec<Token>),
Text(String),
}
impl TokenizedArgument {
pub fn generate<'a>(&'a self, path: &str) -> Cow<'a, str> {
use self::Token::*;
match *self {
TokenizedArgument::Tokens(ref tokens) => {
let mut s = String::new();
for token in tokens {
match *token {
Basename => s += basename(path),
BasenameNoExt => s += remove_extension(basename(path)),
NoExt => s += remove_extension(path),
Parent => s += dirname(path),
Placeholder => s += path,
Text(ref string) => s += string,
}
}
Cow::Owned(s)
}
TokenizedArgument::Text(ref text) => Cow::Borrowed(text),
}
}
}
impl TokenizedCommand {
pub fn new(input: &str) -> TokenizedCommand {
let mut tokens = Vec::new();
let mut start = 0;
let mut flags = 0;
let mut chars = input.char_indices();
let mut text = String::new();
while let Some((id, character)) = chars.next() {
match character {
// Backslashes are useful in cases where we want to use the '{' character
// without having all occurrences of it to collect placeholder tokens.
'\\' => {
if let Some((_, nchar)) = chars.next() {
if nchar != '{' {
text.push(character);
}
text.push(nchar);
}
}
// When a raw '{' is discovered, we will note it's position, and use that for a
// later comparison against valid placeholder tokens.
'{' if flags & OPEN == 0 => {
flags |= OPEN;
start = id;
if !text.is_empty() {
append(&mut tokens, &text);
text.clear();
}
}
// If the `OPEN` bit is set, we will compare the contents between the discovered
// '{' and '}' characters against a list of valid tokens, then pushing the
// corresponding token onto the `tokens` vector.
'}' if flags & OPEN != 0 => {
flags ^= OPEN;
match &input[start + 1..id] {
"" => tokens.push(Token::Placeholder),
"." => tokens.push(Token::NoExt),
"/" => tokens.push(Token::Basename),
"//" => tokens.push(Token::Parent),
"/." => tokens.push(Token::BasenameNoExt),
_ => {
append(&mut tokens, &input[start..id + 1]);
continue;
}
}
flags |= PLACE;
}
// We aren't collecting characters for a text string if the `OPEN` bit is set.
_ if flags & OPEN != 0 => (),
// Push the character onto the text buffer
_ => text.push(character),
}
pub fn new<I, S>(input: I) -> TokenizedCommand
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
lazy_static! {
static ref PLACEHOLDER: Regex = Regex::new(r"\{(/?\.?|//)\}").unwrap();
}
// Take care of any stragglers left behind.
if !text.is_empty() {
append(&mut tokens, &text);
let mut args = Vec::new();
let mut has_placeholder = false;
for arg in input {
let arg = arg.as_ref();
let mut tokens = Vec::new();
let mut start = 0;
for placeholder in PLACEHOLDER.find_iter(arg) {
// Leading text before the placeholder.
if placeholder.start() > start {
tokens.push(Token::Text(arg[start..placeholder.start()].to_owned()));
}
start = placeholder.end();
match placeholder.as_str() {
"{}" => tokens.push(Token::Placeholder),
"{.}" => tokens.push(Token::NoExt),
"{/}" => tokens.push(Token::Basename),
"{//}" => tokens.push(Token::Parent),
"{/.}" => tokens.push(Token::BasenameNoExt),
_ => panic!("Unhandled placeholder"),
}
has_placeholder = true;
}
// Without a placeholder, the argument is just fixed text.
if tokens.is_empty() {
args.push(TokenizedArgument::Text(arg.to_owned()));
continue;
}
if start < arg.len() {
// Trailing text after last placeholder.
tokens.push(Token::Text(arg[start..].to_owned()));
}
args.push(TokenizedArgument::Tokens(tokens));
}
// If a placeholder token was not supplied, append one at the end of the command.
if flags & PLACE == 0 {
append(&mut tokens, " ");
tokens.push(Token::Placeholder)
if !has_placeholder {
args.push(TokenizedArgument::Tokens(vec![Token::Placeholder]));
}
TokenizedCommand { tokens: tokens }
TokenizedCommand { args: args }
}
/// Generates a ticket that is required to execute the generated command.
///
/// Using the internal `tokens` field, and a supplied `input` variable, commands will be
/// written into the `command` buffer. Once all tokens have been processed, the mutable
/// reference of the `command` will be wrapped within a `CommandTicket`, which will be
/// responsible for executing the command and clearing the buffer.
pub fn generate<'a>(
&self,
command: &'a mut String,
input: &Path,
out_perm: Arc<Mutex<()>>,
) -> CommandTicket<'a> {
use self::Token::*;
let input = input.strip_prefix(".").unwrap_or(input).to_string_lossy();
for token in &self.tokens {
match *token {
Basename => *command += &Input::new(&input).basename().get(),
BasenameNoExt => {
*command += &Input::new(&input).basename().remove_extension().get()
}
NoExt => *command += &Input::new(&input).remove_extension().get(),
Parent => *command += &Input::new(&input).dirname().get(),
Placeholder => *command += &Input::new(&input).get(),
Text(ref string) => *command += string,
}
/// Using the internal `args` field, and a supplied `input` variable, arguments will be
/// collected in a Vec. Once all arguments have been processed, the Vec will be wrapped
/// within a `CommandTicket`, which will be responsible for executing the command.
pub fn generate(&self, input: &Path, out_perm: Arc<Mutex<()>>) -> CommandTicket {
let input = input
.strip_prefix(".")
.unwrap_or(input)
.to_string_lossy()
.into_owned();
let mut args = Vec::with_capacity(self.args.len());
for arg in &self.args {
args.push(arg.generate(&input));
}
CommandTicket::new(command, out_perm)
}
}
/// If the last token is a text token, append to that token. Otherwise, create a new token.
fn append(tokens: &mut Vec<Token>, elem: &str) {
// Useful to avoid a borrowing issue with the tokens vector.
let mut append_text = false;
// If the last token is a `Text` token, simply the `elem` at the end.
match tokens.last_mut() {
Some(&mut Token::Text(ref mut string)) => *string += elem,
_ => append_text = true,
};
// Otherwise, we will need to add a new `Text` token that contains the `elem`
if append_text {
tokens.push(Token::Text(String::from(elem)));
CommandTicket::new(args, out_perm)
}
}
#[cfg(test)]
mod tests {
use super::{TokenizedCommand, Token};
use super::{TokenizedCommand, TokenizedArgument, Token};
#[test]
fn tokens() {
let expected = TokenizedCommand {
tokens: vec![Token::Text("echo ${SHELL}: ".into()), Token::Placeholder],
args: vec![
TokenizedArgument::Text("echo".into()),
TokenizedArgument::Text("${SHELL}:".into()),
TokenizedArgument::Tokens(vec![Token::Placeholder]),
],
};
assert_eq!(TokenizedCommand::new("echo $\\{SHELL}: {}"), expected);
assert_eq!(TokenizedCommand::new("echo ${SHELL}:"), expected);
assert_eq!(TokenizedCommand::new(&[&"echo", &"${SHELL}:"]), expected);
assert_eq!(
TokenizedCommand::new("echo {.}"),
TokenizedCommand { tokens: vec![Token::Text("echo ".into()), Token::NoExt] }
TokenizedCommand::new(&["echo", "{.}"]),
TokenizedCommand {
args: vec![
TokenizedArgument::Text("echo".into()),
TokenizedArgument::Tokens(vec![Token::NoExt]),
],
}
);
}
}

View File

@ -6,47 +6,39 @@
// notice may not be copied, modified, or distributed except
// according to those terms.
use std::env;
use std::process::Command;
use std::sync::{Arc, Mutex};
use std::io;
lazy_static! {
/// On non-Windows systems, the `SHELL` environment variable will be used to determine the
/// preferred shell of choice for execution. Windows will simply use `cmd`.
static ref COMMAND: (String, &'static str) = if cfg!(target_os = "windows") {
("cmd".into(), "/C")
} else {
(env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into()), "-c")
};
}
/// A state that offers access to executing a generated command.
///
/// The ticket holds a mutable reference to a string that contains the command to be executed.
/// After execution of the the command via the `then_execute()` method, the string will be
/// cleared so that a new command can be written to the string in the future.
pub struct CommandTicket<'a> {
command: &'a mut String,
/// The ticket holds the collection of arguments of a command to be executed.
pub struct CommandTicket {
args: Vec<String>,
out_perm: Arc<Mutex<()>>,
}
impl<'a> CommandTicket<'a> {
pub fn new(command: &'a mut String, out_perm: Arc<Mutex<()>>) -> CommandTicket<'a> {
CommandTicket { command, out_perm }
impl CommandTicket {
pub fn new<I, S>(args: I, out_perm: Arc<Mutex<()>>) -> CommandTicket
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
CommandTicket {
args: args.into_iter().map(|x| x.as_ref().to_owned()).collect(),
out_perm: out_perm,
}
}
/// Executes the command stored within the ticket, and
/// clearing the command's buffer when finished.'
/// Executes the command stored within the ticket.
#[cfg(not(unix))]
pub fn then_execute(self) {
use std::process::Stdio;
use std::io::Write;
// Spawn a shell with the supplied command.
let cmd = Command::new(COMMAND.0.as_str())
.arg(COMMAND.1)
.arg(&self.command)
// Spawn the supplied command.
let cmd = Command::new(&self.args[0])
.args(&self.args[1..])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output();
@ -66,9 +58,6 @@ impl<'a> CommandTicket<'a> {
}
Err(why) => eprintln!("fd: exec error: {}", why),
}
// Clear the buffer for later re-use.
self.command.clear();
}
#[cfg(all(unix))]
@ -88,10 +77,9 @@ impl<'a> CommandTicket<'a> {
pipe(stderr_fds.as_mut_ptr());
}
// Spawn a shell with the supplied command.
let cmd = Command::new(COMMAND.0.as_str())
.arg(COMMAND.1)
.arg(&self.command)
// Spawn the supplied command.
let cmd = Command::new(&self.args[0])
.args(&self.args[1..])
// Configure the pipes accordingly in the child.
.before_exec(move || unsafe {
// Redirect the child's std{out,err} to the write ends of our pipe.
@ -134,8 +122,5 @@ impl<'a> CommandTicket<'a> {
}
Err(why) => eprintln!("fd: exec error: {}", why),
}
// Clear the command string's buffer for later re-use.
self.command.clear();
}
}

View File

@ -18,7 +18,6 @@ extern crate libc;
extern crate num_cpus;
extern crate regex;
extern crate regex_syntax;
extern crate shell_escape;
pub mod fshelper;
pub mod lscolors;
@ -106,7 +105,7 @@ fn main() {
None
};
let command = matches.value_of("exec").map(|x| TokenizedCommand::new(x));
let command = matches.values_of("exec").map(TokenizedCommand::new);
let config = FdOptions {
case_sensitive,