yash-cli 3.3.3

Extended POSIX shell
Documentation
// This file is part of yash, an extended POSIX shell.
// Copyright (C) 2023 WATANABE Yuki
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

//! Shell startup

use self::args::{Run, Source, Work};
use std::str::FromStr as _;
use yash_env::Env;
use yash_env::input::IgnoreEofConfig;
use yash_env::input::SuspendedJobsGuardConfig;
use yash_env::io::Fd;
use yash_env::option::Option::{Interactive, Monitor, Stdin};
use yash_env::option::State::On;
use yash_env::parser::IsKeyword;
use yash_env::parser::IsName;
use yash_env::prompt::GetPrompt;
use yash_env::semantics::command::RunFunction;
use yash_env::system::resource::GetRlimit;
use yash_env::system::{Chdir, GetCwd, GetUid, Isatty, Sysconf, TcGetPgrp, Times, Umask, Write};
use yash_env::trap::RunSignalTrapIfCaught;
use yash_prompt::ExpandText;
use yash_semantics::expansion::expand_text;
use yash_semantics::{RunReadEvalLoop, Runtime};
use yash_syntax::parser::lex::Lexer;

pub mod args;
pub mod init_file;
pub mod input;

/// Tests whether the shell should be implicitly interactive.
///
/// As per POSIX, "if the shell reads commands from the standard input and the
/// shell's standard input and standard error are attached to a terminal, the
/// shell is considered to be interactive." This function implements this rule.
///
/// This function returns `false` if the interactive option is explicitly
/// specified in the command line arguments to honor the user's intent.
pub fn auto_interactive<S: Isatty>(system: &S, run: &Run) -> bool {
    if run.work.source != Source::Stdin {
        return false;
    }
    if run.options.iter().any(|&(o, _)| o == Interactive) {
        return false;
    }
    system.isatty(Fd::STDIN) && system.isatty(Fd::STDERR)
}

/// Get the environment ready for performing the work.
///
/// This function takes the parsed command-line arguments and applies them to
/// the environment. It also sets up signal dispositions and prepares built-ins
/// and variables. The function returns the work to be performed, which is
/// extracted from the `run` argument.
///
/// This function is _pure_ in that all system calls are performed by the
/// `System` trait object (`env.system`).
pub async fn configure_environment<S>(env: &mut Env<S>, run: Run) -> Work
where
    S: Chdir
        + Clone
        + GetCwd
        + GetRlimit
        + GetUid
        + Runtime
        + Sysconf
        + TcGetPgrp
        + Times
        + Umask
        + Write
        + 'static,
{
    // Apply the parsed options to the environment
    if auto_interactive(&env.system, &run) {
        env.options.set(Interactive, On);
    }
    if run.work.source == self::args::Source::Stdin {
        env.options.set(Stdin, On);
    }
    for &(option, state) in &run.options {
        env.options.set(option, state);
    }
    if env.options.get(Interactive) == On && !run.options.iter().any(|&(o, _)| o == Monitor) {
        env.options.set(Monitor, On);
    }

    // Apply the parsed operands to the environment
    env.arg0 = run.arg0;
    env.variables.positional_params_mut().values = run.positional_params;

    // Configure internal dispositions for signals
    if env.options.get(Interactive) == On {
        env.traps
            .enable_internal_dispositions_for_terminators(&env.system)
            .await
            .ok();
        if env.options.get(Monitor) == On {
            env.traps
                .enable_internal_dispositions_for_stoppers(&env.system)
                .await
                .ok();
        }
    }

    // Make sure the shell is in the foreground if job control is enabled
    if env.options.get(Monitor) == On {
        // Ignore failures as we can still proceed even if we can't get into the foreground
        env.ensure_foreground().await.ok();
    }

    // Prepare built-ins
    env.builtins.extend(yash_builtin::iter());

    // Prepare variables
    env.init_variables();

    // Inject dependencies
    inject_dependencies(env);

    run.work
}

/// Inject dependencies into the environment.
fn inject_dependencies<S: Runtime + 'static>(env: &mut Env<S>) {
    env.any
        .insert(Box::new(SuspendedJobsGuardConfig::with_message(
            "# There are stopped jobs. Type `exit -f` to exit anyway.\n",
        )));
    env.any.insert(Box::new(IgnoreEofConfig::with_message(
        "# Type `exit` to leave the shell when the ignore-eof option is on.\n",
    )));

    env.any.insert(Box::new(IsKeyword::<S>(|_env, word| {
        yash_syntax::parser::lex::Keyword::from_str(word).is_ok()
    })));

    env.any.insert(Box::new(IsName::<S>(|_env, name| {
        yash_syntax::parser::lex::is_name(name)
    })));

    env.any.insert(Box::new(RunReadEvalLoop::<S>(|env, config| {
        Box::pin(async move {
            let mut lexer = Lexer::from(config);
            yash_semantics::read_eval_loop(env, &mut lexer).await
        })
    })));

    env.any.insert(Box::new(RunFunction::<S>(
        |env, function, fields, env_prep_hook| {
            Box::pin(async move {
                yash_semantics::command::simple_command::execute_function_body(
                    env,
                    function,
                    fields,
                    env_prep_hook,
                )
                .await
            })
        },
    )));

    env.any
        .insert(Box::new(RunSignalTrapIfCaught::<S>(|env, signal| {
            Box::pin(async move { yash_semantics::trap::run_trap_if_caught(env, signal).await })
        })));

    env.any.insert(Box::new(ExpandText::<S>(|env, text| {
        Box::pin(async move { expand_text(env, text).await.ok() })
    })));

    env.any.insert(Box::new(GetPrompt::<S>(|env, context| {
        Box::pin(async move {
            let prompt = yash_prompt::fetch_posix(&env.variables, context);
            yash_prompt::expand_posix(env, &prompt, false).await
        })
    })));
}