mod awk;
mod commands;
mod copy;
mod cwd;
mod database;
mod decoders;
pub(crate) mod git;
mod invocation;
mod make;
mod operands;
mod options;
mod sed;
pub(crate) mod tables;
#[cfg(test)]
mod tests;
mod text;
pub(crate) mod tokens;
use crate::action::AtomicAction;
use crate::lexer::{LexError, Token};
use crate::paths;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseOutcome {
Parsed(Vec<AtomicAction>),
Unparseable {
reason: String,
atoms: Vec<AtomicAction>,
},
}
impl ParseOutcome {
#[must_use]
pub fn into_parts(self) -> (Vec<AtomicAction>, Option<String>) {
match self {
Self::Parsed(atoms) => (atoms, None),
Self::Unparseable { reason, atoms } => (atoms, Some(reason)),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ShellContext<'a> {
pub home: &'a str,
pub project: Option<&'a str>,
pub cwd: &'a [Option<String>],
}
impl ShellContext<'_> {
pub(crate) fn paths(&self, word: &str) -> Result<Vec<String>, ClassifyError> {
if let Some(path) = paths::resolve(word, self.home, self.project, None) {
return Ok(vec![path]);
}
self.cwd
.iter()
.map(|cwd| {
paths::resolve(word, self.home, self.project, cwd.as_deref())
.ok_or_else(|| ClassifyError::UnknownDirectory(word.to_owned()))
})
.collect()
}
}
pub const MAX_DEPTH: u8 = 4;
pub const MAX_ATOMS: usize = 2048;
pub const MAX_DIRS: usize = 16;
#[cfg(test)]
#[must_use]
pub fn classify(command: &str, ctx: &ShellContext<'_>) -> ParseOutcome {
classify_globs(command, ctx).0
}
#[must_use]
pub fn classify_globs(command: &str, ctx: &ShellContext<'_>) -> (ParseOutcome, Vec<AtomicAction>) {
let mut sink = Sink::default();
let stopped = commands::classify_into(command, ctx, &mut sink, 0).err();
let outcome = match sink.skipped.or(stopped) {
Some(e) => ParseOutcome::Unparseable {
reason: e.to_string(),
atoms: sink.atoms,
},
None if sink.atoms.is_empty() => ParseOutcome::Unparseable {
reason: "no command found".to_owned(),
atoms: Vec::new(),
},
None => ParseOutcome::Parsed(sink.atoms),
};
(outcome, sink.globs)
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum ClassifyError {
#[error(transparent)]
Lex(#[from] LexError),
#[error("command nesting deeper than {MAX_DEPTH}")]
TooDeep,
#[error("command expands to more than {MAX_ATOMS} actions")]
TooManyActions,
#[error("cannot interpret `{option}` for `{program}`")]
ShellOption { program: String, option: String },
#[error("`{0}` names a path relative to a directory that is not known")]
UnknownDirectory(String),
#[error("`cd` may lead to more than {MAX_DIRS} directories")]
TooManyDirectories,
#[error("unbalanced parentheses")]
UnbalancedParens,
#[error("the shell runs text from `{0}` that the command line does not show")]
UnknownStdin(String),
}
#[derive(Debug, Default)]
pub(crate) struct Sink {
atoms: Vec<AtomicAction>,
skipped: Option<ClassifyError>,
glob_words: Vec<String>,
globs: Vec<AtomicAction>,
}
impl Sink {
pub(crate) fn push(&mut self, atom: AtomicAction) -> Result<(), ClassifyError> {
if self.atoms.contains(&atom) {
return Ok(());
}
if self.atoms.len() >= MAX_ATOMS {
return Err(ClassifyError::TooManyActions);
}
self.atoms.push(atom);
Ok(())
}
pub(crate) fn skip(&mut self, error: ClassifyError) {
self.skipped.get_or_insert(error);
}
fn paths(&mut self, ctx: &ShellContext<'_>, word: &str) -> Vec<String> {
ctx.paths(word).unwrap_or_else(|e| {
self.skip(e);
Vec::new()
})
}
pub(crate) fn note_globs(&mut self, tokens: &[Token]) {
for token in tokens {
if let Token::Word(w) = token
&& w.glob
&& !self.glob_words.contains(&w.text)
{
self.glob_words.push(w.text.clone());
}
}
}
pub(crate) fn read(&mut self, ctx: &ShellContext<'_>, word: &str) -> Result<(), ClassifyError> {
self.fs(ctx, word, |path| AtomicAction::FsRead { path })
}
pub(crate) fn write(
&mut self,
ctx: &ShellContext<'_>,
word: &str,
) -> Result<(), ClassifyError> {
self.fs(ctx, word, |path| AtomicAction::FsWrite { path })
}
fn fs(
&mut self,
ctx: &ShellContext<'_>,
word: &str,
atom: fn(String) -> AtomicAction,
) -> Result<(), ClassifyError> {
let glob =
word.contains(['*', '?', '[']) && self.glob_words.iter().any(|w| w.ends_with(word));
for path in self.paths(ctx, word) {
let atom = atom(path);
if glob && !self.globs.contains(&atom) {
self.globs.push(atom.clone());
}
self.push(atom)?;
}
Ok(())
}
}