diff --git a/src/parser.rs b/src/parser.rs index a6ad40f..55c16df 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3,6 +3,7 @@ use anyhow::bail; use anyhow::Result; use monch::*; +use std::path::PathBuf; // Shell grammar rules this is loosely based on: // https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_10_02 @@ -338,6 +339,15 @@ pub fn parse(input: &str) -> Result { } } +fn home_dir() -> Option { + if let Some(userprofile) = std::env::var_os("USERPROFILE") { + if !userprofile.is_empty() { + return Some(PathBuf::from(userprofile)); + } + } + None +} + fn parse_sequential_list(input: &str) -> ParseResult { let (input, items) = separated_list( terminated(parse_sequential_list_item, skip_whitespace), @@ -758,6 +768,25 @@ fn parse_word_parts( ) } + fn expand_tilde(result: &mut [WordPart]) { + if let Some(WordPart::Text(text)) = result.last_mut() { + if text.contains(" ~ ") + || text.contains("~/") + || (text.find('~').unwrap_or(0) == text.len() - 1) + { + let temp = text.clone().replace( + '~', + home_dir() + .unwrap_or(PathBuf::from("~")) + .to_str() + .unwrap_or("~"), + ); + text.clear(); + text.push_str(temp.as_str()); + } + } + } + move |input| { enum PendingPart<'a> { Char(char), @@ -791,7 +820,7 @@ fn parse_word_parts( if_true(next_char, |&c| match mode { ParseWordPartsMode::DoubleQuotes => c != '"', ParseWordPartsMode::Unquoted => { - !c.is_whitespace() && !"~(){}<>|&;\"'".contains(c) + !c.is_whitespace() && !"(){}<>|&;\"'".contains(c) } }), PendingPart::Char, @@ -828,6 +857,10 @@ fn parse_word_parts( } } + if mode == ParseWordPartsMode::Unquoted { + expand_tilde(&mut result); + } + Ok((input, result)) } } @@ -1507,6 +1540,70 @@ mod test { } } + #[test] + fn test_tilde_unquoted_expansion() { + let home_dir = home_dir() + .unwrap_or(PathBuf::from("~")) + .to_str() + .unwrap_or("~") + .to_string(); + run_test( + parse_sequential_list, + "echo ~", + Ok(SequentialList { + items: vec![SequentialListItem { + is_async: false, + sequence: Sequence::Pipeline(Pipeline { + negated: false, + inner: PipelineInner::Command(Command { + inner: CommandInner::Simple(SimpleCommand { + env_vars: [].to_vec(), + args: [ + Word([WordPart::Text("echo".to_string())].to_vec()), + Word([WordPart::Text(home_dir)].to_vec()), + ] + .to_vec(), + }), + redirect: None, + }), + }), + }], + }), + ); + } + + #[test] + fn test_tilde_as_char() { + run_test( + parse_sequential_list, + "echo \"~\"", + Ok(SequentialList { + items: vec![SequentialListItem { + is_async: false, + sequence: Sequence::Pipeline(Pipeline { + negated: false, + inner: PipelineInner::Command(Command { + inner: CommandInner::Simple(SimpleCommand { + env_vars: [].to_vec(), + args: [ + Word([WordPart::Text("echo".to_string())].to_vec()), + Word( + [WordPart::Quoted( + [WordPart::Text("~".to_string())].to_vec(), + )] + .to_vec(), + ), + ] + .to_vec(), + }), + redirect: None, + }), + }), + }], + }), + ); + } + #[test] fn test_redirects() { let expected = Ok(Command {