use std::borrow::Borrow;
use std::fmt;
use std::ops::Deref;
use super::tokenize::{is_env_assignment, is_valid_env_key};
#[derive(Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct Word(String);
impl Word {
pub fn is_flag(&self) -> bool {
self.0.starts_with('-')
}
pub fn is_assignment(&self) -> bool {
is_env_assignment(&self.0)
}
pub fn as_assignment(&self) -> Option<(&str, &str)> {
let eq_pos = self.0.find('=')?;
let key = &self.0[..eq_pos];
if is_valid_env_key(key) {
Some((key, &self.0[eq_pos + 1..]))
} else {
None
}
}
pub fn basename(&self) -> &str {
match self.0.rsplit_once('/') {
Some((_, name)) if !name.is_empty() => name,
_ => &self.0,
}
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl Deref for Word {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl AsRef<str> for Word {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Borrow<str> for Word {
fn borrow(&self) -> &str {
&self.0
}
}
impl fmt::Display for Word {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl fmt::Debug for Word {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
impl From<String> for Word {
fn from(s: String) -> Self {
Word(s)
}
}
impl From<&str> for Word {
fn from(s: &str) -> Self {
Word(s.to_string())
}
}
impl PartialEq<str> for Word {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for Word {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<Word> for str {
fn eq(&self, other: &Word) -> bool {
self == other.0
}
}
impl PartialEq<Word> for &str {
fn eq(&self, other: &Word) -> bool {
*self == other.0
}
}
impl PartialEq<String> for Word {
fn eq(&self, other: &String) -> bool {
self.0 == *other
}
}
impl PartialEq<Word> for String {
fn eq(&self, other: &Word) -> bool {
*self == other.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Operator {
And,
Or,
Semi,
Pipe,
PipeErr,
Background,
}
impl Operator {
pub fn as_str(&self) -> &'static str {
match self {
Operator::And => "&&",
Operator::Or => "||",
Operator::Semi => ";",
Operator::Pipe => "|",
Operator::PipeErr => "|&",
Operator::Background => "&",
}
}
}
impl fmt::Display for Operator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct ParsedPipeline {
pub segments: Vec<ShellSegment>,
pub operators: Vec<Operator>,
pub structural_substitutions: Vec<SubstitutionSpan>,
pub has_parse_errors: bool,
}
impl ParsedPipeline {
pub fn empty_with_error() -> Self {
Self {
segments: vec![],
operators: vec![],
structural_substitutions: vec![],
has_parse_errors: true,
}
}
pub fn find_pipeline<T>(&self, f: &impl Fn(&ParsedPipeline) -> Option<T>) -> Option<T> {
if let Some(hit) = f(self) {
return Some(hit);
}
for sub in &self.structural_substitutions {
if let Some(hit) = sub.pipeline.find_pipeline(f) {
return Some(hit);
}
}
for seg in &self.segments {
for sub in &seg.substitutions {
if let Some(hit) = sub.pipeline.find_pipeline(f) {
return Some(hit);
}
}
}
None
}
pub fn any_pipeline(&self, f: &impl Fn(&ParsedPipeline) -> bool) -> bool {
self.find_pipeline(&|p| if f(p) { Some(()) } else { None })
.is_some()
}
pub fn find_segment<T>(&self, f: &impl Fn(&ShellSegment) -> Option<T>) -> Option<T> {
for sub in &self.structural_substitutions {
if let Some(hit) = sub.pipeline.find_segment(f) {
return Some(hit);
}
}
for seg in &self.segments {
for sub in &seg.substitutions {
if let Some(hit) = sub.pipeline.find_segment(f) {
return Some(hit);
}
}
if let Some(hit) = f(seg) {
return Some(hit);
}
}
None
}
pub fn filter_segments<T>(&self, f: &impl Fn(&ShellSegment) -> Option<T>) -> Vec<T> {
let mut out = Vec::new();
self.filter_segments_into(f, &mut out);
out
}
fn filter_segments_into<T>(&self, f: &impl Fn(&ShellSegment) -> Option<T>, out: &mut Vec<T>) {
for sub in &self.structural_substitutions {
sub.pipeline.filter_segments_into(f, out);
}
for seg in &self.segments {
for sub in &seg.substitutions {
sub.pipeline.filter_segments_into(f, out);
}
if let Some(hit) = f(seg) {
out.push(hit);
}
}
}
pub fn has_parse_errors_recursive(&self) -> bool {
self.any_pipeline(&|p| p.has_parse_errors)
}
}
#[derive(Debug, Clone)]
pub struct ShellSegment {
pub command: String,
pub words: Vec<Word>,
pub redirection: Option<Redirection>,
pub substitutions: Vec<SubstitutionSpan>,
}
#[derive(Debug, Clone)]
pub struct SubstitutionSpan {
pub start: usize,
pub end: usize,
pub pipeline: ParsedPipeline,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Redirection {
pub operator: &'static str,
pub fd: Option<u32>,
pub target: String,
}
impl fmt::Display for Redirection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.fd {
Some(fd) => write!(
f,
"output redirection ({fd}{} {})",
self.operator, self.target
),
None => write!(f, "output redirection ({} {})", self.operator, self.target),
}
}
}
#[derive(Debug, thiserror::Error)]
#[error("tree-sitter failed to produce a syntax tree")]
pub struct ParseError;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum IndirectExecution {
Eval,
ShellSpawn,
CommandWrapper,
SourceScript,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandCharacteristics {
pub base_command: String,
pub indirect_execution: Option<IndirectExecution>,
pub has_dynamic_command: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedFlag {
pub name: Word,
pub value: Option<Word>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CommandArg {
Flag(ParsedFlag),
Positional(Word),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedCommand {
pub command: Word,
pub args: Vec<CommandArg>,
}
impl ParsedCommand {
pub fn from_words(words: &[Word]) -> Self {
let cmd_idx = words.iter().position(|w| !w.is_assignment());
let Some(cmd_idx) = cmd_idx else {
return ParsedCommand {
command: Word::from(""),
args: vec![],
};
};
let base = Word::from(words[cmd_idx].basename());
let mut args = Vec::new();
let mut past_double_dash = false;
for token in &words[cmd_idx + 1..] {
if past_double_dash {
args.push(CommandArg::Positional(token.clone()));
continue;
}
if token == "--" {
past_double_dash = true;
continue;
}
if let Some(rest) = token.strip_prefix("--") {
if let Some((name, value)) = rest.split_once('=') {
args.push(CommandArg::Flag(ParsedFlag {
name: Word::from(format!("--{name}")),
value: Some(Word::from(value)),
}));
} else {
args.push(CommandArg::Flag(ParsedFlag {
name: token.clone(),
value: None,
}));
}
} else if token.starts_with('-') && token.len() > 1 {
args.push(CommandArg::Flag(ParsedFlag {
name: token.clone(),
value: None,
}));
} else {
args.push(CommandArg::Positional(token.clone()));
}
}
ParsedCommand {
command: base,
args,
}
}
pub fn subcommand(&self) -> Option<&str> {
self.args.iter().find_map(|a| match a {
CommandArg::Positional(s) => Some(s.as_str()),
_ => None,
})
}
pub fn flags(&self) -> impl Iterator<Item = &ParsedFlag> {
self.args.iter().filter_map(|a| match a {
CommandArg::Flag(f) => Some(f),
_ => None,
})
}
pub fn positional(&self) -> impl Iterator<Item = &str> {
self.args.iter().filter_map(|a| match a {
CommandArg::Positional(s) => Some(s.as_str()),
_ => None,
})
}
pub fn has_flag(&self, name: &str) -> bool {
self.flags().any(|f| f.name == name)
}
pub fn to_words(&self) -> Vec<Word> {
let mut words = vec![self.command.clone()];
for arg in &self.args {
match arg {
CommandArg::Flag(f) => match &f.value {
Some(v) => words.push(Word::from(format!("{}={}", f.name, v))),
None => words.push(f.name.clone()),
},
CommandArg::Positional(s) => words.push(s.clone()),
}
}
words
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum ResolvedCommand {
Resolved(ParsedCommand),
Unanalyzable(UnanalyzableCommand),
}
#[derive(Debug, Clone)]
pub struct UnanalyzableCommand {
pub command: String,
pub kind: IndirectExecution,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct WrapperSpec {
pub name: String,
#[serde(default)]
pub short_value_flags: Vec<String>,
#[serde(default)]
pub long_value_flags: Vec<String>,
#[serde(default)]
pub unanalyzable_flags: Vec<String>,
#[serde(default)]
pub skip_env_assignments: bool,
#[serde(default)]
pub has_terminator: bool,
#[serde(default)]
pub skip_positionals: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct CommandConfig {
pub wrappers: Vec<WrapperSpec>,
pub shells: Vec<String>,
pub eval_commands: Vec<String>,
pub source_commands: Vec<String>,
}
#[cfg(test)]
#[path = "types_tests.rs"]
mod types_tests;