Skip to main content

yash_cli/
lib.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2021 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! This is an internal library crate for the yash shell. Currently, **this
18//! crate is not intended to be used as a library by other crates. No part of
19//! this crate is covered by semantic versioning.**
20//!
21//! The entry point for the shell is the [`main`] function, which is to be used
22//! as the `main` function in the binary crate. The function sets up the shell
23//! environment and runs the main read-eval loop.
24
25pub mod startup;
26// mod runner;
27
28use self::startup::args::Parse;
29use self::startup::init_file::run_rcfile;
30use self::startup::input::prepare_input;
31use std::cell::RefCell;
32use std::fmt::Write as _;
33use std::ops::ControlFlow::{Break, Continue};
34use std::rc::Rc;
35use yash_env::Env;
36use yash_env::RealSystem;
37use yash_env::option::{Interactive, Off, On, Portable};
38use yash_env::semantics::{Divert, ExitStatus, exit_or_raise};
39use yash_env::system::concurrency::WriteAll;
40use yash_env::system::resource::GetRlimit;
41use yash_env::system::{
42    Chdir, Concurrent, Disposition, Errno, GetCwd, GetUid, Isatty, Sigaction as _, Signals as _,
43    Sysconf, TcGetPgrp, Times, Umask, Write,
44};
45use yash_semantics::trap::run_exit_trap;
46use yash_semantics::{Runtime, interactive_read_eval_loop, read_eval_loop};
47
48async fn print_help<S>(env: &mut Env<S>)
49where
50    S: Isatty + WriteAll,
51{
52    let mut help = "\
53Usage:
54  yash3 [OPTION...] [FILE [ARGUMENT...]]
55  yash3 [OPTION...] -c COMMAND [COMMAND_NAME [ARGUMENT...]]
56  yash3 [OPTION...] -s [ARGUMENT...]
57
58Startup options:
59      --help
60  -V, --version
61      --profile=FILE
62      --noprofile
63      --rcfile=FILE
64      --norcfile
65
66Shell options:
67  Turn on an option with -o NAME or --NAME, and turn it off with +o NAME or
68  ++NAME. Prefixing NAME with \"no\" reverses the effect, as in -o noglob.
69  Each single-letter option below does the same as the -o form beside it,
70  and swapping its - or + reverses the effect.
71
72"
73    .to_owned();
74    for option in yash_env::option::Option::iter() {
75        match option.short_name() {
76            Some((name, On)) => writeln!(help, "  -{name}  -o {option}"),
77            Some((name, Off)) => writeln!(help, "  +{name}  -o {option}"),
78            None => writeln!(help, "      -o {option}"),
79        }
80        .unwrap();
81    }
82    help.push_str(concat!(
83        "\nSee <",
84        env!("CARGO_PKG_HOMEPAGE"),
85        "> for details.\n"
86    ));
87
88    let result = yash_builtin::common::output(env, &help).await;
89    env.exit_status = result.exit_status();
90}
91
92async fn print_version<S>(env: &mut Env<S>)
93where
94    S: Isatty + WriteAll,
95{
96    let version = env!("CARGO_PKG_VERSION");
97    let result = yash_builtin::common::output(env, &format!("yash {version}\n")).await;
98    env.exit_status = result.exit_status();
99}
100
101#[allow(
102    clippy::await_holding_refcell_ref,
103    reason = "`print_error` does not run concurrently with the input decorators or read-eval loop"
104)]
105async fn run_as_shell_process<S>(env: &mut Env<S>)
106where
107    S: Chdir
108        + Clone
109        + GetCwd
110        + GetRlimit
111        + GetUid
112        + Runtime
113        + Sysconf
114        + TcGetPgrp
115        + Times
116        + Umask
117        + Write
118        + 'static,
119{
120    // Parse the command-line arguments
121    let run = match self::startup::args::parse(std::env::args()) {
122        Ok(Parse::Help) => return print_help(env).await,
123        Ok(Parse::Version) => return print_version(env).await,
124        Ok(Parse::Run(run)) => run,
125        Err(e) => {
126            let arg0 = std::env::args().next().unwrap_or_else(|| "yash".to_owned());
127            env.system.print_error(&format!("{arg0}: {e}\n")).await;
128            env.exit_status = ExitStatus::ERROR;
129            return;
130        }
131    };
132
133    // Import environment variables
134    let portable = run
135        .options
136        .iter()
137        .rev()
138        .find_map(|&(option, state)| (option == Portable).then_some(state))
139        == Some(On);
140    env.variables.extend_env(
141        std::env::vars()
142            .filter(|(name, _)| !portable || yash_env::variable::is_portable_variable_name(name)),
143    );
144
145    let work = self::startup::configure_environment(env, run).await;
146
147    let is_interactive = env.options.get(Interactive) == On;
148
149    // Run initialization files
150    // TODO run profile if login (but not the default profile if portable)
151    run_rcfile(env, work.rcfile).await;
152
153    // Prepare the input for the main read-eval loop
154    let ref_env = RefCell::new(env);
155    let lexer = match prepare_input(&ref_env, &work.source).await {
156        Ok(lexer) => lexer,
157        Err(e) => {
158            let arg0 = std::env::args().next().unwrap_or_else(|| "yash".to_owned());
159            let message = format!("{arg0}: {e}\n");
160            // The borrow checker of Rust 1.79.0 is not smart enough to reason
161            // about the lifetime of `e` here, so we re-borrow from `ref_env`
162            // instead of taking `env` out of `ref_env`.
163            // let mut env = ref_env.into_inner();
164            let mut env = ref_env.borrow_mut();
165            env.system.print_error(&message).await;
166            env.exit_status = match e.errno {
167                Errno::ENOENT | Errno::ENOTDIR | Errno::EILSEQ => ExitStatus::NOT_FOUND,
168                _ => ExitStatus::NOEXEC,
169            };
170            return;
171        }
172    };
173
174    // Run the read-eval loop
175    let result = if is_interactive {
176        interactive_read_eval_loop(&ref_env, &mut { lexer }).await
177    } else {
178        read_eval_loop(&ref_env, &mut { lexer }).await
179    };
180
181    let env = ref_env.into_inner();
182    env.apply_result(result);
183
184    match result {
185        Continue(())
186        | Break(Divert::Continue { .. })
187        | Break(Divert::Break { .. })
188        | Break(Divert::Return(_))
189        | Break(Divert::Interrupt(_))
190        | Break(Divert::Exit(_)) => run_exit_trap(env).await,
191        Break(Divert::Abort(_)) => (),
192    }
193}
194
195pub fn main() -> ! {
196    // SAFETY: This is the only instance of RealSystem we create in the whole
197    // process.
198    let system = unsafe { RealSystem::new() };
199
200    // Rust by default sets SIGPIPE to SIG_IGN, which is not desired.
201    // As an imperfect workaround, we set SIGPIPE to SIG_DFL here.
202    // TODO Use unix_sigpipe: https://github.com/rust-lang/rust/issues/97889
203    system
204        .sigaction(RealSystem::SIGPIPE, Disposition::Default)
205        .ok();
206
207    let system = Rc::new(Concurrent::new(system));
208    let runner = Rc::clone(&system);
209    let task = async {
210        let mut env = Env::with_system(system);
211        run_as_shell_process(&mut env).await;
212        exit_or_raise(&env.system, env.exit_status).await
213    };
214    runner.run_real(task)
215}