Skip to content

Instantly share code, notes, and snippets.

@Roger
Created January 18, 2020 15:52
Show Gist options
  • Save Roger/a8128e4b2e2b66ceec4f355db78b0827 to your computer and use it in GitHub Desktop.
Save Roger/a8128e4b2e2b66ceec4f355db78b0827 to your computer and use it in GitHub Desktop.
shellexpand-combine.rs PoC
use std::borrow::Cow;
use std::env;
use combine::error::ParseError;
use combine::parser::char::{alpha_num, char, letter, string};
use combine::parser::combinator::recognize;
use combine::stream::Stream;
use combine::{choice, easy, many1, optional, parser, satisfy, skip_many, EasyParser, Parser};
#[derive(Debug, PartialEq)]
struct Var {
name: String,
default: Option<Box<Value>>,
}
#[derive(Debug, PartialEq)]
enum Value {
Var(Var),
String(String),
}
fn value_var(name: String, default: Option<Box<Value>>) -> Value {
Value::Var(Var { name, default })
}
#[derive(Debug)]
struct ShellExpand {
values: Vec<Value>,
}
fn varname<Input>() -> impl Parser<Input, Output = String>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
recognize((letter(), skip_many(alpha_num())))
}
fn variable<Input>() -> impl Parser<Input, Output = Value>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
let bracet_var = (
// name
varname(),
// default
optional(string(":-").with(shell_value_strict())),
)
.skip(char('}'))
.map(|(name, default)| value_var(name, default.map(|value| Box::new(value))));
let variable = (
char('$'),
optional(choice((
char('{').with(bracet_var),
varname().map(|name| value_var(name, None)),
))),
)
.map(|(_, value)| match value {
None => Value::String("$".into()),
Some(value) => value,
});
variable
}
parser! {
fn shell_value[Input]()(Input) -> Value
where [Input: Stream<Token = char>]
{
let shell_string = many1(satisfy(|c| c != '$')).map(Value::String);
choice((variable(), shell_string))
}
}
parser! {
fn shell_value_strict[Input]()(Input) -> Value
where [Input: Stream<Token = char>]
{
let shell_string = many1(satisfy(|c| c != '$' && c != '}')).map(Value::String);
choice((variable(), shell_string))
}
}
fn parse_shell<Input>() -> impl Parser<Input, Output = Vec<Value>>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
many1(shell_value())
}
fn expand<'a>(value: &'a Value) -> Cow<'a, str> {
match value {
Value::String(string) => Cow::Borrowed(string),
Value::Var(var) => match env::var(&var.name) {
Ok(value) => Cow::Owned(value),
Err(_) => match &var.default {
Some(env) => expand(&env),
None => Cow::Borrowed(""),
},
},
}
}
fn parse(input: &str) -> String {
let shell_values: Result<(Vec<Value>, &str), easy::ParseError<&str>> =
parse_shell().easy_parse(input);
let result = shell_values.map_err(|e| e.map_position(|p| p.translate_position(input)));
result.unwrap().0.iter().map(expand).collect()
}
fn main() {
let input = "$UNSET Test: :- $}$1 ${UNSET:-$} ${UNSET:-42} $HOME/.config/ ${UNSET:-${UNSET2:-$USER}}";
println!("Input: {}", input);
println!("Result: {}", parse(input));
// Output:
// Input: $UNSET Test: :- $}$1 ${UNSET:-$} ${UNSET:-42} $HOME/.config/ ${UNSET:-${UNSET2:-$USER}}
// Result: Test: :- $}$1 $ 42 /home/roger/.config/ roger
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment