gshell 1.0.3

gshell is a shell for people who live in the terminal. It pairs familiar Unix behavior with a tighter core, fast interaction, and an interface built to stay out of the way.
Documentation
use std::{fs, process::Stdio};

use gshell::{
    parser::Parser,
    runtime::{BootstrapExecutor, Executor},
    shell::{ShellAction, ShellState},
};
use tokio::process::Command;

#[derive(Debug, PartialEq, Eq)]
struct ShellSnapshot {
    code: u8,
    stdout: String,
    stderr: String,
}

#[tokio::test]
async fn bash_compat_for_command_substitution_then_glob() {
    let dir = tempfile::tempdir().expect("temp dir should be created");
    fs::write(dir.path().join("compat-a.txt"), "").expect("file should be writable");
    fs::write(dir.path().join("compat-b.txt"), "").expect("file should be writable");

    let command = "echo $(printf 'compat-*.txt')";
    let gshell = run_gshell(command, dir.path()).await;
    let bash = run_bash(command, dir.path()).await;

    assert_eq!(gshell, bash);
}

#[tokio::test]
async fn bash_compat_for_alias_function_composition() {
    let dir = tempfile::tempdir().expect("temp dir should be created");
    let command = "shopt -s expand_aliases\ngreet() { echo hi; }\nalias say=greet\nsay";

    let gshell = run_gshell("greet() { echo hi; }; alias say=greet; say", dir.path()).await;
    let bash = run_bash(command, dir.path()).await;

    assert_eq!(gshell, bash);
}

#[tokio::test]
async fn bash_compat_for_unquoted_heredoc_expansions() {
    let dir = tempfile::tempdir().expect("temp dir should be created");
    let command = "VALUE=world; out=$(cat <<EOF\n$(printf 'hello') $VALUE\nEOF\n); echo \"$out\"";

    let gshell = run_gshell(command, dir.path()).await;
    let bash = run_bash(command, dir.path()).await;

    assert_eq!(gshell, bash);
}

async fn run_gshell(command: &str, cwd: &std::path::Path) -> ShellSnapshot {
    let parser = Parser::default();
    let executor = BootstrapExecutor;
    let state = ShellState::shared().await.expect("state should initialize");
    state.write().await.set_cwd(cwd.to_path_buf());

    let parsed = parser.parse(command).expect("parse should succeed");
    let action = executor
        .execute(state, &parsed)
        .await
        .expect("execution should succeed");

    match action {
        ShellAction::Continue(output) => ShellSnapshot {
            code: output.exit_code.as_u8(),
            stdout: output.stdout,
            stderr: output.stderr,
        },
        ShellAction::Exit(code) => ShellSnapshot {
            code: code.as_u8(),
            stdout: String::new(),
            stderr: String::new(),
        },
    }
}

async fn run_bash(command: &str, cwd: &std::path::Path) -> ShellSnapshot {
    let output = Command::new("bash")
        .arg("-lc")
        .arg(command)
        .current_dir(cwd)
        .stdin(Stdio::null())
        .output()
        .await
        .expect("bash should execute");

    ShellSnapshot {
        code: output.status.code().unwrap_or(1) as u8,
        stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
        stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
    }
}