Compare commits
2 Commits
fix-window
...
parse-bash
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ff8521612 | ||
|
|
ca22d5d4a3 |
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -12706,6 +12706,13 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||
|
||||
[[package]]
|
||||
name = "shell_parser"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellexpand"
|
||||
version = "2.1.2"
|
||||
|
||||
@@ -131,6 +131,7 @@ members = [
|
||||
"crates/session",
|
||||
"crates/settings",
|
||||
"crates/settings_ui",
|
||||
"crates/shell_parser",
|
||||
"crates/snippet",
|
||||
"crates/snippet_provider",
|
||||
"crates/snippets_ui",
|
||||
|
||||
16
crates/shell_parser/Cargo.toml
Normal file
16
crates/shell_parser/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "shell_parser"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/shell_parser.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
shlex.workspace = true
|
||||
1
crates/shell_parser/LICENSE
Symbolic link
1
crates/shell_parser/LICENSE
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../LICENSE
|
||||
125
crates/shell_parser/src/shell_ast.rs
Normal file
125
crates/shell_parser/src/shell_ast.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ShellAst {
|
||||
Cmd(ShellCmd),
|
||||
Op {
|
||||
operator: Operator,
|
||||
left: Box<ShellAst>,
|
||||
right: Box<ShellAst>,
|
||||
},
|
||||
/// Prints the given string to stdout
|
||||
Echo(String),
|
||||
/// Reads delimited elems from stdin and passes them to the
|
||||
/// given command.
|
||||
/// See https://www.man7.org/linux/man-pages/man1/xargs.1.html
|
||||
Xargs(XargsOptions),
|
||||
}
|
||||
|
||||
pub struct XargsOptions {
|
||||
/// Default is "\n\n"
|
||||
pub delimiter: String,
|
||||
/// Quotes include both single quotes and double quotes
|
||||
pub escape_quotes_and_backslashes: bool,
|
||||
}
|
||||
|
||||
impl XargsOptions {
|
||||
fn from_args<S: AsRef<str>>(args: impl IntoIterator<Item = S>) -> Option<Self> {
|
||||
let mut args = args.into_iter();
|
||||
|
||||
for arg in args {
|
||||
match arg.as_ref() {
|
||||
"-0" | "-null" => {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct ShellCmd {
|
||||
pub command: VarString,
|
||||
pub args: Vec<VarString>,
|
||||
pub stdout_redirect: Option<VarString>,
|
||||
pub stderr_redirect: Option<VarString>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Operator {
|
||||
/// The `|` shell operator (highest precedence)
|
||||
Pipe,
|
||||
/// The `&&` shell operator (medium precedence)
|
||||
And,
|
||||
/// The `;` shell operator (lowest precedence)
|
||||
Semicolon,
|
||||
}
|
||||
|
||||
impl Operator {
|
||||
pub(crate) fn precedence(&self) -> u8 {
|
||||
match self {
|
||||
Operator::Pipe => 3,
|
||||
Operator::And => 2,
|
||||
Operator::Semicolon => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A string, possibly with shell variable substitutions
|
||||
/// in it (e.g. "foo${bar}").
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum VarString {
|
||||
Plaintext(String),
|
||||
Vars {
|
||||
prefix: String,
|
||||
vars: Vec<(String, String)>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<T: Into<String>> From<T> for VarString {
|
||||
fn from(plaintext: T) -> Self {
|
||||
Self::Plaintext(plaintext.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VarString {
|
||||
fn default() -> Self {
|
||||
Self::Plaintext(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl VarString {
|
||||
/// If there is a syntax error, like an unclosed '{' or '$' at the end of the string,
|
||||
/// return Err with the original (owned) token in it.
|
||||
pub fn from_token(token: String) -> Result<Self, String> {
|
||||
let todo = todo!(); // TODO split it up etc.
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Self::Plaintext(string) => string.is_empty(),
|
||||
Self::Vars { prefix, vars } => prefix.is_empty() && vars.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// If the VarString contains a var that lookup_var returns None for,
|
||||
/// return Err with that var name.
|
||||
pub fn resolve<'a>(
|
||||
&'a self,
|
||||
lookup_var: impl Fn(&str) -> Option<&str>,
|
||||
) -> Result<Cow<'a, str>, &'a str> {
|
||||
match self {
|
||||
Self::Plaintext(string) => Ok(Cow::Borrowed(string)),
|
||||
Self::Vars { prefix, vars } => {
|
||||
let mut answer = prefix.to_string();
|
||||
|
||||
for (var, suffix) in vars {
|
||||
answer.push_str(lookup_var(&var).ok_or(var.as_str())?);
|
||||
answer.push_str(&suffix);
|
||||
}
|
||||
|
||||
Ok(Cow::Owned(answer))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
730
crates/shell_parser/src/shell_parser.rs
Normal file
730
crates/shell_parser/src/shell_parser.rs
Normal file
@@ -0,0 +1,730 @@
|
||||
/// Models can request tool calls of bash one-liner commands to run.
|
||||
///
|
||||
/// Instead of giving those command strings to actual `bash`, which
|
||||
/// won't exist on Windows and could bypass the user's security approvals
|
||||
/// (e.g. a shell command of `something-approved; something-not-approved`),
|
||||
/// we parse the bash command ourselves and translate it into a sequence of
|
||||
/// cross-platform Rust commands.
|
||||
///
|
||||
/// We support the operators `|`, `&&`, `;`, `>`, `1>`, `2>`, `&>`, `>&`,
|
||||
/// variables, and that's it. The operators currently have to have whitespace
|
||||
/// around them because the `shlex` crate we use to tokenize the strings does
|
||||
/// not treat operators as word boundaries, even though shells do. Fortunately,
|
||||
/// models consistenlty generate spaces around these operators anyway.
|
||||
///
|
||||
/// If we can't parse the command string, or if it uses shell features we
|
||||
/// don't support, the tool can fall back on prompting the user for whether
|
||||
/// they want to run the exact bash command (which may fail on Windows, and
|
||||
/// which may also fail on other systems if it's trying to do backgrounding
|
||||
/// or something like that.)
|
||||
mod shell_ast;
|
||||
|
||||
pub use shell_ast::*;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Error {
|
||||
/// Backgrounding (`&`) is not supported
|
||||
UsesBackgrounding,
|
||||
/// Redirects (e.g. `>` and `2>` must be followed by a target)
|
||||
RedirectWithoutTarget,
|
||||
/// Some command is mandatory; you can't run e.g. `;` on its own.
|
||||
MissingCommand,
|
||||
|
||||
InvalidVar(String),
|
||||
}
|
||||
|
||||
struct ShellParser<'a> {
|
||||
lexer: shlex::Shlex<'a>,
|
||||
current_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Redirect {
|
||||
Stdout,
|
||||
Stderr,
|
||||
Both,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Default)]
|
||||
pub struct EnvVars {
|
||||
pub kv_pairs: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl<'a> ShellParser<'a> {
|
||||
/// Parse a shell string and build an abstract syntax tree.
|
||||
pub fn parse(string: impl AsRef<str>) -> Result<(EnvVars, ShellAst), Error> {
|
||||
let mut parser = ShellParser::new(string.as_ref());
|
||||
|
||||
let env_vars = parser.parse_env_vars()?;
|
||||
let ast = parser.parse_expression(0)?;
|
||||
|
||||
Ok((env_vars, ast))
|
||||
}
|
||||
|
||||
fn new(input: &'a str) -> Self {
|
||||
let mut lexer = shlex::Shlex::new(input);
|
||||
let current_token = lexer.next();
|
||||
|
||||
Self {
|
||||
lexer,
|
||||
current_token,
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
self.current_token = self.lexer.next();
|
||||
}
|
||||
|
||||
fn peek(&self) -> Option<&str> {
|
||||
self.current_token.as_deref()
|
||||
}
|
||||
|
||||
fn parse_env_vars(&mut self) -> Result<EnvVars, Error> {
|
||||
let mut answer = Vec::new();
|
||||
|
||||
while let Some(token) = self.peek() {
|
||||
if let Some(pos) = token.find('=') {
|
||||
// Bash doesn't allow variables in env var pairs, so this must be something else.
|
||||
if token.contains('$') {
|
||||
break;
|
||||
}
|
||||
|
||||
// Note that `x=y=5` is totally valid in bash;
|
||||
// it sets the env var `x` to be the string "y=5"
|
||||
answer.push((token[..pos].to_string(), token[pos + 1..].to_string()));
|
||||
self.advance();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(EnvVars { kv_pairs: answer })
|
||||
}
|
||||
|
||||
fn parse_expression(&mut self, min_precedence: u8) -> Result<ShellAst, Error> {
|
||||
// Parse the first command or atom
|
||||
let mut left = ShellAst::Cmd(self.parse_command()?);
|
||||
|
||||
// While we have operators with sufficient precedence, keep building the tree
|
||||
loop {
|
||||
let op = match self.parse_operator() {
|
||||
Some(op) if op.precedence() >= min_precedence => op,
|
||||
_ => break,
|
||||
};
|
||||
|
||||
// Consume the operator token
|
||||
self.advance();
|
||||
|
||||
// Special case for trailing semicolons - if we have no more tokens,
|
||||
// we don't need to parse another command
|
||||
if op == Operator::Semicolon && self.peek().is_none() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Parse the right side with higher precedence
|
||||
// For left-associative operators, we use op.precedence() + 1
|
||||
let right = self.parse_expression(op.precedence() + 1)?;
|
||||
|
||||
// Build the operation node
|
||||
left = ShellAst::Op {
|
||||
operator: op,
|
||||
left: Box::new(left),
|
||||
right: Box::new(right),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(left)
|
||||
}
|
||||
|
||||
fn parse_operator(&self) -> Option<Operator> {
|
||||
match self.peek()? {
|
||||
"|" => Some(Operator::Pipe),
|
||||
"&&" => Some(Operator::And),
|
||||
";" => Some(Operator::Semicolon),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_redirection(&mut self, cmd: &mut ShellCmd, redirect: Redirect) -> Result<(), Error> {
|
||||
self.advance(); // consume the redirection operator
|
||||
|
||||
let target = self.peek().ok_or_else(|| Error::RedirectWithoutTarget)?;
|
||||
|
||||
match redirect {
|
||||
Redirect::Stdout => {
|
||||
cmd.stdout_redirect = Some(target.to_string());
|
||||
}
|
||||
Redirect::Stderr => {
|
||||
cmd.stderr_redirect = Some(target.to_string());
|
||||
}
|
||||
Redirect::Both => {
|
||||
cmd.stdout_redirect = Some(target.to_string());
|
||||
cmd.stderr_redirect = Some(target.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
self.advance(); // consume the target
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_command(&mut self) -> Result<ShellCmd, Error> {
|
||||
let mut cmd = ShellCmd::default();
|
||||
|
||||
// Process tokens until we hit an operator or end of input
|
||||
loop {
|
||||
let redirect;
|
||||
|
||||
match self.peek() {
|
||||
Some(token) => {
|
||||
match token {
|
||||
"|" | "&&" | ";" => break, // These are operators, not part of the command
|
||||
">" | "1>" => {
|
||||
redirect = Some(Redirect::Stdout);
|
||||
}
|
||||
"2>" => {
|
||||
redirect = Some(Redirect::Stderr);
|
||||
}
|
||||
"&>" | ">&" => {
|
||||
redirect = Some(Redirect::Both);
|
||||
}
|
||||
"&" => {
|
||||
// Reject ampersand as it's used for backgrounding processes
|
||||
return Err(Error::UsesBackgrounding);
|
||||
}
|
||||
_ => {
|
||||
redirect = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
break; // We ran out of tokens; exit the loop.
|
||||
}
|
||||
}
|
||||
|
||||
// We do this separate conditional after the borrow from the peek()
|
||||
// has expired, to avoid a borrow checker error.
|
||||
match redirect {
|
||||
Some(redirect) => {
|
||||
self.handle_redirection(&mut cmd, redirect)?;
|
||||
}
|
||||
None => {
|
||||
// It's either the command name or an argument
|
||||
let mut token = self.current_token.take().unwrap();
|
||||
self.advance();
|
||||
|
||||
// Handle trailing semicolon(s)
|
||||
let original_token_len = token.len();
|
||||
while token.ends_with(';') {
|
||||
token.pop();
|
||||
}
|
||||
|
||||
let had_semicolon = token.len() != original_token_len;
|
||||
let var_string = VarString::from_token(token).map_err(Error::InvalidVar)?;
|
||||
|
||||
if cmd.command.is_empty() {
|
||||
cmd.command = var_string;
|
||||
} else {
|
||||
cmd.args.push(var_string);
|
||||
}
|
||||
|
||||
if had_semicolon {
|
||||
// Put the semicolon back as the next token,
|
||||
// so that after we break we end up parsing it.
|
||||
self.current_token = Some(";".to_string());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.command.is_empty() {
|
||||
return Err(Error::MissingCommand);
|
||||
}
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
|
||||
// #[test]
|
||||
// fn test_simple_command() {
|
||||
// // Basic command with no args or operators
|
||||
// let cmd = "ls";
|
||||
// let (vars, ast) = ShellParser::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
// assert_eq!(vars, EnvVars::default());
|
||||
|
||||
// if let ShellAst::Cmd(shell_cmd) = ast {
|
||||
// assert_eq!(shell_cmd.command, "ls".into());
|
||||
// assert!(shell_cmd.args.is_empty());
|
||||
// assert_eq!(shell_cmd.stdout_redirect, None);
|
||||
// assert_eq!(shell_cmd.stderr_redirect, None);
|
||||
// } else {
|
||||
// panic!("Expected Command node");
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_command_with_args() {
|
||||
// // Command with arguments
|
||||
// let cmd = "ls -la /home";
|
||||
// let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
// if let ShellAst::Cmd(shell_cmd) = ast {
|
||||
// assert_eq!(shell_cmd.command, "ls");
|
||||
// assert_eq!(shell_cmd.args, vec!["-la".to_string(), "/home".to_string()]);
|
||||
// assert_eq!(shell_cmd.stdout_redirect, None);
|
||||
// assert_eq!(shell_cmd.stderr_redirect, None);
|
||||
// } else {
|
||||
// panic!("Expected Command node");
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_simple_pipe() {
|
||||
// // Test pipe operator
|
||||
// let cmd = "ls -l | grep txt";
|
||||
// let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
// if let ShellAst::Op {
|
||||
// operator,
|
||||
// left,
|
||||
// right,
|
||||
// } = ast
|
||||
// {
|
||||
// assert_eq!(operator, Operator::Pipe);
|
||||
|
||||
// if let ShellAst::Cmd(left_cmd) = *left {
|
||||
// assert_eq!(left_cmd.command, "ls");
|
||||
// assert_eq!(left_cmd.args, vec!["-l".to_string()]);
|
||||
// } else {
|
||||
// panic!("Expected Command node for left side");
|
||||
// }
|
||||
|
||||
// if let ShellAst::Cmd(right_cmd) = *right {
|
||||
// assert_eq!(right_cmd.command, "grep");
|
||||
// assert_eq!(right_cmd.args, vec!["txt".to_string()]);
|
||||
// } else {
|
||||
// panic!("Expected Command node for right side");
|
||||
// }
|
||||
// } else {
|
||||
// panic!("Expected Operation node");
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_simple_and() {
|
||||
// // Test && operator
|
||||
// let cmd = "mkdir test && cd test";
|
||||
// let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
// if let ShellAst::Op {
|
||||
// operator,
|
||||
// left,
|
||||
// right,
|
||||
// } = ast
|
||||
// {
|
||||
// assert_eq!(operator, Operator::And);
|
||||
|
||||
// if let ShellAst::Cmd(left_cmd) = *left {
|
||||
// assert_eq!(left_cmd.command, "mkdir");
|
||||
// assert_eq!(left_cmd.args, vec!["test".to_string()]);
|
||||
// } else {
|
||||
// panic!("Expected Command node for left side");
|
||||
// }
|
||||
|
||||
// if let ShellAst::Cmd(right_cmd) = *right {
|
||||
// assert_eq!(right_cmd.command, "cd");
|
||||
// assert_eq!(right_cmd.args, vec!["test".to_string()]);
|
||||
// } else {
|
||||
// panic!("Expected Command node for right side");
|
||||
// }
|
||||
// } else {
|
||||
// panic!("Expected Operation node");
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_complex_chain_with_precedence() {
|
||||
// // Test a more complex chain with different precedence levels
|
||||
// let cmd = "echo hello | grep e && ls -l ; echo done";
|
||||
// let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
// // The tree should be structured with precedence:
|
||||
// // - Pipe has highest precedence
|
||||
// // - Then And
|
||||
// // - Then Semicolon (lowest)
|
||||
|
||||
// if let ShellAst::Op {
|
||||
// operator,
|
||||
// left,
|
||||
// right,
|
||||
// } = &ast
|
||||
// {
|
||||
// assert_eq!(*operator, Operator::Semicolon);
|
||||
|
||||
// if let ShellAst::Op {
|
||||
// operator,
|
||||
// left: inner_left,
|
||||
// right: inner_right,
|
||||
// } = &**left
|
||||
// {
|
||||
// assert_eq!(*operator, Operator::And);
|
||||
|
||||
// if let ShellAst::Op {
|
||||
// operator,
|
||||
// left: pipe_left,
|
||||
// right: pipe_right,
|
||||
// } = &**inner_left
|
||||
// {
|
||||
// assert_eq!(*operator, Operator::Pipe);
|
||||
|
||||
// if let ShellAst::Cmd(cmd) = &**pipe_left {
|
||||
// assert_eq!(cmd.command, "echo");
|
||||
// assert_eq!(cmd.args, vec!["hello".to_string()]);
|
||||
// } else {
|
||||
// panic!("Expected Command node for pipe left branch");
|
||||
// }
|
||||
|
||||
// if let ShellAst::Cmd(cmd) = &**pipe_right {
|
||||
// assert_eq!(cmd.command, "grep");
|
||||
// assert_eq!(cmd.args, vec!["e".to_string()]);
|
||||
// } else {
|
||||
// panic!("Expected Command node for pipe right branch");
|
||||
// }
|
||||
// } else {
|
||||
// panic!("Expected Pipe operation node");
|
||||
// }
|
||||
|
||||
// if let ShellAst::Cmd(cmd) = &**inner_right {
|
||||
// assert_eq!(cmd.command, "ls");
|
||||
// assert_eq!(cmd.args, vec!["-l".to_string()]);
|
||||
// } else {
|
||||
// panic!("Expected Command node for and right branch");
|
||||
// }
|
||||
// } else {
|
||||
// panic!("Expected And operation node");
|
||||
// }
|
||||
|
||||
// if let ShellAst::Cmd(cmd) = &**right {
|
||||
// assert_eq!(cmd.command, "echo");
|
||||
// assert_eq!(cmd.args, vec!["done".to_string()]);
|
||||
// } else {
|
||||
// panic!("Expected Command node for semicolon right branch");
|
||||
// }
|
||||
// } else {
|
||||
// panic!("Expected Semicolon operation node");
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_stdout_redirection() {
|
||||
// // Test stdout redirection
|
||||
// let cmd = "echo hello > output.txt";
|
||||
// let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
// if let ShellAst::Cmd(shell_cmd) = ast {
|
||||
// assert_eq!(shell_cmd.command, "echo");
|
||||
// assert_eq!(shell_cmd.args, vec!["hello".to_string()]);
|
||||
// assert_eq!(shell_cmd.stdout_redirect, Some("output.txt".to_string()));
|
||||
// assert_eq!(shell_cmd.stderr_redirect, None);
|
||||
// } else {
|
||||
// panic!("Expected Command node");
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_stderr_redirection() {
|
||||
// // Test stderr redirection
|
||||
// let cmd = "find / -name test 2> errors.log";
|
||||
// let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
// if let ShellAst::Cmd(shell_cmd) = ast {
|
||||
// assert_eq!(shell_cmd.command, "find");
|
||||
// assert_eq!(
|
||||
// shell_cmd.args,
|
||||
// vec!["/".to_string(), "-name".to_string(), "test".to_string()]
|
||||
// );
|
||||
// assert_eq!(shell_cmd.stdout_redirect, None);
|
||||
// assert_eq!(shell_cmd.stderr_redirect, Some("errors.log".to_string()));
|
||||
// } else {
|
||||
// panic!("Expected Command node");
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_both_redirections() {
|
||||
// // Test both stdout and stderr redirection
|
||||
// let cmd = "make &> build.log";
|
||||
// let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
// if let ShellAst::Cmd(shell_cmd) = ast {
|
||||
// assert_eq!(shell_cmd.command, "make");
|
||||
// assert!(shell_cmd.args.is_empty());
|
||||
// assert_eq!(shell_cmd.stdout_redirect, Some("build.log".to_string()));
|
||||
// assert_eq!(shell_cmd.stderr_redirect, Some("build.log".to_string()));
|
||||
// } else {
|
||||
// panic!("Expected Command node");
|
||||
// }
|
||||
|
||||
// // Test alternative syntax
|
||||
// let cmd = "make >& build.log";
|
||||
// let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
// if let ShellAst::Cmd(shell_cmd) = ast {
|
||||
// assert_eq!(shell_cmd.command, "make");
|
||||
// assert!(shell_cmd.args.is_empty());
|
||||
// assert_eq!(shell_cmd.stdout_redirect, Some("build.log".to_string()));
|
||||
// assert_eq!(shell_cmd.stderr_redirect, Some("build.log".to_string()));
|
||||
// } else {
|
||||
// panic!("Expected Command node");
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_multiple_operators() {
|
||||
// // Test multiple operators in a single command
|
||||
// let cmd =
|
||||
// "find . -name \"*.rs\" | grep impl && echo \"Found implementations\" ; echo \"Done\"";
|
||||
|
||||
// // Verify the AST structure
|
||||
// let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
// if let ShellAst::Op {
|
||||
// operator: semicolon_op,
|
||||
// left: semicolon_left,
|
||||
// right: semicolon_right,
|
||||
// } = ast
|
||||
// {
|
||||
// assert_eq!(semicolon_op, Operator::Semicolon);
|
||||
|
||||
// if let ShellAst::Op {
|
||||
// operator: and_op,
|
||||
// left: and_left,
|
||||
// right: and_right,
|
||||
// } = *semicolon_left
|
||||
// {
|
||||
// assert_eq!(and_op, Operator::And);
|
||||
|
||||
// if let ShellAst::Op {
|
||||
// operator: pipe_op,
|
||||
// left: pipe_left,
|
||||
// right: pipe_right,
|
||||
// } = *and_left
|
||||
// {
|
||||
// assert_eq!(pipe_op, Operator::Pipe);
|
||||
|
||||
// if let ShellAst::Cmd(cmd) = *pipe_left {
|
||||
// assert_eq!(cmd.command, "find");
|
||||
// assert_eq!(
|
||||
// cmd.args,
|
||||
// vec![".".to_string(), "-name".to_string(), "*.rs".to_string()]
|
||||
// );
|
||||
// } else {
|
||||
// panic!("Expected Command node for pipe left");
|
||||
// }
|
||||
|
||||
// if let ShellAst::Cmd(cmd) = *pipe_right {
|
||||
// assert_eq!(cmd.command, "grep");
|
||||
// assert_eq!(cmd.args, vec!["impl".to_string()]);
|
||||
// } else {
|
||||
// panic!("Expected Command node for pipe right");
|
||||
// }
|
||||
// } else {
|
||||
// panic!("Expected Pipe operation");
|
||||
// }
|
||||
|
||||
// if let ShellAst::Cmd(cmd) = *and_right {
|
||||
// assert_eq!(cmd.command, "echo");
|
||||
// assert_eq!(cmd.args, vec!["Found implementations".to_string()]);
|
||||
// } else {
|
||||
// panic!("Expected Command node for and right");
|
||||
// }
|
||||
// } else {
|
||||
// panic!("Expected And operation");
|
||||
// }
|
||||
|
||||
// if let ShellAst::Cmd(cmd) = *semicolon_right {
|
||||
// assert_eq!(cmd.command, "echo");
|
||||
// assert_eq!(cmd.args, vec!["Done".to_string()]);
|
||||
// } else {
|
||||
// panic!("Expected Command node for semicolon right");
|
||||
// }
|
||||
// } else {
|
||||
// panic!("Expected Semicolon operation at root");
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_pipe_with_redirections() {
|
||||
// // Test pipe with redirections
|
||||
// let cmd = "cat file.txt | grep error > results.txt 2> errors.log";
|
||||
// let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
// if let ShellAst::Op {
|
||||
// operator,
|
||||
// left,
|
||||
// right,
|
||||
// } = ast
|
||||
// {
|
||||
// assert_eq!(operator, Operator::Pipe);
|
||||
|
||||
// if let ShellAst::Cmd(left_cmd) = *left {
|
||||
// assert_eq!(left_cmd.command, "cat");
|
||||
// assert_eq!(left_cmd.args, vec!["file.txt".to_string()]);
|
||||
// assert_eq!(left_cmd.stdout_redirect, None);
|
||||
// assert_eq!(left_cmd.stderr_redirect, None);
|
||||
// } else {
|
||||
// panic!("Expected Command node for left side");
|
||||
// }
|
||||
|
||||
// if let ShellAst::Cmd(right_cmd) = *right {
|
||||
// assert_eq!(right_cmd.command, "grep");
|
||||
// assert_eq!(right_cmd.args, vec!["error".to_string()]);
|
||||
// assert_eq!(right_cmd.stdout_redirect, Some("results.txt".to_string()));
|
||||
// assert_eq!(right_cmd.stderr_redirect, Some("errors.log".to_string()));
|
||||
// } else {
|
||||
// panic!("Expected Command node for right side");
|
||||
// }
|
||||
// } else {
|
||||
// panic!("Expected Operation node");
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_quoted_arguments() {
|
||||
// // Test quoted arguments
|
||||
// let cmd = "echo \"hello world\" | grep \"o w\"";
|
||||
// let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
// if let ShellAst::Op {
|
||||
// operator,
|
||||
// left,
|
||||
// right,
|
||||
// } = ast
|
||||
// {
|
||||
// assert_eq!(operator, Operator::Pipe);
|
||||
|
||||
// if let ShellAst::Cmd(left_cmd) = *left {
|
||||
// assert_eq!(left_cmd.command, "echo");
|
||||
// assert_eq!(left_cmd.args, vec!["hello world".to_string()]);
|
||||
// } else {
|
||||
// panic!("Expected Command node for left side");
|
||||
// }
|
||||
|
||||
// if let ShellAst::Cmd(right_cmd) = *right {
|
||||
// assert_eq!(right_cmd.command, "grep");
|
||||
// assert_eq!(right_cmd.args, vec!["o w".to_string()]);
|
||||
// } else {
|
||||
// panic!("Expected Command node for right side");
|
||||
// }
|
||||
// } else {
|
||||
// panic!("Expected Operation node");
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_unsupported_features() {
|
||||
// // Test unsupported shell features
|
||||
// let result = ShellAst::parse("echo $HOME");
|
||||
// assert!(result.is_err());
|
||||
|
||||
// let result = ShellAst::parse("echo `date`");
|
||||
// assert!(result.is_err());
|
||||
|
||||
// let result = ShellAst::parse("echo $(date)");
|
||||
// assert!(result.is_err());
|
||||
|
||||
// let result = ShellAst::parse("for i in {1..5}; do echo $i; done");
|
||||
// assert!(result.is_err());
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_complex_command() {
|
||||
// let cmd = "find /path/to/dir -type f -name \"*.txt\" -exec grep \"pattern with spaces\";";
|
||||
// let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
// if let ShellAst::Cmd(shell_cmd) = ast {
|
||||
// assert_eq!(shell_cmd.command, "find");
|
||||
// assert_eq!(
|
||||
// shell_cmd.args,
|
||||
// vec![
|
||||
// "/path/to/dir".to_string(),
|
||||
// "-type".to_string(),
|
||||
// "f".to_string(),
|
||||
// "-name".to_string(),
|
||||
// "*.txt".to_string(),
|
||||
// "-exec".to_string(),
|
||||
// "grep".to_string(),
|
||||
// "pattern with spaces".to_string(),
|
||||
// ]
|
||||
// );
|
||||
// assert_eq!(shell_cmd.stdout_redirect, None);
|
||||
// assert_eq!(shell_cmd.stderr_redirect, None);
|
||||
// } else {
|
||||
// panic!("Expected Command node");
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_complex_find() {
|
||||
// let cmd = "find . -type f -not -path \"*/\\.*\" -not -path \"*target/*\" -exec grep -l \"project-name\" {} \\;";
|
||||
// let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
// if let ShellAst::Cmd(shell_cmd) = ast {
|
||||
// assert_eq!(shell_cmd.command, "find");
|
||||
// assert_eq!(
|
||||
// shell_cmd.args,
|
||||
// vec![
|
||||
// ".".to_string(),
|
||||
// "-type".to_string(),
|
||||
// "f".to_string(),
|
||||
// "-not".to_string(),
|
||||
// "-path".to_string(),
|
||||
// "*/\\.*".to_string(),
|
||||
// "-not".to_string(),
|
||||
// "-path".to_string(),
|
||||
// "*target/*".to_string(),
|
||||
// "-exec".to_string(),
|
||||
// "grep".to_string(),
|
||||
// "-l".to_string(),
|
||||
// "project-name".to_string(),
|
||||
// "{}".to_string(),
|
||||
// "\\;".to_string()
|
||||
// ]
|
||||
// );
|
||||
// assert_eq!(shell_cmd.stdout_redirect, None);
|
||||
// assert_eq!(shell_cmd.stderr_redirect, None);
|
||||
// } else {
|
||||
// panic!("Expected Command node");
|
||||
// };
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_missing_command() {
|
||||
// assert_eq!(ShellAst::parse(""), Err(Error::MissingCommand));
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_redirect_without_target() {
|
||||
// assert_eq!(ShellAst::parse("a b >"), Err(Error::RedirectWithoutTarget));
|
||||
// assert_eq!(ShellAst::parse("ls 2>"), Err(Error::RedirectWithoutTarget));
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_backgrounding_unsupported() {
|
||||
// assert_eq!(
|
||||
// Err(Error::UsesBackgrounding),
|
||||
// ShellAst::parse("grep & file.txt")
|
||||
// );
|
||||
|
||||
// assert_eq!(
|
||||
// Err(Error::UsesBackgrounding),
|
||||
// ShellAst::parse("grep & file.txt")
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
Reference in New Issue
Block a user