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]
258 pub fn line(&self) -> usize {
259 self.line
260 }
261
262 fn new(name: String, arguments: Vec<CommandArgument>, line: usize) -> Self {
263 Self {
264 name,
265 arguments,
266 start_line: line,
267 line,
268 }
269 }
270
271 fn with_lines(
272 name: String,
273 arguments: Vec<CommandArgument>,
274 start_line: usize,
275 line: usize,
276 ) -> Self {
277 Self {
278 name,
279 arguments,
280 start_line,
281 line,
282 }
283 }
284
285 fn add_line_offset(&mut self, offset: usize) {
286 self.start_line = self.start_line.saturating_add(offset);
287 self.line = self.line.saturating_add(offset);
288 for argument in &mut self.arguments {
289 if let CommandArgument::Commands(commands) = argument {
290 commands.add_line_offset(offset);
291 }
292 }
293 }
294
295 fn drain_nested_assignments_into(&mut self, assignments: &mut Vec<EnvironmentAssignment>) {
296 for argument in &mut self.arguments {
297 if let CommandArgument::Commands(commands) = argument {
298 assignments.append(&mut commands.assignments);
299 }
300 }
301 }
302
303 #[must_use]
305 pub fn start_line(&self) -> usize {
306 self.start_line
307 }
308
309 #[must_use]
311 pub fn to_tmux_string(&self) -> String {
312 std::iter::once(self.name.clone())
313 .chain(self.arguments.iter().map(CommandArgument::to_tmux_string))
314 .collect::<Vec<_>>()
315 .join(" ")
316 }
317
318 #[must_use]
321 pub fn to_tmux_reparse_string(&self) -> String {
322 std::iter::once(self.name.clone())
323 .chain(
324 self.arguments
325 .iter()
326 .map(CommandArgument::to_reparse_string),
327 )
328 .collect::<Vec<_>>()
329 .join(" ")
330 }
331}
332
333#[derive(Debug, Clone, PartialEq, Eq)]
335pub enum CommandArgument {
336 String(String),
338 Commands(ParsedCommands),
340}
341
342impl CommandArgument {
343 #[must_use]
345 pub fn as_string(&self) -> Option<&str> {
346 match self {
347 Self::String(value) => Some(value),
348 Self::Commands(_) => None,
349 }
350 }
351
352 #[must_use]
354 pub fn to_tmux_string(&self) -> String {
355 match self {
356 Self::String(value) => escape_argument(value),
357 Self::Commands(commands) => format!("{{ {} }}", commands.to_tmux_string()),
358 }
359 }
360
361 fn to_reparse_string(&self) -> String {
362 match self {
363 Self::String(value) => escape_argument_for_reparse(value),
364 Self::Commands(commands) => {
365 format!("{{ {} }}", commands.to_tmux_reparse_string())
366 }
367 }
368 }
369
370 #[must_use]
373 pub fn to_tmux_reparse_string(&self) -> String {
374 self.to_reparse_string()
375 }
376}
377
378#[derive(Debug, Clone, PartialEq, Eq)]
380pub struct EnvironmentAssignment {
381 name: String,
382 value: String,
383 hidden: bool,
384}
385
386impl EnvironmentAssignment {
387 #[must_use]
389 pub fn name(&self) -> &str {
390 &self.name
391 }
392
393 #[must_use]
395 pub fn value(&self) -> &str {
396 &self.value
397 }
398
399 #[must_use]
401 pub fn hidden(&self) -> bool {
402 self.hidden
403 }
404
405 fn from_equals(value: String, hidden: bool) -> Self {
406 let (name, value) = value
407 .split_once('=')
408 .expect("lexer only classifies assignments containing '='");
409 Self {
410 name: name.to_owned(),
411 value: value.to_owned(),
412 hidden,
413 }
414 }
415}
416
417#[derive(Debug, Clone)]
419pub struct CommandParser {
420 environment: Vec<(String, String)>,
421 format_variables: Vec<(String, String)>,
422 home_dir: Option<String>,
423 user_home_dirs: Vec<(String, String)>,
424 command_aliases: Vec<CommandAlias>,
425 exact_commands: &'static [CommandEntry],
426 max_command_bytes: usize,
427}
428
429impl Default for CommandParser {
430 fn default() -> Self {
431 Self {
432 environment: Vec::new(),
433 format_variables: Vec::new(),
434 home_dir: None,
435 user_home_dirs: Vec::new(),
436 command_aliases: Vec::new(),
437 exact_commands: &[],
438 max_command_bytes: DEFAULT_MAX_COMMAND_BYTES,
439 }
440 }
441}
442
443impl CommandParser {
444 #[must_use]
446 pub fn new() -> Self {
447 let mut parser = Self::default();
448 parser.command_aliases.extend(CommandAlias::builtin());
449 parser
450 }
451
452 #[must_use]
454 pub fn with_environment_value(
455 mut self,
456 name: impl Into<String>,
457 value: impl Into<String>,
458 ) -> Self {
459 self.environment.push((name.into(), value.into()));
460 self
461 }
462
463 #[must_use]
465 pub fn with_format_value(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
466 self.format_variables.push((name.into(), value.into()));
467 self
468 }
469
470 #[must_use]
472 pub fn with_environment_store(mut self, environment: &EnvironmentStore) -> Self {
473 self.environment.extend(
474 environment
475 .global_entries()
476 .map(|(name, value)| (name.to_owned(), value.to_owned())),
477 );
478 self
479 }
480
481 #[must_use]
483 pub fn with_home_dir(mut self, home_dir: impl Into<String>) -> Self {
484 self.home_dir = Some(home_dir.into());
485 self
486 }
487
488 #[must_use]
490 pub fn with_user_home_dir(
491 mut self,
492 user: impl Into<String>,
493 home_dir: impl Into<String>,
494 ) -> Self {
495 self.user_home_dirs.push((user.into(), home_dir.into()));
496 self
497 }
498
499 pub fn with_command_alias(
501 mut self,
502 definition: impl Into<String>,
503 ) -> Result<Self, CommandParseError> {
504 let definition = definition.into();
505 let Some(alias) = CommandAlias::parse(definition) else {
506 return Err(CommandParseError::new(
507 0,
508 "command-alias entry must be name=value",
509 ));
510 };
511 self.command_aliases.push(alias);
512 Ok(self)
513 }
514
515 #[must_use]
517 pub fn with_command_aliases<I, S>(mut self, definitions: I) -> Self
518 where
519 I: IntoIterator<Item = S>,
520 S: Into<String>,
521 {
522 self.command_aliases.clear();
523 self.command_aliases
524 .extend(definitions.into_iter().filter_map(CommandAlias::parse));
525 self
526 }
527
528 #[must_use]
534 pub fn with_exact_commands(mut self, commands: &'static [CommandEntry]) -> Self {
535 self.exact_commands = commands;
536 self
537 }
538
539 #[must_use]
541 pub fn with_max_command_bytes(mut self, max_command_bytes: usize) -> Self {
542 self.max_command_bytes = max_command_bytes;
543 self
544 }
545
546 pub fn parse(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
548 self.parse_inner(input, false, CommandGrouping::ByLine)
549 }
550
551 pub fn parse_structure(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
556 let mut parser = GrammarParser::new(Lexer::new(input, self), CommandGrouping::ByLine);
557 let commands = parser.parse_all()?;
558 ensure_parsed_command_lengths(&commands, self.max_command_bytes)?;
559 Ok(commands)
560 }
561
562 pub fn parse_source_file(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
570 self.parse_source_file_inner(input, false, CommandGrouping::ByLine)
571 }
572
573 pub fn parse_source_file_structure(
577 &self,
578 input: &str,
579 ) -> Result<ParsedCommands, CommandParseError> {
580 let mut parser = GrammarParser::new_source_file(
581 Lexer::new_source_file(input, self),
582 CommandGrouping::ByLine,
583 );
584 let commands = parser.parse_all()?;
585 ensure_parsed_command_lengths(&commands, self.max_command_bytes)?;
586 Ok(commands)
587 }
588
589 pub fn parse_one_group(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
595 self.parse_inner(input, false, CommandGrouping::OneGroup)
596 }
597
598 pub fn parse_arguments<I, S>(&self, arguments: I) -> Result<ParsedCommands, CommandParseError>
603 where
604 I: IntoIterator<Item = S>,
605 S: AsRef<str>,
606 {
607 self.parse_arguments_inner(arguments, false)
608 }
609
610 pub fn parse_arguments_with_assignments<I, S>(
617 &self,
618 arguments: I,
619 ) -> Result<ParsedCommands, CommandParseError>
620 where
621 I: IntoIterator<Item = S>,
622 S: AsRef<str>,
623 {
624 self.parse_arguments_inner(arguments, true)
625 }
626
627 fn parse_arguments_inner<I, S>(
628 &self,
629 arguments: I,
630 classify_assignments: bool,
631 ) -> Result<ParsedCommands, CommandParseError>
632 where
633 I: IntoIterator<Item = S>,
634 S: AsRef<str>,
635 {
636 let arguments = arguments
637 .into_iter()
638 .map(|argument| argument.as_ref().to_owned())
639 .collect::<Vec<_>>();
640 let command_bytes = arguments
641 .iter()
642 .map(String::len)
643 .sum::<usize>()
644 .saturating_add(arguments.len().saturating_sub(1));
645 ensure_command_length(command_bytes, 0, self.max_command_bytes)?;
646
647 let mut commands = ParsedCommands::with_grouping(CommandGrouping::ByLine);
648 let mut current = Vec::new();
649 let mut group_has_assignment = false;
650
651 for argument in arguments {
652 let mut value = argument;
653 let mut ends_command = false;
654
655 if value.ends_with(';') {
656 value.pop();
657 if value.ends_with('\\') {
658 value.pop();
659 value.push(';');
660 } else {
661 ends_command = true;
662 }
663 }
664
665 if !ends_command || !value.is_empty() {
666 if classify_assignments
667 && current.is_empty()
668 && !group_has_assignment
669 && is_parse_time_assignment(&value)
670 {
671 commands.push_assignment(EnvironmentAssignment::from_equals(value, false));
672 group_has_assignment = true;
673 } else {
674 current.push(CommandArgument::String(value));
675 }
676 }
677 if ends_command && !current.is_empty() {
678 commands.push_command(command_from_arguments(std::mem::take(&mut current), 1)?);
679 }
680 if ends_command {
681 group_has_assignment = false;
682 }
683 }
684
685 if !current.is_empty() {
686 commands.push_command(command_from_arguments(current, 1)?);
687 }
688
689 self.expand_and_lookup(commands, false)
690 }
691
692 fn parse_inner(
693 &self,
694 input: &str,
695 no_alias: bool,
696 grouping: CommandGrouping,
697 ) -> Result<ParsedCommands, CommandParseError> {
698 let mut parser = GrammarParser::new(Lexer::new(input, self), grouping);
699 let commands = parser.parse_all()?;
700 ensure_parsed_command_lengths(&commands, self.max_command_bytes)?;
701 self.expand_and_lookup(commands, no_alias)
702 }
703
704 fn parse_source_file_inner(
705 &self,
706 input: &str,
707 no_alias: bool,
708 grouping: CommandGrouping,
709 ) -> Result<ParsedCommands, CommandParseError> {
710 let mut parser =
711 GrammarParser::new_source_file(Lexer::new_source_file(input, self), grouping);
712 let commands = parser.parse_all()?;
713 ensure_parsed_command_lengths(&commands, self.max_command_bytes)?;
714 self.expand_and_lookup(commands, no_alias)
715 }
716
717 fn expand_and_lookup(
718 &self,
719 commands: ParsedCommands,
720 no_alias: bool,
721 ) -> Result<ParsedCommands, CommandParseError> {
722 let assignments = commands.assignments.clone();
723 let mut output = ParsedCommands {
724 commands: Vec::new(),
725 assignments: commands.assignments,
726 grouping: commands.grouping,
727 };
728
729 for mut command in commands.commands {
730 if !no_alias {
731 if let Some(alias) = self.find_command_alias(&command.name) {
732 let mut alias_parser = self.clone();
733 alias_parser.environment.extend(
734 assignments
735 .iter()
736 .map(|assignment| (assignment.name.clone(), assignment.value.clone())),
737 );
738 let mut replacement = alias_parser
739 .parse_inner(alias, true, CommandGrouping::OneGroup)
740 .map_err(|error| error.with_line(command.line))?;
741 for replacement_command in &mut replacement.commands {
742 replacement_command.line = command.line;
743 replacement_command.start_line = command.start_line;
744 }
745 if let Some(last) = replacement.commands.last_mut() {
746 last.arguments.append(&mut command.arguments);
747 }
748 output.append(replacement);
749 continue;
750 }
751 }
752
753 for argument in &mut command.arguments {
754 if let CommandArgument::Commands(nested) = argument {
755 let nested_commands = std::mem::take(nested);
756 *nested = self.expand_and_lookup(nested_commands, no_alias)?;
757 }
758 }
759
760 let entry = lookup_command_at(&command.name, command.line, self.exact_commands)?;
761 command.name = entry.name.to_owned();
762 output.push_command(command);
763 }
764
765 Ok(output)
766 }
767
768 fn find_command_alias(&self, name: &str) -> Option<&str> {
769 self.command_aliases
770 .iter()
771 .find(|alias| alias.name() == name)
772 .map(CommandAlias::value)
773 }
774
775 fn lookup_environment(&self, name: &str) -> Option<&str> {
776 self.environment
777 .iter()
778 .rev()
779 .find(|(candidate, _)| candidate == name)
780 .map(|(_, value)| value.as_str())
781 }
782
783 fn expand_tilde(&self, user: &str) -> Option<&str> {
784 if user.is_empty() {
785 return self
786 .lookup_environment("HOME")
787 .filter(|home| !home.is_empty())
788 .or(self.home_dir.as_deref());
789 }
790
791 self.user_home_dirs
792 .iter()
793 .find(|(candidate, _)| candidate == user)
794 .map(|(_, home)| home.as_str())
795 }
796
797 fn condition_is_true(&self, value: &str) -> bool {
798 let expanded = if value.contains("#{") {
799 render_template(
800 value,
801 &ParseTimeFormatVariables {
802 values: &self.format_variables,
803 },
804 )
805 } else {
806 value.to_owned()
807 };
808
809 is_truthy(&expanded)
810 }
811}
812
813fn ensure_command_length(
814 bytes: usize,
815 line: usize,
816 max_command_bytes: usize,
817) -> Result<(), CommandParseError> {
818 if bytes > max_command_bytes {
819 return Err(CommandParseError::new(line, "command too long"));
820 }
821 Ok(())
822}
823
824fn ensure_parsed_command_lengths(
825 commands: &ParsedCommands,
826 max_command_bytes: usize,
827) -> Result<(), CommandParseError> {
828 for command in commands.commands() {
829 ensure_parsed_command_length(command, max_command_bytes)?;
830 }
831 Ok(())
832}
833
834fn ensure_parsed_command_length(
835 command: &ParsedCommand,
836 max_command_bytes: usize,
837) -> Result<(), CommandParseError> {
838 let mut bytes = command.name.len();
839 for argument in command.arguments() {
840 bytes = bytes.saturating_add(1);
841 match argument {
842 CommandArgument::String(value) => {
843 bytes = bytes.saturating_add(value.len());
844 }
845 CommandArgument::Commands(commands) => {
846 ensure_parsed_command_lengths(commands, max_command_bytes)?;
847 }
848 }
849 }
850 ensure_command_length(bytes, command.line(), max_command_bytes)
851}
852
853struct ParseTimeFormatVariables<'a> {
854 values: &'a [(String, String)],
855}
856
857impl FormatVariables for ParseTimeFormatVariables<'_> {
858 fn format_value(&self, _variable: FormatVariable) -> Option<String> {
859 None
860 }
861
862 fn format_value_by_name(&self, name: &str) -> Option<String> {
863 self.values
864 .iter()
865 .rev()
866 .find(|(candidate, _)| candidate == name)
867 .map(|(_, value)| value.clone())
868 }
869}
870
871#[derive(Debug, Clone, PartialEq, Eq)]
873pub struct CommandParseError {
874 line: usize,
875 message: String,
876 kind: CommandParseErrorKind,
877}
878
879#[derive(Debug, Clone, Copy, PartialEq, Eq)]
881pub enum CommandParseErrorKind {
882 Structural,
884 Lookup,
886 Other,
888}
889
890impl CommandParseError {
891 #[must_use]
893 pub fn line(&self) -> usize {
894 self.line
895 }
896
897 #[must_use]
899 pub fn message(&self) -> &str {
900 &self.message
901 }
902
903 #[must_use]
905 pub const fn kind(&self) -> CommandParseErrorKind {
906 self.kind
907 }
908
909 pub(crate) fn new(line: usize, message: impl Into<String>) -> Self {
910 Self {
911 line,
912 message: message.into(),
913 kind: CommandParseErrorKind::Other,
914 }
915 }
916
917 pub(crate) fn structural(line: usize, message: impl Into<String>) -> Self {
918 Self {
919 line,
920 message: message.into(),
921 kind: CommandParseErrorKind::Structural,
922 }
923 }
924
925 pub(crate) fn lookup(line: usize, message: impl Into<String>) -> Self {
926 Self {
927 line,
928 message: message.into(),
929 kind: CommandParseErrorKind::Lookup,
930 }
931 }
932
933 fn with_line(mut self, line: usize) -> Self {
934 self.line = line;
935 self
936 }
937}
938
939impl fmt::Display for CommandParseError {
940 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
941 formatter.write_str(&self.message)
942 }
943}
944
945impl Error for CommandParseError {}
946
947fn command_from_arguments(
948 mut arguments: Vec<CommandArgument>,
949 line: usize,
950) -> Result<ParsedCommand, CommandParseError> {
951 let Some(CommandArgument::String(name)) = arguments.first() else {
952 return Err(CommandParseError::new(line, "no command"));
953 };
954 let name = name.clone();
955 arguments.remove(0);
956 Ok(ParsedCommand::new(name, arguments, line))
957}
958
959pub(crate) fn escape_argument(value: &str) -> String {
960 if value.is_empty() {
961 return "''".to_owned();
962 }
963 if is_single_char_escaped_argument(value) {
964 return escape_unquoted_argument(value);
965 }
966 if !value.chars().any(argument_needs_double_quotes) {
967 if value.contains('"') {
968 return format!("'{}'", escape_single_quoted_argument(value));
969 }
970 return escape_unquoted_argument(value);
971 }
972
973 format!("\"{}\"", escape_double_quoted_argument(value))
974}
975
976fn escape_argument_for_reparse(value: &str) -> String {
977 let single_quoted_display =
978 value.contains('"') && !value.chars().any(argument_needs_double_quotes);
979 if single_quoted_display && value.chars().any(|ch| ch == '\\' || ch.is_ascii_control()) {
980 return format!("\"{}\"", escape_double_quoted_argument(value));
981 }
982 escape_argument(value)
983}
984
985fn is_single_char_escaped_argument(value: &str) -> bool {
986 let mut chars = value.chars();
987 let Some(ch) = chars.next() else {
988 return false;
989 };
990 chars.next().is_none() && matches!(ch, ';' | '{' | '}' | '\'' | '"' | '#' | '$' | '%')
991}
992
993fn argument_needs_double_quotes(ch: char) -> bool {
994 matches!(ch, ' ' | ';' | '{' | '}' | '\'' | '#' | '$' | '%')
995 || (ch.is_whitespace() && !matches!(ch, '\n' | '\r' | '\t'))
996}
997
998fn escape_unquoted_argument(value: &str) -> String {
999 let mut escaped = String::with_capacity(value.len());
1000 for (index, ch) in value.chars().enumerate() {
1001 match ch {
1002 '~' if index == 0 => escaped.push_str(r"\~"),
1003 ';' | '{' | '}' | '\'' | '"' | '#' | '$' | '%' => {
1004 escaped.push('\\');
1005 escaped.push(ch);
1006 }
1007 '\n' => escaped.push_str(r"\n"),
1008 '\r' => escaped.push_str(r"\r"),
1009 '\t' => escaped.push_str(r"\t"),
1010 '\\' => escaped.push_str(r"\\"),
1011 _ => escape_control_argument_char(&mut escaped, ch),
1012 }
1013 }
1014 escaped
1015}
1016
1017fn escape_single_quoted_argument(value: &str) -> String {
1018 let mut escaped = String::with_capacity(value.len());
1019 for ch in value.chars() {
1020 match ch {
1021 '\n' => escaped.push_str(r"\n"),
1022 '\r' => escaped.push_str(r"\r"),
1023 '\t' => escaped.push_str(r"\t"),
1024 '\\' => escaped.push_str(r"\\"),
1025 _ => escape_control_argument_char(&mut escaped, ch),
1026 }
1027 }
1028 escaped
1029}
1030
1031fn escape_double_quoted_argument(value: &str) -> String {
1032 let mut escaped = String::with_capacity(value.len());
1033 let mut chars = value.chars().enumerate().peekable();
1034 while let Some((index, ch)) = chars.next() {
1035 match ch {
1036 '~' if index == 0 => escaped.push_str(r"\~"),
1037 '\n' => escaped.push_str(r"\n"),
1038 '\r' => escaped.push_str(r"\r"),
1039 '\t' => escaped.push_str(r"\t"),
1040 '\u{7}' => escaped.push_str(r"\a"),
1041 '\u{8}' => escaped.push_str(r"\b"),
1042 '\u{b}' => escaped.push_str(r"\v"),
1043 '\u{c}' => escaped.push_str(r"\f"),
1044 '\u{1b}' => escaped.push_str(r"\033"),
1045 '$' if chars
1046 .peek()
1047 .is_some_and(|(_, next)| dollar_starts_variable(*next)) =>
1048 {
1049 escaped.push_str(r"\$")
1050 }
1051 '\\' | '"' => {
1052 escaped.push('\\');
1053 escaped.push(ch);
1054 }
1055 _ => escape_control_argument_char(&mut escaped, ch),
1056 }
1057 }
1058 escaped
1059}
1060
1061fn escape_control_argument_char(escaped: &mut String, ch: char) {
1062 match ch {
1063 '\u{7}' => escaped.push_str(r"\a"),
1064 '\u{8}' => escaped.push_str(r"\b"),
1065 '\u{b}' => escaped.push_str(r"\v"),
1066 '\u{c}' => escaped.push_str(r"\f"),
1067 '\u{1b}' => escaped.push_str(r"\033"),
1068 '\0'..='\u{1f}' | '\u{7f}' => {
1069 escaped.push('\\');
1070 escaped.push_str(&format!("{:03o}", ch as u32));
1071 }
1072 _ => escaped.push(ch),
1073 }
1074}
1075
1076fn dollar_starts_variable(ch: char) -> bool {
1077 ch == '{' || ch == '_' || ch.is_ascii_alphabetic()
1078}
1079
1080#[cfg(test)]
1081#[path = "command_parser/tests.rs"]
1082mod tests;