use anyhow::{bail, Context, Result};
use crate::app::Status;
use crate::common::{get_clipboard, path_to_string};
use crate::modes::Quote;
use crate::{log_info, log_line};
pub const SAME_WINDOW_TOKEN: &str = "%t";
pub fn shell_command_parser(command: &str, status: &Status) -> Result<Vec<String>> {
let Ok(tokens) = Lexer::new(command).lexer() else {
return shell_command_parser_error("Syntax error in the command", command);
};
let Ok(args) = Parser::new(tokens).parse(status) else {
return shell_command_parser_error("Couldn't parse the command", command);
};
build_args(args)
}
fn shell_command_parser_error(message: &str, command: &str) -> Result<Vec<String>> {
log_info!("{message} {command}");
log_line!("{message} {command}");
bail!("{message} {command}");
}
#[derive(Debug)]
enum Token {
Identifier(String),
StringLiteral((char, String)),
FmExpansion(FmExpansion),
}
#[derive(Debug)]
enum FmExpansion {
Selected,
SelectedFilename,
SelectedPath,
Extension,
Flagged,
Term,
Clipboard,
Invalid,
}
impl FmExpansion {
fn from(c: char) -> Self {
match c {
's' => Self::Selected,
'n' => Self::SelectedFilename,
'd' => Self::SelectedPath,
'e' => Self::Extension,
'f' => Self::Flagged,
't' => Self::Term,
'c' => Self::Clipboard,
_ => Self::Invalid,
}
}
fn parse(&self, status: &Status) -> Result<Vec<String>> {
match self {
Self::Invalid => bail!("Invalid Fm Expansion"),
Self::Term => Self::term(status),
Self::Selected => Self::selected(status),
Self::Flagged => Self::flagged(status),
Self::SelectedPath => Self::path(status),
Self::SelectedFilename => Self::filename(status),
Self::Clipboard => Self::clipboard(),
Self::Extension => Self::extension(status),
}
}
fn selected(status: &Status) -> Result<Vec<String>> {
Ok(vec![status.current_tab().current_file_string()?.quote()?])
}
fn path(status: &Status) -> Result<Vec<String>> {
Ok(vec![status.current_tab().directory_str().quote()?])
}
fn filename(status: &Status) -> Result<Vec<String>> {
Ok(vec![status
.current_tab()
.selected_path()
.context("No selected file")?
.file_name()
.context("No filename")?
.quote()?])
}
fn extension(status: &Status) -> Result<Vec<String>> {
Ok(vec![status
.current_tab()
.selected_path()
.context("No selected file")?
.extension()
.context("No extension")?
.quote()?])
}
fn flagged(status: &Status) -> Result<Vec<String>> {
Ok(status
.menu
.flagged
.content
.iter()
.map(path_to_string)
.filter_map(|s| s.quote().ok())
.collect())
}
fn term(_status: &Status) -> Result<Vec<String>> {
Ok(vec![SAME_WINDOW_TOKEN.to_owned()])
}
fn clipboard() -> Result<Vec<String>> {
let Some(clipboard) = get_clipboard() else {
bail!("Couldn't read the clipboard");
};
Ok(clipboard.split_whitespace().map(|s| s.to_owned()).collect())
}
}
enum State {
Start,
Arg,
StringLiteral(char),
FmExpansion,
}
struct Lexer {
command: String,
}
impl Lexer {
fn new(command: &str) -> Self {
Self {
command: command.trim().to_owned(),
}
}
fn lexer(&self) -> Result<Vec<Token>> {
let mut tokens = vec![];
let mut state = State::Start;
let mut current = String::new();
for c in self.command.chars() {
match &state {
State::Start => {
if c == '"' || c == '\'' {
state = State::StringLiteral(c);
} else if c == '%' {
state = State::FmExpansion;
} else {
state = State::Arg;
current.push(c);
}
}
State::Arg => {
if c == '%' {
tokens.push(Token::Identifier(current.clone()));
current.clear();
state = State::FmExpansion;
} else if c == '"' || c == '\'' {
tokens.push(Token::Identifier(current.clone()));
current.clear();
state = State::StringLiteral(c);
} else {
current.push(c);
}
}
State::StringLiteral(quote_type) => {
if c == *quote_type {
tokens.push(Token::StringLiteral((c, current.clone())));
current.clear();
state = State::Start;
} else {
current.push(c);
}
}
State::FmExpansion => {
if c.is_alphanumeric() {
let expansion = FmExpansion::from(c);
if let FmExpansion::Invalid = expansion {
bail!("Invalid FmExpansion %{c}")
}
if let FmExpansion::Term = expansion {
if !tokens.is_empty() {
bail!("Term expansion can only be the first argument")
}
}
tokens.push(Token::FmExpansion(expansion));
current.clear();
state = State::Start;
} else {
bail!("Invalid FmExpansion %{c}.")
}
}
}
}
match &state {
State::Arg => tokens.push(Token::Identifier(current)),
State::StringLiteral(quote) => tokens.push(Token::StringLiteral((*quote,current))),
State::FmExpansion => bail!("Invalid syntax for {command}. Matching an FmExpansion with {current} which is impossible.", command=self.command),
State::Start => (),
}
Ok(tokens)
}
}
struct Parser {
tokens: Vec<Token>,
}
impl Parser {
fn new(tokens: Vec<Token>) -> Self {
Self { tokens }
}
fn parse(&self, status: &Status) -> Result<Vec<String>> {
if self.tokens.is_empty() {
bail!("Empty tokens")
}
let mut args: Vec<String> = vec![];
for token in self.tokens.iter() {
match token {
Token::Identifier(identifier) => args.push(identifier.to_owned()),
Token::FmExpansion(fm_expansion) => {
let Ok(mut expansion) = fm_expansion.parse(status) else {
log_line!("Invalid expansion {fm_expansion:?}");
log_info!("Invalid expansion {fm_expansion:?}");
bail!("Invalid expansion {fm_expansion:?}")
};
args.append(&mut expansion)
}
Token::StringLiteral((quote, string)) => {
args.push(format!("{quote}{string}{quote}"))
}
};
}
Ok(args)
}
}
fn build_args(args: Vec<String>) -> Result<Vec<String>> {
log_info!("build_args {args:?}");
if args.is_empty() {
bail!("Empty command");
}
if args[0].starts_with("sudo") {
Ok(build_sudo_args(args))
} else if args[0].starts_with(SAME_WINDOW_TOKEN) {
Ok(args)
} else {
Ok(build_normal_args(args))
}
}
fn build_sudo_args(args: Vec<String>) -> Vec<String> {
let rebuild = args.join("");
rebuild.split_whitespace().map(|s| s.to_owned()).collect()
}
fn build_normal_args(args: Vec<String>) -> Vec<String> {
vec!["sh".to_owned(), "-c".to_owned(), args.join("")]
}