mod locals;
mod payload;
mod piped;
mod pipelines;
mod wrappers;
use super::tables::{
ENV_BUILTINS, HOST_PROGRAMS, MAKES, PACKAGE_RUNNERS, RESERVED_CLOSERS, RESERVED_PREFIXES,
SHELLS, SOURCE_BUILTINS, WRITE_ALL_PATHS, inline_flags,
};
use super::tokens::{
assignment_name, basename, flag_payload, for_variable, strip_at, word_env_refs,
};
use super::{ClassifyError, MAX_DEPTH, ShellContext, Sink};
use super::{
copy, cwd, database, decoders, embedded, git, invocation, make, operands, options, text,
};
use crate::action::AtomicAction;
use crate::host;
use crate::lexer::{self, Operator, Token, Word};
use crate::paths;
use piped::Piped;
use wrappers::wrapped_command;
#[derive(Debug)]
enum Item {
Command(SimpleCommand),
Open,
Close,
}
#[derive(Debug, Default, Clone)]
struct SimpleCommand {
words: Vec<Word>,
reads: Vec<String>,
writes: Vec<String>,
stdin: Vec<Word>,
literal_stdin: Vec<String>,
piped: Vec<Piped>,
}
impl SimpleCommand {
fn is_empty(&self) -> bool {
self.words.is_empty()
&& self.reads.is_empty()
&& self.writes.is_empty()
&& self.stdin.is_empty()
&& self.literal_stdin.is_empty()
}
fn stdin_texts(&self) -> impl Iterator<Item = &str> {
let expanded = self.stdin.iter().map(|w| w.text.as_str());
expanded.chain(self.literal_stdin.iter().map(String::as_str))
}
}
pub(super) fn classify_into(
command: &str,
ctx: &ShellContext<'_>,
sink: &mut Sink,
depth: u8,
) -> Result<(), ClassifyError> {
if depth > MAX_DEPTH {
return Err(ClassifyError::TooDeep);
}
let (tokens, too_many) = lexer::expand_braces(lexer::lex(command)?);
if let Some(e) = too_many {
sink.skip(e.into());
}
sink.note_globs(&tokens);
let mut dirs = ctx.cwd.to_vec();
let mut saved = Vec::new();
for item in group_commands(&tokens) {
match item {
Item::Open => saved.push(dirs.clone()),
Item::Close => match saved.pop() {
Some(outer) => dirs = outer,
None => sink.skip(ClassifyError::UnbalancedParens),
},
Item::Command(cmd) => {
let here = ShellContext { cwd: &dirs, ..*ctx };
if let Err(e) = classify_simple(&cmd, &here, sink, depth) {
sink.skip(e);
}
let moved = cwd::targets(&cmd.words, &here);
cwd::merge(&mut dirs, moved)?;
}
}
}
if !saved.is_empty() {
sink.skip(ClassifyError::UnbalancedParens);
}
pipelines::push(&tokens, sink)?;
decoders::push(&tokens, sink)
}
fn group_commands(tokens: &[Token]) -> Vec<Item> {
let mut commands = Vec::new();
let mut current = SimpleCommand::default();
let mut pending_redirect: Option<Operator> = None;
for token in tokens {
match token {
Token::Operator(op) if op.is_separator() || op.is_grouping() => {
pending_redirect = None;
if !current.is_empty() {
let piped = piped::data(¤t).filter(|_| *op == Operator::Pipe);
commands.push(Item::Command(std::mem::take(&mut current)));
current.piped.extend(piped);
} else if *op == Operator::Pipe && matches!(commands.last(), Some(Item::Close)) {
current.piped.push(Piped::Output("( … )".to_owned()));
}
match op {
Operator::OpenParen => commands.push(Item::Open),
Operator::CloseParen => commands.push(Item::Close),
_ => {}
}
}
Token::Operator(op) => pending_redirect = Some(*op),
Token::HereDoc {
body,
literal: true,
} => current.literal_stdin.push(body.text.clone()),
Token::HereDoc { body, .. } => current.stdin.push(body.clone()),
Token::Word(w) => match pending_redirect.take() {
Some(Operator::RedirectIn) => current.reads.push(w.text.clone()),
Some(Operator::HereString) => current.stdin.push(w.clone()),
Some(Operator::RedirectOut | Operator::RedirectAppend) => {
current.writes.push(w.text.clone());
}
Some(Operator::RedirectReadWrite) => {
current.reads.push(w.text.clone());
current.writes.push(w.text.clone());
}
Some(Operator::DuplicateDescriptor) => {
if !w.text.chars().all(|c| c.is_ascii_digit() || c == '-') {
current.writes.push(w.text.clone());
}
}
None | Some(_) => current.words.push(w.clone()),
},
}
}
if !current.is_empty() {
commands.push(Item::Command(current));
}
commands
}
fn classify_simple(
cmd: &SimpleCommand,
ctx: &ShellContext<'_>,
sink: &mut Sink,
depth: u8,
) -> Result<(), ClassifyError> {
if locals::classify_with_values(cmd, ctx, sink, depth)? {
return Ok(());
}
for w in cmd.words.iter().chain(&cmd.stdin) {
for inner in &w.substitutions {
classify_into(inner, ctx, sink, depth + 1)?;
}
}
sink.enter(depth);
for w in &cmd.stdin {
for name in word_env_refs(w) {
sink.push(AtomicAction::EnvRead { name })?;
}
}
for r in &cmd.reads {
sink.redirect_read(ctx, r)?;
}
for w in &cmd.writes {
sink.redirect_write(ctx, w)?;
}
let mut idx = cmd
.words
.iter()
.take_while(|w| !w.quoted && RESERVED_PREFIXES.contains(&w.text.as_str()))
.count();
if let [only] = &cmd.words[idx..]
&& !only.quoted
&& RESERVED_CLOSERS.contains(&only.text.as_str())
{
return Ok(());
}
if let Some(loop_var) = for_variable(&cmd.words[idx..]) {
sink.push(AtomicAction::EnvSet {
name: loop_var.to_owned(),
})?;
for word in &cmd.words[idx + 3..] {
for name in word_env_refs(word) {
sink.push(AtomicAction::EnvRead { name })?;
}
}
sink.note_local(
loop_var,
locals::loop_values(&cmd.words[idx + 3..], ctx.home),
);
return Ok(());
}
let first_assignment = idx;
while let Some(word) = cmd.words.get(idx) {
let Some(name) = assignment_name(&word.text) else {
break;
};
sink.push(AtomicAction::EnvSet {
name: name.to_owned(),
})?;
for name in word_env_refs(word) {
sink.push(AtomicAction::EnvRead { name })?;
}
idx += 1;
}
let words = &cmd.words[idx..];
if words.is_empty() {
for word in &cmd.words[first_assignment..idx] {
if let Some(name) = assignment_name(&word.text) {
let value = locals::assignment_value(word, ctx.home);
sink.note_local(name, value.map(|v| vec![v]));
}
}
return Ok(());
}
let argv: Vec<String> = words.iter().map(|w| w.text.clone()).collect();
let program = basename(&argv[0]);
if program == "git" {
git::push_shell(&argv, ctx, sink)?;
} else {
sink.push(AtomicAction::Shell { argv: argv.clone() })?;
}
if ENV_BUILTINS.contains(&program) {
for name in argv[1..].iter().filter_map(|w| assignment_name(w)) {
sink.push(AtomicAction::EnvSet {
name: name.to_owned(),
})?;
}
locals::note_builtin(&words[1..], program, ctx.home, sink);
}
if program == "printenv" {
for name in argv[1..].iter().filter(|a| !a.starts_with('-')) {
sink.push(AtomicAction::EnvRead { name: name.clone() })?;
}
}
if SOURCE_BUILTINS.contains(&program)
&& let Some(file) = argv.get(1)
{
sink.read(ctx, file)?;
}
let mut text_operands = text::classify(&argv, program, ctx, sink)?;
let stdin = cmd
.stdin_texts()
.chain(cmd.piped.iter().filter_map(Piped::text));
text_operands
.data
.extend(database::classify(&argv, program, stdin, ctx, sink)?);
text_operands.data.extend(git::config_name(&argv));
text_operands
.data
.extend(options::fd_pattern(&argv, program).or(options::grep_pattern(&argv, program)));
super::path_options::classify(&argv, program, &mut text_operands, ctx, sink)?;
classify_arguments(&argv, program, words, &text_operands, ctx, sink)?;
for code in embedded::commands(&argv, program) {
classify_into(&code, ctx, sink, depth + 1)?;
}
if MAKES.contains(&program) {
return make::classify(&argv, ctx, sink, depth);
}
if program == "eval" && argv.len() > 1 {
return classify_into(&argv[1..].join(" "), ctx, sink, depth + 1);
}
if SHELLS.contains(&program) {
let run = invocation::parse(program, &argv)?;
let piped = cmd.piped.iter().filter_map(Piped::text);
let stdin_code = cmd.stdin_texts().chain(piped).filter(|_| run.reads_stdin);
for code in run.code.into_iter().chain(stdin_code) {
classify_into(code, ctx, sink, depth + 1)?;
}
if let Some(script) = run.script {
sink.read(ctx, script)?;
}
if !run.reads_stdin || !cmd.piped.iter().any(Piped::is_hidden) {
return Ok(());
}
sink.push(AtomicAction::Shell {
argv: vec![program.to_owned(), "@stdin".to_owned()],
})?;
return match cmd.piped.iter().find_map(Piped::unknown) {
Some(producer) => Err(ClassifyError::UnknownStdin(producer.into())),
None => Ok(()),
};
}
if PACKAGE_RUNNERS.contains(&program)
&& let Some(payload) = flag_payload(&argv, &["-c", "--call"])
{
return classify_into(payload, ctx, sink, depth + 1);
}
if let Some(inner) = wrapped_command(&argv, program) {
return classify_wrapped(inner, ctx, sink, depth);
}
let inline = inline_flags(program);
if let Some(flags) = inline
&& let Some(payload) = flag_payload(&argv, flags)
{
payload::scan_payload(payload, ctx, sink)?;
}
if inline.is_some() {
for text in cmd.stdin_texts() {
payload::scan_payload(text, ctx, sink)?;
}
}
options::classify(&argv, program, ctx, sink, depth)
}
pub(super) fn classify_wrapped(
inner: &[String],
ctx: &ShellContext<'_>,
sink: &mut Sink,
depth: u8,
) -> Result<(), ClassifyError> {
if depth >= MAX_DEPTH {
return Err(ClassifyError::TooDeep);
}
let cmd = SimpleCommand {
words: inner
.iter()
.map(|t| Word {
text: t.clone(),
literal: std::iter::once(0..t.len()).collect(),
..Word::default()
})
.collect(),
..SimpleCommand::default()
};
classify_simple(&cmd, ctx, sink, depth + 1)
}
fn classify_arguments(
argv: &[String],
program: &str,
words: &[Word],
text_operands: &text::Operands,
ctx: &ShellContext<'_>,
sink: &mut Sink,
) -> Result<(), ClassifyError> {
let copy = copy::destination(argv, program).filter(|d| {
paths::looks_like_path(d.word) || operands::file_operand(d.word, program, true).is_some()
});
let mut sources = Vec::new();
let options_end = argv.iter().position(|a| a == "--");
let names_hosts =
HOST_PROGRAMS.contains(&program) || (program == "git" && git::names_hosts(argv));
for (i, (tok, word)) in argv.iter().zip(words).enumerate() {
for name in word_env_refs(word) {
sink.push(AtomicAction::EnvRead { name })?;
}
if program == "env"
&& i > 0
&& let Some(name) = assignment_name(tok)
{
sink.push(AtomicAction::EnvSet {
name: name.to_owned(),
})?;
}
let is_destination = copy.as_ref().is_some_and(|d| d.index == i);
if i == 0 || text_operands.data.contains(&i) || is_destination {
continue;
}
let options_ended = options_end.is_some_and(|end| i > end);
if options_ended || !tok.starts_with('-') {
sources.push(tok.as_str());
}
if let Some(path) = host::file_url_path(tok) {
sink.read(ctx, &path)?;
continue;
}
let candidate = strip_at(tok);
let file = if paths::looks_like_path(candidate) {
Some(candidate)
} else if program == "dd"
&& let Some((key, value)) = tok.split_once('=')
{
match key {
"if" => sink.read(ctx, value)?,
"of" => sink.write(ctx, value)?,
_ => {}
}
continue;
} else if (!word.quoted || names_hosts || tok.contains("://"))
&& let Some(host) = host::of_argument(program, tok)
{
sink.push(if tok.contains("://") || names_hosts {
AtomicAction::Net { host }
} else {
AtomicAction::NetWord { host }
})?;
continue;
} else {
operands::file_operand(tok, program, options_ended)
};
if let Some(file) = file {
if WRITE_ALL_PATHS.contains(&program) || text_operands.writes.contains(&i) {
sink.write(ctx, file)?;
} else {
sink.read(ctx, file)?;
}
}
}
if let Some(dest) = © {
for path in copy::written(dest, &sources) {
sink.write(ctx, &path)?;
}
}
Ok(())
}