parser including redirects and formatstrings

This commit is contained in:
Daniel Bulant 2025-04-06 14:19:25 +02:00
parent 1cda409a65
commit b454c95be5
No known key found for this signature in database
3 changed files with 110 additions and 69 deletions

View file

@ -61,8 +61,7 @@ set obj = ${
$dynkey: $value2 $dynkey: $value2
} }
set literal = "$var" set literal = "$var"
# or perhaps ` ? set formatted = `$var`
set formatted = f"$var"
# these are builtin commands rather than syntax structures (unlike set/while etc) # these are builtin commands rather than syntax structures (unlike set/while etc)
# they simply accept arguments and work with them as with any other # they simply accept arguments and work with them as with any other

View file

@ -8,10 +8,21 @@ pub struct Index {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Primitive { pub enum FormatStringPart {
Number(f64),
String(String), String(String),
Variable(String), Variable(String),
// could be globs in the future
}
#[derive(Debug, Clone)]
pub struct FormatString {
values: Vec<FormatStringPart>
}
#[derive(Debug, Clone)]
pub enum Primitive {
Number(f64),
FormatString(FormatString),
Index(Index) Index(Index)
} }
@ -173,7 +184,7 @@ pub fn parse<'a>() -> impl Parser<'a, &'a str, Vec<Statement>, chumsky::extra::D
.ignored() .ignored()
.boxed(); .boxed();
let direct_string = none_of("$()[]{}\\\"\n;|&") let direct_string = none_of("$()[]{}\\\"\n;|&<>#")
.and_is(text::whitespace().at_least(1).not()) .and_is(text::whitespace().at_least(1).not())
.ignored() .ignored()
.or(escape.clone()) .or(escape.clone())
@ -202,23 +213,32 @@ pub fn parse<'a>() -> impl Parser<'a, &'a str, Vec<Statement>, chumsky::extra::D
let comment = just('#').then(any().and_is(just('\n').not()).repeated()); let comment = just('#').then(any().and_is(just('\n').not()).repeated());
let empty = text::inline_whitespace().ignored() let empty_block = text::whitespace().ignored()
.or(comment.ignored()) .or(comment.ignored())
.or(eol.ignored()); .or(eol.ignored());
let and = just("&&"); let and = just("&&");
let or = just("||"); let or = just("||");
let not = just('!');
let pipe = just('|');//.then(just('|').rewind().not()); let pipe = just('|');//.then(just('|').rewind().not());
// let pipe_target = just('>'); let pipe_target = just(">");
// let pipe_target_append = just(">>"); let pipe_target_append = just(">>");
// let pipe_source = just('<'); let pipe_source = just("<");
recursive(|expr| { recursive(|expr| {
let format_string_part = choice((
variable.map(|s: &str| FormatStringPart::Variable(s.to_string())),
string.map(FormatStringPart::String),
))
.repeated()
.at_least(1)
.collect()
.map(|v| FormatString {
values: v
});
let primitive = choice(( let primitive = choice((
number.map(Primitive::Number), number.map(Primitive::Number),
variable.map(|s: &str| Primitive::Variable(s.to_string())), format_string_part.map(Primitive::FormatString),
string.map(Primitive::String),
)); ));
let group = expr.clone() let group = expr.clone()
@ -256,9 +276,12 @@ pub fn parse<'a>() -> impl Parser<'a, &'a str, Vec<Statement>, chumsky::extra::D
.collect() .collect()
.delimited_by(just('('), just(')')); .delimited_by(just('('), just(')'));
let block = expr.clone() let block = choice((
.or(empty.to(vec![])) expr.clone(),
.delimited_by(just('{'), just('}')); empty_block.to(vec![]),
))
.delimited_by(just('{'), just('}'))
.boxed();
let cmdname = value.clone() let cmdname = value.clone()
.and_is(choice(( .and_is(choice((
@ -270,8 +293,7 @@ pub fn parse<'a>() -> impl Parser<'a, &'a str, Vec<Statement>, chumsky::extra::D
just("break"), just("break"),
just("continue"), just("continue"),
just("return"), just("return"),
just("fn"), just("fn")
just("!")
)).then(end()).not()); )).then(end()).not());
let args = value.clone() let args = value.clone()
@ -281,15 +303,16 @@ pub fn parse<'a>() -> impl Parser<'a, &'a str, Vec<Statement>, chumsky::extra::D
.collect(); .collect();
let command = let command =
cmdname text::inline_whitespace().ignore_then(cmdname)
.padded_by(text::inline_whitespace()) .then_ignore(text::inline_whitespace().at_least(1))
.then(args) .then(args)
.map(|(name, args): (Value, Vec<Value>)| { .map(|(name, args): (Value, Vec<Value>)| {
Command { Command {
name: Box::new(name), name: Box::new(name),
args: args.into_iter().map(|v| v.clone()).collect() args: args.into_iter().map(|v| v.clone()).collect()
} }
}); })
.boxed();
let set = just("set") let set = just("set")
.then_ignore(text::inline_whitespace().at_least(1)) .then_ignore(text::inline_whitespace().at_least(1))
@ -301,7 +324,8 @@ pub fn parse<'a>() -> impl Parser<'a, &'a str, Vec<Statement>, chumsky::extra::D
name: Box::new(name), name: Box::new(name),
value: Box::new(value) value: Box::new(value)
} }
}); })
.boxed();
let else_ = just("else") let else_ = just("else")
.then(text::inline_whitespace().at_least(1)) .then(text::inline_whitespace().at_least(1))
@ -321,7 +345,8 @@ pub fn parse<'a>() -> impl Parser<'a, &'a str, Vec<Statement>, chumsky::extra::D
body, body,
else_body else_body
} }
}); })
.boxed();
let while_ = just("while") let while_ = just("while")
.then_ignore(text::inline_whitespace().at_least(1)) .then_ignore(text::inline_whitespace().at_least(1))
@ -334,7 +359,8 @@ pub fn parse<'a>() -> impl Parser<'a, &'a str, Vec<Statement>, chumsky::extra::D
body, body,
else_body else_body
} }
}); })
.boxed();
let for_ = just("for") let for_ = just("for")
.then_ignore(text::inline_whitespace().at_least(1)) .then_ignore(text::inline_whitespace().at_least(1))
@ -350,7 +376,8 @@ pub fn parse<'a>() -> impl Parser<'a, &'a str, Vec<Statement>, chumsky::extra::D
body, body,
else_body else_body
} }
}); })
.boxed();
let loop_ = just("loop") let loop_ = just("loop")
.ignore_then(block.clone().padded_by(text::inline_whitespace())) .ignore_then(block.clone().padded_by(text::inline_whitespace()))
@ -358,14 +385,16 @@ pub fn parse<'a>() -> impl Parser<'a, &'a str, Vec<Statement>, chumsky::extra::D
Loop { Loop {
body body
} }
}); })
.boxed();
let return_ = just("return") let return_ = just("return")
.then_ignore(text::inline_whitespace().at_least(1)) .then_ignore(text::inline_whitespace().at_least(1))
.ignore_then(value.clone().or_not()) .ignore_then(value.clone().or_not())
.map(|v: Option<Value>| { .map(|v: Option<Value>| {
Statement::Return(v) Statement::Return(v)
}); })
.boxed();
let function = just("fn") let function = just("fn")
.then_ignore(text::inline_whitespace().at_least(1)) .then_ignore(text::inline_whitespace().at_least(1))
@ -378,72 +407,86 @@ pub fn parse<'a>() -> impl Parser<'a, &'a str, Vec<Statement>, chumsky::extra::D
args: args, args: args,
body body
} }
}); })
.boxed();
let statement = choice(( let command = command.map(Statement::Command);
set.map(Statement::Set),
let mapable = choice((
if_.map(Statement::If), if_.map(Statement::If),
while_.map(Statement::While), while_.map(Statement::While),
for_.map(Statement::For), for_.map(Statement::For),
loop_.map(Statement::Loop), loop_.map(Statement::Loop),
function.map(Statement::Function), command,
return_,
just("break").to(Statement::Break),
just("continue").to(Statement::Continue),
command.map(Statement::Command),
)).padded_by(text::inline_whitespace().ignored().or(comment.ignored())).boxed(); )).padded_by(text::inline_whitespace().ignored().or(comment.ignored())).boxed();
let not = not.repeated().at_least(1) let pipe_target = mapable.clone()
.foldr(
text::inline_whitespace()
.ignore_then(statement.clone()),
|_, rhs| Statement::Not(Not {
value: Box::new(rhs)
})
).boxed();
let or = statement.clone()
.foldl( .foldl(
or pipe_target.or(pipe_target_append)
.padded_by(text::inline_whitespace()) .padded_by(text::inline_whitespace())
.ignore_then(statement.clone()) .ignore_then(value.clone())
.repeated(), .repeated(),
|lhs, rhs| Statement::Or(Or { |lhs, rhs| Statement::TargetFilePipe(TargetFilePipe {
lhs: Box::new(lhs), cmd: Some(Box::new(lhs)),
rhs: Box::new(rhs) target: Box::new(rhs),
overwrite: false
})).boxed(); })).boxed();
let and = statement.clone() let pipe_source = pipe_target.clone()
.foldl( .foldl(
and pipe_source
.padded_by(text::inline_whitespace()) .padded_by(text::inline_whitespace())
.ignore_then(statement.clone()) .ignore_then(value.clone())
.repeated(), .repeated(),
|lhs, rhs| Statement::And(And { |lhs, rhs| Statement::SourceFilePipe(SourceFilePipe {
lhs: Box::new(lhs), cmd: Some(Box::new(lhs)),
rhs: Box::new(rhs) source: Box::new(rhs)
})).boxed(); })).boxed();
let pipe = statement.clone() let pipe = pipe_source.clone()
.foldl( .foldl(
pipe pipe
.padded_by(text::inline_whitespace()) .padded_by(text::inline_whitespace())
.ignore_then(statement.clone()) .ignore_then(pipe_source)
.repeated(), .repeated(),
|lhs, rhs| Statement::CommandPipe(CommandPipe { |lhs, rhs| Statement::CommandPipe(CommandPipe {
lhs: Box::new(lhs), lhs: Box::new(lhs),
rhs: Box::new(rhs) rhs: Box::new(rhs)
})).boxed(); })).boxed();
let statement_pair = choice(( let or = pipe.clone()
or, .foldl(
and, or
not, .padded_by(text::inline_whitespace())
pipe, .ignore_then(pipe)
statement .repeated(),
|lhs, rhs| Statement::Or(Or {
lhs: Box::new(lhs),
rhs: Box::new(rhs)
})).boxed();
let and = or.clone()
.foldl(
and
.padded_by(text::inline_whitespace())
.ignore_then(or)
.repeated(),
|lhs, rhs| Statement::And(And {
lhs: Box::new(lhs),
rhs: Box::new(rhs)
})).boxed();
let statement = choice((
set.map(Statement::Set),
function.map(Statement::Function),
return_,
just("break").to(Statement::Break),
just("continue").to(Statement::Continue),
and
)); ));
statement_pair statement
.padded_by(text::inline_whitespace().ignored().or(comment.ignored()))
.separated_by(eol.repeated().at_least(1)) .separated_by(eol.repeated().at_least(1))
.at_least(1) .at_least(1)
.allow_trailing() .allow_trailing()

View file

@ -1,7 +1,6 @@
let test = val set test = val
echo var $test echo var $test
let test = $test set test = $test
echo var2 f"$test" echo last exit code $status
echo last exit code $?
false false
echo last false exit code $? echo last false exit code $status