Created
January 18, 2020 15:52
-
-
Save Roger/a8128e4b2e2b66ceec4f355db78b0827 to your computer and use it in GitHub Desktop.
shellexpand-combine.rs PoC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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