bnb-shell 0.1.0

A fast, polished, cross-platform terminal shell built in Rust with Powerlevel10k aesthetics.
use crate::parser::ast::{Pipeline, Redirection};
use std::env;
use std::fs::{File, OpenOptions};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};

fn get_home_dir() -> Option<String> {
    env::var("HOME").or_else(|_| env::var("USERPROFILE")).ok()
}

fn find_binary(cmd: &str) -> Option<PathBuf> {
    let path = Path::new(cmd);
    if path.is_absolute() || cmd.contains('/') || cmd.contains('\\') {
        if path.is_file() {
            return Some(path.to_path_buf());
        }
        #[cfg(target_os = "windows")]
        {
            let with_exe = path.with_extension("exe");
            if with_exe.is_file() {
                return Some(with_exe);
            }
        }
        return None;
    }

    let mut search_dirs = Vec::new();

    if let Ok(path_var) = env::var("PATH") {
        search_dirs.extend(env::split_paths(&path_var));
    }

    if cfg!(not(target_os = "windows")) {
        let default_dirs = [
            "/bin",
            "/usr/bin",
            "/usr/local/bin",
            "/opt/homebrew/bin",
            "/usr/sbin",
            "/sbin",
        ];
        for d in default_dirs {
            search_dirs.push(PathBuf::from(d));
        }
    }

    if let Some(home) = get_home_dir() {
        search_dirs.push(PathBuf::from(home).join(".cargo/bin"));
    }

    for dir in search_dirs {
        let candidate = dir.join(cmd);
        if candidate.is_file() {
            return Some(candidate);
        }
        #[cfg(target_os = "windows")]
        {
            let cand_exe = dir.join(format!("{}.exe", cmd));
            if cand_exe.is_file() {
                return Some(cand_exe);
            }
        }
    }

    None
}

pub fn execute_pipeline(pipeline: &Pipeline) -> Result<i32, String> {
    let num_cmds = pipeline.commands.len();
    if num_cmds == 0 {
        return Ok(0);
    }

    if num_cmds == 1 {
        let cmd = &pipeline.commands[0];
        if cmd.args.is_empty() {
            return Ok(0);
        }

        let binary_path = find_binary(&cmd.args[0]);

        let mut sys_cmd = match binary_path {
            Some(path) => {
                let mut c = Command::new(path);
                if cmd.args.len() > 1 {
                    c.args(&cmd.args[1..]);
                }
                c
            }
            None => {
                let full_cmd = cmd.args.join(" ");
                if cfg!(target_os = "windows") {
                    let mut c = Command::new("cmd.exe");
                    c.args(["/C", &full_cmd]);
                    c
                } else {
                    let mut c = Command::new("zsh");
                    c.args(["-c", &format!("source ~/.zshrc 2>/dev/null; {}", full_cmd)]);
                    c
                }
            }
        };

        for redir in &cmd.redirections {
            match redir {
                Redirection::OutputTruncate(path) => {
                    let file = File::create(path)
                        .map_err(|e| format!("bnb: cannot create file {}: {}", path, e))?;
                    sys_cmd.stdout(Stdio::from(file));
                }
                Redirection::OutputAppend(path) => {
                    let file = OpenOptions::new()
                        .create(true)
                        .append(true)
                        .open(path)
                        .map_err(|e| format!("bnb: cannot append to file {}: {}", path, e))?;
                    sys_cmd.stdout(Stdio::from(file));
                }
                Redirection::Input(path) => {
                    let file = File::open(path)
                        .map_err(|_| format!("bnb: {}: No such file or directory", path))?;
                    sys_cmd.stdin(Stdio::from(file));
                }
            }
        }

        if pipeline.run_in_background {
            let child = sys_cmd.spawn().map_err(|e| format!("bnb: error: {}", e))?;
            println!("[+] Job backgrounded PID: {}", child.id());
            return Ok(0);
        } else {
            let mut child = sys_cmd.spawn().map_err(|e| format!("bnb: error: {}", e))?;
            let status = child.wait().map_err(|e| format!("bnb: wait error: {}", e))?;
            return Ok(status.code().unwrap_or(1));
        }
    }

    let mut children = Vec::new();
    let mut prev_stdout: Option<Stdio> = None;

    for (i, cmd) in pipeline.commands.iter().enumerate() {
        if cmd.args.is_empty() {
            continue;
        }

        let binary_path = find_binary(&cmd.args[0]);
        let mut sys_cmd = match binary_path {
            Some(path) => {
                let mut c = Command::new(path);
                if cmd.args.len() > 1 {
                    c.args(&cmd.args[1..]);
                }
                c
            }
            None => {
                let full_cmd = cmd.args.join(" ");
                if cfg!(target_os = "windows") {
                    let mut c = Command::new("cmd.exe");
                    c.args(["/C", &full_cmd]);
                    c
                } else {
                    let mut c = Command::new("zsh");
                    c.args(["-c", &format!("source ~/.zshrc 2>/dev/null; {}", full_cmd)]);
                    c
                }
            }
        };

        if let Some(stdin) = prev_stdout.take() {
            sys_cmd.stdin(stdin);
        }

        if i < num_cmds - 1 {
            sys_cmd.stdout(Stdio::piped());
        }

        for redir in &cmd.redirections {
            match redir {
                Redirection::OutputTruncate(path) => {
                    if let Ok(file) = File::create(path) {
                        sys_cmd.stdout(Stdio::from(file));
                    }
                }
                Redirection::OutputAppend(path) => {
                    if let Ok(file) = OpenOptions::new().create(true).append(true).open(path) {
                        sys_cmd.stdout(Stdio::from(file));
                    }
                }
                Redirection::Input(path) => {
                    if let Ok(file) = File::open(path) {
                        sys_cmd.stdin(Stdio::from(file));
                    }
                }
            }
        }

        let mut child = sys_cmd.spawn().map_err(|e| format!("bnb: error: {}", e))?;

        if i < num_cmds - 1 {
            if let Some(stdout) = child.stdout.take() {
                prev_stdout = Some(Stdio::from(stdout));
            }
        }

        children.push(child);
    }

    let mut last_code = 0;
    if !pipeline.run_in_background {
        for mut child in children {
            if let Ok(status) = child.wait() {
                last_code = status.code().unwrap_or(0);
            }
        }
    } else if let Some(last_child) = children.last() {
        println!("[+] Job backgrounded PID: {}", last_child.id());
    }

    Ok(last_code)
}