2017-10-14 18:04:11 +02:00
|
|
|
// TODO: Possible optimization could avoid pushing characters on a buffer.
|
|
|
|
mod command;
|
|
|
|
mod job;
|
|
|
|
mod paths;
|
|
|
|
|
|
|
|
use std::fmt::{self, Display, Formatter};
|
|
|
|
use std::path::Path;
|
|
|
|
|
|
|
|
use self::paths::{basename, dirname, remove_extension};
|
|
|
|
use self::command::CommandTicket;
|
|
|
|
pub use self::job::job;
|
|
|
|
|
|
|
|
/// Designates what should be written to a buffer
|
|
|
|
///
|
|
|
|
/// Each `Token` contains either text, or a placeholder variant, which will be used to generate
|
|
|
|
/// commands after all tokens for a given command template have been collected.
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
|
|
pub enum Token {
|
|
|
|
Placeholder,
|
|
|
|
Basename,
|
|
|
|
Parent,
|
|
|
|
NoExt,
|
|
|
|
BasenameNoExt,
|
|
|
|
Text(String),
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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.
|
|
|
|
///
|
|
|
|
/// 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.
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
|
|
pub struct TokenizedCommand {
|
|
|
|
pub tokens: Vec<Token>,
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2017-10-14 19:14:18 +02:00
|
|
|
},
|
2017-10-14 18:04:11 +02:00
|
|
|
// 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();
|
|
|
|
}
|
2017-10-14 19:14:18 +02:00
|
|
|
},
|
2017-10-14 18:04:11 +02:00
|
|
|
// 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] {
|
2017-10-14 19:14:18 +02:00
|
|
|
"" => tokens.push(Token::Placeholder),
|
2017-10-14 18:04:11 +02:00
|
|
|
"." => tokens.push(Token::NoExt),
|
|
|
|
"/" => tokens.push(Token::Basename),
|
|
|
|
"//" => tokens.push(Token::Parent),
|
|
|
|
"/." => tokens.push(Token::BasenameNoExt),
|
2017-10-14 19:14:18 +02:00
|
|
|
_ => {
|
|
|
|
append(&mut tokens, &input[start..id+1]);
|
|
|
|
continue
|
|
|
|
},
|
2017-10-14 18:04:11 +02:00
|
|
|
}
|
2017-10-14 19:14:18 +02:00
|
|
|
flags |= PLACE;
|
|
|
|
},
|
2017-10-14 18:04:11 +02:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Take care of any stragglers left behind.
|
|
|
|
if !text.is_empty() { append(&mut tokens, &text); }
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2017-10-14 19:14:18 +02:00
|
|
|
TokenizedCommand { tokens: tokens }
|
2017-10-14 18:04:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/// 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) -> CommandTicket<'a> {
|
|
|
|
for token in &self.tokens {
|
|
|
|
match *token {
|
|
|
|
Token::Basename => *command += basename(&input.to_string_lossy()),
|
|
|
|
Token::BasenameNoExt => *command += remove_extension(basename(&input.to_string_lossy())),
|
|
|
|
Token::NoExt => *command += remove_extension(&input.to_string_lossy()),
|
|
|
|
Token::Parent => *command += dirname(&input.to_string_lossy()),
|
|
|
|
Token::Placeholder => *command += &input.to_string_lossy(),
|
|
|
|
Token::Text(ref string) => *command += string,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
CommandTicket::new(command)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Display for TokenizedCommand {
|
|
|
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
|
|
for token in &self.tokens {
|
|
|
|
match *token {
|
|
|
|
Token::Placeholder => f.write_str("{}")?,
|
|
|
|
Token::Basename => f.write_str("{/}")?,
|
|
|
|
Token::Parent => f.write_str("{//}")?,
|
|
|
|
Token::NoExt => f.write_str("{.}")?,
|
|
|
|
Token::BasenameNoExt => f.write_str("{/.}")?,
|
|
|
|
Token::Text(ref string) => f.write_str(string)?,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::{TokenizedCommand, Token};
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn tokens() {
|
|
|
|
let expected = TokenizedCommand {
|
|
|
|
tokens: vec![
|
|
|
|
Token::Text("echo ${SHELL}: ".into()),
|
|
|
|
Token::Placeholder,
|
|
|
|
],
|
|
|
|
};
|
|
|
|
|
|
|
|
assert_eq!(TokenizedCommand::new("echo $\\{SHELL}: {}"), expected);
|
|
|
|
assert_eq!(TokenizedCommand::new("echo ${SHELL}:"), expected);
|
2017-10-14 19:14:18 +02:00
|
|
|
assert_eq!(TokenizedCommand::new("echo {.}"), TokenizedCommand {
|
|
|
|
tokens: vec![
|
|
|
|
Token::Text("echo ".into()),
|
|
|
|
Token::NoExt,
|
|
|
|
],
|
|
|
|
});
|
2017-10-14 18:04:11 +02:00
|
|
|
}
|
|
|
|
}
|