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