1use std::error::Error;
12use std::fmt;
13
14use crate::{
15 formats::{is_truthy, render_template, FormatVariable, FormatVariables},
16 EnvironmentStore,
17};
18
19#[path = "command_parser/aliases.rs"]
20mod aliases;
21#[path = "command_parser/grammar.rs"]
22mod grammar;
23#[path = "command_parser/lexer.rs"]
24mod lexer;
25#[path = "command_parser/lookup.rs"]
26mod lookup;
27#[path = "command_parser/table.rs"]
28mod table;
29
30use aliases::CommandAlias;
31use grammar::GrammarParser;
32use lexer::Lexer;
33use lookup::lookup_command_at;
34pub use table::{CommandEntry, COMMAND_TABLE};
35
36pub fn parse_command_string(input: &str) -> Result<ParsedCommands, CommandParseError> {
38 CommandParser::new().parse(input)
39}
40
41pub fn parse_command_arguments<I, S>(arguments: I) -> Result<ParsedCommands, CommandParseError>
43where
44 I: IntoIterator<Item = S>,
45 S: AsRef<str>,
46{
47 CommandParser::new().parse_arguments(arguments)
48}
49
50pub fn lookup_command(name: &str) -> Result<&'static CommandEntry, CommandParseError> {
52 lookup_command_at(name, 0)
53}
54
55#[derive(Debug, Clone, Default, PartialEq, Eq)]
57pub struct ParsedCommands {
58 commands: Vec<ParsedCommand>,
59 assignments: Vec<EnvironmentAssignment>,
60 grouping: CommandGrouping,
61}
62
63impl ParsedCommands {
64 fn with_grouping(grouping: CommandGrouping) -> Self {
65 Self {
66 commands: Vec::new(),
67 assignments: Vec::new(),
68 grouping,
69 }
70 }
71
72 #[must_use]
74 pub fn commands(&self) -> &[ParsedCommand] {
75 &self.commands
76 }
77
78 #[must_use]
80 pub fn assignments(&self) -> &[EnvironmentAssignment] {
81 &self.assignments
82 }
83
84 #[must_use]
86 pub const fn grouping(&self) -> CommandGrouping {
87 self.grouping
88 }
89
90 #[must_use]
92 pub fn into_commands(self) -> Vec<ParsedCommand> {
93 self.commands
94 }
95
96 #[must_use]
98 pub fn is_empty(&self) -> bool {
99 self.commands.is_empty()
100 }
101
102 fn push_assignment(&mut self, assignment: EnvironmentAssignment) {
103 self.assignments.push(assignment);
104 }
105
106 fn push_command(&mut self, command: ParsedCommand) {
107 self.commands.push(command);
108 }
109
110 pub fn append(&mut self, mut other: Self) {
113 self.assignments.append(&mut other.assignments);
114 self.commands.append(&mut other.commands);
115 }
116
117 #[must_use]
119 pub fn to_tmux_string(&self) -> String {
120 self.commands
121 .iter()
122 .map(ParsedCommand::to_tmux_string)
123 .collect::<Vec<_>>()
124 .join(" ; ")
125 }
126}
127
128#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
130pub enum CommandGrouping {
131 #[default]
133 ByLine,
134 OneGroup,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct ParsedCommand {
141 name: String,
142 arguments: Vec<CommandArgument>,
143 line: usize,
144}
145
146impl ParsedCommand {
147 #[must_use]
149 pub fn name(&self) -> &str {
150 &self.name
151 }
152
153 #[must_use]
155 pub fn arguments(&self) -> &[CommandArgument] {
156 &self.arguments
157 }
158
159 #[must_use]
161 pub fn line(&self) -> usize {
162 self.line
163 }
164
165 fn new(name: String, arguments: Vec<CommandArgument>, line: usize) -> Self {
166 Self {
167 name,
168 arguments,
169 line,
170 }
171 }
172
173 fn to_tmux_string(&self) -> String {
174 std::iter::once(self.name.clone())
175 .chain(self.arguments.iter().map(CommandArgument::to_tmux_string))
176 .collect::<Vec<_>>()
177 .join(" ")
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
183pub enum CommandArgument {
184 String(String),
186 Commands(ParsedCommands),
188}
189
190impl CommandArgument {
191 #[must_use]
193 pub fn as_string(&self) -> Option<&str> {
194 match self {
195 Self::String(value) => Some(value),
196 Self::Commands(_) => None,
197 }
198 }
199
200 #[must_use]
202 pub fn to_tmux_string(&self) -> String {
203 match self {
204 Self::String(value) => escape_argument(value),
205 Self::Commands(commands) => format!("{{ {} }}", commands.to_tmux_string()),
206 }
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq)]
212pub struct EnvironmentAssignment {
213 name: String,
214 value: String,
215 hidden: bool,
216}
217
218impl EnvironmentAssignment {
219 #[must_use]
221 pub fn name(&self) -> &str {
222 &self.name
223 }
224
225 #[must_use]
227 pub fn value(&self) -> &str {
228 &self.value
229 }
230
231 #[must_use]
233 pub fn hidden(&self) -> bool {
234 self.hidden
235 }
236
237 fn from_equals(value: String, hidden: bool) -> Self {
238 let (name, value) = value
239 .split_once('=')
240 .expect("lexer only classifies assignments containing '='");
241 Self {
242 name: name.to_owned(),
243 value: value.to_owned(),
244 hidden,
245 }
246 }
247}
248
249#[derive(Debug, Clone, Default)]
251pub struct CommandParser {
252 environment: Vec<(String, String)>,
253 format_variables: Vec<(String, String)>,
254 home_dir: Option<String>,
255 user_home_dirs: Vec<(String, String)>,
256 command_aliases: Vec<CommandAlias>,
257}
258
259impl CommandParser {
260 #[must_use]
262 pub fn new() -> Self {
263 let mut parser = Self::default();
264 parser.command_aliases.extend(CommandAlias::builtin());
265 parser
266 }
267
268 #[must_use]
270 pub fn with_environment_value(
271 mut self,
272 name: impl Into<String>,
273 value: impl Into<String>,
274 ) -> Self {
275 self.environment.push((name.into(), value.into()));
276 self
277 }
278
279 #[must_use]
281 pub fn with_format_value(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
282 self.format_variables.push((name.into(), value.into()));
283 self
284 }
285
286 #[must_use]
288 pub fn with_environment_store(mut self, environment: &EnvironmentStore) -> Self {
289 self.environment.extend(
290 environment
291 .global_entries()
292 .map(|(name, value)| (name.to_owned(), value.to_owned())),
293 );
294 self
295 }
296
297 #[must_use]
299 pub fn with_home_dir(mut self, home_dir: impl Into<String>) -> Self {
300 self.home_dir = Some(home_dir.into());
301 self
302 }
303
304 #[must_use]
306 pub fn with_user_home_dir(
307 mut self,
308 user: impl Into<String>,
309 home_dir: impl Into<String>,
310 ) -> Self {
311 self.user_home_dirs.push((user.into(), home_dir.into()));
312 self
313 }
314
315 pub fn with_command_alias(
317 mut self,
318 definition: impl Into<String>,
319 ) -> Result<Self, CommandParseError> {
320 let definition = definition.into();
321 let Some(alias) = CommandAlias::parse(definition) else {
322 return Err(CommandParseError::new(
323 0,
324 "command-alias entry must be name=value",
325 ));
326 };
327 self.command_aliases.push(alias);
328 Ok(self)
329 }
330
331 #[must_use]
333 pub fn with_command_aliases<I, S>(mut self, definitions: I) -> Self
334 where
335 I: IntoIterator<Item = S>,
336 S: Into<String>,
337 {
338 self.command_aliases.clear();
339 self.command_aliases
340 .extend(definitions.into_iter().filter_map(CommandAlias::parse));
341 self
342 }
343
344 pub fn parse(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
346 self.parse_inner(input, false, CommandGrouping::ByLine)
347 }
348
349 pub fn parse_one_group(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
355 self.parse_inner(input, false, CommandGrouping::OneGroup)
356 }
357
358 pub fn parse_arguments<I, S>(&self, arguments: I) -> Result<ParsedCommands, CommandParseError>
363 where
364 I: IntoIterator<Item = S>,
365 S: AsRef<str>,
366 {
367 let mut commands = ParsedCommands::with_grouping(CommandGrouping::ByLine);
368 let mut current = Vec::new();
369
370 for argument in arguments {
371 let mut value = argument.as_ref().to_owned();
372 let mut ends_command = false;
373
374 if value.ends_with(';') {
375 value.pop();
376 if value.ends_with('\\') {
377 value.pop();
378 value.push(';');
379 } else {
380 ends_command = true;
381 }
382 }
383
384 if !ends_command || !value.is_empty() {
385 current.push(CommandArgument::String(value));
386 }
387 if ends_command && !current.is_empty() {
388 commands.push_command(command_from_arguments(std::mem::take(&mut current), 1)?);
389 }
390 }
391
392 if !current.is_empty() {
393 commands.push_command(command_from_arguments(current, 1)?);
394 }
395
396 self.expand_and_lookup(commands, false)
397 }
398
399 fn parse_inner(
400 &self,
401 input: &str,
402 no_alias: bool,
403 grouping: CommandGrouping,
404 ) -> Result<ParsedCommands, CommandParseError> {
405 let mut parser = GrammarParser::new(Lexer::new(input, self), grouping);
406 let commands = parser.parse_all()?;
407 self.expand_and_lookup(commands, no_alias)
408 }
409
410 fn expand_and_lookup(
411 &self,
412 commands: ParsedCommands,
413 no_alias: bool,
414 ) -> Result<ParsedCommands, CommandParseError> {
415 let assignments = commands.assignments.clone();
416 let mut output = ParsedCommands {
417 commands: Vec::new(),
418 assignments: commands.assignments,
419 grouping: commands.grouping,
420 };
421
422 for mut command in commands.commands {
423 if !no_alias {
424 if let Some(alias) = self.find_command_alias(&command.name) {
425 let mut alias_parser = self.clone();
426 alias_parser.environment.extend(
427 assignments
428 .iter()
429 .map(|assignment| (assignment.name.clone(), assignment.value.clone())),
430 );
431 let mut replacement =
432 alias_parser.parse_inner(alias, true, CommandGrouping::OneGroup)?;
433 for replacement_command in &mut replacement.commands {
434 replacement_command.line = command.line;
435 }
436 if let Some(last) = replacement.commands.last_mut() {
437 last.arguments.append(&mut command.arguments);
438 }
439 output.append(replacement);
440 continue;
441 }
442 }
443
444 for argument in &mut command.arguments {
445 if let CommandArgument::Commands(nested) = argument {
446 let nested_commands = std::mem::take(nested);
447 *nested = self.expand_and_lookup(nested_commands, no_alias)?;
448 }
449 }
450
451 let entry = lookup_command_at(&command.name, command.line)?;
452 command.name = entry.name.to_owned();
453 output.push_command(command);
454 }
455
456 Ok(output)
457 }
458
459 fn find_command_alias(&self, name: &str) -> Option<&str> {
460 self.command_aliases
461 .iter()
462 .rev()
463 .find(|alias| alias.name() == name)
464 .map(CommandAlias::value)
465 }
466
467 fn lookup_environment(&self, name: &str) -> Option<&str> {
468 self.environment
469 .iter()
470 .rev()
471 .find(|(candidate, _)| candidate == name)
472 .map(|(_, value)| value.as_str())
473 }
474
475 fn expand_tilde(&self, user: &str) -> Option<&str> {
476 if user.is_empty() {
477 return self
478 .lookup_environment("HOME")
479 .filter(|home| !home.is_empty())
480 .or(self.home_dir.as_deref());
481 }
482
483 self.user_home_dirs
484 .iter()
485 .find(|(candidate, _)| candidate == user)
486 .map(|(_, home)| home.as_str())
487 }
488
489 fn condition_is_true(&self, value: &str) -> bool {
490 let expanded = if value.contains("#{") {
491 render_template(
492 value,
493 &ParseTimeFormatVariables {
494 values: &self.format_variables,
495 },
496 )
497 } else {
498 value.to_owned()
499 };
500
501 is_truthy(&expanded)
502 }
503}
504
505struct ParseTimeFormatVariables<'a> {
506 values: &'a [(String, String)],
507}
508
509impl FormatVariables for ParseTimeFormatVariables<'_> {
510 fn format_value(&self, _variable: FormatVariable) -> Option<String> {
511 None
512 }
513
514 fn format_value_by_name(&self, name: &str) -> Option<String> {
515 self.values
516 .iter()
517 .rev()
518 .find(|(candidate, _)| candidate == name)
519 .map(|(_, value)| value.clone())
520 }
521}
522
523#[derive(Debug, Clone, PartialEq, Eq)]
525pub struct CommandParseError {
526 line: usize,
527 message: String,
528}
529
530impl CommandParseError {
531 #[must_use]
533 pub fn line(&self) -> usize {
534 self.line
535 }
536
537 #[must_use]
539 pub fn message(&self) -> &str {
540 &self.message
541 }
542
543 pub(crate) fn new(line: usize, message: impl Into<String>) -> Self {
544 Self {
545 line,
546 message: message.into(),
547 }
548 }
549}
550
551impl fmt::Display for CommandParseError {
552 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
553 formatter.write_str(&self.message)
554 }
555}
556
557impl Error for CommandParseError {}
558
559fn command_from_arguments(
560 mut arguments: Vec<CommandArgument>,
561 line: usize,
562) -> Result<ParsedCommand, CommandParseError> {
563 let Some(CommandArgument::String(name)) = arguments.first() else {
564 return Err(CommandParseError::new(line, "no command"));
565 };
566 let name = name.clone();
567 arguments.remove(0);
568 Ok(ParsedCommand::new(name, arguments, line))
569}
570
571fn escape_argument(value: &str) -> String {
572 if value.is_empty() {
573 return "''".to_owned();
574 }
575 if !value
576 .chars()
577 .any(|ch| ch.is_whitespace() || matches!(ch, ';' | '{' | '}' | '\'' | '"' | '#'))
578 {
579 return value.to_owned();
580 }
581
582 format!("'{}'", value.replace('\'', "'\\''"))
583}
584
585#[cfg(test)]
586#[path = "command_parser/tests.rs"]
587mod tests;