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 max_command_bytes: usize,
297}
298
299impl Default for CommandParser {
300 fn default() -> Self {
301 Self {
302 environment: Vec::new(),
303 format_variables: Vec::new(),
304 home_dir: None,
305 user_home_dirs: Vec::new(),
306 command_aliases: Vec::new(),
307 max_command_bytes: DEFAULT_MAX_COMMAND_BYTES,
308 }
309 }
310}
311
312impl CommandParser {
313 #[must_use]
315 pub fn new() -> Self {
316 let mut parser = Self::default();
317 parser.command_aliases.extend(CommandAlias::builtin());
318 parser
319 }
320
321 #[must_use]
323 pub fn with_environment_value(
324 mut self,
325 name: impl Into<String>,
326 value: impl Into<String>,
327 ) -> Self {
328 self.environment.push((name.into(), value.into()));
329 self
330 }
331
332 #[must_use]
334 pub fn with_format_value(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
335 self.format_variables.push((name.into(), value.into()));
336 self
337 }
338
339 #[must_use]
341 pub fn with_environment_store(mut self, environment: &EnvironmentStore) -> Self {
342 self.environment.extend(
343 environment
344 .global_entries()
345 .map(|(name, value)| (name.to_owned(), value.to_owned())),
346 );
347 self
348 }
349
350 #[must_use]
352 pub fn with_home_dir(mut self, home_dir: impl Into<String>) -> Self {
353 self.home_dir = Some(home_dir.into());
354 self
355 }
356
357 #[must_use]
359 pub fn with_user_home_dir(
360 mut self,
361 user: impl Into<String>,
362 home_dir: impl Into<String>,
363 ) -> Self {
364 self.user_home_dirs.push((user.into(), home_dir.into()));
365 self
366 }
367
368 pub fn with_command_alias(
370 mut self,
371 definition: impl Into<String>,
372 ) -> Result<Self, CommandParseError> {
373 let definition = definition.into();
374 let Some(alias) = CommandAlias::parse(definition) else {
375 return Err(CommandParseError::new(
376 0,
377 "command-alias entry must be name=value",
378 ));
379 };
380 self.command_aliases.push(alias);
381 Ok(self)
382 }
383
384 #[must_use]
386 pub fn with_command_aliases<I, S>(mut self, definitions: I) -> Self
387 where
388 I: IntoIterator<Item = S>,
389 S: Into<String>,
390 {
391 self.command_aliases.clear();
392 self.command_aliases
393 .extend(definitions.into_iter().filter_map(CommandAlias::parse));
394 self
395 }
396
397 #[must_use]
399 pub fn with_max_command_bytes(mut self, max_command_bytes: usize) -> Self {
400 self.max_command_bytes = max_command_bytes;
401 self
402 }
403
404 pub fn parse(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
406 self.parse_inner(input, false, CommandGrouping::ByLine)
407 }
408
409 pub fn parse_one_group(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
415 self.parse_inner(input, false, CommandGrouping::OneGroup)
416 }
417
418 pub fn parse_arguments<I, S>(&self, arguments: I) -> Result<ParsedCommands, CommandParseError>
423 where
424 I: IntoIterator<Item = S>,
425 S: AsRef<str>,
426 {
427 let arguments = arguments
428 .into_iter()
429 .map(|argument| argument.as_ref().to_owned())
430 .collect::<Vec<_>>();
431 let command_bytes = arguments
432 .iter()
433 .map(String::len)
434 .sum::<usize>()
435 .saturating_add(arguments.len().saturating_sub(1));
436 ensure_command_length(command_bytes, 0, self.max_command_bytes)?;
437
438 let mut commands = ParsedCommands::with_grouping(CommandGrouping::ByLine);
439 let mut current = Vec::new();
440
441 for argument in arguments {
442 let mut value = argument;
443 let mut ends_command = false;
444
445 if value.ends_with(';') {
446 value.pop();
447 if value.ends_with('\\') {
448 value.pop();
449 value.push(';');
450 } else {
451 ends_command = true;
452 }
453 }
454
455 if !ends_command || !value.is_empty() {
456 current.push(CommandArgument::String(value));
457 }
458 if ends_command && !current.is_empty() {
459 commands.push_command(command_from_arguments(std::mem::take(&mut current), 1)?);
460 }
461 }
462
463 if !current.is_empty() {
464 commands.push_command(command_from_arguments(current, 1)?);
465 }
466
467 self.expand_and_lookup(commands, false)
468 }
469
470 fn parse_inner(
471 &self,
472 input: &str,
473 no_alias: bool,
474 grouping: CommandGrouping,
475 ) -> Result<ParsedCommands, CommandParseError> {
476 let mut parser = GrammarParser::new(Lexer::new(input, self), grouping);
477 let commands = parser.parse_all()?;
478 ensure_parsed_command_lengths(&commands, self.max_command_bytes)?;
479 self.expand_and_lookup(commands, no_alias)
480 }
481
482 fn expand_and_lookup(
483 &self,
484 commands: ParsedCommands,
485 no_alias: bool,
486 ) -> Result<ParsedCommands, CommandParseError> {
487 let assignments = commands.assignments.clone();
488 let mut output = ParsedCommands {
489 commands: Vec::new(),
490 assignments: commands.assignments,
491 grouping: commands.grouping,
492 };
493
494 for mut command in commands.commands {
495 if !no_alias {
496 if let Some(alias) = self.find_command_alias(&command.name) {
497 let mut alias_parser = self.clone();
498 alias_parser.environment.extend(
499 assignments
500 .iter()
501 .map(|assignment| (assignment.name.clone(), assignment.value.clone())),
502 );
503 let mut replacement =
504 alias_parser.parse_inner(alias, true, CommandGrouping::OneGroup)?;
505 for replacement_command in &mut replacement.commands {
506 replacement_command.line = command.line;
507 }
508 if let Some(last) = replacement.commands.last_mut() {
509 last.arguments.append(&mut command.arguments);
510 }
511 output.append(replacement);
512 continue;
513 }
514 }
515
516 for argument in &mut command.arguments {
517 if let CommandArgument::Commands(nested) = argument {
518 let nested_commands = std::mem::take(nested);
519 *nested = self.expand_and_lookup(nested_commands, no_alias)?;
520 }
521 }
522
523 let entry = lookup_command_at(&command.name, command.line)?;
524 command.name = entry.name.to_owned();
525 output.push_command(command);
526 }
527
528 Ok(output)
529 }
530
531 fn find_command_alias(&self, name: &str) -> Option<&str> {
532 self.command_aliases
533 .iter()
534 .rev()
535 .find(|alias| alias.name() == name)
536 .map(CommandAlias::value)
537 }
538
539 fn lookup_environment(&self, name: &str) -> Option<&str> {
540 self.environment
541 .iter()
542 .rev()
543 .find(|(candidate, _)| candidate == name)
544 .map(|(_, value)| value.as_str())
545 }
546
547 fn expand_tilde(&self, user: &str) -> Option<&str> {
548 if user.is_empty() {
549 return self
550 .lookup_environment("HOME")
551 .filter(|home| !home.is_empty())
552 .or(self.home_dir.as_deref());
553 }
554
555 self.user_home_dirs
556 .iter()
557 .find(|(candidate, _)| candidate == user)
558 .map(|(_, home)| home.as_str())
559 }
560
561 fn condition_is_true(&self, value: &str) -> bool {
562 let expanded = if value.contains("#{") {
563 render_template(
564 value,
565 &ParseTimeFormatVariables {
566 values: &self.format_variables,
567 },
568 )
569 } else {
570 value.to_owned()
571 };
572
573 is_truthy(&expanded)
574 }
575}
576
577fn ensure_command_length(
578 bytes: usize,
579 line: usize,
580 max_command_bytes: usize,
581) -> Result<(), CommandParseError> {
582 if bytes > max_command_bytes {
583 return Err(CommandParseError::new(line, "command too long"));
584 }
585 Ok(())
586}
587
588fn ensure_parsed_command_lengths(
589 commands: &ParsedCommands,
590 max_command_bytes: usize,
591) -> Result<(), CommandParseError> {
592 for command in commands.commands() {
593 ensure_parsed_command_length(command, max_command_bytes)?;
594 }
595 Ok(())
596}
597
598fn ensure_parsed_command_length(
599 command: &ParsedCommand,
600 max_command_bytes: usize,
601) -> Result<(), CommandParseError> {
602 let mut bytes = command.name.len();
603 for argument in command.arguments() {
604 bytes = bytes.saturating_add(1);
605 match argument {
606 CommandArgument::String(value) => {
607 bytes = bytes.saturating_add(value.len());
608 }
609 CommandArgument::Commands(commands) => {
610 ensure_parsed_command_lengths(commands, max_command_bytes)?;
611 }
612 }
613 }
614 ensure_command_length(bytes, command.line(), max_command_bytes)
615}
616
617struct ParseTimeFormatVariables<'a> {
618 values: &'a [(String, String)],
619}
620
621impl FormatVariables for ParseTimeFormatVariables<'_> {
622 fn format_value(&self, _variable: FormatVariable) -> Option<String> {
623 None
624 }
625
626 fn format_value_by_name(&self, name: &str) -> Option<String> {
627 self.values
628 .iter()
629 .rev()
630 .find(|(candidate, _)| candidate == name)
631 .map(|(_, value)| value.clone())
632 }
633}
634
635#[derive(Debug, Clone, PartialEq, Eq)]
637pub struct CommandParseError {
638 line: usize,
639 message: String,
640}
641
642impl CommandParseError {
643 #[must_use]
645 pub fn line(&self) -> usize {
646 self.line
647 }
648
649 #[must_use]
651 pub fn message(&self) -> &str {
652 &self.message
653 }
654
655 pub(crate) fn new(line: usize, message: impl Into<String>) -> Self {
656 Self {
657 line,
658 message: message.into(),
659 }
660 }
661}
662
663impl fmt::Display for CommandParseError {
664 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
665 formatter.write_str(&self.message)
666 }
667}
668
669impl Error for CommandParseError {}
670
671fn command_from_arguments(
672 mut arguments: Vec<CommandArgument>,
673 line: usize,
674) -> Result<ParsedCommand, CommandParseError> {
675 let Some(CommandArgument::String(name)) = arguments.first() else {
676 return Err(CommandParseError::new(line, "no command"));
677 };
678 let name = name.clone();
679 arguments.remove(0);
680 Ok(ParsedCommand::new(name, arguments, line))
681}
682
683fn escape_argument(value: &str) -> String {
684 if value.is_empty() {
685 return "''".to_owned();
686 }
687 if !value.chars().any(argument_needs_quotes) {
688 return value.replace('\\', r"\\");
689 }
690
691 if value.contains('"') && !value.contains('\'') && !value.contains('$') {
692 return format!("'{value}'");
693 }
694
695 format!("\"{}\"", escape_double_quoted_argument(value))
696}
697
698fn argument_needs_quotes(ch: char) -> bool {
699 ch.is_whitespace() || matches!(ch, ';' | '{' | '}' | '\'' | '"' | '#' | '$')
700}
701
702fn escape_double_quoted_argument(value: &str) -> String {
703 let mut escaped = String::with_capacity(value.len());
704 for ch in value.chars() {
705 match ch {
706 '$' => escaped.push_str(r"\$"),
707 '\\' | '"' => {
708 escaped.push('\\');
709 escaped.push(ch);
710 }
711 _ => escaped.push(ch),
712 }
713 }
714 escaped
715}
716
717#[cfg(test)]
718#[path = "command_parser/tests.rs"]
719mod tests;