use std::error::Error;
use std::fmt;
use crate::{
formats::{is_truthy, render_template, FormatVariable, FormatVariables},
EnvironmentStore,
};
#[path = "command_parser/aliases.rs"]
mod aliases;
#[path = "command_parser/grammar.rs"]
mod grammar;
#[path = "command_parser/lexer.rs"]
mod lexer;
#[path = "command_parser/lookup.rs"]
mod lookup;
#[path = "command_parser/table.rs"]
mod table;
use aliases::CommandAlias;
use grammar::GrammarParser;
use lexer::Lexer;
use lookup::lookup_command_at;
pub use table::{CommandEntry, COMMAND_TABLE};
const DEFAULT_MAX_COMMAND_BYTES: usize = 16 * 1024;
pub const SOURCE_FILE_MAX_COMMAND_BYTES: usize = 1024 * 1024;
pub fn parse_command_string(input: &str) -> Result<ParsedCommands, CommandParseError> {
CommandParser::new().parse(input)
}
pub fn parse_command_arguments<I, S>(arguments: I) -> Result<ParsedCommands, CommandParseError>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
CommandParser::new().parse_arguments(arguments)
}
#[must_use]
pub fn is_parse_time_assignment(argument: &str) -> bool {
let Some((name, _)) = argument.split_once('=') else {
return false;
};
let mut characters = name.chars();
let Some(first) = characters.next() else {
return false;
};
(first.is_ascii_alphabetic() || first == '_')
&& characters.all(|character| character.is_ascii_alphanumeric() || character == '_')
}
pub fn lookup_command(name: &str) -> Result<&'static CommandEntry, CommandParseError> {
lookup_command_at(name, 0, &[])
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ParsedCommands {
commands: Vec<ParsedCommand>,
assignments: Vec<EnvironmentAssignment>,
grouping: CommandGrouping,
}
impl ParsedCommands {
fn with_grouping(grouping: CommandGrouping) -> Self {
Self {
commands: Vec::new(),
assignments: Vec::new(),
grouping,
}
}
#[must_use]
pub fn commands(&self) -> &[ParsedCommand] {
&self.commands
}
#[must_use]
pub fn assignments(&self) -> &[EnvironmentAssignment] {
&self.assignments
}
#[must_use]
pub const fn grouping(&self) -> CommandGrouping {
self.grouping
}
#[must_use]
pub fn into_commands(self) -> Vec<ParsedCommand> {
self.commands
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.commands.is_empty()
}
fn push_assignment(&mut self, assignment: EnvironmentAssignment) {
self.assignments.push(assignment);
}
fn push_command(&mut self, mut command: ParsedCommand) {
command.drain_nested_assignments_into(&mut self.assignments);
self.commands.push(command);
}
pub fn append(&mut self, mut other: Self) {
self.assignments.append(&mut other.assignments);
self.commands.append(&mut other.commands);
}
pub fn add_line_offset(&mut self, offset: usize) {
if offset == 0 {
return;
}
for command in &mut self.commands {
command.add_line_offset(offset);
}
}
#[must_use]
pub fn to_tmux_string(&self) -> String {
let mut rendered = String::new();
let mut previous_line = None;
for command in &self.commands {
if !rendered.is_empty() {
if previous_line.is_some_and(|line| line != command.line()) {
rendered.push_str(" ;; ");
} else {
rendered.push_str(" ; ");
}
}
rendered.push_str(&command.to_tmux_string());
previous_line = Some(command.line());
}
rendered
}
#[must_use]
pub fn to_tmux_binding_string(&self) -> String {
self.commands
.iter()
.map(ParsedCommand::to_tmux_reparse_string)
.collect::<Vec<_>>()
.join(" \\; ")
}
#[must_use]
pub fn to_tmux_reparse_string(&self) -> String {
let mut rendered = self.assignments_to_tmux_reparse_string();
let mut previous_line = None;
for command in &self.commands {
if !rendered.is_empty() {
if previous_line.is_some_and(|line| line != command.line()) {
rendered.push_str(" ;; ");
} else {
rendered.push_str(" ; ");
}
}
rendered.push_str(&command.to_tmux_reparse_string());
previous_line = Some(command.line());
}
rendered
}
#[must_use]
pub fn assignments_to_tmux_reparse_string(&self) -> String {
self.assignments
.iter()
.map(|assignment| {
let rendered = escape_argument_for_reparse(&format!(
"{}={}",
assignment.name(),
assignment.value()
));
if assignment.hidden() {
format!("%hidden {rendered}")
} else {
rendered
}
})
.collect::<Vec<_>>()
.join(" ; ")
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum CommandGrouping {
#[default]
ByLine,
OneGroup,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedCommand {
name: String,
arguments: Vec<CommandArgument>,
start_line: usize,
line: usize,
}
impl ParsedCommand {
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn arguments(&self) -> &[CommandArgument] {
&self.arguments
}
#[must_use]
pub fn with_arguments(mut self, arguments: Vec<CommandArgument>) -> Self {
self.arguments = arguments;
self
}
#[must_use]
pub fn line(&self) -> usize {
self.line
}
fn new(name: String, arguments: Vec<CommandArgument>, line: usize) -> Self {
Self {
name,
arguments,
start_line: line,
line,
}
}
fn with_lines(
name: String,
arguments: Vec<CommandArgument>,
start_line: usize,
line: usize,
) -> Self {
Self {
name,
arguments,
start_line,
line,
}
}
fn add_line_offset(&mut self, offset: usize) {
self.start_line = self.start_line.saturating_add(offset);
self.line = self.line.saturating_add(offset);
for argument in &mut self.arguments {
if let CommandArgument::Commands(commands) = argument {
commands.add_line_offset(offset);
}
}
}
fn drain_nested_assignments_into(&mut self, assignments: &mut Vec<EnvironmentAssignment>) {
for argument in &mut self.arguments {
if let CommandArgument::Commands(commands) = argument {
assignments.append(&mut commands.assignments);
}
}
}
#[must_use]
pub fn start_line(&self) -> usize {
self.start_line
}
#[must_use]
pub fn to_tmux_string(&self) -> String {
std::iter::once(self.name.clone())
.chain(self.arguments.iter().map(CommandArgument::to_tmux_string))
.collect::<Vec<_>>()
.join(" ")
}
#[must_use]
pub fn to_tmux_reparse_string(&self) -> String {
std::iter::once(self.name.clone())
.chain(
self.arguments
.iter()
.map(CommandArgument::to_reparse_string),
)
.collect::<Vec<_>>()
.join(" ")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CommandArgument {
String(String),
Commands(ParsedCommands),
}
impl CommandArgument {
#[must_use]
pub fn as_string(&self) -> Option<&str> {
match self {
Self::String(value) => Some(value),
Self::Commands(_) => None,
}
}
#[must_use]
pub fn to_tmux_string(&self) -> String {
match self {
Self::String(value) => escape_argument(value),
Self::Commands(commands) => format!("{{ {} }}", commands.to_tmux_string()),
}
}
fn to_reparse_string(&self) -> String {
match self {
Self::String(value) => escape_argument_for_reparse(value),
Self::Commands(commands) => {
format!("{{ {} }}", commands.to_tmux_reparse_string())
}
}
}
#[must_use]
pub fn to_tmux_reparse_string(&self) -> String {
self.to_reparse_string()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnvironmentAssignment {
name: String,
value: String,
hidden: bool,
}
impl EnvironmentAssignment {
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
#[must_use]
pub fn hidden(&self) -> bool {
self.hidden
}
fn from_equals(value: String, hidden: bool) -> Self {
let (name, value) = value
.split_once('=')
.expect("lexer only classifies assignments containing '='");
Self {
name: name.to_owned(),
value: value.to_owned(),
hidden,
}
}
}
#[derive(Debug, Clone)]
pub struct CommandParser {
environment: Vec<(String, String)>,
format_variables: Vec<(String, String)>,
home_dir: Option<String>,
user_home_dirs: Vec<(String, String)>,
command_aliases: Vec<CommandAlias>,
exact_commands: &'static [CommandEntry],
max_command_bytes: usize,
}
impl Default for CommandParser {
fn default() -> Self {
Self {
environment: Vec::new(),
format_variables: Vec::new(),
home_dir: None,
user_home_dirs: Vec::new(),
command_aliases: Vec::new(),
exact_commands: &[],
max_command_bytes: DEFAULT_MAX_COMMAND_BYTES,
}
}
}
impl CommandParser {
#[must_use]
pub fn new() -> Self {
let mut parser = Self::default();
parser.command_aliases.extend(CommandAlias::builtin());
parser
}
#[must_use]
pub fn with_environment_value(
mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.environment.push((name.into(), value.into()));
self
}
#[must_use]
pub fn with_format_value(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.format_variables.push((name.into(), value.into()));
self
}
#[must_use]
pub fn with_environment_store(mut self, environment: &EnvironmentStore) -> Self {
self.environment.extend(
environment
.global_entries()
.map(|(name, value)| (name.to_owned(), value.to_owned())),
);
self
}
#[must_use]
pub fn with_home_dir(mut self, home_dir: impl Into<String>) -> Self {
self.home_dir = Some(home_dir.into());
self
}
#[must_use]
pub fn with_user_home_dir(
mut self,
user: impl Into<String>,
home_dir: impl Into<String>,
) -> Self {
self.user_home_dirs.push((user.into(), home_dir.into()));
self
}
pub fn with_command_alias(
mut self,
definition: impl Into<String>,
) -> Result<Self, CommandParseError> {
let definition = definition.into();
let Some(alias) = CommandAlias::parse(definition) else {
return Err(CommandParseError::new(
0,
"command-alias entry must be name=value",
));
};
self.command_aliases.push(alias);
Ok(self)
}
#[must_use]
pub fn with_command_aliases<I, S>(mut self, definitions: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.command_aliases.clear();
self.command_aliases
.extend(definitions.into_iter().filter_map(CommandAlias::parse));
self
}
#[must_use]
pub fn with_exact_commands(mut self, commands: &'static [CommandEntry]) -> Self {
self.exact_commands = commands;
self
}
#[must_use]
pub fn with_max_command_bytes(mut self, max_command_bytes: usize) -> Self {
self.max_command_bytes = max_command_bytes;
self
}
pub fn parse(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
self.parse_inner(input, false, CommandGrouping::ByLine)
}
pub fn parse_structure(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
let mut parser = GrammarParser::new(Lexer::new(input, self), CommandGrouping::ByLine);
let commands = parser.parse_all()?;
ensure_parsed_command_lengths(&commands, self.max_command_bytes)?;
Ok(commands)
}
pub fn parse_source_file(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
self.parse_source_file_inner(input, false, CommandGrouping::ByLine)
}
pub fn parse_source_file_structure(
&self,
input: &str,
) -> Result<ParsedCommands, CommandParseError> {
let mut parser = GrammarParser::new_source_file(
Lexer::new_source_file(input, self),
CommandGrouping::ByLine,
);
let commands = parser.parse_all()?;
ensure_parsed_command_lengths(&commands, self.max_command_bytes)?;
Ok(commands)
}
pub fn parse_one_group(&self, input: &str) -> Result<ParsedCommands, CommandParseError> {
self.parse_inner(input, false, CommandGrouping::OneGroup)
}
pub fn parse_arguments<I, S>(&self, arguments: I) -> Result<ParsedCommands, CommandParseError>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.parse_arguments_inner(arguments, false)
}
pub fn parse_arguments_with_assignments<I, S>(
&self,
arguments: I,
) -> Result<ParsedCommands, CommandParseError>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.parse_arguments_inner(arguments, true)
}
fn parse_arguments_inner<I, S>(
&self,
arguments: I,
classify_assignments: bool,
) -> Result<ParsedCommands, CommandParseError>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let arguments = arguments
.into_iter()
.map(|argument| argument.as_ref().to_owned())
.collect::<Vec<_>>();
let command_bytes = arguments
.iter()
.map(String::len)
.sum::<usize>()
.saturating_add(arguments.len().saturating_sub(1));
ensure_command_length(command_bytes, 0, self.max_command_bytes)?;
let mut commands = ParsedCommands::with_grouping(CommandGrouping::ByLine);
let mut current = Vec::new();
let mut group_has_assignment = false;
for argument in arguments {
let mut value = argument;
let mut ends_command = false;
if value.ends_with(';') {
value.pop();
if value.ends_with('\\') {
value.pop();
value.push(';');
} else {
ends_command = true;
}
}
if !ends_command || !value.is_empty() {
if classify_assignments
&& current.is_empty()
&& !group_has_assignment
&& is_parse_time_assignment(&value)
{
commands.push_assignment(EnvironmentAssignment::from_equals(value, false));
group_has_assignment = true;
} else {
current.push(CommandArgument::String(value));
}
}
if ends_command && !current.is_empty() {
commands.push_command(command_from_arguments(std::mem::take(&mut current), 1)?);
}
if ends_command {
group_has_assignment = false;
}
}
if !current.is_empty() {
commands.push_command(command_from_arguments(current, 1)?);
}
self.expand_and_lookup(commands, false)
}
fn parse_inner(
&self,
input: &str,
no_alias: bool,
grouping: CommandGrouping,
) -> Result<ParsedCommands, CommandParseError> {
let mut parser = GrammarParser::new(Lexer::new(input, self), grouping);
let commands = parser.parse_all()?;
ensure_parsed_command_lengths(&commands, self.max_command_bytes)?;
self.expand_and_lookup(commands, no_alias)
}
fn parse_source_file_inner(
&self,
input: &str,
no_alias: bool,
grouping: CommandGrouping,
) -> Result<ParsedCommands, CommandParseError> {
let mut parser =
GrammarParser::new_source_file(Lexer::new_source_file(input, self), grouping);
let commands = parser.parse_all()?;
ensure_parsed_command_lengths(&commands, self.max_command_bytes)?;
self.expand_and_lookup(commands, no_alias)
}
fn expand_and_lookup(
&self,
commands: ParsedCommands,
no_alias: bool,
) -> Result<ParsedCommands, CommandParseError> {
let assignments = commands.assignments.clone();
let mut output = ParsedCommands {
commands: Vec::new(),
assignments: commands.assignments,
grouping: commands.grouping,
};
for mut command in commands.commands {
if !no_alias {
if let Some(alias) = self.find_command_alias(&command.name) {
let mut alias_parser = self.clone();
alias_parser.environment.extend(
assignments
.iter()
.map(|assignment| (assignment.name.clone(), assignment.value.clone())),
);
let mut replacement = alias_parser
.parse_inner(alias, true, CommandGrouping::OneGroup)
.map_err(|error| error.with_line(command.line))?;
for replacement_command in &mut replacement.commands {
replacement_command.line = command.line;
replacement_command.start_line = command.start_line;
}
if let Some(last) = replacement.commands.last_mut() {
last.arguments.append(&mut command.arguments);
}
output.append(replacement);
continue;
}
}
for argument in &mut command.arguments {
if let CommandArgument::Commands(nested) = argument {
let nested_commands = std::mem::take(nested);
*nested = self.expand_and_lookup(nested_commands, no_alias)?;
}
}
let entry = lookup_command_at(&command.name, command.line, self.exact_commands)?;
command.name = entry.name.to_owned();
output.push_command(command);
}
Ok(output)
}
fn find_command_alias(&self, name: &str) -> Option<&str> {
self.command_aliases
.iter()
.find(|alias| alias.name() == name)
.map(CommandAlias::value)
}
fn lookup_environment(&self, name: &str) -> Option<&str> {
self.environment
.iter()
.rev()
.find(|(candidate, _)| candidate == name)
.map(|(_, value)| value.as_str())
}
fn expand_tilde(&self, user: &str) -> Option<&str> {
if user.is_empty() {
return self
.lookup_environment("HOME")
.filter(|home| !home.is_empty())
.or(self.home_dir.as_deref());
}
self.user_home_dirs
.iter()
.find(|(candidate, _)| candidate == user)
.map(|(_, home)| home.as_str())
}
fn condition_is_true(&self, value: &str) -> bool {
let expanded = if value.contains("#{") {
render_template(
value,
&ParseTimeFormatVariables {
values: &self.format_variables,
},
)
} else {
value.to_owned()
};
is_truthy(&expanded)
}
}
fn ensure_command_length(
bytes: usize,
line: usize,
max_command_bytes: usize,
) -> Result<(), CommandParseError> {
if bytes > max_command_bytes {
return Err(CommandParseError::new(line, "command too long"));
}
Ok(())
}
fn ensure_parsed_command_lengths(
commands: &ParsedCommands,
max_command_bytes: usize,
) -> Result<(), CommandParseError> {
for command in commands.commands() {
ensure_parsed_command_length(command, max_command_bytes)?;
}
Ok(())
}
fn ensure_parsed_command_length(
command: &ParsedCommand,
max_command_bytes: usize,
) -> Result<(), CommandParseError> {
let mut bytes = command.name.len();
for argument in command.arguments() {
bytes = bytes.saturating_add(1);
match argument {
CommandArgument::String(value) => {
bytes = bytes.saturating_add(value.len());
}
CommandArgument::Commands(commands) => {
ensure_parsed_command_lengths(commands, max_command_bytes)?;
}
}
}
ensure_command_length(bytes, command.line(), max_command_bytes)
}
struct ParseTimeFormatVariables<'a> {
values: &'a [(String, String)],
}
impl FormatVariables for ParseTimeFormatVariables<'_> {
fn format_value(&self, _variable: FormatVariable) -> Option<String> {
None
}
fn format_value_by_name(&self, name: &str) -> Option<String> {
self.values
.iter()
.rev()
.find(|(candidate, _)| candidate == name)
.map(|(_, value)| value.clone())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandParseError {
line: usize,
message: String,
kind: CommandParseErrorKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandParseErrorKind {
Structural,
Lookup,
Other,
}
impl CommandParseError {
#[must_use]
pub fn line(&self) -> usize {
self.line
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
#[must_use]
pub const fn kind(&self) -> CommandParseErrorKind {
self.kind
}
pub(crate) fn new(line: usize, message: impl Into<String>) -> Self {
Self {
line,
message: message.into(),
kind: CommandParseErrorKind::Other,
}
}
pub(crate) fn structural(line: usize, message: impl Into<String>) -> Self {
Self {
line,
message: message.into(),
kind: CommandParseErrorKind::Structural,
}
}
pub(crate) fn lookup(line: usize, message: impl Into<String>) -> Self {
Self {
line,
message: message.into(),
kind: CommandParseErrorKind::Lookup,
}
}
fn with_line(mut self, line: usize) -> Self {
self.line = line;
self
}
}
impl fmt::Display for CommandParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.message)
}
}
impl Error for CommandParseError {}
fn command_from_arguments(
mut arguments: Vec<CommandArgument>,
line: usize,
) -> Result<ParsedCommand, CommandParseError> {
let Some(CommandArgument::String(name)) = arguments.first() else {
return Err(CommandParseError::new(line, "no command"));
};
let name = name.clone();
arguments.remove(0);
Ok(ParsedCommand::new(name, arguments, line))
}
pub(crate) fn escape_argument(value: &str) -> String {
if value.is_empty() {
return "''".to_owned();
}
if is_single_char_escaped_argument(value) {
return escape_unquoted_argument(value);
}
if !value.chars().any(argument_needs_double_quotes) {
if value.contains('"') {
return format!("'{}'", escape_single_quoted_argument(value));
}
return escape_unquoted_argument(value);
}
format!("\"{}\"", escape_double_quoted_argument(value))
}
fn escape_argument_for_reparse(value: &str) -> String {
let single_quoted_display =
value.contains('"') && !value.chars().any(argument_needs_double_quotes);
if single_quoted_display && value.chars().any(|ch| ch == '\\' || ch.is_ascii_control()) {
return format!("\"{}\"", escape_double_quoted_argument(value));
}
escape_argument(value)
}
fn is_single_char_escaped_argument(value: &str) -> bool {
let mut chars = value.chars();
let Some(ch) = chars.next() else {
return false;
};
chars.next().is_none() && matches!(ch, ';' | '{' | '}' | '\'' | '"' | '#' | '$' | '%')
}
fn argument_needs_double_quotes(ch: char) -> bool {
matches!(ch, ' ' | ';' | '{' | '}' | '\'' | '#' | '$' | '%')
|| (ch.is_whitespace() && !matches!(ch, '\n' | '\r' | '\t'))
}
fn escape_unquoted_argument(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
for (index, ch) in value.chars().enumerate() {
match ch {
'~' if index == 0 => escaped.push_str(r"\~"),
';' | '{' | '}' | '\'' | '"' | '#' | '$' | '%' => {
escaped.push('\\');
escaped.push(ch);
}
'\n' => escaped.push_str(r"\n"),
'\r' => escaped.push_str(r"\r"),
'\t' => escaped.push_str(r"\t"),
'\\' => escaped.push_str(r"\\"),
_ => escape_control_argument_char(&mut escaped, ch),
}
}
escaped
}
fn escape_single_quoted_argument(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
for ch in value.chars() {
match ch {
'\n' => escaped.push_str(r"\n"),
'\r' => escaped.push_str(r"\r"),
'\t' => escaped.push_str(r"\t"),
'\\' => escaped.push_str(r"\\"),
_ => escape_control_argument_char(&mut escaped, ch),
}
}
escaped
}
fn escape_double_quoted_argument(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
let mut chars = value.chars().enumerate().peekable();
while let Some((index, ch)) = chars.next() {
match ch {
'~' if index == 0 => escaped.push_str(r"\~"),
'\n' => escaped.push_str(r"\n"),
'\r' => escaped.push_str(r"\r"),
'\t' => escaped.push_str(r"\t"),
'\u{7}' => escaped.push_str(r"\a"),
'\u{8}' => escaped.push_str(r"\b"),
'\u{b}' => escaped.push_str(r"\v"),
'\u{c}' => escaped.push_str(r"\f"),
'\u{1b}' => escaped.push_str(r"\033"),
'$' if chars
.peek()
.is_some_and(|(_, next)| dollar_starts_variable(*next)) =>
{
escaped.push_str(r"\$")
}
'\\' | '"' => {
escaped.push('\\');
escaped.push(ch);
}
_ => escape_control_argument_char(&mut escaped, ch),
}
}
escaped
}
fn escape_control_argument_char(escaped: &mut String, ch: char) {
match ch {
'\u{7}' => escaped.push_str(r"\a"),
'\u{8}' => escaped.push_str(r"\b"),
'\u{b}' => escaped.push_str(r"\v"),
'\u{c}' => escaped.push_str(r"\f"),
'\u{1b}' => escaped.push_str(r"\033"),
'\0'..='\u{1f}' | '\u{7f}' => {
escaped.push('\\');
escaped.push_str(&format!("{:03o}", ch as u32));
}
_ => escaped.push(ch),
}
}
fn dollar_starts_variable(ch: char) -> bool {
ch == '{' || ch == '_' || ch.is_ascii_alphabetic()
}
#[cfg(test)]
#[path = "command_parser/tests.rs"]
mod tests;