Skip to main content

ase_shell/commands/
parse.rs

1use std::{env, path::PathBuf};
2
3use glob::glob;
4
5use super::targets::{StderrTarget, StdoutTarget};
6
7pub fn needs_more_input(raw: &str) -> bool {
8  let r = raw.trim();
9  !r.is_empty() && shlex::split(r).is_none()
10}
11
12pub struct ParsedInvocation {
13  pub cmd_name: String,
14  pub args: Vec<String>,
15  pub stdout: StdoutTarget,
16  pub stderr: StderrTarget,
17}
18
19impl ParsedInvocation {
20  pub fn from_tokens(tokens: Vec<String>) -> Option<Self> {
21    let mut iter = tokens.into_iter();
22    let cmd_name = iter.next()?;
23    let rest: Vec<String> = iter.collect();
24
25    let mut stdout = StdoutTarget::Stdout;
26    let mut stderr = StderrTarget::Stderr;
27    let mut args = Vec::new();
28    let mut i = 0;
29
30    while i < rest.len() {
31      match rest[i].as_str() {
32        ">>" | "1>>" => {
33          if i + 1 < rest.len() {
34            let target = expand_single_path(&rest[i + 1]);
35            stdout = StdoutTarget::Append(PathBuf::from(target));
36            i += 2;
37            continue;
38          } else {
39            args.push(rest[i].clone());
40            i += 1;
41            continue;
42          }
43        }
44        ">" | "1>" => {
45          if i + 1 < rest.len() {
46            let target = expand_single_path(&rest[i + 1]);
47            stdout = StdoutTarget::Overwrite(PathBuf::from(target));
48            i += 2;
49            continue;
50          } else {
51            args.push(rest[i].clone());
52            i += 1;
53            continue;
54          }
55        }
56        "2>>" => {
57          if i + 1 < rest.len() {
58            let target = expand_single_path(&rest[i + 1]);
59            stderr = StderrTarget::Append(PathBuf::from(target));
60            i += 2;
61            continue;
62          } else {
63            args.push(rest[i].clone());
64            i += 1;
65            continue;
66          }
67        }
68        "2>" => {
69          if i + 1 < rest.len() {
70            let target = expand_single_path(&rest[i + 1]);
71            stderr = StderrTarget::Overwrite(PathBuf::from(target));
72            i += 2;
73            continue;
74          } else {
75            args.push(rest[i].clone());
76            i += 1;
77            continue;
78          }
79        }
80        _ => {
81          let expanded = expand_arg(&rest[i]);
82          args.extend(expanded);
83          i += 1;
84        }
85      }
86    }
87
88    Some(ParsedInvocation {
89      cmd_name,
90      args,
91      stdout,
92      stderr,
93    })
94  }
95}
96
97fn expand_arg(token: &str) -> Vec<String> {
98  let token = expand_vars_and_tilde(token);
99
100  if has_glob_meta(&token) {
101    let mut results = Vec::new();
102    if let Ok(paths) = glob(&token) {
103      for entry in paths.flatten() {
104        if let Some(s) = entry.to_str() {
105          results.push(s.to_string());
106        }
107      }
108    }
109    if !results.is_empty() {
110      return results;
111    }
112  }
113
114  vec![token]
115}
116
117fn expand_single_path(token: &str) -> String {
118  expand_vars_and_tilde(token)
119}
120
121fn expand_vars_and_tilde(token: &str) -> String {
122  let mut s = token.to_string();
123
124  if let Some(home) = env::var_os("HOME") {
125    if let Some(stripped) = s.strip_prefix('~') {
126      if stripped.is_empty() || stripped.starts_with('/') {
127        if let Some(home_str) = home.to_str() {
128          s = format!("{home_str}{stripped}");
129        }
130      }
131    }
132  }
133
134  s = expand_vars(&s);
135  s
136}
137
138fn expand_vars(s: &str) -> String {
139  let mut out = String::with_capacity(s.len());
140  let bytes = s.as_bytes();
141  let mut i = 0;
142
143  while i < bytes.len() {
144    if bytes[i] == b'$' {
145      let start = i + 1;
146      let mut j = start;
147      while j < bytes.len() && (bytes[j] == b'_' || bytes[j].is_ascii_alphanumeric()) {
148        j += 1;
149      }
150      if j > start {
151        let name = &s[start..j];
152        if let Ok(val) = env::var(name) {
153          out.push_str(&val);
154        }
155        i = j;
156        continue;
157      }
158    }
159
160    out.push(bytes[i] as char);
161    i += 1;
162  }
163
164  out
165}
166
167fn has_glob_meta(s: &str) -> bool {
168  s.chars().any(|c| matches!(c, '*' | '?' | '['))
169}