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
36const DEFAULT_MAX_COMMAND_BYTES: usize = 16 * 1024;
37pub const SOURCE_FILE_MAX_COMMAND_BYTES: usize = 1024 * 1024;
39
40pub fn parse_command_string(input: &str) -> Result<ParsedCommands, CommandParseError> {
42 CommandParser::new().parse(input)
43}
44
45pub fn parse_command_arguments<I, S>(arguments: I) -> Result<ParsedCommands, CommandParseError>
47where
48 I: IntoIterator<Item = S>,
49 S: AsRef<str>,
50{
51 CommandParser::new().parse_arguments(arguments)
52}
53
54pub fn lookup_command(name: &str) -> Result<&'static CommandEntry, CommandParseError> {
56 lookup_command_at(name, 0, &[])
57}
58
59#[derive(Debug, Clone, Default, PartialEq, Eq)]
61pub struct ParsedCommands {
62 commands: Vec<ParsedCommand>,
63 assignments: Vec<EnvironmentAssignment>,
64 grouping: CommandGrouping,
65}
66
67impl ParsedCommands {
68 fn with_grouping(grouping: CommandGrouping) -> Self {
69 Self {
70 commands: Vec::new(),
71 assignments: Vec::new(),
72 grouping,
73 }
74 }
75
76 #[must_use]
78 pub fn commands(&self) -> &[ParsedCommand] {
79 &self.commands
80 }
81
82 #[must_use]
84 pub fn assignments(&self) -> &[EnvironmentAssignment] {
85 &self.assignments
86 }
87
88 #[must_use]
90 pub const fn grouping(&self) -> CommandGrouping {
91 self.grouping
92 }
93
94 #[must_use]
96 pub fn into_commands(self) -> Vec<ParsedCommand> {
97 self.commands
98 }
99
100 #[must_use]
102 pub fn is_empty(&self) -> bool {
103 self.commands.is_empty()
104 }
105
106 fn push_assignment(&mut self, assignment: EnvironmentAssignment) {
107 self.assignments.push(assignment);
108 }
109
110 fn push_command(&mut self, command: ParsedCommand) {
111 self.commands.push(command);
112 }
113
114 pub fn append(&mut self, mut other: Self) {
117 self.assignments.append(&mut other.assignments);
118 self.commands.append(&mut other.commands);
119 }
120
121 pub fn add_line_offset(&mut self, offset: usize) {
126 if offset == 0 {
127 return;
128 }
129 for command in &mut self.commands {
130 command.add_line_offset(offset);
131 }
132 }
133
134 #[must_use]
136 pub fn to_tmux_string(&self) -> String {
137 self.commands
138 .iter()
139 .map(ParsedCommand::to_tmux_string)
140 .collect::<Vec<_>>()
141 .join(" ; ")
142 }
143
144 #[must_use]
147 pub fn to_tmux_binding_string(&self) -> String {
148 self.commands
149 .iter()
150 .map(ParsedCommand::to_tmux_string)
151 .collect::<Vec<_>>()
152 .join(" \\; ")
153 }
154}
155
156#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
158pub enum CommandGrouping {
159 #[default]
161 ByLine,
162 OneGroup,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct ParsedCommand {
169 name: String,
170 arguments: Vec<CommandArgument>,
171 line: usize,
172}
173
174impl ParsedCommand {
175 #[must_use]
177 pub fn name(&self) -> &str {
178 &self.name
179 }
180
181 #[must_use]
183 pub fn arguments(&self) -> &[CommandArgument] {
184 &self.arguments
185 }
186
187 #[must_use]
189 pub fn line(&self) -> usize {
190 self.line
191 }
192
193 fn new(name: String, arguments: Vec<CommandArgument>, line: usize) -> Self {
194 Self {
195 name,
196 arguments,
197 line,
198 }
199 }
200
201 fn add_line_offset(&mut self, offset: usize) {
202 self.line = self.line.saturating_add(offset);
203 for argument in &mut self.arguments {
204 if let CommandArgument::Commands(commands) = argument {
205 commands.add_line_offset(offset);
206 }
207 }
208 }
209
210 #[must_use]
212 pub fn to_tmux_string(&self) -> String {
213 std::iter::once(self.name.clone())
214 .chain(self.arguments.iter().map(CommandArgument::to_tmux_string))
215 .collect::<Vec<_>>()
216 .join(" ")
217 }
218}
219
220#[derive(Debug, Clone, PartialEq, Eq)]
222pub enum CommandArgument {
223 String(String),
225 Commands(ParsedCommands),
227}
228
229impl CommandArgument {
230 #[must_use]
232 pub fn as_string(&self) -> Option<&str> {
233 match self {
234 Self::String(value) => Some(value),
235 Self::Commands(_) => None,
236 }
237 }
238
239 #[must_use]
241 pub fn to_tmux_string(&self) -> String {
242 match self {
243 Self::String(value) => escape_argument(value),
244 Self::Commands(commands) => format!("{{ {} }}", commands.to_tmux_string()),
245 }
246 }
247}
248
249#[derive(Debug, Clone, PartialEq, Eq)]
251pub struct EnvironmentAssignment {
252 name: String,
253 value: String,
254 hidden: bool,
255}
256
257impl EnvironmentAssignment {
258 #[must_use]
260 pub fn name(&self) -> &str {
261 &self.name
262 }
263
264 #[must_use]
266 pub fn value(&self) -> &str {
267 &self.value
268 }
269
270 #[must_use]
272 pub fn hidden(&self) -> bool {
273 self.hidden
274 }
275
276 fn from_equals(value: String, hidden: bool) -> Self {
277 let (name, value) = value
278 .split_once('=')
279 .expect("lexer only classifies assignments containing '='");
280 Self {
281 name: name.to_owned(),
282 value: value.to_owned(),
283 hidden,
284 }
285 }
286}
287
288#[derive(Debug, Clone)]
290pub struct CommandParser {
291 environment: Vec<(String, String)>,
292 format_variables: Vec<(String, String)>,
293 home_dir: Option<String>,
294 user_home_dirs: Vec<(String, String)>,
295 command_aliases: Vec<CommandAlias>,
296 exact_commands: &'static [CommandEntry],
297 max_command_bytes: usize,
298}
299
300impl Default for CommandParser {
301 fn default() -> Self {
302 Self {
303 environment: Vec::new(),
304 format_variables: Vec::new(),
305 home_dir: None,
306 user_home_dirs: Vec::new(),
307 command_aliases: Vec::new(),
308 exact_commands: &[],
309 max_command_bytes: DEFAULT_MAX_COMMAND_BYTES,
310 }
311 }
312}
313
314impl CommandParser {
315 #[must_use]
317 pub fn new() -> Self {
318 let mut parser = Self::default();
319 parser.command_aliases.extend(CommandAlias::builtin());
320 parser
321 }
322
323 #[must_use]
325 pub fn with_environment_value(
326 mut self,
327 name: impl Into<String>,
328 value: impl Into<String>,
329 ) -> Self {
330 self.environment.push((name.into(), value.into()));
331 self
332 }
333
334 #[must_use]
336 pub fn with_format_value(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
337 self.format_variables.push((name.into(), value.into()));
338 self
339 }
340
341 #[must_use]
343 pub fn with_environment_store(mut self, environment: &EnvironmentStore) -> Self {
344 self.environment.extend(
345 environment
346 .global_entries()
347 .map(|(name, value)| (name.to_owned(), value.to_owned())),
348 );
349 self
350 }
351
352 #[must_use]
354 pub fn with_home_dir(mut self, home_dir: impl Into<String>) -> Self {
355 self.home_dir = Some(home_dir.into());
356 self
357 }
358
359 #[must_use]
361 pub fn with_user_home_dir(
362 mut self,
363 user: impl Into<String>,
364 home_dir: impl Into<String>,
365 ) -> Self {
366 self.user_home_dirs.push((user.into(), home_dir.into()));
367 self
368 }
369
370 pub fn with_command_alias(
372 mut self,
373 definition: impl Into<String>,
374 ) -> Result<Self, CommandParseError> {
375 let definition = definition.into();
376 let Some(alias) = CommandAlias::parse(definition) else {
377 return Err(CommandParseError::new(
378 0,
379 "command-alias entry must be name=value",
380 ));
381 };
382 self.command_aliases.push(alias);
383 Ok(self)
384 }
385
386 #[must_use]
388 pub fn with_command_aliases<I, S>(mut self, definitions: I) -> Self
389 where
390 I: IntoIterator<Item = S>,
391 S: Into<String>,
392 {
393 self.command_aliases.clear();
394 self.command_aliases
395 .extend(definitions.into_iter().filter_map(CommandAlias::parse));
396 self
397 }
398
399 #[must_use]
405 pub fn with_exact_commands(mut self, commands: &'static [CommandEntry]) -> Self {
406 self.exact_commands = commands;
407 self
408 }
409
410 #[must_use]
412 pub fn with_max_command_bytes(mut self, max_command_bytes: usize) -> Self {
413 self.max_command_bytes = max_command_bytes;
414 self
415 }
416
417 pub fn parse(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
419 self.parse_inner(input, false, CommandGrouping::ByLine)
420 }
421
422 pub fn parse_one_group(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
428 self.parse_inner(input, false, CommandGrouping::OneGroup)
429 }
430
431 pub fn parse_arguments<I, S>(&self, arguments: I) -> Result<ParsedCommands, CommandParseError>
436 where
437 I: IntoIterator<Item = S>,
438 S: AsRef<str>,
439 {
440 let arguments = arguments
441 .into_iter()
442 .map(|argument| argument.as_ref().to_owned())
443 .collect::<Vec<_>>();
444 let command_bytes = arguments
445 .iter()
446 .map(String::len)
447 .sum::<usize>()
448 .saturating_add(arguments.len().saturating_sub(1));
449 ensure_command_length(command_bytes, 0, self.max_command_bytes)?;
450
451 let mut commands = ParsedCommands::with_grouping(CommandGrouping::ByLine);
452 let mut current = Vec::new();
453
454 for argument in arguments {
455 let mut value = argument;
456 let mut ends_command = false;
457
458 if value.ends_with(';') {
459 value.pop();
460 if value.ends_with('\\') {
461 value.pop();
462 value.push(';');
463 } else {
464 ends_command = true;
465 }
466 }
467
468 if !ends_command || !value.is_empty() {
469 current.push(CommandArgument::String(value));
470 }
471 if ends_command && !current.is_empty() {
472 commands.push_command(command_from_arguments(std::mem::take(&mut current), 1)?);
473 }
474 }
475
476 if !current.is_empty() {
477 commands.push_command(command_from_arguments(current, 1)?);
478 }
479
480 self.expand_and_lookup(commands, false)
481 }
482
483 fn parse_inner(
484 &self,
485 input: &str,
486 no_alias: bool,
487 grouping: CommandGrouping,
488 ) -> Result<ParsedCommands, CommandParseError> {
489 let mut parser = GrammarParser::new(Lexer::new(input, self), grouping);
490 let commands = parser.parse_all()?;
491 ensure_parsed_command_lengths(&commands, self.max_command_bytes)?;
492 self.expand_and_lookup(commands, no_alias)
493 }
494
495 fn expand_and_lookup(
496 &self,
497 commands: ParsedCommands,
498 no_alias: bool,
499 ) -> Result<ParsedCommands, CommandParseError> {
500 let assignments = commands.assignments.clone();
501 let mut output = ParsedCommands {
502 commands: Vec::new(),
503 assignments: commands.assignments,
504 grouping: commands.grouping,
505 };
506
507 for mut command in commands.commands {
508 if !no_alias {
509 if let Some(alias) = self.find_command_alias(&command.name) {
510 let mut alias_parser = self.clone();
511 alias_parser.environment.extend(
512 assignments
513 .iter()
514 .map(|assignment| (assignment.name.clone(), assignment.value.clone())),
515 );
516 let mut replacement =
517 alias_parser.parse_inner(alias, true, CommandGrouping::OneGroup)?;
518 for replacement_command in &mut replacement.commands {
519 replacement_command.line = command.line;
520 }
521 if let Some(last) = replacement.commands.last_mut() {
522 last.arguments.append(&mut command.arguments);
523 }
524 output.append(replacement);
525 continue;
526 }
527 }
528
529 for argument in &mut command.arguments {
530 if let CommandArgument::Commands(nested) = argument {
531 let nested_commands = std::mem::take(nested);
532 *nested = self.expand_and_lookup(nested_commands, no_alias)?;
533 }
534 }
535
536 let entry = lookup_command_at(&command.name, command.line, self.exact_commands)?;
537 command.name = entry.name.to_owned();
538 output.push_command(command);
539 }
540
541 Ok(output)
542 }
543
544 fn find_command_alias(&self, name: &str) -> Option<&str> {
545 self.command_aliases
546 .iter()
547 .rev()
548 .find(|alias| alias.name() == name)
549 .map(CommandAlias::value)
550 }
551
552 fn lookup_environment(&self, name: &str) -> Option<&str> {
553 self.environment
554 .iter()
555 .rev()
556 .find(|(candidate, _)| candidate == name)
557 .map(|(_, value)| value.as_str())
558 }
559
560 fn expand_tilde(&self, user: &str) -> Option<&str> {
561 if user.is_empty() {
562 return self
563 .lookup_environment("HOME")
564 .filter(|home| !home.is_empty())
565 .or(self.home_dir.as_deref());
566 }
567
568 self.user_home_dirs
569 .iter()
570 .find(|(candidate, _)| candidate == user)
571 .map(|(_, home)| home.as_str())
572 }
573
574 fn condition_is_true(&self, value: &str) -> bool {
575 let expanded = if value.contains("#{") {
576 render_template(
577 value,
578 &ParseTimeFormatVariables {
579 values: &self.format_variables,
580 },
581 )
582 } else {
583 value.to_owned()
584 };
585
586 is_truthy(&expanded)
587 }
588}
589
590fn ensure_command_length(
591 bytes: usize,
592 line: usize,
593 max_command_bytes: usize,
594) -> Result<(), CommandParseError> {
595 if bytes > max_command_bytes {
596 return Err(CommandParseError::new(line, "command too long"));
597 }
598 Ok(())
599}
600
601fn ensure_parsed_command_lengths(
602 commands: &ParsedCommands,
603 max_command_bytes: usize,
604) -> Result<(), CommandParseError> {
605 for command in commands.commands() {
606 ensure_parsed_command_length(command, max_command_bytes)?;
607 }
608 Ok(())
609}
610
611fn ensure_parsed_command_length(
612 command: &ParsedCommand,
613 max_command_bytes: usize,
614) -> Result<(), CommandParseError> {
615 let mut bytes = command.name.len();
616 for argument in command.arguments() {
617 bytes = bytes.saturating_add(1);
618 match argument {
619 CommandArgument::String(value) => {
620 bytes = bytes.saturating_add(value.len());
621 }
622 CommandArgument::Commands(commands) => {
623 ensure_parsed_command_lengths(commands, max_command_bytes)?;
624 }
625 }
626 }
627 ensure_command_length(bytes, command.line(), max_command_bytes)
628}
629
630struct ParseTimeFormatVariables<'a> {
631 values: &'a [(String, String)],
632}
633
634impl FormatVariables for ParseTimeFormatVariables<'_> {
635 fn format_value(&self, _variable: FormatVariable) -> Option<String> {
636 None
637 }
638
639 fn format_value_by_name(&self, name: &str) -> Option<String> {
640 self.values
641 .iter()
642 .rev()
643 .find(|(candidate, _)| candidate == name)
644 .map(|(_, value)| value.clone())
645 }
646}
647
648#[derive(Debug, Clone, PartialEq, Eq)]
650pub struct CommandParseError {
651 line: usize,
652 message: String,
653}
654
655impl CommandParseError {
656 #[must_use]
658 pub fn line(&self) -> usize {
659 self.line
660 }
661
662 #[must_use]
664 pub fn message(&self) -> &str {
665 &self.message
666 }
667
668 pub(crate) fn new(line: usize, message: impl Into<String>) -> Self {
669 Self {
670 line,
671 message: message.into(),
672 }
673 }
674}
675
676impl fmt::Display for CommandParseError {
677 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
678 formatter.write_str(&self.message)
679 }
680}
681
682impl Error for CommandParseError {}
683
684fn command_from_arguments(
685 mut arguments: Vec<CommandArgument>,
686 line: usize,
687) -> Result<ParsedCommand, CommandParseError> {
688 let Some(CommandArgument::String(name)) = arguments.first() else {
689 return Err(CommandParseError::new(line, "no command"));
690 };
691 let name = name.clone();
692 arguments.remove(0);
693 Ok(ParsedCommand::new(name, arguments, line))
694}
695
696fn escape_argument(value: &str) -> String {
697 if value.is_empty() {
698 return "''".to_owned();
699 }
700 if !value.chars().any(argument_needs_quotes) {
701 return value.replace('\\', r"\\");
702 }
703
704 if value.contains('"') && !value.contains('\'') && !value.contains('$') {
705 return format!("'{value}'");
706 }
707
708 format!("\"{}\"", escape_double_quoted_argument(value))
709}
710
711fn argument_needs_quotes(ch: char) -> bool {
712 ch.is_whitespace() || matches!(ch, ';' | '{' | '}' | '\'' | '"' | '#' | '$')
713}
714
715fn escape_double_quoted_argument(value: &str) -> String {
716 let mut escaped = String::with_capacity(value.len());
717 for ch in value.chars() {
718 match ch {
719 '$' => escaped.push_str(r"\$"),
720 '\\' | '"' => {
721 escaped.push('\\');
722 escaped.push(ch);
723 }
724 _ => escaped.push(ch),
725 }
726 }
727 escaped
728}
729
730#[cfg(test)]
731#[path = "command_parser/tests.rs"]
732mod tests;