lumesh 0.18.2

a lighting shell ⚡ bash alternative
Documentation
use crate::{
    Environment, Expression, RuntimeError, RuntimeErrorKind, childman,
    expression::pty::exec_in_pty,
    runtime::{IFS_CMD, ifs_contains},
    utils::{expand_home, get_current_path},
};

use super::eval::State;

use glob::glob;
// use portable_pty::ChildKiller;
// use portable_pty::{CommandBuilder, PtySize, native_pty_system};
use std::{
    io::Write,
    process::{Command, Stdio},
};

/// mode: 1=null_stdout, 2=null_err, 4=err_to_stdout,
/// 8=background, 11=background,shutdown_all
/// 16=pty
/// 执行单个命令(支持管道)
#[allow(clippy::too_many_arguments)]
fn exec_single_cmd(
    job: &Expression,
    cmdstr: &String,
    args: Option<Vec<String>>,
    env: &mut Environment,
    input: Option<Vec<u8>>, // 前一条命令的输出(None 表示第一个命令)
    pipe_out: bool,
    mode: u8,
    depth: usize,
) -> Result<Option<Vec<u8>>, RuntimeError> {
    // dbg!("------ exec:------", &cmdstr, &args);
    // dbg!(&mode, &pipe_out, &input.is_some());
    // dbg!(&input);
    if mode & 16 != 0 {
        // spawn_in_pty(cmdstr, args, env, input);
        return exec_in_pty(cmdstr, args, env, input)
            .map_err(|e| RuntimeError::new(e, job.clone(), depth));
    }
    let mut cmd = Command::new(cmdstr);

    let ar = args.unwrap_or_default();
    let ar_display = ar.join(" ");

    cmd.args(ar)
        .envs(env.get_root().get_bindings_string())
        .current_dir(get_current_path(env));

    // 设置 stdin
    if input.is_some() {
        cmd.stdin(Stdio::piped());
    } else {
        cmd.stdin(Stdio::inherit());
    }

    // 设置 stdout(如果是交互式命令,直接接管终端)
    if pipe_out {
        cmd.stdout(Stdio::piped());
    } else if mode & 1 != 0 {
        cmd.stdout(Stdio::null());
    } else {
        // if mode == 0 {
        cmd.stdout(Stdio::inherit());
    }

    // 设置 stderr
    if mode & 2 != 0 {
        cmd.stderr(Stdio::null());
    } else if mode & 4 != 0 {
        cmd.stderr(Stdio::piped());
    } else {
        cmd.stderr(Stdio::inherit());
    }

    // 执行命令
    let mut child = cmd.spawn().map_err(|e| match &e.kind() {
        std::io::ErrorKind::NotFound => RuntimeError::new(
            RuntimeErrorKind::ProgramNotFound(cmdstr.clone()),
            job.clone(),
            depth,
        ),
        std::io::ErrorKind::PermissionDenied => RuntimeError::new(
            RuntimeErrorKind::PermissionDenied(cmdstr.clone()),
            job.clone(),
            depth,
        ),
        _ => RuntimeError::from_io_error(
            e,
            format!("spawn cmd `{cmdstr}`").into(),
            job.clone(),
            depth,
        ),
    })?;

    // 写入输入
    if let Some(input) = input {
        if let Some(mut stdin) = child.stdin.take() {
            // take() 拿走所有权
            stdin.write_all(&input).map_err(|e| {
                RuntimeError::from_io_error(
                    e,
                    format!("pipe stdin to `{cmdstr}`").into(),
                    job.clone(),
                    depth,
                )
            })?;
            // stdin 在这里超出作用域被 drop,管道写端关闭 -> 子进程读到 EOF
        }
    }
    // 非管道模式下,若需要把 stderr 合并到 stdout,用独立线程并发转发,避免与 stdout/stdin 读写产生死锁
    let stderr_thread = if !pipe_out && mode & 4 != 0 {
        child.stderr.take().map(|mut stderr| {
            std::thread::spawn(move || {
                let _ = std::io::copy(&mut stderr, &mut std::io::stdout());
            })
        })
    } else {
        None
    };
    // 中断信号处理:SIGINT 由全局 handler 捕获(在 repl.rs 中安装),
    // 仅设置标志位,不会杀死 lume 自身。
    // 子进程会收到终端发送的 SIGINT 并退出,wait 随后返回。
    // 仅前台任务
    if mode & 8 == 0 {
        childman::set_child(child.id());
    }
    // 获取输出
    if pipe_out {
        // 管道捕获
        let output = child.wait_with_output().map_err(|e| {
            RuntimeError::from_io_error(
                e,
                format!("wait output of cmd `{cmdstr}`").into(),
                job.clone(),
                depth,
            )
        })?;
        childman::clear_child();

        if output.status.success() {
            if mode & 1 == 0 {
                //未关闭标准输出才返回结果
                Ok(Some(output.stdout))
            } else {
                Ok(None)
            }
        } else if mode & 4 != 0 {
            //错误输出>标准输出
            let mut combined = Vec::new();
            combined.extend(output.stdout);
            // println!(
            //     "err output: {}",
            //     String::from_utf8_lossy(&output.stderr.clone().as_ref())
            // );
            combined.extend(output.stderr);
            // println!("Combined output: {}", String::from_utf8_lossy(&combined));
            Ok(Some(combined))
        } else {
            if mode & 2 == 0 {
                //未关闭错误输出才返回错误
                let stderr = String::from_utf8_lossy(output.stderr.as_ref())
                    .trim()
                    .to_string();
                if stderr.is_empty() {
                    // return Err(RuntimeError::CommandFailed2(cmdstr.to_owned(), stderr));
                    return Err(RuntimeError::new(
                        RuntimeErrorKind::CommandFailed2(cmdstr.to_owned(), stderr),
                        job.clone(),
                        depth,
                    ));
                }
            } else if mode & 1 == 0 {
                // 如果关闭了错误输出,则尝试返回标准输出,二者可能同时存在。
                return Ok(Some(output.stdout));
            }
            Ok(None)
        }
    } else if mode & 8 != 0 {
        // 后台运行:注册进任务表
        let cmdline = format!("{cmdstr} {}", ar_display); // ar_display 需要在函数前面保留一份参数拼接文本
        crate::jobman::add_job(child, cmdline);
        Ok(None)
    } else {
        // 正常模式
        // 正常模式:轮询等待,允许被 SIGTSTP 打断转入后台
        let status = loop {
            if childman::check_and_clear_sigtstp() {
                childman::clear_child();
                let cmdline = format!("{cmdstr} {ar_display}");
                let id = crate::jobman::add_job(child, cmdline);
                println!("\n[{id}] job turned to background");
                return Ok(None);
            }
            match child.try_wait() {
                Ok(Some(status)) => break status,
                Ok(None) => {
                    std::thread::sleep(std::time::Duration::from_millis(20));
                    continue;
                }
                Err(e) => {
                    return Err(RuntimeError::from_io_error(
                        e,
                        format!("wait cmd `{cmdstr}`").into(),
                        job.clone(),
                        depth,
                    ));
                }
            }
        };
        childman::clear_child();

        // 等 stderr 转发线程完成,确保输出顺序/完整性
        if let Some(t) = stderr_thread {
            let _ = t.join();
        }

        if status.success() {
            Ok(None)
        } else if mode & 2 == 0 {
            //未关闭错误输出才返回错误
            // windows spectial
            #[cfg(windows)]
            if let Some(code) = status.code() {
                let emsg = match (cmdstr.as_ref(), code) {
                    ("explorer", 1) => return Ok(None), // 任务转交
                    ("findstr", 1) => return Ok(None),  // 未找到匹配是正常情况
                    ("findstr", 2) => "Invalid arguments",
                    ("fc", 1) => return Ok(None), // 文件不同是正常情况
                    ("comp", 1) => return Ok(None), // 文件不同是正常情况
                    ("tasklist", 128) => "No matching processes",
                    _ if code > 128 => "Fatal signal",
                    _ => return Ok(None),
                };
                return Err(RuntimeError::new(
                    RuntimeErrorKind::CommandFailed2(
                        cmdstr.to_owned(),
                        format!("{emsg}\n{}", status.to_string()),
                    ),
                    job.clone(),
                    depth,
                ));
            }

            Err(RuntimeError::new(
                RuntimeErrorKind::CommandFailed2(cmdstr.to_owned(), status.to_string()),
                job.clone(),
                depth,
            ))
        } else {
            Ok(None)
        }
    }
}

// 管道
pub fn handle_command(
    job: &Expression,
    cmd: &String,
    args: &[Expression],
    state: &mut State,
    env: &mut Environment,
    depth: usize,
) -> Result<Expression, RuntimeError> {
    // dbg!("   3.--->handle_command:", &cmd, &args);

    let is_in_assign = state.contains(State::IN_ASSIGN);
    let pipe_out = is_in_assign || state.contains(State::IN_PIPE);
    let mut cmd_args = vec![];
    // state.set(State::SKIP_BUILTIN_SEEK | State::IN_ASSIGN);
    state.set(State::IN_ASSIGN);

    for arg in args {
        // for flattened_arg in Expression::flatten(vec![arg.eval_mut(env, depth + 1)?]) {
        // dbg!("     4.--->arg:", &arg, arg.type_name());
        let e_arg = arg.eval_mut(state, env, depth + 1)?;
        // dbg!("     4.--->evaluated_arg:", &e_arg, e_arg.type_name());

        match e_arg {
            Expression::Symbol(s) => {
                let s = expand_home(&s);
                if s.contains('*') {
                    let mut matched = false;
                    if let Ok(g) = glob(&s) {
                        for path in g.filter_map(Result::ok) {
                            matched = true;
                            cmd_args.push(path.to_string_lossy().to_string());
                        }
                    }
                    if !matched {
                        return Err(RuntimeError {
                            kind: RuntimeErrorKind::WildcardNotMatched(s.to_string()),
                            context: job.clone(),
                            depth,
                        });
                        // cmd_args.push(s);
                    }
                } else {
                    cmd_args.push(s.into())
                }
            }
            Expression::SymbolRaw(s) => {
                cmd_args.push(s.into());
            }
            Expression::String(st) => {
                let s = expand_home(&st).to_string();

                // 分割多参数字符串
                if ifs_contains(IFS_CMD, env) {
                    let ifs = env.get("IFS");
                    let sp = match &ifs {
                        Some(Expression::String(fs)) => s.split_terminator(fs.as_str()),
                        _ => s.split_terminator("\n"),
                    };
                    sp.for_each(|v| cmd_args.push(v.to_string()));
                } else {
                    cmd_args.push(s.to_string())
                }
            }
            Expression::List(ls) => {
                ls.iter().for_each(|a| cmd_args.push(format!("{a}")));
            }
            Expression::Bytes(b) => cmd_args.push(String::from_utf8_lossy(&b).to_string()),
            Expression::None | Expression::Blank => continue,
            _ => cmd_args.push(format!("{e_arg}")),
        }
    }
    // state.clear(State::SKIP_BUILTIN_SEEK);
    if !is_in_assign {
        state.clear(State::IN_ASSIGN);
    }

    let cmd_mode: u8 =
        match state.contains(State::PTY_MODE) || crate::expression::pty::needs_pty(cmd.as_str()) {
            true => 16,
            false => match cmd_args.last() {
                Some(s) => match s.as_str() {
                    "&" => {
                        cmd_args.pop();
                        11
                    }
                    "&-" => {
                        cmd_args.pop();
                        1
                    }
                    "&?" => {
                        cmd_args.pop();
                        2
                    }
                    "&." => {
                        cmd_args.pop();
                        3
                    }
                    "&+" => {
                        cmd_args.pop();
                        4
                    }
                    _ => 0,
                },
                _ => 0,
            },
        };
    // dbg!(args, &cmd_args);
    let last_input = state.pipe_out();
    let pipe_input = to_bytes(last_input);
    let result = exec_single_cmd(
        job,
        cmd,
        Some(cmd_args),
        env,
        pipe_input,
        pipe_out,
        cmd_mode,
        depth,
    )?;
    Ok(to_expr(result))
}

#[inline]
pub fn to_expr(bytes_out: Option<Vec<u8>>) -> Expression {
    match bytes_out {
        Some(b) => match String::from_utf8(b) {
            Ok(s) => Expression::String(s.trim().to_string()),
            Err(e) => Expression::Bytes(e.into_bytes()), // 保留原始字节,不做有损转换
                                                         // TODO 检查管道接收bytes情况
        },
        _ => Expression::None,
    }
}

#[inline]
fn to_bytes(expr_out: Option<Expression>) -> Option<Vec<u8>> {
    expr_out.map(|p| {
        if let Expression::Bytes(b) = p {
            b
        } else {
            p.to_string().as_bytes().to_owned()
        }
    })
}