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/grammar.rs"]
20mod grammar;
21#[path = "command_parser/lexer.rs"]
22mod lexer;
23#[path = "command_parser/lookup.rs"]
24mod lookup;
25#[path = "command_parser/table.rs"]
26mod table;
27
28use grammar::GrammarParser;
29use lexer::Lexer;
30use lookup::lookup_command_at;
31pub use table::{CommandEntry, COMMAND_TABLE};
32
33pub fn parse_command_string(input: &str) -> Result<ParsedCommands, CommandParseError> {
35 CommandParser::new().parse(input)
36}
37
38pub fn parse_command_arguments<I, S>(arguments: I) -> Result<ParsedCommands, CommandParseError>
40where
41 I: IntoIterator<Item = S>,
42 S: AsRef<str>,
43{
44 CommandParser::new().parse_arguments(arguments)
45}
46
47pub fn lookup_command(name: &str) -> Result<&'static CommandEntry, CommandParseError> {
49 lookup_command_at(name, 0)
50}
51
52#[derive(Debug, Clone, Default, PartialEq, Eq)]
54pub struct ParsedCommands {
55 commands: Vec<ParsedCommand>,
56 assignments: Vec<EnvironmentAssignment>,
57 grouping: CommandGrouping,
58}
59
60impl ParsedCommands {
61 fn with_grouping(grouping: CommandGrouping) -> Self {
62 Self {
63 commands: Vec::new(),
64 assignments: Vec::new(),
65 grouping,
66 }
67 }
68
69 #[must_use]
71 pub fn commands(&self) -> &[ParsedCommand] {
72 &self.commands
73 }
74
75 #[must_use]
77 pub fn assignments(&self) -> &[EnvironmentAssignment] {
78 &self.assignments
79 }
80
81 #[must_use]
83 pub const fn grouping(&self) -> CommandGrouping {
84 self.grouping
85 }
86
87 #[must_use]
89 pub fn into_commands(self) -> Vec<ParsedCommand> {
90 self.commands
91 }
92
93 #[must_use]
95 pub fn is_empty(&self) -> bool {
96 self.commands.is_empty()
97 }
98
99 fn push_assignment(&mut self, assignment: EnvironmentAssignment) {
100 self.assignments.push(assignment);
101 }
102
103 fn push_command(&mut self, command: ParsedCommand) {
104 self.commands.push(command);
105 }
106
107 pub fn append(&mut self, mut other: Self) {
110 self.assignments.append(&mut other.assignments);
111 self.commands.append(&mut other.commands);
112 }
113
114 #[must_use]
116 pub fn to_tmux_string(&self) -> String {
117 self.commands
118 .iter()
119 .map(ParsedCommand::to_tmux_string)
120 .collect::<Vec<_>>()
121 .join(" ; ")
122 }
123}
124
125#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
127pub enum CommandGrouping {
128 #[default]
130 ByLine,
131 OneGroup,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct ParsedCommand {
138 name: String,
139 arguments: Vec<CommandArgument>,
140 line: usize,
141}
142
143impl ParsedCommand {
144 #[must_use]
146 pub fn name(&self) -> &str {
147 &self.name
148 }
149
150 #[must_use]
152 pub fn arguments(&self) -> &[CommandArgument] {
153 &self.arguments
154 }
155
156 #[must_use]
158 pub fn line(&self) -> usize {
159 self.line
160 }
161
162 fn new(name: String, arguments: Vec<CommandArgument>, line: usize) -> Self {
163 Self {
164 name,
165 arguments,
166 line,
167 }
168 }
169
170 fn to_tmux_string(&self) -> String {
171 std::iter::once(self.name.clone())
172 .chain(self.arguments.iter().map(CommandArgument::to_tmux_string))
173 .collect::<Vec<_>>()
174 .join(" ")
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
180pub enum CommandArgument {
181 String(String),
183 Commands(ParsedCommands),
185}
186
187impl CommandArgument {
188 #[must_use]
190 pub fn as_string(&self) -> Option<&str> {
191 match self {
192 Self::String(value) => Some(value),
193 Self::Commands(_) => None,
194 }
195 }
196
197 #[must_use]
199 pub fn to_tmux_string(&self) -> String {
200 match self {
201 Self::String(value) => escape_argument(value),
202 Self::Commands(commands) => format!("{{ {} }}", commands.to_tmux_string()),
203 }
204 }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
209pub struct EnvironmentAssignment {
210 name: String,
211 value: String,
212 hidden: bool,
213}
214
215impl EnvironmentAssignment {
216 #[must_use]
218 pub fn name(&self) -> &str {
219 &self.name
220 }
221
222 #[must_use]
224 pub fn value(&self) -> &str {
225 &self.value
226 }
227
228 #[must_use]
230 pub fn hidden(&self) -> bool {
231 self.hidden
232 }
233
234 fn from_equals(value: String, hidden: bool) -> Self {
235 let (name, value) = value
236 .split_once('=')
237 .expect("lexer only classifies assignments containing '='");
238 Self {
239 name: name.to_owned(),
240 value: value.to_owned(),
241 hidden,
242 }
243 }
244}
245
246#[derive(Debug, Clone, Default)]
248pub struct CommandParser {
249 environment: Vec<(String, String)>,
250 format_variables: Vec<(String, String)>,
251 home_dir: Option<String>,
252 user_home_dirs: Vec<(String, String)>,
253 command_aliases: Vec<CommandAlias>,
254}
255
256impl CommandParser {
257 #[must_use]
259 pub fn new() -> Self {
260 let mut parser = Self::default();
261 parser
262 .command_aliases
263 .extend(
264 DEFAULT_COMMAND_ALIASES
265 .iter()
266 .map(|(name, value)| CommandAlias {
267 name: (*name).to_owned(),
268 value: (*value).to_owned(),
269 }),
270 );
271 parser
272 }
273
274 #[must_use]
276 pub fn with_environment_value(
277 mut self,
278 name: impl Into<String>,
279 value: impl Into<String>,
280 ) -> Self {
281 self.environment.push((name.into(), value.into()));
282 self
283 }
284
285 #[must_use]
287 pub fn with_format_value(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
288 self.format_variables.push((name.into(), value.into()));
289 self
290 }
291
292 #[must_use]
294 pub fn with_environment_store(mut self, environment: &EnvironmentStore) -> Self {
295 self.environment.extend(
296 environment
297 .global_entries()
298 .map(|(name, value)| (name.to_owned(), value.to_owned())),
299 );
300 self
301 }
302
303 #[must_use]
305 pub fn with_home_dir(mut self, home_dir: impl Into<String>) -> Self {
306 self.home_dir = Some(home_dir.into());
307 self
308 }
309
310 #[must_use]
312 pub fn with_user_home_dir(
313 mut self,
314 user: impl Into<String>,
315 home_dir: impl Into<String>,
316 ) -> Self {
317 self.user_home_dirs.push((user.into(), home_dir.into()));
318 self
319 }
320
321 pub fn with_command_alias(
323 mut self,
324 definition: impl Into<String>,
325 ) -> Result<Self, CommandParseError> {
326 let definition = definition.into();
327 let Some((name, value)) = definition.split_once('=') else {
328 return Err(CommandParseError::new(
329 0,
330 "command-alias entry must be name=value",
331 ));
332 };
333 self.command_aliases.push(CommandAlias {
334 name: name.to_owned(),
335 value: value.to_owned(),
336 });
337 Ok(self)
338 }
339
340 pub fn parse(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
342 self.parse_inner(input, false, CommandGrouping::ByLine)
343 }
344
345 pub fn parse_one_group(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
351 self.parse_inner(input, false, CommandGrouping::OneGroup)
352 }
353
354 pub fn parse_arguments<I, S>(&self, arguments: I) -> Result<ParsedCommands, CommandParseError>
359 where
360 I: IntoIterator<Item = S>,
361 S: AsRef<str>,
362 {
363 let mut commands = ParsedCommands::with_grouping(CommandGrouping::ByLine);
364 let mut current = Vec::new();
365
366 for argument in arguments {
367 let mut value = argument.as_ref().to_owned();
368 let mut ends_command = false;
369
370 if value.ends_with(';') {
371 value.pop();
372 if value.ends_with('\\') {
373 value.pop();
374 value.push(';');
375 } else {
376 ends_command = true;
377 }
378 }
379
380 if !ends_command || !value.is_empty() {
381 current.push(CommandArgument::String(value));
382 }
383 if ends_command && !current.is_empty() {
384 commands.push_command(command_from_arguments(std::mem::take(&mut current), 1)?);
385 }
386 }
387
388 if !current.is_empty() {
389 commands.push_command(command_from_arguments(current, 1)?);
390 }
391
392 self.expand_and_lookup(commands, false)
393 }
394
395 fn parse_inner(
396 &self,
397 input: &str,
398 no_alias: bool,
399 grouping: CommandGrouping,
400 ) -> Result<ParsedCommands, CommandParseError> {
401 let mut parser = GrammarParser::new(Lexer::new(input, self), grouping);
402 let commands = parser.parse_all()?;
403 self.expand_and_lookup(commands, no_alias)
404 }
405
406 fn expand_and_lookup(
407 &self,
408 commands: ParsedCommands,
409 no_alias: bool,
410 ) -> Result<ParsedCommands, CommandParseError> {
411 let assignments = commands.assignments.clone();
412 let mut output = ParsedCommands {
413 commands: Vec::new(),
414 assignments: commands.assignments,
415 grouping: commands.grouping,
416 };
417
418 for mut command in commands.commands {
419 if !no_alias {
420 if let Some(alias) = self.find_command_alias(&command.name) {
421 let mut alias_parser = self.clone();
422 alias_parser.environment.extend(
423 assignments
424 .iter()
425 .map(|assignment| (assignment.name.clone(), assignment.value.clone())),
426 );
427 let mut replacement =
428 alias_parser.parse_inner(alias, true, CommandGrouping::OneGroup)?;
429 for replacement_command in &mut replacement.commands {
430 replacement_command.line = command.line;
431 }
432 if let Some(last) = replacement.commands.last_mut() {
433 last.arguments.append(&mut command.arguments);
434 }
435 output.append(replacement);
436 continue;
437 }
438 }
439
440 for argument in &mut command.arguments {
441 if let CommandArgument::Commands(nested) = argument {
442 let nested_commands = std::mem::take(nested);
443 *nested = self.expand_and_lookup(nested_commands, no_alias)?;
444 }
445 }
446
447 let entry = lookup_command_at(&command.name, command.line)?;
448 command.name = entry.name.to_owned();
449 output.push_command(command);
450 }
451
452 Ok(output)
453 }
454
455 fn find_command_alias(&self, name: &str) -> Option<&str> {
456 self.command_aliases
457 .iter()
458 .find(|alias| alias.name == name)
459 .map(|alias| alias.value.as_str())
460 }
461
462 fn lookup_environment(&self, name: &str) -> Option<&str> {
463 self.environment
464 .iter()
465 .rev()
466 .find(|(candidate, _)| candidate == name)
467 .map(|(_, value)| value.as_str())
468 }
469
470 fn expand_tilde(&self, user: &str) -> Option<&str> {
471 if user.is_empty() {
472 return self
473 .lookup_environment("HOME")
474 .filter(|home| !home.is_empty())
475 .or(self.home_dir.as_deref());
476 }
477
478 self.user_home_dirs
479 .iter()
480 .find(|(candidate, _)| candidate == user)
481 .map(|(_, home)| home.as_str())
482 }
483
484 fn condition_is_true(&self, value: &str) -> bool {
485 let expanded = if value.contains("#{") {
486 render_template(
487 value,
488 &ParseTimeFormatVariables {
489 values: &self.format_variables,
490 },
491 )
492 } else {
493 value.to_owned()
494 };
495
496 is_truthy(&expanded)
497 }
498}
499
500const DEFAULT_COMMAND_ALIASES: &[(&str, &str)] = &[
501 ("split-pane", "split-window"),
502 ("splitp", "split-window"),
503 ("server-info", "show-messages -JT"),
504 ("info", "show-messages -JT"),
505 ("choose-window", "choose-tree -w"),
506 ("choose-session", "choose-tree -s"),
507];
508
509#[derive(Debug, Clone)]
510struct CommandAlias {
511 name: String,
512 value: String,
513}
514
515struct ParseTimeFormatVariables<'a> {
516 values: &'a [(String, String)],
517}
518
519impl FormatVariables for ParseTimeFormatVariables<'_> {
520 fn format_value(&self, _variable: FormatVariable) -> Option<String> {
521 None
522 }
523
524 fn format_value_by_name(&self, name: &str) -> Option<String> {
525 self.values
526 .iter()
527 .rev()
528 .find(|(candidate, _)| candidate == name)
529 .map(|(_, value)| value.clone())
530 }
531}
532
533#[derive(Debug, Clone, PartialEq, Eq)]
535pub struct CommandParseError {
536 line: usize,
537 message: String,
538}
539
540impl CommandParseError {
541 #[must_use]
543 pub fn line(&self) -> usize {
544 self.line
545 }
546
547 #[must_use]
549 pub fn message(&self) -> &str {
550 &self.message
551 }
552
553 pub(crate) fn new(line: usize, message: impl Into<String>) -> Self {
554 Self {
555 line,
556 message: message.into(),
557 }
558 }
559}
560
561impl fmt::Display for CommandParseError {
562 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
563 formatter.write_str(&self.message)
564 }
565}
566
567impl Error for CommandParseError {}
568
569fn command_from_arguments(
570 mut arguments: Vec<CommandArgument>,
571 line: usize,
572) -> Result<ParsedCommand, CommandParseError> {
573 let Some(CommandArgument::String(name)) = arguments.first() else {
574 return Err(CommandParseError::new(line, "no command"));
575 };
576 let name = name.clone();
577 arguments.remove(0);
578 Ok(ParsedCommand::new(name, arguments, line))
579}
580
581fn escape_argument(value: &str) -> String {
582 if value.is_empty() {
583 return "''".to_owned();
584 }
585 if !value
586 .chars()
587 .any(|ch| ch.is_whitespace() || matches!(ch, ';' | '{' | '}' | '\'' | '"' | '#'))
588 {
589 return value.to_owned();
590 }
591
592 format!("'{}'", value.replace('\'', "'\\''"))
593}
594
595#[cfg(test)]
596#[path = "command_parser/tests.rs"]
597mod tests;