Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ monch = "0.5.0"
thiserror = "2.0.9"
tokio = { version = "1", features = ["fs", "io-std", "io-util", "macros", "process", "rt-multi-thread", "sync", "time"], optional = true }
deno_path_util = "0.3.2"
sys_traits = { version = "0.1.8", features = ["real"] }
sys_traits = { version = "0.1.9", features = ["real", "winapi", "libc"] }

[target.'cfg(unix)'.dependencies]
nix = { version = "0.29.0", features = ["signal"], optional = true }
Expand Down
20 changes: 14 additions & 6 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ pub enum WordPart {
Text(String),
/// Variable substitution (ex. `$MY_VAR`)
Variable(String),
/// Tilde expansion (ex. `~`)
Tilde,
/// Command substitution (ex. `$(command)`)
Command(SequentialList),
/// Quoted string (ex. `"hello"` or `'test'`)
Expand Down Expand Up @@ -751,30 +753,35 @@ fn parse_word_parts(
or7(
parse_special_shell_var,
parse_escaped_dollar_sign,
parse_escaped_char('~'),
parse_escaped_char('`'),
parse_escaped_char('"'),
parse_escaped_char('('),
parse_escaped_char(')'),
if_true(parse_escaped_char('\''), move |_| {
mode == ParseWordPartsMode::DoubleQuotes
}),
or(
parse_escaped_char(')'),
if_true(parse_escaped_char('\''), move |_| {
mode == ParseWordPartsMode::DoubleQuotes
}),
),
)
}

move |input| {
enum PendingPart<'a> {
Char(char),
Variable(&'a str),
Tilde,
Command(SequentialList),
Parts(Vec<WordPart>),
}

let (input, parts) = many0(or7(
or(
or3(
map(tag("$?"), |_| PendingPart::Variable("?")),
map(first_escaped_char(mode), PendingPart::Char),
map(parse_command_substitution, PendingPart::Command),
),
map(parse_command_substitution, PendingPart::Command),
map(ch('~'), |_| PendingPart::Tilde),
map(preceded(ch('$'), parse_env_var_name), PendingPart::Variable),
|input| {
let (_, _) = ch('`')(input)?;
Expand Down Expand Up @@ -821,6 +828,7 @@ fn parse_word_parts(
result.push(WordPart::Text(c.to_string()));
}
}
PendingPart::Tilde => result.push(WordPart::Tilde),
PendingPart::Command(s) => result.push(WordPart::Command(s)),
PendingPart::Variable(v) => {
result.push(WordPart::Variable(v.to_string()))
Expand Down
4 changes: 3 additions & 1 deletion src/shell/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,7 @@ pub enum EvaluateWordTextError {
NotUtf8Pattern { part: OsString },
#[error("glob: no matches found '{}'", pattern)]
NoFilesMatched { pattern: String },
#[error("Invalid utf-8: {}", err)]
#[error("invalid utf-8: {}", err)]
InvalidUtf8 {
#[from]
err: FromUtf8Error,
Expand Down Expand Up @@ -904,6 +904,8 @@ fn evaluate_word_parts(
None
}
WordPart::Variable(name) => state.get_var(OsStr::new(&name)).cloned(),
WordPart::Tilde => sys_traits::impls::real_home_dir_with_env(state)
.map(|s| s.into_os_string()),
WordPart::Command(list) => Some(
evaluate_command_substitution(
list,
Expand Down
6 changes: 6 additions & 0 deletions src/shell/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ impl ShellState {
}
}

impl sys_traits::BaseEnvVar for ShellState {
fn base_env_var_os(&self, key: &OsStr) -> Option<OsString> {
self.env_vars.get(key).cloned()
}
}

#[derive(Debug, PartialEq, Eq)]
pub enum EnvChange {
// `export ENV_VAR=VALUE`
Expand Down
14 changes: 14 additions & 0 deletions tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,20 @@ async fn paren_escapes() {
.await;
}

#[tokio::test]
async fn tilde_expansion() {
TestBuilder::new()
.command(concat!(
r"export HOME=/home/dir && echo ~/test && echo ~ && ",
r"echo \~ && export HOME=/test && echo ~ && ",
r"HOME=/nope echo ~ && ",
r"HOME=/nope $(echo echo ~)"
))
.assert_stdout("/home/dir/test\n/home/dir\n~\n/test\n/test\n/test\n")
.run()
.await;
}

#[tokio::test]
async fn cross_platform_shebang() {
// with -S
Expand Down
Loading