Fix glob, simplify AST code a bit

This commit is contained in:
Axel Liljencrantz 2024-07-30 01:02:02 +02:00
parent 8c2bf5ab79
commit 0861da103a
12 changed files with 743 additions and 601 deletions

View File

@ -203,12 +203,12 @@ impl<'input> Lexer<'input> {
return Some(Token::Flag(&self.full_str[i..end_idx + 1], Location::new(i, end_idx + 1)).into());
}
Some((i, ch)) if string_or_glob_first_char(ch) => {
Some((i, ch)) if string_or_file_or_glob_first_char(ch) => {
let mut end_idx = i;
loop {
let cc2 = self.chars.peek();
match cc2 {
Some((_, ch2)) if string_or_glob_char(*ch2) => {
Some((_, ch2)) if string_or_file_or_glob_char(*ch2) => {
end_idx = self.chars.next().unwrap().0;
}
_ => {
@ -222,26 +222,17 @@ impl<'input> Lexer<'input> {
return match s {
"and" => Some(Token::LogicalOperator(s, Location::new(i, end_idx + 1)).into()),
"or" => Some(Token::LogicalOperator(s, Location::new(i, end_idx + 1)).into()),
_ => Some(Token::StringOrGlob(s, Location::new(i, end_idx + 1)).into()),
};
}
Some((i, ch)) if file_or_glob_first_char(ch) => {
let mut end_idx = i;
loop {
let cc2 = self.chars.peek();
match cc2 {
Some((_, ch2)) if file_or_glob_char(*ch2) => {
end_idx = self.chars.next().unwrap().0;
_ => {
if s.contains('*') || s.contains('?') {
Some(Token::Glob(s, Location::new(i, end_idx + 1)).into())
}
_ => {
break
else if s.contains('/') || s.contains('.') || s.starts_with('~') {
Some(Token::File(s, Location::new(i, end_idx + 1)).into())
} else {
Some(Token::String(s, Location::new(i, end_idx + 1)).into())
}
}
}
let s = &self.full_str[i..end_idx + 1];
return Some(Token::FileOrGlob(s, Location::new(i, end_idx + 1)).into());
};
}
Some((i, '"')) => {
@ -529,12 +520,12 @@ impl<'input> Lexer<'input> {
}
}
fn string_or_glob_first_char(ch: char) -> bool {
(ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '*' || ch == '?' || ch == '_' || ch == '-'
fn string_or_file_or_glob_first_char(ch: char) -> bool {
(ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '*' || ch == '?' || ch == '_' || ch == '-' || ch == '.' || ch == '~' || ch == '/'
}
fn string_or_glob_char(ch: char) -> bool {
(ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '*' || ch == '?' || ch == '.' || ch == '_' || ch == '-'
fn string_or_file_or_glob_char(ch: char) -> bool {
string_or_file_or_glob_first_char(ch) || (ch >= '0' && ch <= '9')
}
fn identifier_first_char(ch: char) -> bool {
@ -553,14 +544,6 @@ fn number_or_underscore_char(ch: char) -> bool {
(ch >= '0' && ch <= '9') || ch == '_'
}
fn file_or_glob_first_char(ch: char) -> bool {
ch == '.' || ch == '~' || ch == '/'
}
fn file_or_glob_char(ch: char) -> bool {
string_or_glob_char(ch) || ch == '/'
}
fn whitespace_char(ch: char) -> bool {
(ch == ' ') || (ch == '\r')
}

View File

@ -178,322 +178,6 @@ fn propose_name(name: &TrackedString, v: ValueDefinition) -> ValueDefinition {
}
}
impl Node {
pub fn prefix(&self, pos: usize) -> CrushResult<Node> {
match self {
Node::Identifier(s) => Ok(Node::Identifier(s.prefix(pos))),
_ => Ok(self.clone()),
}
}
pub fn location(&self) -> Location {
use Node::*;
match self {
Glob(s) | Identifier(s) | Symbol(s) |
String(s) | Integer(s) | Float(s) |
Regex(s) | File(s, _) =>
s.location,
Assignment(a, _, _, b) =>
a.location().union(b.location()),
Unary(s, a) =>
s.location.union(a.location()),
GetItem(a, b) => a.location().union(b.location()),
GetAttr(p, n) => p.location().union(n.location),
Substitution(j) => j.location,
Closure(_, j) => {
// Fixme: Can't tab complete or error report on parameters because they're not currently tracked
j.location
}
}
}
pub fn compile_command(&self, env: &Scope) -> CrushResult<ArgumentDefinition> {
self.compile(env, true)
}
pub fn compile_argument(&self, env: &Scope) -> CrushResult<ArgumentDefinition> {
self.compile(env, false)
}
pub fn type_name(&self) -> &str {
match self {
Node::Assignment(_, _, _, _) => "assignment",
Node::Unary(_, _) => "unary operator",
Node::Glob(_) => "glob",
Node::Identifier(_) => "identifier",
Node::Regex(_) => "regular expression literal",
Node::Symbol(_) => "symbol",
Node::String(_) => "quoted string literal",
Node::File(_, _) => "file literal",
Node::Integer(_) => "integer literal",
Node::Float(_) => "floating point number literal",
Node::GetItem(_, _) => "subscript",
Node::GetAttr(_, _) => "member access",
Node::Substitution(_) => "command substitution",
Node::Closure(_, _) => "closure",
}
}
pub fn compile(&self, env: &Scope, is_command: bool) -> CrushResult<ArgumentDefinition> {
Ok(ArgumentDefinition::unnamed(match self {
Node::Assignment(target, style, op, value) => match op.deref() {
"=" => {
return match target.as_ref() {
Node::Symbol(t) | Node::Identifier(t) => Ok(ArgumentDefinition::named_with_style(
t,
*style,
propose_name(&t, value.compile_argument(env)?.unnamed_value()?),
)),
_ => error(format!("Invalid left side in named argument. Expected a string or identifier, got a {}", target.type_name())),
};
}
_ => return error("Invalid assignment operator"),
},
Node::GetItem(a, o) => ValueDefinition::JobDefinition(
Job::new(vec![self
.compile_as_special_command(env)?
.unwrap()],
a.location().union(o.location()),
)),
Node::Unary(op, r) => match op.string.as_str() {
"@" => {
return Ok(ArgumentDefinition::list(
r.compile_argument(env)?.unnamed_value()?,
));
}
"@@" => {
return Ok(ArgumentDefinition::dict(
r.compile_argument(env)?.unnamed_value()?,
));
}
_ => return error("Unknown operator"),
},
Node::Identifier(l) => ValueDefinition::Identifier(l.clone()),
Node::Regex(l) => ValueDefinition::Value(
Value::Regex(
l.string.clone(),
to_crush_error(Regex::new(&l.string.clone()))?, ),
l.location,
),
Node::String(t) => ValueDefinition::Value(Value::from(unescape(&t.string)?), t.location),
Node::Integer(s) =>
ValueDefinition::Value(
Value::Integer(to_crush_error(
s.string.replace("_", "").parse::<i128>()
)?),
s.location),
Node::Float(s) =>
ValueDefinition::Value(
Value::Float(to_crush_error(
s.string.replace("_", "").parse::<f64>()
)?),
s.location),
Node::GetAttr(node, identifier) =>
ValueDefinition::GetAttr(Box::new(node.compile(env, is_command)?.unnamed_value()?), identifier.clone()),
Node::Symbol(f) =>
if is_command {
ValueDefinition::Identifier(f.clone())
} else {
ValueDefinition::Value(Value::from(f), f.location)
},
Node::Substitution(s) => ValueDefinition::JobDefinition(s.compile(env)?),
Node::Closure(s, c) => {
let param = s.as_ref().map(|v| {
v.iter()
.map(|p| p.generate(env))
.collect::<CrushResult<Vec<Parameter>>>()
});
let p = match param {
None => None,
Some(Ok(p)) => Some(p),
Some(Err(e)) => return Err(e),
};
ValueDefinition::ClosureDefinition(None, p, c.compile(env)?, c.location)
}
Node::Glob(g) => ValueDefinition::Value(Value::Glob(Glob::new(&g.string)), g.location),
Node::File(s, quoted) => ValueDefinition::Value(
Value::from(
if *quoted { PathBuf::from(&unescape(&s.string)?) } else { PathBuf::from(&s.string) }
),
s.location,
),
}))
}
fn compile_standalone_assignment(
target: &Box<Node>,
op: &String,
value: &Node,
env: &Scope,
) -> CrushResult<Option<CommandInvocation>> {
match op.deref() {
"=" => match target.as_ref() {
Node::Identifier(t) => Node::function_invocation(
env.global_static_cmd(vec!["global", "var", "set"])?,
t.location,
vec![ArgumentDefinition::named(
t,
propose_name(&t, value.compile_argument(env)?.unnamed_value()?),
)],
),
Node::GetItem(container, key) => container.method_invocation(
&TrackedString::new("__setitem__", key.location()),
vec![
ArgumentDefinition::unnamed(key.compile_argument(env)?.unnamed_value()?),
ArgumentDefinition::unnamed(value.compile_argument(env)?.unnamed_value()?),
],
env,
true,
),
Node::GetAttr(container, attr) => container.method_invocation(
&TrackedString::new("__setattr__", attr.location),
vec![
ArgumentDefinition::unnamed(ValueDefinition::Value(Value::from(attr),
attr.location)),
ArgumentDefinition::unnamed(value.compile_argument(env)?.unnamed_value()?),
],
env,
true,
),
_ => error("Invalid left side in assignment"),
},
":=" => match target.as_ref() {
Node::Identifier(t) => Node::function_invocation(
env.global_static_cmd(vec!["global", "var", "let"])?,
t.location,
vec![ArgumentDefinition::named(
t,
propose_name(&t, value.compile_argument(env)?.unnamed_value()?),
)],
),
_ => error("Invalid left side in declaration"),
},
_ => error("Unknown assignment operator"),
}
}
pub fn compile_as_special_command(&self, env: &Scope) -> CrushResult<Option<CommandInvocation>> {
match self {
Node::Assignment(target, _style, op, value) => {
Node::compile_standalone_assignment(target, op, value, env)
}
Node::GetItem(val, key) => {
val.method_invocation(&TrackedString::new("__getitem__", key.location()), vec![key.compile_argument(env)?], env, true)
}
Node::Unary(op, _) => match op.string.as_ref() {
"@" | "@@" => Ok(None),
_ => error("Unknown operator"),
},
Node::Glob(_)
| Node::Identifier(_)
| Node::Regex(_)
| Node::Symbol(_)
| Node::String(_)
| Node::Integer(_)
| Node::Float(_)
| Node::GetAttr(_, _)
| Node::Substitution(_)
| Node::Closure(_, _)
| Node::File(_, _) => Ok(None),
}
}
fn function_invocation(
function: Command,
location: Location,
arguments: Vec<ArgumentDefinition>,
) -> CrushResult<Option<CommandInvocation>> {
Ok(Some(CommandInvocation::new(
ValueDefinition::Value(Value::from(function), location),
arguments,
)))
}
fn method_invocation(
&self,
name: &TrackedString,
arguments: Vec<ArgumentDefinition>,
env: &Scope,
as_command: bool,
) -> CrushResult<Option<CommandInvocation>> {
Ok(Some(CommandInvocation::new(
ValueDefinition::GetAttr(
Box::from(self.compile(env, as_command)?.unnamed_value()?),
name.clone(),
),
arguments,
)))
}
pub fn parse_symbol_or_glob(is: impl Into<TrackedString>) -> Box<Node> {
let s = is.into();
let path = expand_user(s.string.clone()).unwrap_or_else(|_| { s.string.clone() });
let ts = TrackedString::new(&path, s.location);
if path.contains('*') || path.contains('?') {
Box::from(Node::Glob(ts))
} else if s.string.contains('/') || s.string.contains('.') {
Box::from(Node::File(ts, false))
} else {
Box::from(Node::Symbol(ts))
}
}
pub fn identifier(is: impl Into<TrackedString>) -> Box<Node> {
let s = is.into();
if s.string.starts_with("$") {
Box::from(Node::Identifier(s.slice_to_end(1)))
} else {
Box::from(Node::Identifier(s))
}
}
pub fn parse_file_or_glob(is: impl Into<TrackedString>) -> Box<Node> {
let s = is.into();
let path = expand_user(s.string.clone()).unwrap_or_else(|_| { s.string.clone() });
let ts = TrackedString::new(&path, s.location);
if ts.string.contains('*') || ts.string.contains('?') {
Box::from(Node::Glob(ts.clone()))
} else {
Box::from(Node::File(ts.clone(), false))
}
}
pub fn file(is: impl Into<TrackedString>, quoted: bool) -> Box<Node> {
Box::from(Node::File(is.into(), quoted))
}
pub fn string(is: impl Into<TrackedString>) -> Box<Node> {
Box::from(Node::String(is.into()))
}
pub fn integer(is: impl Into<TrackedString>) -> Box<Node> {
Box::from(Node::Integer(is.into()))
}
pub fn float(is: impl Into<TrackedString>) -> Box<Node> {
Box::from(Node::Float(is.into()))
}
pub fn regex(is: impl Into<TrackedString>) -> Box<Node> {
let ts = is.into();
let s = ts.string;
Box::from(Node::Regex(TrackedString::new(&s[3..s.len() - 1], ts.location)))
}
}
fn attr(parts: &[&str], location: Location) -> Node {
let mut res = Node::Identifier(TrackedString::new(parts[0], location));
for part in &parts[1..] {

View File

@ -1,8 +1,19 @@
use crate::lang::argument::SwitchStyle;
use crate::lang::ast::{CommandNode, JobListNode, JobNode};
use std::ops::Deref;
use std::path::PathBuf;
use regex::Regex;
use crate::lang::argument::{ArgumentDefinition, SwitchStyle};
use crate::lang::ast::{CommandNode, expand_user, JobListNode, JobNode, propose_name};
use crate::lang::ast::location::Location;
use crate::lang::ast::parameter_node::ParameterNode;
use crate::lang::ast::tracked_string::TrackedString;
use crate::lang::command::{Command, Parameter};
use crate::lang::command_invocation::CommandInvocation;
use crate::lang::errors::{CrushResult, error, to_crush_error};
use crate::lang::job::Job;
use crate::lang::state::scope::Scope;
use crate::lang::value::{Value, ValueDefinition};
use crate::util::escape::unescape;
use crate::util::glob::Glob;
/**
A type representing a node in the abstract syntax tree that is the output of parsing a Crush script.
@ -14,8 +25,8 @@ pub enum Node {
Glob(TrackedString),
Identifier(TrackedString),
Regex(TrackedString),
Symbol(TrackedString),
String(TrackedString),
// true if filename is quoted
String(TrackedString, bool),
// true if filename is quoted
File(TrackedString, bool),
Integer(TrackedString),
@ -63,3 +74,301 @@ impl Node {
}
}
}
impl Node {
pub fn prefix(&self, pos: usize) -> CrushResult<Node> {
match self {
Node::Identifier(s) => Ok(Node::Identifier(s.prefix(pos))),
_ => Ok(self.clone()),
}
}
pub fn location(&self) -> Location {
use Node::*;
match self {
Glob(s) | Identifier(s) |
String(s, _) | Integer(s) | Float(s) |
Regex(s) | File(s, _) =>
s.location,
Assignment(a, _, _, b) =>
a.location().union(b.location()),
Unary(s, a) =>
s.location.union(a.location()),
GetItem(a, b) => a.location().union(b.location()),
GetAttr(p, n) => p.location().union(n.location),
Substitution(j) => j.location,
Closure(_, j) => {
// Fixme: Can't tab complete or error report on parameters because they're not currently tracked
j.location
}
}
}
pub fn compile_command(&self, env: &Scope) -> CrushResult<ArgumentDefinition> {
self.compile(env, true)
}
pub fn compile_argument(&self, env: &Scope) -> CrushResult<ArgumentDefinition> {
self.compile(env, false)
}
pub fn type_name(&self) -> &str {
match self {
Node::Assignment(_, _, _, _) => "assignment",
Node::Unary(_, _) => "unary operator",
Node::Glob(_) => "glob",
Node::Identifier(_) => "identifier",
Node::Regex(_) => "regular expression literal",
Node::String(_, _) => "string literal",
Node::File(_, _) => "file literal",
Node::Integer(_) => "integer literal",
Node::Float(_) => "floating point number literal",
Node::GetItem(_, _) => "subscript",
Node::GetAttr(_, _) => "member access",
Node::Substitution(_) => "command substitution",
Node::Closure(_, _) => "closure",
}
}
pub fn compile(&self, env: &Scope, is_command: bool) -> CrushResult<ArgumentDefinition> {
Ok(ArgumentDefinition::unnamed(match self {
Node::Assignment(target, style, op, value) => match op.deref() {
"=" => {
return match target.as_ref() {
Node::String(t, false) | Node::Identifier(t) => Ok(ArgumentDefinition::named_with_style(
t,
*style,
propose_name(&t, value.compile_argument(env)?.unnamed_value()?),
)),
_ => error(format!("Invalid left side in named argument. Expected a string or identifier, got a {}", target.type_name())),
};
}
_ => return error("Invalid assignment operator"),
},
Node::GetItem(a, o) => ValueDefinition::JobDefinition(
Job::new(vec![self
.compile_as_special_command(env)?
.unwrap()],
a.location().union(o.location()),
)),
Node::Unary(op, r) => match op.string.as_str() {
"@" => {
return Ok(ArgumentDefinition::list(
r.compile_argument(env)?.unnamed_value()?,
));
}
"@@" => {
return Ok(ArgumentDefinition::dict(
r.compile_argument(env)?.unnamed_value()?,
));
}
_ => return error("Unknown operator"),
},
Node::Identifier(l) => ValueDefinition::Identifier(l.clone()),
Node::Regex(l) => ValueDefinition::Value(
Value::Regex(
l.string.clone(),
to_crush_error(Regex::new(&l.string.clone()))?, ),
l.location,
),
Node::String(t, true) => ValueDefinition::Value(Value::from(unescape(&t.string)?), t.location),
Node::String(f, false) =>
if is_command {
ValueDefinition::Identifier(f.clone())
} else {
ValueDefinition::Value(Value::from(f), f.location)
},
Node::Integer(s) =>
ValueDefinition::Value(
Value::Integer(to_crush_error(
s.string.replace("_", "").parse::<i128>()
)?),
s.location),
Node::Float(s) =>
ValueDefinition::Value(
Value::Float(to_crush_error(
s.string.replace("_", "").parse::<f64>()
)?),
s.location),
Node::GetAttr(node, identifier) =>
ValueDefinition::GetAttr(Box::new(node.compile(env, is_command)?.unnamed_value()?), identifier.clone()),
Node::Substitution(s) => ValueDefinition::JobDefinition(s.compile(env)?),
Node::Closure(s, c) => {
let param = s.as_ref().map(|v| {
v.iter()
.map(|p| p.generate(env))
.collect::<CrushResult<Vec<Parameter>>>()
});
let p = match param {
None => None,
Some(Ok(p)) => Some(p),
Some(Err(e)) => return Err(e),
};
ValueDefinition::ClosureDefinition(None, p, c.compile(env)?, c.location)
}
Node::Glob(g) => ValueDefinition::Value(Value::Glob(Glob::new(&g.string)), g.location),
Node::File(s, quoted) => ValueDefinition::Value(
Value::from(
if *quoted { PathBuf::from(&unescape(&s.string)?) } else { PathBuf::from(&s.string) }
),
s.location,
),
}))
}
fn compile_standalone_assignment(
target: &Box<Node>,
op: &String,
value: &Node,
env: &Scope,
) -> CrushResult<Option<CommandInvocation>> {
match op.deref() {
"=" => match target.as_ref() {
Node::Identifier(t) => Node::function_invocation(
env.global_static_cmd(vec!["global", "var", "set"])?,
t.location,
vec![ArgumentDefinition::named(
t,
propose_name(&t, value.compile_argument(env)?.unnamed_value()?),
)],
),
Node::GetItem(container, key) => container.method_invocation(
&TrackedString::new("__setitem__", key.location()),
vec![
ArgumentDefinition::unnamed(key.compile_argument(env)?.unnamed_value()?),
ArgumentDefinition::unnamed(value.compile_argument(env)?.unnamed_value()?),
],
env,
true,
),
Node::GetAttr(container, attr) => container.method_invocation(
&TrackedString::new("__setattr__", attr.location),
vec![
ArgumentDefinition::unnamed(ValueDefinition::Value(Value::from(attr),
attr.location)),
ArgumentDefinition::unnamed(value.compile_argument(env)?.unnamed_value()?),
],
env,
true,
),
_ => error("Invalid left side in assignment"),
},
":=" => match target.as_ref() {
Node::Identifier(t) => Node::function_invocation(
env.global_static_cmd(vec!["global", "var", "let"])?,
t.location,
vec![ArgumentDefinition::named(
t,
propose_name(&t, value.compile_argument(env)?.unnamed_value()?),
)],
),
_ => error("Invalid left side in declaration"),
},
_ => error("Unknown assignment operator"),
}
}
pub fn compile_as_special_command(&self, env: &Scope) -> CrushResult<Option<CommandInvocation>> {
match self {
Node::Assignment(target, _style, op, value) => {
Node::compile_standalone_assignment(target, op, value, env)
}
Node::GetItem(val, key) => {
val.method_invocation(&TrackedString::new("__getitem__", key.location()), vec![key.compile_argument(env)?], env, true)
}
Node::Unary(op, _) => match op.string.as_ref() {
"@" | "@@" => Ok(None),
_ => error("Unknown operator"),
},
Node::Glob(_)
| Node::Identifier(_)
| Node::Regex(_)
| Node::String(_, _)
| Node::Integer(_)
| Node::Float(_)
| Node::GetAttr(_, _)
| Node::Substitution(_)
| Node::Closure(_, _)
| Node::File(_, _) => Ok(None),
}
}
fn function_invocation(
function: Command,
location: Location,
arguments: Vec<ArgumentDefinition>,
) -> CrushResult<Option<CommandInvocation>> {
Ok(Some(CommandInvocation::new(
ValueDefinition::Value(Value::from(function), location),
arguments,
)))
}
fn method_invocation(
&self,
name: &TrackedString,
arguments: Vec<ArgumentDefinition>,
env: &Scope,
as_command: bool,
) -> CrushResult<Option<CommandInvocation>> {
Ok(Some(CommandInvocation::new(
ValueDefinition::GetAttr(
Box::from(self.compile(env, as_command)?.unnamed_value()?),
name.clone(),
),
arguments,
)))
}
pub fn identifier(is: impl Into<TrackedString>) -> Box<Node> {
let s = is.into();
if s.string.starts_with("$") {
Box::from(Node::Identifier(s.slice_to_end(1)))
} else {
Box::from(Node::Identifier(s))
}
}
pub fn file(is: impl Into<TrackedString>, quoted: bool) -> Box<Node> {
Box::from(Node::File(is.into(), quoted))
}
pub fn quoted_string(is: impl Into<TrackedString>) -> Box<Node> {
Box::from(Node::String(is.into(), true))
}
pub fn unquoted_string(is: impl Into<TrackedString>) -> Box<Node> {
Box::from(Node::String(is.into(), false))
}
pub fn glob(is: impl Into<TrackedString>) -> Box<Node> {
Box::from(Node::Glob(is.into()))
}
pub fn integer(is: impl Into<TrackedString>) -> Box<Node> {
Box::from(Node::Integer(is.into()))
}
pub fn float(is: impl Into<TrackedString>) -> Box<Node> {
Box::from(Node::Float(is.into()))
}
pub fn regex(is: impl Into<TrackedString>) -> Box<Node> {
let ts = is.into();
let s = ts.string;
Box::from(Node::Regex(TrackedString::new(&s[3..s.len() - 1], ts.location)))
}
}

View File

@ -14,11 +14,12 @@ pub enum Token<'input> {
Star(Location),
Slash(Location),
QuotedString(&'input str, Location),
StringOrGlob(&'input str, Location),
Identifier(&'input str, Location),
Flag(&'input str, Location),
QuotedFile(&'input str, Location),
FileOrGlob(&'input str, Location),
Glob(&'input str, Location),
File(&'input str, Location),
String(&'input str, Location),
Regex(&'input str, Location),
Integer(&'input str, Location),
Float(&'input str, Location),
@ -45,11 +46,12 @@ impl Token<'_> {
Token::UnaryOperator(_, l) |
Token::ComparisonOperator(_, l) |
Token::QuotedString(_, l) |
Token::StringOrGlob(_, l) |
Token::String(_, l) |
Token::File(_, l) |
Token::Glob(_, l) |
Token::Identifier(_, l) |
Token::Flag(_, l) |
Token::QuotedFile(_, l) |
Token::FileOrGlob(_, l) |
Token::Regex(_, l) |
Token::Integer(_, l) |
Token::Float(_, l) |
@ -81,11 +83,12 @@ impl Token<'_> {
Token::UnaryOperator(s, _) |
Token::ComparisonOperator(s, _) |
Token::QuotedString(s, _) |
Token::StringOrGlob(s, _) |
Token::String(s, _) |
Token::File(s, _) |
Token::Glob(s, _) |
Token::Identifier(s, _) |
Token::Flag(s, _) |
Token::QuotedFile(s, _) |
Token::FileOrGlob(s, _) |
Token::Regex(s, _) |
Token::Integer(s, _) |
Token::Separator(s, _) |
@ -128,11 +131,12 @@ impl<'a> Into<Spanned<'a>> for Token<'a> {
Token::LogicalOperator(_, l) |
Token::UnaryOperator(_, l) |
Token::QuotedString(_, l) |
Token::StringOrGlob(_, l) |
Token::String(_, l) |
Token::File(_, l) |
Token::Glob(_, l) |
Token::Identifier(_, l) |
Token::Flag(_, l) |
Token::QuotedFile(_, l) |
Token::FileOrGlob(_, l) |
Token::Regex(_, l) |
Token::Integer(_, l) |
Token::ComparisonOperator(_, l) |

View File

@ -88,11 +88,12 @@ impl From<Token<'_>> for TrackedString {
Token::UnaryOperator(_, l) |
Token::ComparisonOperator(_, l) |
Token::QuotedString(_, l) |
Token::StringOrGlob(_, l) |
Token::String(_, l) |
Token::File(_, l) |
Token::Glob(_, l) |
Token::Identifier(_, l) |
Token::Flag(_, l) |
Token::QuotedFile(_, l) |
Token::FileOrGlob(_, l) |
Token::Regex(_, l) |
Token::Integer(_, l) |
Token::Float(_, l) |

View File

@ -355,7 +355,7 @@ pub fn complete(
mod tests {
use super::*;
use crate::lang::value::Value;
use crate::util::directory_lister::FakeDirectoryLister;
use crate::util::directory_lister::tests::FakeDirectoryLister;
use signature::signature;
use crate::lang::state::contexts::CommandContext;

View File

@ -218,7 +218,9 @@ fn fetch_value(node: &Node, scope: &Scope, is_command: bool) -> CrushResult<Opti
match node {
Node::Identifier(l) => scope.get(&l.string),
Node::Symbol(l) =>
Node::String(s, true) => Ok(Some(Value::from(s))),
Node::String(l, false) =>
if is_command {
scope.get(&l.string)
} else {
@ -231,8 +233,6 @@ fn fetch_value(node: &Node, scope: &Scope, is_command: bool) -> CrushResult<Opti
None => Ok(None),
},
Node::String(s) => Ok(Some(Value::from(s))),
Node::Integer(s) => Ok(Some(Value::Integer(to_crush_error(
s.string.replace("_", "").parse::<i128>()
)?))),
@ -264,7 +264,7 @@ fn parse_previous_argument(arg: &Node) -> PreviousArgument {
match arg {
Node::Assignment(key, _, op, value) => {
match (key.as_ref(), op.as_str()) {
(Node::Symbol(name), "=") => {
(Node::String(name, false), "=") => {
let inner = parse_previous_argument(value.as_ref());
return PreviousArgument {
name: Some(name.string.clone()),
@ -307,7 +307,10 @@ pub fn parse(
Ok(ParseResult::PartialLabel(
label.prefix(cursor).string)),
Node::Symbol(label) =>
Node::String(string, true) =>
Ok(ParseResult::PartialQuotedString(string.prefix(cursor).string)),
Node::String(label, false) =>
Ok(ParseResult::PartialField(
label.prefix(cursor).string)),
@ -321,9 +324,6 @@ pub fn parse(
if *quoted { unescape(&path.string)? } else { path.string.clone() },
*quoted)),
Node::String(string) =>
Ok(ParseResult::PartialQuotedString(string.prefix(cursor).string)),
Node::GetItem(_, _) => { panic!("AAA"); }
_ => error(format!("Can't extract command to complete. Unknown node type {}", cmd.type_name())),
@ -363,7 +363,7 @@ pub fn parse(
if argument_complete {
match arg.deref() {
Node::Symbol(l) => {
Node::String(l, false) => {
let substring = &l.string[0..min(l.string.len(), cursor - l.location.start)];
Ok(ParseResult::PartialArgument(
PartialCommandResult {
@ -390,7 +390,7 @@ pub fn parse(
}
)),
Node::Symbol(l) =>
Node::String(l, false) =>
Ok(ParseResult::PartialArgument(
PartialCommandResult {
command: c,
@ -423,7 +423,7 @@ pub fn parse(
}
)),
Node::String(s) =>
Node::String(s, true) =>
Ok(ParseResult::PartialArgument(
PartialCommandResult {
command: c,

View File

@ -54,9 +54,9 @@ impl RustylineHelper {
vec!["global".to_string(), "crush".to_string(), "highlight".to_string()]) {
use Token::*;
let res = match token_type {
Flag(_, _) | StringOrGlob(_, _) | QuotedString(_, _) => highlight.get(&Value::from("string_literal")),
Flag(_, _) | String(_, _) | QuotedString(_, _) => highlight.get(&Value::from("string_literal")),
Regex(_, _) => highlight.get(&Value::from("string_literal")),
FileOrGlob(_, _) | QuotedFile(_, _) => highlight.get(&Value::from("file_literal")),
File(_, _) | Glob(_, _) | QuotedFile(_, _) => highlight.get(&Value::from("file_literal")),
Float(_, _) | Integer(_, _) => highlight.get(&Value::from("numeric_literal")),
Unnamed(_) | Named( _) | Pipe( _) | LogicalOperator(_, _) | UnaryOperator(_, _) |
ComparisonOperator(_, _) | Equals( _) | Declare( _) | GetItemEnd( _) | GetItemStart( _) | SubEnd( _) |

View File

@ -95,7 +95,7 @@ ItemExpr: Box<Node> = {
<l: Identifier> => Node::identifier(l),
<l: Regex> => Node::regex(l),
<l:QuotedFile> => Node::file(l, true),
<s:QuotedString> => Node::string(s),
<s:QuotedString> => Node::quoted_string(s),
<i:Integer> => Node::integer(i),
<f:Float> => Node::float(f),
<i: ItemExpr> GetItemStart <e: Assignment> GetItemEnd => Box::from(Node::GetItem(i, e)),
@ -194,12 +194,13 @@ Default: Option<Node> = {
}
Item: Box<Node> = {
<l: StringOrGlob> => Node::parse_symbol_or_glob(l),
<l: String> => Node::unquoted_string(l),
<l: File> => Node::file(l, false),
<l: Glob> => Node::glob(l),
<l: Identifier> => Node::identifier(l),
<l: Regex> => Node::regex(l),
<l: FileOrGlob> => Node::parse_file_or_glob(l),
<l:QuotedFile> => Node::file(l, true),
<l:QuotedString> => Node::string(l),
<l:QuotedString> => Node::quoted_string(l),
<l:Integer> => Node::integer(l),
<l:Float> => Node::float(l),
@ -207,19 +208,19 @@ Item: Box<Node> = {
let ts = TrackedString::from(l);
Box::from(
if ts.string.starts_with("--") {
Node::Assignment(Box::from(Node::Symbol(ts.slice_to_end(2))),
Node::Assignment(Node::unquoted_string(ts.slice_to_end(2)),
SwitchStyle::Double,
"=".to_string(),
Box::from(Node::Identifier(TrackedString::new("true", ts.location()))))
} else {
Node::Assignment(Box::from(Node::Symbol(ts.slice_to_end(1))),
Node::Assignment(Node::unquoted_string(ts.slice_to_end(1)),
SwitchStyle::Single,
"=".to_string(),
Box::from(Node::Identifier(TrackedString::new("true", ts.location()))))
})
},
<i: Item> GetItemStart <e: Assignment> GetItemEnd => Box::from(Node::GetItem(i, e)),
<i: Item> MemberOperator <start: @L> <l: StringOrGlob> <end: @R> => Box::from(Node::GetAttr(i, TrackedString::from(l))),
<i: Item> MemberOperator <start: @L> <l: String> <end: @R> => Box::from(Node::GetAttr(i, TrackedString::from(l))),
JobStart Separators? <s: Signature> <l: JobListWithoutSeparator> JobEnd => Box::from(Node::Closure(s, l)),
SubStart <j:Job> SubEnd => Box::from(Node::Substitution(j)),
<l: @L>ExprModeStart <e:Expr> SubEnd <r: @R> => Box::from(Node::Substitution(e.expression_to_job())),
@ -246,11 +247,12 @@ extern {
Plus=> Token::Plus(<Location>),
Minus=> Token::Minus(<Location>),
QuotedString=> Token::QuotedString(<&'input str>, <Location>),
StringOrGlob=> Token::StringOrGlob(<&'input str>, <Location>),
String=> Token::String(<&'input str>, <Location>),
File=> Token::File(<&'input str>, <Location>),
Glob=> Token::Glob(<&'input str>, <Location>),
Identifier=> Token::Identifier(<&'input str>, <Location>),
Flag=> Token::Flag(<&'input str>, <Location>),
QuotedFile=> Token::QuotedFile(<&'input str>, <Location>),
FileOrGlob=> Token::FileOrGlob(<&'input str>, <Location>),
Regex=> Token::Regex(<&'input str>, <Location>),
Separator=> Token::Separator(<&'input str>, <Location>),
Integer=> Token::Integer(<&'input str>, <Location>),

View File

@ -106,11 +106,12 @@ impl Parser {
Token::GetItemStart( _) => { stack.push("]"); }
Token::SubEnd( _) | Token::JobEnd( _) | Token::GetItemEnd( _) => { stack.pop(); }
Token::QuotedString(_, _) => {}
Token::StringOrGlob(_, _) => {}
Token::String(_, _) => {}
Token::File(_, _) => {}
Token::Glob(_, _) => {}
Token::Identifier(_, _) => {}
Token::Flag(_, _) => {}
Token::QuotedFile(_, _) => {}
Token::FileOrGlob(_, _) => {}
Token::Regex(_, _) => {}
Token::Integer(_, _) => {}
Token::Float(_, _) => {}
@ -141,7 +142,7 @@ mod tests {
let tok = p().tokenize("{aaa}\n").unwrap();
assert_eq!(tok, vec![
Token::JobStart(Location::from(0)),
Token::StringOrGlob("aaa", Location::new(1,4)),
Token::String("aaa", Location::new(1,4)),
Token::JobEnd(Location::from(4)),
Token::Separator("\n", Location::from(5)),
]);

View File

@ -73,117 +73,117 @@ impl Iterator for RealIter {
}
}
#[cfg(test)]
pub struct FakeDirectoryLister {
cwd: PathBuf,
map: OrderedMap<PathBuf, Vec<FakeListerEntry>>,
}
#[cfg(test)]
impl FakeDirectoryLister {
pub fn new(cwd: impl Into<PathBuf>) -> FakeDirectoryLister {
FakeDirectoryLister {
map: OrderedMap::new(),
cwd: cwd.into(),
}
pub mod tests {
use std::collections::VecDeque;
use ordered_map::{Entry, OrderedMap};
use crate::lang::errors::mandate;
use super::*;
pub struct FakeDirectoryLister {
cwd: PathBuf,
map: OrderedMap<PathBuf, Vec<FakeListerEntry>>,
}
pub fn add(&mut self, path: impl Into<PathBuf>, content: &[&str]) -> &mut FakeDirectoryLister {
let g = path.into();
let path = if g.is_relative() {
self.cwd.join(g)
} else {
g
};
let mut content = content.iter()
.map(|n| FakeListerEntry {
name: PathBuf::from(n),
is_directory: false,
})
.collect::<Vec<_>>();
match self.map.entry(path.clone()) {
Entry::Occupied(mut e) => {
content.append(&mut e.value().clone());
e.insert(content);
impl FakeDirectoryLister {
pub fn new(cwd: impl Into<PathBuf>) -> FakeDirectoryLister {
FakeDirectoryLister {
map: OrderedMap::new(),
cwd: cwd.into(),
}
Entry::Vacant(e) => { e.insert(content.to_vec()) }
}
let mut parent = PathBuf::from(path);
while let Some(p) = parent.parent() {
let mut v = vec![
FakeListerEntry {
name: PathBuf::from(parent.components().last().unwrap().as_os_str()),
is_directory: true,
}];
pub fn add(&mut self, path: impl Into<PathBuf>, content: &[&str]) -> &mut FakeDirectoryLister {
let g = path.into();
let path = if g.is_relative() {
self.cwd.join(g)
} else {
g
};
match self.map.entry(p.to_path_buf()) {
let mut content = content.iter()
.map(|n| FakeListerEntry {
name: PathBuf::from(n),
is_directory: false,
})
.collect::<Vec<_>>();
match self.map.entry(path.clone()) {
Entry::Occupied(mut e) => {
if !e.value().contains(&v[0]) {
let mut tmp = e.value().clone();
tmp.append(&mut v);
e.insert(tmp);
content.append(&mut e.value().clone());
e.insert(content);
}
Entry::Vacant(e) => { e.insert(content.to_vec()) }
}
let mut parent = PathBuf::from(path);
while let Some(p) = parent.parent() {
let mut v = vec![
FakeListerEntry {
name: PathBuf::from(parent.components().last().unwrap().as_os_str()),
is_directory: true,
}];
match self.map.entry(p.to_path_buf()) {
Entry::Occupied(mut e) => {
if !e.value().contains(&v[0]) {
let mut tmp = e.value().clone();
tmp.append(&mut v);
e.insert(tmp);
}
}
Entry::Vacant(e) => {
e.insert(v);
}
}
Entry::Vacant(e) => {
e.insert(v);
}
}
parent = p.to_path_buf();
parent = p.to_path_buf();
}
self
}
self
}
}
#[cfg(test)]
impl DirectoryLister for FakeDirectoryLister {
type DirectoryIter = FakeIter;
impl DirectoryLister for FakeDirectoryLister {
type DirectoryIter = FakeIter;
fn list(&self, path: impl Into<PathBuf>) -> CrushResult<Self::DirectoryIter> {
let g = path.into();
let path = if g.is_relative() {
self.cwd.join(&g)
} else {
g.clone()
};
fn list(&self, path: impl Into<PathBuf>) -> CrushResult<Self::DirectoryIter> {
let g = path.into();
let path = if g.is_relative() {
self.cwd.join(&g)
} else {
g.clone()
};
Ok(
FakeIter {
vec: VecDeque::from(
mandate(
self.map.get(&path)
.map(|v|
v.iter().map(|f| Directory {
name: f.name.clone(),
full_path: g.join(&f.name),
is_directory: f.is_directory
}).collect::<Vec<_>>()),
"Unknown directory")?.clone()),
}
)
Ok(
FakeIter {
vec: VecDeque::from(
mandate(
self.map.get(&path)
.map(|v|
v.iter().map(|f| Directory {
name: f.name.clone(),
full_path: g.join(&f.name),
is_directory: f.is_directory
}).collect::<Vec<_>>()),
format!("Unknown directory {:?}", path))?.clone()),
}
)
}
}
}
#[cfg(test)]
pub struct FakeIter {
vec: VecDeque<Directory>,
}
#[cfg(test)]
impl Iterator for FakeIter {
type Item = Directory;
fn next(&mut self) -> Option<Self::Item> {
self.vec.pop_front()
pub struct FakeIter {
vec: VecDeque<Directory>,
}
impl Iterator for FakeIter {
type Item = Directory;
fn next(&mut self) -> Option<Self::Item> {
self.vec.pop_front()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn as_strs(it: FakeIter) -> Vec<String> {
let mut res = it.map(|d| d.name.to_str().unwrap().to_string()).collect::<Vec<_>>();

View File

@ -1,8 +1,9 @@
use crate::lang::errors::{CrushResult, data_error};
use std::collections::VecDeque;
use std::collections::{HashSet, VecDeque};
use std::path::{Path, PathBuf};
use std::fmt::{Display, Formatter};
use crate::util::directory_lister::{directory_lister, DirectoryLister};
use crate::util::directory_lister::{Directory, directory_lister, DirectoryLister};
use crate::util::glob::CompileState::{Regular, WasAny, WasDot, WasSeparator};
#[derive(Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub struct Glob {
@ -16,6 +17,7 @@ enum Tile {
Single,
Any,
Recursive,
Separator,
}
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
@ -30,37 +32,88 @@ impl Display for Glob {
}
}
enum CompileState {
WasAny,
WasSeparator,
WasDot,
Regular,
}
fn compile(s: &str) -> Vec<Tile> {
let mut res = Vec::new();
let mut was_any = false;
let mut state = WasSeparator;
for c in s.chars() {
if was_any {
match c {
'*' => res.push(Tile::Recursive),
'?' => {
res.push(Tile::Any);
res.push(Tile::Single);
}
c => {
res.push(Tile::Any);
res.push(Tile::Char(c));
match state {
Regular =>
match c {
'*' => state = WasAny,
'/' => {
state = WasSeparator;
res.push(Tile::Separator)
}
'?' => res.push(Tile::Single),
c => res.push(Tile::Char(c)),
},
WasAny => {
state = Regular;
match c {
'*' => res.push(Tile::Recursive),
'?' => {
res.push(Tile::Any);
res.push(Tile::Single);
}
'/' => {
state = WasSeparator;
res.push(Tile::Any);
res.push(Tile::Separator);
}
c => {
res.push(Tile::Any);
res.push(Tile::Char(c));
}
}
}
was_any = false;
} else {
match c {
'*' => was_any = true,
'?' => {
res.push(Tile::Single);
WasSeparator => {
state = Regular;
match c {
'.' => state = WasDot,
'*' => state = WasAny,
'/' => {
state = WasSeparator;
res.push(Tile::Separator)
}
'?' => res.push(Tile::Single),
c => res.push(Tile::Char(c)),
}
c => {
res.push(Tile::Char(c));
}
WasDot => {
state = Regular;
match c {
'*' => {
res.push(Tile::Char('.'));
state = WasAny
}
'/' => {
state = WasSeparator;
}
'?' => {
res.push(Tile::Char('.'));
res.push(Tile::Single)
}
c => {
res.push(Tile::Char('.'));
res.push(Tile::Char(c))
}
}
}
}
}
if was_any {
res.push(Tile::Any);
match state {
WasAny => res.push(Tile::Any),
WasSeparator => {}
Regular => {}
WasDot => {}
}
res
}
@ -82,44 +135,119 @@ impl Glob {
}
}
struct GlobState<'s> {
pattern: &'s [Tile],
directory: PathBuf,
}
enum GlobMatchResult<'s> {
FullMatch,
PartialMatch { remaining_pattern: &'s [Tile] },
}
fn glob_file_match<'a>(pattern: &'a [Tile], path: &[char], entry: &Directory, out: &mut HashSet<PathBuf>, queue: &mut VecDeque<GlobState<'a>>) -> CrushResult<()> {
match (pattern.first(), path.first()) {
(None, None) => {
out.insert(entry.full_path.clone());
}
(Some(Tile::Char(_)), None) | (Some(Tile::Single), None) => {}
(Some(Tile::Any), None) => {
if pattern.len() == 1 {
out.insert(entry.full_path.clone());
}
if entry.is_directory {
if pattern[1..] == [Tile::Separator] {
out.insert(entry.full_path.clone());
}
if let Some(Tile::Separator) = pattern.get(1) {
queue.push_back(GlobState {
pattern: &pattern[2..],
directory: entry.full_path.clone(),
});
}
}
}
(Some(Tile::Recursive), None) => {
if pattern.len() == 1 {
out.insert(entry.full_path.clone());
}
if entry.is_directory {
if pattern[1..] == [Tile::Separator] {
out.insert(entry.full_path.clone());
}
if let Some(Tile::Separator) = pattern.get(1) {
queue.push_back(GlobState {
pattern: &pattern[2..],
directory: entry.full_path.clone(),
});
}
queue.push_back(GlobState {
pattern,
directory: entry.full_path.clone(),
});
}
}
(Some(Tile::Separator), None) => {
if pattern.len() == 1 {
if entry.is_directory {
out.insert(entry.full_path.clone());
}
} else {
if entry.is_directory {
queue.push_back(GlobState {
pattern: &pattern[1..],
directory: entry.full_path.clone(),
});
}
}
}
(None, Some(ch)) => {}
(Some(Tile::Char(ch1)), Some(ch2)) if ch1 == ch2 => {
return glob_file_match(&pattern[1..], &path[1..], entry, out, queue)
}
(Some(Tile::Char(_)), Some(_)) => {}
(Some(Tile::Single), Some(_)) => {
return glob_file_match(&pattern[1..], &path[1..], entry, out, queue)
}
(Some(Tile::Any), Some(_)) | (Some(Tile::Recursive), Some(_)) => {
glob_file_match(&pattern[1..], path, entry, out, queue)?;
glob_file_match(pattern, &path[1..], entry, out, queue)?;
}
(Some(Tile::Separator), Some(_)) => {}
}
Ok(())
}
fn glob_files(pattern: &[Tile], cwd: &Path, out: &mut Vec<PathBuf>, lister: &impl DirectoryLister) -> CrushResult<()> {
if pattern.is_empty() {
return Ok(());
}
let mut queue = VecDeque::new();
let mut dedup = HashSet::new();
queue.push_back(if matches!(pattern[0], Tile::Char('/')) {
("/".to_string(), PathBuf::from("/"))
GlobState { pattern: &pattern[1..], directory: PathBuf::from("/") }
} else {
("".to_string(), cwd.to_path_buf())
GlobState { pattern, directory: cwd.to_path_buf() }
});
while !queue.is_empty() {
let (s, next_dir) = queue.pop_front().unwrap();
for entry in lister.list(&next_dir)? {
match entry.name.to_str() {
Some(name) => {
let mut ss = format!("{}{}", s, name);
let res = glob_match(pattern, &ss);
if res.matches {
out.push(PathBuf::from(&ss))
}
if res.prefix && entry.is_directory {
if !res.matches {
let with_trailing_slash = format!("{}/", ss);
if glob_match(pattern, &with_trailing_slash).matches {
out.push(PathBuf::from(&with_trailing_slash))
}
}
ss.push('/');
queue.push_back((ss, entry.full_path));
}
}
None => return data_error("Invalid file name"),
while let Some(state) = queue.pop_front() {
for entry in lister.list(&state.directory)? {
if let Some(path) = entry.name.to_str() {
let path_vec: Vec<char> = path.chars().collect();
glob_file_match(state.pattern, &path_vec, &entry, &mut dedup, &mut queue);
}
}
}
for e in dedup.drain() {
out.push(e);
}
Ok(())
}
@ -183,18 +311,6 @@ fn glob_match(pattern: &[Tile], value: &str) -> GlobResult {
},
},
Some(Tile::Char('/')) => match value.chars().next() {
Some('/') => glob_match(&pattern[1..], &value[1..]),
Some(_) => GlobResult {
matches: false,
prefix: false,
},
None => GlobResult {
matches: false,
prefix: true,
},
},
Some(Tile::Char(g)) => match value.chars().next() {
Some(v) => {
if *g == v {
@ -212,95 +328,108 @@ fn glob_match(pattern: &[Tile], value: &str) -> GlobResult {
prefix: false,
},
},
Some(Tile::Separator) => match value.chars().next() {
Some('/') => glob_match(&pattern[1..], &value[1..]),
Some(_) => GlobResult {
matches: false,
prefix: false,
},
None => GlobResult {
matches: false,
prefix: true,
},
},
}
}
#[cfg(test)]
mod tests {
use itertools::Itertools;
use super::*;
use crate::util::directory_lister::FakeDirectoryLister;
use crate::util::directory_lister::tests::FakeDirectoryLister;
#[test]
fn test_glob_match() {
assert_eq!(
glob_match(&compile("%%"), "a"),
glob_match(&compile("**"), "a"),
GlobResult {
matches: true,
prefix: true,
}
);
assert_eq!(
glob_match(&compile("%%"), "a/b/c/d"),
glob_match(&compile("**"), "a/b/c/d"),
GlobResult {
matches: true,
prefix: true,
}
);
assert_eq!(
glob_match(&compile("%%"), "a/b/c/d/"),
glob_match(&compile("**"), "a/b/c/d/"),
GlobResult {
matches: true,
prefix: true,
}
);
assert_eq!(
glob_match(&compile("%%/"), "a/"),
glob_match(&compile("**/"), "a/"),
GlobResult {
matches: true,
prefix: true,
}
);
assert_eq!(
glob_match(&compile("%%/"), "a/b/c/d/"),
glob_match(&compile("**/"), "a/b/c/d/"),
GlobResult {
matches: true,
prefix: true,
}
);
assert_eq!(
glob_match(&compile("%%/"), "a"),
glob_match(&compile("**/"), "a"),
GlobResult {
matches: false,
prefix: true,
}
);
assert_eq!(
glob_match(&compile("%%/"), "a/b/c/d"),
glob_match(&compile("**/"), "a/b/c/d"),
GlobResult {
matches: false,
prefix: true,
}
);
assert_eq!(
glob_match(&compile("%%a"), "aaa"),
glob_match(&compile("**a"), "aaa"),
GlobResult {
matches: true,
prefix: true,
}
);
assert_eq!(
glob_match(&compile("%%a/"), "aaa/"),
glob_match(&compile("**a/"), "aaa/"),
GlobResult {
matches: true,
prefix: true,
}
);
assert_eq!(
glob_match(&compile("%%a"), "aaa/"),
glob_match(&compile("**a"), "aaa/"),
GlobResult {
matches: false,
prefix: true,
}
);
assert_eq!(
glob_match(&compile("aaa/%"), "aaa"),
glob_match(&compile("aaa/*"), "aaa"),
GlobResult {
matches: false,
prefix: true,
}
);
assert_eq!(
glob_match(&compile("a/%/c"), "a/bbbb"),
glob_match(&compile("a/*/c"), "a/bbbb"),
GlobResult {
matches: false,
prefix: true,
@ -342,49 +471,49 @@ mod tests {
}
);
assert_eq!(
glob_match(&compile("%%a"), "bbb"),
glob_match(&compile("**a"), "bbb"),
GlobResult {
matches: false,
prefix: true,
}
);
assert_eq!(
glob_match(&compile("%"), "a/b"),
glob_match(&compile("*"), "a/b"),
GlobResult {
matches: false,
prefix: false,
}
);
assert_eq!(
glob_match(&compile("%%c"), "a/b"),
glob_match(&compile("**c"), "a/b"),
GlobResult {
matches: false,
prefix: true,
}
);
assert_eq!(
glob_match(&compile("a/%/c"), "a/b/c"),
glob_match(&compile("a/*/c"), "a/b/c"),
GlobResult {
matches: true,
prefix: false,
}
);
assert_eq!(
glob_match(&compile("a/b%/c"), "a/b/c"),
glob_match(&compile("a/b*/c"), "a/b/c"),
GlobResult {
matches: true,
prefix: false,
}
);
assert_eq!(
glob_match(&compile("a/%b/c"), "a/d/c"),
glob_match(&compile("a/*b/c"), "a/d/c"),
GlobResult {
matches: false,
prefix: false,
}
);
assert_eq!(
glob_match(&compile("a/%/c/"), "a/b/c/"),
glob_match(&compile("a/*/c/"), "a/b/c/"),
GlobResult {
matches: true,
prefix: false,
@ -394,60 +523,89 @@ mod tests {
fn lister() -> FakeDirectoryLister {
let mut res = FakeDirectoryLister::new("/home/rabbit");
res.add("example_data/tree", &vec!["a"])
.add("example_data/tree/sub", &vec!["b", "c"]);
res.add("tree", &vec!["a"])
.add("tree/sub", &vec!["b", "c"]);
res
}
#[test]
fn test_glob_files() {
fn check_file_glob_count(glob: &str, count: usize) {
let mut out = Vec::new();
let _ = glob_files(
&compile("%%"),
&PathBuf::from("example_data/tree"),
let ggg = glob_files(
&compile(glob),
&PathBuf::from(""),
&mut out,
&lister(),
);
assert_eq!(out.len(), 4);
out.clear();
let _ = glob_files(
&compile("%%/"),
&PathBuf::from("example_data/tree"),
assert_eq!(out.len(), count);
}
fn check_file_glob(glob: &str, matches: &[&str]) {
let mut out = Vec::new();
let ggg = glob_files(
&compile(glob),
&PathBuf::from(""),
&mut out,
&lister(),
);
assert_eq!(out.len(), 1);
out.clear();
let _ = glob_files(
&compile("./tree/s%"),
&PathBuf::from("example_data"),
&mut out,
&lister(),
);
assert_eq!(out.len(), 1);
out.clear();
let _ = glob_files(
&compile("%%/%"),
&PathBuf::from("example_data/tree"),
&mut out,
&lister(),
);
assert_eq!(out.len(), 3);
out.clear();
let _ = glob_files(
&compile("?%%/?"),
&PathBuf::from("example_data/tree"),
&mut out,
&lister(),
);
assert_eq!(out.len(), 2);
out.clear();
let _ = glob_files(
&compile("%%b"),
&PathBuf::from("example_data/tree"),
&mut out,
&lister(),
);
assert_eq!(out.len(), 2);
let set: HashSet<String> = out.drain(..).map(|e|{e.to_str().unwrap().to_string()}).collect();
assert_eq!(set.len(), matches.len());
for el in matches {
assert!(
set.contains(*el),
"The element '{}' wasn't present in glob result. The following items were found: {}",
el,
set.iter().map({|e| format!("'{}'", e)}).join(", "));
}
}
#[test]
fn test_glob_files_literal() {
check_file_glob("tree", &["tree"]);
}
#[test]
fn test_glob_files_single() {
check_file_glob("tre?", &["tree"]);
}
#[test]
fn test_glob_files_any() {
check_file_glob("*", &["tree"]);
}
#[test]
fn test_glob_files_recursive() {
check_file_glob_count("**", 5);
}
#[test]
fn test_glob_files_recursive_directories() {
check_file_glob("**/", &["tree", "tree/sub"]);
}
#[test]
fn test_glob_files_with_separator() {
check_file_glob("tree/s*", &["tree/sub"]);
}
#[test]
fn test_glob_files_leading_dot() {
check_file_glob("./tree/s*", &["tree/sub"]);
}
#[test]
fn test_glob_files_recursive_any() {
check_file_glob_count("**/*", 4);
}
#[test]
fn test_glob_files_recursive_any_directory() {
check_file_glob("**/*/", &["tree/sub"]);
}
#[test]
fn test_glob_files_single_recursive_any() {
check_file_glob_count("?**/*", 4);
}
#[test]
fn test_glob_files_trailing_letter() {
check_file_glob("**b", &["tree/sub/b", "tree/sub"]);
}
}