command-stream 1.5.2

Modern shell command execution library with streaming, async iteration, and event support
Documentation
use crate::{CommandContext, CommandResult, VirtualUtils};
use regex::{Regex, RegexBuilder};
use std::path::{Path, PathBuf};

pub(super) async fn run(command: &str, context: CommandContext) -> CommandResult {
    let result = match command {
        "find" => find(&context),
        "grep" => grep(&context),
        "sed" => sed(&context),
        "ln" => link(&context),
        "chmod" => chmod(&context),
        _ => unreachable!(),
    };
    match result {
        Ok(output) => CommandResult::success(output),
        Err(error) => CommandResult::error(format!("{command}: {error}\n")),
    }
}

type ExtraResult = std::result::Result<String, Box<dyn std::error::Error + Send + Sync>>;
fn path(value: &str, ctx: &CommandContext) -> PathBuf {
    VirtualUtils::resolve_path(value, Some(&ctx.get_cwd()))
}
fn operands(ctx: &CommandContext) -> (String, Vec<&str>) {
    if ctx
        .args
        .first()
        .is_some_and(|value| value.starts_with('-') && value != "-")
    {
        (
            ctx.args[0].clone(),
            ctx.args.iter().skip(1).map(String::as_str).collect(),
        )
    } else {
        (String::new(), ctx.args.iter().map(String::as_str).collect())
    }
}

fn walk(actual: &Path, display: &Path, output: &mut Vec<String>) -> std::io::Result<()> {
    output.push(display.display().to_string());
    if actual.symlink_metadata()?.file_type().is_dir() {
        let mut entries = std::fs::read_dir(actual)?.collect::<std::io::Result<Vec<_>>>()?;
        entries.sort_by_key(|entry| entry.file_name());
        for entry in entries {
            walk(&entry.path(), &display.join(entry.file_name()), output)?;
        }
    }
    Ok(())
}
fn find(ctx: &CommandContext) -> ExtraResult {
    if ctx.args.is_empty() {
        return Err("no paths given".into());
    }
    let mut output = Vec::new();
    for file in &ctx.args {
        walk(&path(file, ctx), Path::new(file), &mut output)?;
    }
    Ok(format!("{}\n", output.join("\n")))
}
fn grep(ctx: &CommandContext) -> ExtraResult {
    let (flags, args) = operands(ctx);
    if flags.chars().any(|flag| !"-ivnl".contains(flag)) {
        return Err("unsupported option".into());
    }
    let Some(pattern) = args.first() else {
        return Err("missing pattern".into());
    };
    let regex = RegexBuilder::new(pattern)
        .case_insensitive(flags.contains('i'))
        .build()?;
    let files = &args[1..];
    let mut output = String::new();
    for file in files {
        let text = std::fs::read_to_string(path(file, ctx))?;
        for (index, line) in text.split_inclusive('\n').enumerate() {
            if regex.is_match(line.trim_end_matches('\n')) != flags.contains('v') {
                if flags.contains('l') {
                    output.push_str(&format!("{file}\n"));
                    break;
                }
                if flags.contains('n') {
                    output.push_str(&format!("{}:", index + 1));
                }
                output.push_str(line);
                if !line.ends_with('\n') {
                    output.push('\n');
                }
            }
        }
    }
    Ok(output)
}
fn sed(ctx: &CommandContext) -> ExtraResult {
    let (flags, args) = operands(ctx);
    if flags.chars().any(|flag| !"-ig".contains(flag)) {
        return Err("unsupported option".into());
    }
    if args.len() < 3 {
        return Err("expected pattern, replacement and file".into());
    }
    let regex = Regex::new(args[0])?;
    let mut output = String::new();
    for file in &args[2..] {
        let source = std::fs::read_to_string(path(file, ctx))?;
        let replaced = source
            .split_inclusive('\n')
            .map(|line| {
                if flags.contains('g') {
                    regex.replace_all(line, args[1]).into_owned()
                } else {
                    regex.replacen(line, 1, args[1]).into_owned()
                }
            })
            .collect::<String>();
        if flags.contains('i') {
            std::fs::write(path(file, ctx), &replaced)?;
        }
        output.push_str(&replaced);
    }
    Ok(output)
}
fn link(ctx: &CommandContext) -> ExtraResult {
    let (flags, args) = operands(ctx);
    if flags.chars().any(|flag| !"-sf".contains(flag)) {
        return Err("unsupported option".into());
    }
    if args.len() != 2 {
        return Err("expected source and destination".into());
    }
    let source = path(args[0], ctx);
    let mut destination = path(args[1], ctx);
    if destination.is_dir() {
        destination = destination.join(Path::new(args[0]).file_name().ok_or("invalid source")?);
    }
    if flags.contains('f') && destination.symlink_metadata().is_ok() {
        std::fs::remove_file(&destination)?;
    }
    if flags.contains('s') {
        #[cfg(unix)]
        std::os::unix::fs::symlink(args[0], destination)?;
        #[cfg(windows)]
        {
            if source.is_dir() {
                std::os::windows::fs::symlink_dir(args[0], destination)?;
            } else {
                std::os::windows::fs::symlink_file(args[0], destination)?;
            }
        }
    } else {
        std::fs::hard_link(source, destination)?;
    }
    Ok(String::new())
}
fn chmod(ctx: &CommandContext) -> ExtraResult {
    let (flags, args) = operands(ctx);
    if flags.chars().any(|flag| !"-R".contains(flag)) {
        return Err("unsupported option".into());
    }
    if args.len() < 2 {
        return Err("expected octal mode and file".into());
    }
    let mode = u32::from_str_radix(args[0], 8)?;
    if mode > 0o7777 {
        return Err("invalid octal mode".into());
    }
    for file in &args[1..] {
        let mut paths = Vec::new();
        let actual = path(file, ctx);
        if flags.contains('R') {
            walk(&actual, &actual, &mut paths)?;
        } else {
            paths.push(actual.display().to_string());
        }
        for target in paths {
            let mut permissions = std::fs::metadata(&target)?.permissions();
            #[cfg(unix)]
            {
                use std::os::unix::fs::PermissionsExt;
                permissions.set_mode(mode);
            }
            #[cfg(windows)]
            permissions.set_readonly(mode & 0o200 == 0);
            std::fs::set_permissions(target, permissions)?;
        }
    }
    Ok(String::new())
}