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_structure(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
427 let mut parser = GrammarParser::new(Lexer::new(input, self), CommandGrouping::ByLine);
428 let commands = parser.parse_all()?;
429 ensure_parsed_command_lengths(&commands, self.max_command_bytes)?;
430 Ok(commands)
431 }
432
433 pub fn parse_source_file(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
441 self.parse_source_file_inner(input, false, CommandGrouping::ByLine)
442 }
443
444 pub fn parse_source_file_structure(
448 &self,
449 input: &str,
450 ) -> Result<ParsedCommands, CommandParseError> {
451 let mut parser =
452 GrammarParser::new(Lexer::new_source_file(input, self), CommandGrouping::ByLine);
453 let commands = parser.parse_all()?;
454 ensure_parsed_command_lengths(&commands, self.max_command_bytes)?;
455 Ok(commands)
456 }
457
458 pub fn parse_one_group(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
464 self.parse_inner(input, false, CommandGrouping::OneGroup)
465 }
466
467 pub fn parse_arguments<I, S>(&self, arguments: I) -> Result<ParsedCommands, CommandParseError>
472 where
473 I: IntoIterator<Item = S>,
474 S: AsRef<str>,
475 {
476 let arguments = arguments
477 .into_iter()
478 .map(|argument| argument.as_ref().to_owned())
479 .collect::<Vec<_>>();
480 let command_bytes = arguments
481 .iter()
482 .map(String::len)
483 .sum::<usize>()
484 .saturating_add(arguments.len().saturating_sub(1));
485 ensure_command_length(command_bytes, 0, self.max_command_bytes)?;
486
487 let mut commands = ParsedCommands::with_grouping(CommandGrouping::ByLine);
488 let mut current = Vec::new();
489
490 for argument in arguments {
491 let mut value = argument;
492 let mut ends_command = false;
493
494 if value.ends_with(';') {
495 value.pop();
496 if value.ends_with('\\') {
497 value.pop();
498 value.push(';');
499 } else {
500 ends_command = true;
501 }
502 }
503
504 if !ends_command || !value.is_empty() {
505 current.push(CommandArgument::String(value));
506 }
507 if ends_command && !current.is_empty() {
508 commands.push_command(command_from_arguments(std::mem::take(&mut current), 1)?);
509 }
510 }
511
512 if !current.is_empty() {
513 commands.push_command(command_from_arguments(current, 1)?);
514 }
515
516 self.expand_and_lookup(commands, false)
517 }
518
519 fn parse_inner(
520 &self,
521 input: &str,
522 no_alias: bool,
523 grouping: CommandGrouping,
524 ) -> Result<ParsedCommands, CommandParseError> {
525 let mut parser = GrammarParser::new(Lexer::new(input, self), grouping);
526 let commands = parser.parse_all()?;
527 ensure_parsed_command_lengths(&commands, self.max_command_bytes)?;
528 self.expand_and_lookup(commands, no_alias)
529 }
530
531 fn parse_source_file_inner(
532 &self,
533 input: &str,
534 no_alias: bool,
535 grouping: CommandGrouping,
536 ) -> Result<ParsedCommands, CommandParseError> {
537 let mut parser = GrammarParser::new(Lexer::new_source_file(input, self), grouping);
538 let commands = parser.parse_all()?;
539 ensure_parsed_command_lengths(&commands, self.max_command_bytes)?;
540 self.expand_and_lookup(commands, no_alias)
541 }
542
543 fn expand_and_lookup(
544 &self,
545 commands: ParsedCommands,
546 no_alias: bool,
547 ) -> Result<ParsedCommands, CommandParseError> {
548 let assignments = commands.assignments.clone();
549 let mut output = ParsedCommands {
550 commands: Vec::new(),
551 assignments: commands.assignments,
552 grouping: commands.grouping,
553 };
554
555 for mut command in commands.commands {
556 if !no_alias {
557 if let Some(alias) = self.find_command_alias(&command.name) {
558 let mut alias_parser = self.clone();
559 alias_parser.environment.extend(
560 assignments
561 .iter()
562 .map(|assignment| (assignment.name.clone(), assignment.value.clone())),
563 );
564 let mut replacement = alias_parser
565 .parse_inner(alias, true, CommandGrouping::OneGroup)
566 .map_err(|error| error.with_line(command.line))?;
567 for replacement_command in &mut replacement.commands {
568 replacement_command.line = command.line;
569 }
570 if let Some(last) = replacement.commands.last_mut() {
571 last.arguments.append(&mut command.arguments);
572 }
573 output.append(replacement);
574 continue;
575 }
576 }
577
578 for argument in &mut command.arguments {
579 if let CommandArgument::Commands(nested) = argument {
580 let nested_commands = std::mem::take(nested);
581 *nested = self.expand_and_lookup(nested_commands, no_alias)?;
582 }
583 }
584
585 let entry = lookup_command_at(&command.name, command.line, self.exact_commands)?;
586 command.name = entry.name.to_owned();
587 output.push_command(command);
588 }
589
590 Ok(output)
591 }
592
593 fn find_command_alias(&self, name: &str) -> Option<&str> {
594 self.command_aliases
595 .iter()
596 .rev()
597 .find(|alias| alias.name() == name)
598 .map(CommandAlias::value)
599 }
600
601 fn lookup_environment(&self, name: &str) -> Option<&str> {
602 self.environment
603 .iter()
604 .rev()
605 .find(|(candidate, _)| candidate == name)
606 .map(|(_, value)| value.as_str())
607 }
608
609 fn expand_tilde(&self, user: &str) -> Option<&str> {
610 if user.is_empty() {
611 return self
612 .lookup_environment("HOME")
613 .filter(|home| !home.is_empty())
614 .or(self.home_dir.as_deref());
615 }
616
617 self.user_home_dirs
618 .iter()
619 .find(|(candidate, _)| candidate == user)
620 .map(|(_, home)| home.as_str())
621 }
622
623 fn condition_is_true(&self, value: &str) -> bool {
624 let expanded = if value.contains("#{") {
625 render_template(
626 value,
627 &ParseTimeFormatVariables {
628 values: &self.format_variables,
629 },
630 )
631 } else {
632 value.to_owned()
633 };
634
635 is_truthy(&expanded)
636 }
637}
638
639fn ensure_command_length(
640 bytes: usize,
641 line: usize,
642 max_command_bytes: usize,
643) -> Result<(), CommandParseError> {
644 if bytes > max_command_bytes {
645 return Err(CommandParseError::new(line, "command too long"));
646 }
647 Ok(())
648}
649
650fn ensure_parsed_command_lengths(
651 commands: &ParsedCommands,
652 max_command_bytes: usize,
653) -> Result<(), CommandParseError> {
654 for command in commands.commands() {
655 ensure_parsed_command_length(command, max_command_bytes)?;
656 }
657 Ok(())
658}
659
660fn ensure_parsed_command_length(
661 command: &ParsedCommand,
662 max_command_bytes: usize,
663) -> Result<(), CommandParseError> {
664 let mut bytes = command.name.len();
665 for argument in command.arguments() {
666 bytes = bytes.saturating_add(1);
667 match argument {
668 CommandArgument::String(value) => {
669 bytes = bytes.saturating_add(value.len());
670 }
671 CommandArgument::Commands(commands) => {
672 ensure_parsed_command_lengths(commands, max_command_bytes)?;
673 }
674 }
675 }
676 ensure_command_length(bytes, command.line(), max_command_bytes)
677}
678
679struct ParseTimeFormatVariables<'a> {
680 values: &'a [(String, String)],
681}
682
683impl FormatVariables for ParseTimeFormatVariables<'_> {
684 fn format_value(&self, _variable: FormatVariable) -> Option<String> {
685 None
686 }
687
688 fn format_value_by_name(&self, name: &str) -> Option<String> {
689 self.values
690 .iter()
691 .rev()
692 .find(|(candidate, _)| candidate == name)
693 .map(|(_, value)| value.clone())
694 }
695}
696
697#[derive(Debug, Clone, PartialEq, Eq)]
699pub struct CommandParseError {
700 line: usize,
701 message: String,
702 kind: CommandParseErrorKind,
703}
704
705#[derive(Debug, Clone, Copy, PartialEq, Eq)]
707pub enum CommandParseErrorKind {
708 Structural,
710 Lookup,
712 Other,
714}
715
716impl CommandParseError {
717 #[must_use]
719 pub fn line(&self) -> usize {
720 self.line
721 }
722
723 #[must_use]
725 pub fn message(&self) -> &str {
726 &self.message
727 }
728
729 #[must_use]
731 pub const fn kind(&self) -> CommandParseErrorKind {
732 self.kind
733 }
734
735 pub(crate) fn new(line: usize, message: impl Into<String>) -> Self {
736 Self {
737 line,
738 message: message.into(),
739 kind: CommandParseErrorKind::Other,
740 }
741 }
742
743 pub(crate) fn structural(line: usize, message: impl Into<String>) -> Self {
744 Self {
745 line,
746 message: message.into(),
747 kind: CommandParseErrorKind::Structural,
748 }
749 }
750
751 pub(crate) fn lookup(line: usize, message: impl Into<String>) -> Self {
752 Self {
753 line,
754 message: message.into(),
755 kind: CommandParseErrorKind::Lookup,
756 }
757 }
758
759 fn with_line(mut self, line: usize) -> Self {
760 self.line = line;
761 self
762 }
763}
764
765impl fmt::Display for CommandParseError {
766 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
767 formatter.write_str(&self.message)
768 }
769}
770
771impl Error for CommandParseError {}
772
773fn command_from_arguments(
774 mut arguments: Vec<CommandArgument>,
775 line: usize,
776) -> Result<ParsedCommand, CommandParseError> {
777 let Some(CommandArgument::String(name)) = arguments.first() else {
778 return Err(CommandParseError::new(line, "no command"));
779 };
780 let name = name.clone();
781 arguments.remove(0);
782 Ok(ParsedCommand::new(name, arguments, line))
783}
784
785fn escape_argument(value: &str) -> String {
786 if value.is_empty() {
787 return "''".to_owned();
788 }
789 if !value.chars().any(argument_needs_quotes) {
790 return value.replace('\\', r"\\");
791 }
792
793 if value.contains('"') && !value.contains('\'') && !value.contains('$') {
794 return format!("'{value}'");
795 }
796
797 format!("\"{}\"", escape_double_quoted_argument(value))
798}
799
800fn argument_needs_quotes(ch: char) -> bool {
801 ch.is_whitespace() || matches!(ch, ';' | '{' | '}' | '\'' | '"' | '#' | '$')
802}
803
804fn escape_double_quoted_argument(value: &str) -> String {
805 let mut escaped = String::with_capacity(value.len());
806 for ch in value.chars() {
807 match ch {
808 '$' => escaped.push_str(r"\$"),
809 '\\' | '"' => {
810 escaped.push('\\');
811 escaped.push(ch);
812 }
813 _ => escaped.push(ch),
814 }
815 }
816 escaped
817}
818
819#[cfg(test)]
820#[path = "command_parser/tests.rs"]
821mod tests;