fm/modes/utils/
shell_parser.rs1use anyhow::{bail, Context, Result};
2
3use crate::app::Status;
4use crate::common::{get_clipboard, path_to_string};
5use crate::modes::Quote;
6use crate::{log_info, log_line};
7
8pub const SAME_WINDOW_TOKEN: &str = "%t";
11
12pub fn shell_command_parser(command: &str, status: &Status) -> Result<Vec<String>> {
32 let Ok(tokens) = Lexer::new(command).lexer() else {
33 return shell_command_parser_error("Syntax error in the command", command);
34 };
35 let Ok(args) = Parser::new(tokens).parse(status) else {
36 return shell_command_parser_error("Couldn't parse the command", command);
37 };
38 build_args(args)
39}
40
41fn shell_command_parser_error(message: &str, command: &str) -> Result<Vec<String>> {
42 log_info!("{message} {command}");
43 log_line!("{message} {command}");
44 bail!("{message} {command}");
45}
46
47#[derive(Debug)]
48enum Token {
49 Identifier(String),
50 StringLiteral((char, String)),
51 FmExpansion(FmExpansion),
52}
53
54#[derive(Debug)]
55enum FmExpansion {
56 Selected,
57 SelectedFilename,
58 SelectedPath,
59 Extension,
60 Flagged,
61 Term,
62 Clipboard,
63 Invalid,
64}
65
66impl FmExpansion {
67 fn from(c: char) -> Self {
68 match c {
69 's' => Self::Selected,
70 'n' => Self::SelectedFilename,
71 'd' => Self::SelectedPath,
72 'e' => Self::Extension,
73 'f' => Self::Flagged,
74 't' => Self::Term,
75 'c' => Self::Clipboard,
76 _ => Self::Invalid,
77 }
78 }
79
80 fn parse(&self, status: &Status) -> Result<Vec<String>> {
81 match self {
82 Self::Invalid => bail!("Invalid Fm Expansion"),
83 Self::Term => Self::term(status),
84 Self::Selected => Self::selected(status),
85 Self::Flagged => Self::flagged(status),
86 Self::SelectedPath => Self::path(status),
87 Self::SelectedFilename => Self::filename(status),
88 Self::Clipboard => Self::clipboard(),
89 Self::Extension => Self::extension(status),
90 }
91 }
92
93 fn selected(status: &Status) -> Result<Vec<String>> {
94 Ok(vec![status.current_tab().current_file_string()?.quote()?])
95 }
96
97 fn path(status: &Status) -> Result<Vec<String>> {
98 Ok(vec![status.current_tab().directory_str().quote()?])
99 }
100
101 fn filename(status: &Status) -> Result<Vec<String>> {
102 Ok(vec![status
103 .current_tab()
104 .selected_path()
105 .context("No selected file")?
106 .file_name()
107 .context("No filename")?
108 .quote()?])
109 }
110
111 fn extension(status: &Status) -> Result<Vec<String>> {
112 Ok(vec![status
113 .current_tab()
114 .selected_path()
115 .context("No selected file")?
116 .extension()
117 .context("No extension")?
118 .quote()?])
119 }
120
121 fn flagged(status: &Status) -> Result<Vec<String>> {
122 Ok(status
123 .menu
124 .flagged
125 .content
126 .iter()
127 .map(path_to_string)
128 .filter_map(|s| s.quote().ok())
129 .collect())
130 }
131
132 fn term(_status: &Status) -> Result<Vec<String>> {
133 Ok(vec![SAME_WINDOW_TOKEN.to_owned()])
134 }
135
136 fn clipboard() -> Result<Vec<String>> {
137 let Some(clipboard) = get_clipboard() else {
138 bail!("Couldn't read the clipboard");
139 };
140 Ok(clipboard.split_whitespace().map(|s| s.to_owned()).collect())
141 }
142}
143
144enum State {
145 Start,
146 Arg,
147 StringLiteral(char),
148 FmExpansion,
149}
150
151struct Lexer {
152 command: String,
153}
154
155impl Lexer {
156 fn new(command: &str) -> Self {
157 Self {
158 command: command.trim().to_owned(),
159 }
160 }
161
162 fn lexer(&self) -> Result<Vec<Token>> {
163 let mut tokens = vec![];
164 let mut state = State::Start;
165 let mut current = String::new();
166
167 for c in self.command.chars() {
168 match &state {
169 State::Start => {
170 if c == '"' || c == '\'' {
171 state = State::StringLiteral(c);
172 } else if c == '%' {
173 state = State::FmExpansion;
174 } else {
175 state = State::Arg;
176 current.push(c);
177 }
178 }
179 State::Arg => {
180 if c == '%' {
181 tokens.push(Token::Identifier(current.clone()));
182 current.clear();
183 state = State::FmExpansion;
184 } else if c == '"' || c == '\'' {
185 tokens.push(Token::Identifier(current.clone()));
186 current.clear();
187 state = State::StringLiteral(c);
188 } else {
189 current.push(c);
190 }
191 }
192 State::StringLiteral(quote_type) => {
193 if c == *quote_type {
194 tokens.push(Token::StringLiteral((c, current.clone())));
195 current.clear();
196 state = State::Start;
197 } else {
198 current.push(c);
199 }
200 }
201 State::FmExpansion => {
202 if c.is_alphanumeric() {
203 let expansion = FmExpansion::from(c);
204 if let FmExpansion::Invalid = expansion {
205 bail!("Invalid FmExpansion %{c}")
206 }
207 if let FmExpansion::Term = expansion {
208 if !tokens.is_empty() {
209 bail!("Term expansion can only be the first argument")
210 }
211 }
212 tokens.push(Token::FmExpansion(expansion));
213 current.clear();
214 state = State::Start;
215 } else {
216 bail!("Invalid FmExpansion %{c}.")
217 }
218 }
219 }
220 }
221
222 match &state {
223 State::Arg => tokens.push(Token::Identifier(current)),
224 State::StringLiteral(quote) => tokens.push(Token::StringLiteral((*quote,current))),
225 State::FmExpansion => bail!("Invalid syntax for {command}. Matching an FmExpansion with {current} which is impossible.", command=self.command),
226 State::Start => (),
227 }
228
229 Ok(tokens)
230 }
231}
232
233struct Parser {
234 tokens: Vec<Token>,
235}
236
237impl Parser {
238 fn new(tokens: Vec<Token>) -> Self {
239 Self { tokens }
240 }
241
242 fn parse(&self, status: &Status) -> Result<Vec<String>> {
243 if self.tokens.is_empty() {
244 bail!("Empty tokens")
245 }
246 let mut args: Vec<String> = vec![];
247 for token in self.tokens.iter() {
248 match token {
249 Token::Identifier(identifier) => args.push(identifier.to_owned()),
250 Token::FmExpansion(fm_expansion) => {
251 let Ok(mut expansion) = fm_expansion.parse(status) else {
252 log_line!("Invalid expansion {fm_expansion:?}");
253 log_info!("Invalid expansion {fm_expansion:?}");
254 bail!("Invalid expansion {fm_expansion:?}")
255 };
256 args.append(&mut expansion)
257 }
258 Token::StringLiteral((quote, string)) => {
259 args.push(format!("{quote}{string}{quote}"))
260 }
261 };
262 }
263 Ok(args)
264 }
265}
266
267fn build_args(args: Vec<String>) -> Result<Vec<String>> {
268 log_info!("build_args {args:?}");
269 if args.is_empty() {
270 bail!("Empty command");
271 }
272 if args[0].starts_with("sudo") {
273 Ok(build_sudo_args(args))
274 } else if args[0].starts_with(SAME_WINDOW_TOKEN) {
275 Ok(args)
276 } else {
277 Ok(build_normal_args(args))
278 }
279}
280
281fn build_sudo_args(args: Vec<String>) -> Vec<String> {
282 let rebuild = args.join("");
283 rebuild.split_whitespace().map(|s| s.to_owned()).collect()
284}
285
286fn build_normal_args(args: Vec<String>) -> Vec<String> {
287 vec!["sh".to_owned(), "-c".to_owned(), args.join("")]
288}
289