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
54#[must_use]
59pub fn is_parse_time_assignment(argument: &str) -> bool {
60 let Some((name, _)) = argument.split_once('=') else {
61 return false;
62 };
63 let mut characters = name.chars();
64 let Some(first) = characters.next() else {
65 return false;
66 };
67 (first.is_ascii_alphabetic() || first == '_')
68 && characters.all(|character| character.is_ascii_alphanumeric() || character == '_')
69}
70
71pub fn lookup_command(name: &str) -> Result<&'static CommandEntry, CommandParseError> {
73 lookup_command_at(name, 0, &[])
74}
75
76#[derive(Debug, Clone, Default, PartialEq, Eq)]
78pub struct ParsedCommands {
79 commands: Vec<ParsedCommand>,
80 assignments: Vec<EnvironmentAssignment>,
81 grouping: CommandGrouping,
82}
83
84impl ParsedCommands {
85 fn with_grouping(grouping: CommandGrouping) -> Self {
86 Self {
87 commands: Vec::new(),
88 assignments: Vec::new(),
89 grouping,
90 }
91 }
92
93 #[must_use]
95 pub fn commands(&self) -> &[ParsedCommand] {
96 &self.commands
97 }
98
99 #[must_use]
101 pub fn assignments(&self) -> &[EnvironmentAssignment] {
102 &self.assignments
103 }
104
105 #[must_use]
107 pub const fn grouping(&self) -> CommandGrouping {
108 self.grouping
109 }
110
111 #[must_use]
113 pub fn into_commands(self) -> Vec<ParsedCommand> {
114 self.commands
115 }
116
117 #[must_use]
119 pub fn is_empty(&self) -> bool {
120 self.commands.is_empty()
121 }
122
123 fn push_assignment(&mut self, assignment: EnvironmentAssignment) {
124 self.assignments.push(assignment);
125 }
126
127 fn push_command(&mut self, mut command: ParsedCommand) {
128 command.drain_nested_assignments_into(&mut self.assignments);
129 self.commands.push(command);
130 }
131
132 pub fn append(&mut self, mut other: Self) {
135 self.assignments.append(&mut other.assignments);
136 self.commands.append(&mut other.commands);
137 }
138
139 pub fn add_line_offset(&mut self, offset: usize) {
144 if offset == 0 {
145 return;
146 }
147 for command in &mut self.commands {
148 command.add_line_offset(offset);
149 }
150 }
151
152 #[must_use]
154 pub fn to_tmux_string(&self) -> String {
155 let mut rendered = String::new();
156 let mut previous_line = None;
157 for command in &self.commands {
158 if !rendered.is_empty() {
159 if previous_line.is_some_and(|line| line != command.line()) {
160 rendered.push_str(" ;; ");
161 } else {
162 rendered.push_str(" ; ");
163 }
164 }
165 rendered.push_str(&command.to_tmux_string());
166 previous_line = Some(command.line());
167 }
168 rendered
169 }
170
171 #[must_use]
174 pub fn to_tmux_binding_string(&self) -> String {
175 self.commands
176 .iter()
177 .map(ParsedCommand::to_tmux_reparse_string)
178 .collect::<Vec<_>>()
179 .join(" \\; ")
180 }
181
182 #[must_use]
185 pub fn to_tmux_reparse_string(&self) -> String {
186 let mut rendered = self.assignments_to_tmux_reparse_string();
187 let mut previous_line = None;
188 for command in &self.commands {
189 if !rendered.is_empty() {
190 if previous_line.is_some_and(|line| line != command.line()) {
191 rendered.push_str(" ;; ");
192 } else {
193 rendered.push_str(" ; ");
194 }
195 }
196 rendered.push_str(&command.to_tmux_reparse_string());
197 previous_line = Some(command.line());
198 }
199 rendered
200 }
201
202 #[must_use]
204 pub fn assignments_to_tmux_reparse_string(&self) -> String {
205 self.assignments
206 .iter()
207 .map(|assignment| {
208 let rendered = escape_argument_for_reparse(&format!(
209 "{}={}",
210 assignment.name(),
211 assignment.value()
212 ));
213 if assignment.hidden() {
214 format!("%hidden {rendered}")
215 } else {
216 rendered
217 }
218 })
219 .collect::<Vec<_>>()
220 .join(" ; ")
221 }
222}
223
224#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
226pub enum CommandGrouping {
227 #[default]
229 ByLine,
230 OneGroup,
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
236pub struct ParsedCommand {
237 name: String,
238 arguments: Vec<CommandArgument>,
239 start_line: usize,
240 line: usize,
241}
242
243impl ParsedCommand {
244 #[must_use]
246 pub fn name(&self) -> &str {
247 &self.name
248 }
249
250 #[must_use]
252 pub fn arguments(&self) -> &[CommandArgument] {
253 &self.arguments
254 }
255
256 #[must_use]
259 pub fn with_arguments(mut self, arguments: Vec<CommandArgument>) -> Self {
260 self.arguments = arguments;
261 self
262 }
263
264 #[must_use]
266 pub fn line(&self) -> usize {
267 self.line
268 }
269
270 fn new(name: String, arguments: Vec<CommandArgument>, line: usize) -> Self {
271 Self {
272 name,
273 arguments,
274 start_line: line,
275 line,
276 }
277 }
278
279 fn with_lines(
280 name: String,
281 arguments: Vec<CommandArgument>,
282 start_line: usize,
283 line: usize,
284 ) -> Self {
285 Self {
286 name,
287 arguments,
288 start_line,
289 line,
290 }
291 }
292
293 fn add_line_offset(&mut self, offset: usize) {
294 self.start_line = self.start_line.saturating_add(offset);
295 self.line = self.line.saturating_add(offset);
296 for argument in &mut self.arguments {
297 if let CommandArgument::Commands(commands) = argument {
298 commands.add_line_offset(offset);
299 }
300 }
301 }
302
303 fn drain_nested_assignments_into(&mut self, assignments: &mut Vec<EnvironmentAssignment>) {
304 for argument in &mut self.arguments {
305 if let CommandArgument::Commands(commands) = argument {
306 assignments.append(&mut commands.assignments);
307 }
308 }
309 }
310
311 #[must_use]
313 pub fn start_line(&self) -> usize {
314 self.start_line
315 }
316
317 #[must_use]
319 pub fn to_tmux_string(&self) -> String {
320 std::iter::once(self.name.clone())
321 .chain(self.arguments.iter().map(CommandArgument::to_tmux_string))
322 .collect::<Vec<_>>()
323 .join(" ")
324 }
325
326 #[must_use]
329 pub fn to_tmux_reparse_string(&self) -> String {
330 std::iter::once(self.name.clone())
331 .chain(
332 self.arguments
333 .iter()
334 .map(CommandArgument::to_reparse_string),
335 )
336 .collect::<Vec<_>>()
337 .join(" ")
338 }
339}
340
341#[derive(Debug, Clone, PartialEq, Eq)]
343pub enum CommandArgument {
344 String(String),
346 Commands(ParsedCommands),
348}
349
350impl CommandArgument {
351 #[must_use]
353 pub fn as_string(&self) -> Option<&str> {
354 match self {
355 Self::String(value) => Some(value),
356 Self::Commands(_) => None,
357 }
358 }
359
360 #[must_use]
362 pub fn to_tmux_string(&self) -> String {
363 match self {
364 Self::String(value) => escape_argument(value),
365 Self::Commands(commands) => format!("{{ {} }}", commands.to_tmux_string()),
366 }
367 }
368
369 fn to_reparse_string(&self) -> String {
370 match self {
371 Self::String(value) => escape_argument_for_reparse(value),
372 Self::Commands(commands) => {
373 format!("{{ {} }}", commands.to_tmux_reparse_string())
374 }
375 }
376 }
377
378 #[must_use]
381 pub fn to_tmux_reparse_string(&self) -> String {
382 self.to_reparse_string()
383 }
384}
385
386#[derive(Debug, Clone, PartialEq, Eq)]
388pub struct EnvironmentAssignment {
389 name: String,
390 value: String,
391 hidden: bool,
392}
393
394impl EnvironmentAssignment {
395 #[must_use]
397 pub fn name(&self) -> &str {
398 &self.name
399 }
400
401 #[must_use]
403 pub fn value(&self) -> &str {
404 &self.value
405 }
406
407 #[must_use]
409 pub fn hidden(&self) -> bool {
410 self.hidden
411 }
412
413 fn from_equals(value: String, hidden: bool) -> Self {
414 let (name, value) = value
415 .split_once('=')
416 .expect("lexer only classifies assignments containing '='");
417 Self {
418 name: name.to_owned(),
419 value: value.to_owned(),
420 hidden,
421 }
422 }
423}
424
425#[derive(Debug, Clone)]
427pub struct CommandParser {
428 environment: Vec<(String, String)>,
429 format_variables: Vec<(String, String)>,
430 home_dir: Option<String>,
431 user_home_dirs: Vec<(String, String)>,
432 command_aliases: Vec<CommandAlias>,
433 exact_commands: &'static [CommandEntry],
434 max_command_bytes: usize,
435}
436
437impl Default for CommandParser {
438 fn default() -> Self {
439 Self {
440 environment: Vec::new(),
441 format_variables: Vec::new(),
442 home_dir: None,
443 user_home_dirs: Vec::new(),
444 command_aliases: Vec::new(),
445 exact_commands: &[],
446 max_command_bytes: DEFAULT_MAX_COMMAND_BYTES,
447 }
448 }
449}
450
451impl CommandParser {
452 #[must_use]
454 pub fn new() -> Self {
455 let mut parser = Self::default();
456 parser.command_aliases.extend(CommandAlias::builtin());
457 parser
458 }
459
460 #[must_use]
462 pub fn with_environment_value(
463 mut self,
464 name: impl Into<String>,
465 value: impl Into<String>,
466 ) -> Self {
467 self.environment.push((name.into(), value.into()));
468 self
469 }
470
471 #[must_use]
473 pub fn with_format_value(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
474 self.format_variables.push((name.into(), value.into()));
475 self
476 }
477
478 #[must_use]
480 pub fn with_environment_store(mut self, environment: &EnvironmentStore) -> Self {
481 self.environment.extend(
482 environment
483 .global_entries()
484 .map(|(name, value)| (name.to_owned(), value.to_owned())),
485 );
486 self
487 }
488
489 #[must_use]
491 pub fn with_home_dir(mut self, home_dir: impl Into<String>) -> Self {
492 self.home_dir = Some(home_dir.into());
493 self
494 }
495
496 #[must_use]
498 pub fn with_user_home_dir(
499 mut self,
500 user: impl Into<String>,
501 home_dir: impl Into<String>,
502 ) -> Self {
503 self.user_home_dirs.push((user.into(), home_dir.into()));
504 self
505 }
506
507 pub fn with_command_alias(
509 mut self,
510 definition: impl Into<String>,
511 ) -> Result<Self, CommandParseError> {
512 let definition = definition.into();
513 let Some(alias) = CommandAlias::parse(definition) else {
514 return Err(CommandParseError::new(
515 0,
516 "command-alias entry must be name=value",
517 ));
518 };
519 self.command_aliases.push(alias);
520 Ok(self)
521 }
522
523 #[must_use]
525 pub fn with_command_aliases<I, S>(mut self, definitions: I) -> Self
526 where
527 I: IntoIterator<Item = S>,
528 S: Into<String>,
529 {
530 self.command_aliases.clear();
531 self.command_aliases
532 .extend(definitions.into_iter().filter_map(CommandAlias::parse));
533 self
534 }
535
536 #[must_use]
542 pub fn with_exact_commands(mut self, commands: &'static [CommandEntry]) -> Self {
543 self.exact_commands = commands;
544 self
545 }
546
547 #[must_use]
549 pub fn with_max_command_bytes(mut self, max_command_bytes: usize) -> Self {
550 self.max_command_bytes = max_command_bytes;
551 self
552 }
553
554 pub fn parse(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
556 self.parse_inner(input, false, CommandGrouping::ByLine)
557 }
558
559 pub fn parse_structure(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
564 let mut parser = GrammarParser::new(Lexer::new(input, self), CommandGrouping::ByLine);
565 let commands = parser.parse_all()?;
566 ensure_parsed_command_lengths(&commands, self.max_command_bytes)?;
567 Ok(commands)
568 }
569
570 pub fn parse_source_file(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
578 self.parse_source_file_inner(input, false, CommandGrouping::ByLine)
579 }
580
581 pub fn parse_source_file_structure(
585 &self,
586 input: &str,
587 ) -> Result<ParsedCommands, CommandParseError> {
588 let mut parser = GrammarParser::new_source_file(
589 Lexer::new_source_file(input, self),
590 CommandGrouping::ByLine,
591 );
592 let commands = parser.parse_all()?;
593 ensure_parsed_command_lengths(&commands, self.max_command_bytes)?;
594 Ok(commands)
595 }
596
597 pub fn parse_one_group(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
603 self.parse_inner(input, false, CommandGrouping::OneGroup)
604 }
605
606 pub fn parse_arguments<I, S>(&self, arguments: I) -> Result<ParsedCommands, CommandParseError>
611 where
612 I: IntoIterator<Item = S>,
613 S: AsRef<str>,
614 {
615 self.parse_arguments_inner(arguments, false)
616 }
617
618 pub fn parse_arguments_with_assignments<I, S>(
625 &self,
626 arguments: I,
627 ) -> Result<ParsedCommands, CommandParseError>
628 where
629 I: IntoIterator<Item = S>,
630 S: AsRef<str>,
631 {
632 self.parse_arguments_inner(arguments, true)
633 }
634
635 fn parse_arguments_inner<I, S>(
636 &self,
637 arguments: I,
638 classify_assignments: bool,
639 ) -> Result<ParsedCommands, CommandParseError>
640 where
641 I: IntoIterator<Item = S>,
642 S: AsRef<str>,
643 {
644 let arguments = arguments
645 .into_iter()
646 .map(|argument| argument.as_ref().to_owned())
647 .collect::<Vec<_>>();
648 let command_bytes = arguments
649 .iter()
650 .map(String::len)
651 .sum::<usize>()
652 .saturating_add(arguments.len().saturating_sub(1));
653 ensure_command_length(command_bytes, 0, self.max_command_bytes)?;
654
655 let mut commands = ParsedCommands::with_grouping(CommandGrouping::ByLine);
656 let mut current = Vec::new();
657 let mut group_has_assignment = false;
658
659 for argument in arguments {
660 let mut value = argument;
661 let mut ends_command = false;
662
663 if value.ends_with(';') {
664 value.pop();
665 if value.ends_with('\\') {
666 value.pop();
667 value.push(';');
668 } else {
669 ends_command = true;
670 }
671 }
672
673 if !ends_command || !value.is_empty() {
674 if classify_assignments
675 && current.is_empty()
676 && !group_has_assignment
677 && is_parse_time_assignment(&value)
678 {
679 commands.push_assignment(EnvironmentAssignment::from_equals(value, false));
680 group_has_assignment = true;
681 } else {
682 current.push(CommandArgument::String(value));
683 }
684 }
685 if ends_command && !current.is_empty() {
686 commands.push_command(command_from_arguments(std::mem::take(&mut current), 1)?);
687 }
688 if ends_command {
689 group_has_assignment = false;
690 }
691 }
692
693 if !current.is_empty() {
694 commands.push_command(command_from_arguments(current, 1)?);
695 }
696
697 self.expand_and_lookup(commands, false)
698 }
699
700 fn parse_inner(
701 &self,
702 input: &str,
703 no_alias: bool,
704 grouping: CommandGrouping,
705 ) -> Result<ParsedCommands, CommandParseError> {
706 let mut parser = GrammarParser::new(Lexer::new(input, self), grouping);
707 let commands = parser.parse_all()?;
708 ensure_parsed_command_lengths(&commands, self.max_command_bytes)?;
709 self.expand_and_lookup(commands, no_alias)
710 }
711
712 fn parse_source_file_inner(
713 &self,
714 input: &str,
715 no_alias: bool,
716 grouping: CommandGrouping,
717 ) -> Result<ParsedCommands, CommandParseError> {
718 let mut parser =
719 GrammarParser::new_source_file(Lexer::new_source_file(input, self), grouping);
720 let commands = parser.parse_all()?;
721 ensure_parsed_command_lengths(&commands, self.max_command_bytes)?;
722 self.expand_and_lookup(commands, no_alias)
723 }
724
725 fn expand_and_lookup(
726 &self,
727 commands: ParsedCommands,
728 no_alias: bool,
729 ) -> Result<ParsedCommands, CommandParseError> {
730 let assignments = commands.assignments.clone();
731 let mut output = ParsedCommands {
732 commands: Vec::new(),
733 assignments: commands.assignments,
734 grouping: commands.grouping,
735 };
736
737 for mut command in commands.commands {
738 if !no_alias {
739 if let Some(alias) = self.find_command_alias(&command.name) {
740 let mut alias_parser = self.clone();
741 alias_parser.environment.extend(
742 assignments
743 .iter()
744 .map(|assignment| (assignment.name.clone(), assignment.value.clone())),
745 );
746 let mut replacement = alias_parser
747 .parse_inner(alias, true, CommandGrouping::OneGroup)
748 .map_err(|error| error.with_line(command.line))?;
749 for replacement_command in &mut replacement.commands {
750 replacement_command.line = command.line;
751 replacement_command.start_line = command.start_line;
752 }
753 if let Some(last) = replacement.commands.last_mut() {
754 last.arguments.append(&mut command.arguments);
755 }
756 output.append(replacement);
757 continue;
758 }
759 }
760
761 for argument in &mut command.arguments {
762 if let CommandArgument::Commands(nested) = argument {
763 let nested_commands = std::mem::take(nested);
764 *nested = self.expand_and_lookup(nested_commands, no_alias)?;
765 }
766 }
767
768 let entry = lookup_command_at(&command.name, command.line, self.exact_commands)?;
769 command.name = entry.name.to_owned();
770 output.push_command(command);
771 }
772
773 Ok(output)
774 }
775
776 fn find_command_alias(&self, name: &str) -> Option<&str> {
777 self.command_aliases
778 .iter()
779 .find(|alias| alias.name() == name)
780 .map(CommandAlias::value)
781 }
782
783 fn lookup_environment(&self, name: &str) -> Option<&str> {
784 self.environment
785 .iter()
786 .rev()
787 .find(|(candidate, _)| candidate == name)
788 .map(|(_, value)| value.as_str())
789 }
790
791 fn expand_tilde(&self, user: &str) -> Option<&str> {
792 if user.is_empty() {
793 return self
794 .lookup_environment("HOME")
795 .filter(|home| !home.is_empty())
796 .or(self.home_dir.as_deref());
797 }
798
799 self.user_home_dirs
800 .iter()
801 .find(|(candidate, _)| candidate == user)
802 .map(|(_, home)| home.as_str())
803 }
804
805 fn condition_is_true(&self, value: &str) -> bool {
806 let expanded = if value.contains("#{") {
807 render_template(
808 value,
809 &ParseTimeFormatVariables {
810 values: &self.format_variables,
811 },
812 )
813 } else {
814 value.to_owned()
815 };
816
817 is_truthy(&expanded)
818 }
819}
820
821fn ensure_command_length(
822 bytes: usize,
823 line: usize,
824 max_command_bytes: usize,
825) -> Result<(), CommandParseError> {
826 if bytes > max_command_bytes {
827 return Err(CommandParseError::new(line, "command too long"));
828 }
829 Ok(())
830}
831
832fn ensure_parsed_command_lengths(
833 commands: &ParsedCommands,
834 max_command_bytes: usize,
835) -> Result<(), CommandParseError> {
836 for command in commands.commands() {
837 ensure_parsed_command_length(command, max_command_bytes)?;
838 }
839 Ok(())
840}
841
842fn ensure_parsed_command_length(
843 command: &ParsedCommand,
844 max_command_bytes: usize,
845) -> Result<(), CommandParseError> {
846 let mut bytes = command.name.len();
847 for argument in command.arguments() {
848 bytes = bytes.saturating_add(1);
849 match argument {
850 CommandArgument::String(value) => {
851 bytes = bytes.saturating_add(value.len());
852 }
853 CommandArgument::Commands(commands) => {
854 ensure_parsed_command_lengths(commands, max_command_bytes)?;
855 }
856 }
857 }
858 ensure_command_length(bytes, command.line(), max_command_bytes)
859}
860
861struct ParseTimeFormatVariables<'a> {
862 values: &'a [(String, String)],
863}
864
865impl FormatVariables for ParseTimeFormatVariables<'_> {
866 fn format_value(&self, _variable: FormatVariable) -> Option<String> {
867 None
868 }
869
870 fn format_value_by_name(&self, name: &str) -> Option<String> {
871 self.values
872 .iter()
873 .rev()
874 .find(|(candidate, _)| candidate == name)
875 .map(|(_, value)| value.clone())
876 }
877}
878
879#[derive(Debug, Clone, PartialEq, Eq)]
881pub struct CommandParseError {
882 line: usize,
883 message: String,
884 kind: CommandParseErrorKind,
885}
886
887#[derive(Debug, Clone, Copy, PartialEq, Eq)]
889pub enum CommandParseErrorKind {
890 Structural,
892 Lookup,
894 Other,
896}
897
898impl CommandParseError {
899 #[must_use]
901 pub fn line(&self) -> usize {
902 self.line
903 }
904
905 #[must_use]
907 pub fn message(&self) -> &str {
908 &self.message
909 }
910
911 #[must_use]
913 pub const fn kind(&self) -> CommandParseErrorKind {
914 self.kind
915 }
916
917 pub(crate) fn new(line: usize, message: impl Into<String>) -> Self {
918 Self {
919 line,
920 message: message.into(),
921 kind: CommandParseErrorKind::Other,
922 }
923 }
924
925 pub(crate) fn structural(line: usize, message: impl Into<String>) -> Self {
926 Self {
927 line,
928 message: message.into(),
929 kind: CommandParseErrorKind::Structural,
930 }
931 }
932
933 pub(crate) fn lookup(line: usize, message: impl Into<String>) -> Self {
934 Self {
935 line,
936 message: message.into(),
937 kind: CommandParseErrorKind::Lookup,
938 }
939 }
940
941 fn with_line(mut self, line: usize) -> Self {
942 self.line = line;
943 self
944 }
945}
946
947impl fmt::Display for CommandParseError {
948 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
949 formatter.write_str(&self.message)
950 }
951}
952
953impl Error for CommandParseError {}
954
955fn command_from_arguments(
956 mut arguments: Vec<CommandArgument>,
957 line: usize,
958) -> Result<ParsedCommand, CommandParseError> {
959 let Some(CommandArgument::String(name)) = arguments.first() else {
960 return Err(CommandParseError::new(line, "no command"));
961 };
962 let name = name.clone();
963 arguments.remove(0);
964 Ok(ParsedCommand::new(name, arguments, line))
965}
966
967pub(crate) fn escape_argument(value: &str) -> String {
968 if value.is_empty() {
969 return "''".to_owned();
970 }
971 if is_single_char_escaped_argument(value) {
972 return escape_unquoted_argument(value);
973 }
974 if !value.chars().any(argument_needs_double_quotes) {
975 if value.contains('"') {
976 return format!("'{}'", escape_single_quoted_argument(value));
977 }
978 return escape_unquoted_argument(value);
979 }
980
981 format!("\"{}\"", escape_double_quoted_argument(value))
982}
983
984fn escape_argument_for_reparse(value: &str) -> String {
985 let single_quoted_display =
986 value.contains('"') && !value.chars().any(argument_needs_double_quotes);
987 if single_quoted_display && value.chars().any(|ch| ch == '\\' || ch.is_ascii_control()) {
988 return format!("\"{}\"", escape_double_quoted_argument(value));
989 }
990 escape_argument(value)
991}
992
993fn is_single_char_escaped_argument(value: &str) -> bool {
994 let mut chars = value.chars();
995 let Some(ch) = chars.next() else {
996 return false;
997 };
998 chars.next().is_none() && matches!(ch, ';' | '{' | '}' | '\'' | '"' | '#' | '$' | '%')
999}
1000
1001fn argument_needs_double_quotes(ch: char) -> bool {
1002 matches!(ch, ' ' | ';' | '{' | '}' | '\'' | '#' | '$' | '%')
1003 || (ch.is_whitespace() && !matches!(ch, '\n' | '\r' | '\t'))
1004}
1005
1006fn escape_unquoted_argument(value: &str) -> String {
1007 let mut escaped = String::with_capacity(value.len());
1008 for (index, ch) in value.chars().enumerate() {
1009 match ch {
1010 '~' if index == 0 => escaped.push_str(r"\~"),
1011 ';' | '{' | '}' | '\'' | '"' | '#' | '$' | '%' => {
1012 escaped.push('\\');
1013 escaped.push(ch);
1014 }
1015 '\n' => escaped.push_str(r"\n"),
1016 '\r' => escaped.push_str(r"\r"),
1017 '\t' => escaped.push_str(r"\t"),
1018 '\\' => escaped.push_str(r"\\"),
1019 _ => escape_control_argument_char(&mut escaped, ch),
1020 }
1021 }
1022 escaped
1023}
1024
1025fn escape_single_quoted_argument(value: &str) -> String {
1026 let mut escaped = String::with_capacity(value.len());
1027 for ch in value.chars() {
1028 match ch {
1029 '\n' => escaped.push_str(r"\n"),
1030 '\r' => escaped.push_str(r"\r"),
1031 '\t' => escaped.push_str(r"\t"),
1032 '\\' => escaped.push_str(r"\\"),
1033 _ => escape_control_argument_char(&mut escaped, ch),
1034 }
1035 }
1036 escaped
1037}
1038
1039fn escape_double_quoted_argument(value: &str) -> String {
1040 let mut escaped = String::with_capacity(value.len());
1041 let mut chars = value.chars().enumerate().peekable();
1042 while let Some((index, ch)) = chars.next() {
1043 match ch {
1044 '~' if index == 0 => escaped.push_str(r"\~"),
1045 '\n' => escaped.push_str(r"\n"),
1046 '\r' => escaped.push_str(r"\r"),
1047 '\t' => escaped.push_str(r"\t"),
1048 '\u{7}' => escaped.push_str(r"\a"),
1049 '\u{8}' => escaped.push_str(r"\b"),
1050 '\u{b}' => escaped.push_str(r"\v"),
1051 '\u{c}' => escaped.push_str(r"\f"),
1052 '\u{1b}' => escaped.push_str(r"\033"),
1053 '$' if chars
1054 .peek()
1055 .is_some_and(|(_, next)| dollar_starts_variable(*next)) =>
1056 {
1057 escaped.push_str(r"\$")
1058 }
1059 '\\' | '"' => {
1060 escaped.push('\\');
1061 escaped.push(ch);
1062 }
1063 _ => escape_control_argument_char(&mut escaped, ch),
1064 }
1065 }
1066 escaped
1067}
1068
1069fn escape_control_argument_char(escaped: &mut String, ch: char) {
1070 match ch {
1071 '\u{7}' => escaped.push_str(r"\a"),
1072 '\u{8}' => escaped.push_str(r"\b"),
1073 '\u{b}' => escaped.push_str(r"\v"),
1074 '\u{c}' => escaped.push_str(r"\f"),
1075 '\u{1b}' => escaped.push_str(r"\033"),
1076 '\0'..='\u{1f}' | '\u{7f}' => {
1077 escaped.push('\\');
1078 escaped.push_str(&format!("{:03o}", ch as u32));
1079 }
1080 _ => escaped.push(ch),
1081 }
1082}
1083
1084fn dollar_starts_variable(ch: char) -> bool {
1085 ch == '{' || ch == '_' || ch.is_ascii_alphabetic()
1086}
1087
1088#[cfg(test)]
1089#[path = "command_parser/tests.rs"]
1090mod tests;