Skip to content

Commit

Permalink
Show $PS2 prompt for continuation lines in the read built-in (#380)
Browse files Browse the repository at this point in the history
  • Loading branch information
magicant committed Jul 6, 2024
2 parents 3deeda0 + d680905 commit 6fa7626
Show file tree
Hide file tree
Showing 11 changed files with 98 additions and 20 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions check-extra.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,15 @@ cargo tomlfmt --dryrun --path yash-syntax/Cargo.toml

# Make sure we don't have any unnecessary dependencies in Cargo.toml.
RUSTFLAGS='-D unused_crate_dependencies' cargo check --lib --all-features
RUSTFLAGS='-D unused_crate_dependencies' cargo check --package 'yash-builtin' --no-default-features
RUSTFLAGS='-D unused_crate_dependencies' cargo check --package 'yash-builtin' --no-default-features --features yash-semantics
RUSTFLAGS='-D unused_crate_dependencies' cargo check --package 'yash-syntax' --no-default-features

# Make sure the crates can be built with all combinations of features.
cargo build --package 'yash-arith' --all-targets
cargo build --package 'yash-builtin' --all-targets
cargo build --package 'yash-builtin' --all-targets --no-default-features
cargo build --package 'yash-builtin' --all-targets --no-default-features --features yash-semantics
cargo build --package 'yash-cli' --all-targets
cargo build --package 'yash-env' --all-targets
cargo build --package 'yash-env-test-helper' --all-targets
Expand Down
1 change: 1 addition & 0 deletions yash-builtin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- The read built-in now shows a prompt when reading a continued line.
- The break and continue built-ins now return `ExitStatus::ERROR` for syntax
errors and `ExitStatus::FAILURE` for semantic errors. Previously, they always
returned `ExitStatus::ERROR` for both types of errors, while the documentation
Expand Down
5 changes: 4 additions & 1 deletion yash-builtin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ keywords = ["posix", "shell"]
categories = ["command-line-utilities"]

[features]
default = ["yash-semantics"]
default = ["yash-prompt", "yash-semantics"]
# yash-prompt is used in the read built-in, which requires yash-semantics.
yash-prompt = ["dep:yash-prompt", "yash-semantics"]
yash-semantics = ["dep:yash-semantics", "dep:enumset"]

[dependencies]
Expand All @@ -23,6 +25,7 @@ enumset = { version = "1.1.2", optional = true }
itertools = "0.13.0"
thiserror = "1.0.47"
yash-env = { path = "../yash-env", version = "0.2.0" }
yash-prompt = { path = "../yash-prompt", version = "0.1.0", optional = true }
yash-quote = { path = "../yash-quote", version = "1.1.1" }
yash-semantics = { path = "../yash-semantics", version = "0.2.0", optional = true }
yash-syntax = { path = "../yash-syntax", version = "0.9.0" }
Expand Down
2 changes: 2 additions & 0 deletions yash-builtin/src/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
//! value of the `PS2` variable as a prompt if the shell is interactive and the
//! input is from a terminal.
//!
//! Prompting requires the optional `yash-prompt` feature.
//!
//! # Options
//!
//! The **`-r`** option disables the interpretation of backslashes.
Expand Down
37 changes: 35 additions & 2 deletions yash-builtin/src/read/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ fn plain(value: char) -> AttrChar {
/// continuation, this function removes the backslash-newline pair and continues
/// reading the next line. When reading the second and subsequent lines, this
/// function displays the value of the `PS2` variable as a prompt if the shell
/// is interactive and the input is from a terminal.
/// is interactive and the input is from a terminal. This requires the optional
/// `yash-prompt` feature.
///
/// If successful, this function returns a vector of [`AttrChar`]s representing
/// the line read and a boolean value indicating whether the line was terminated
Expand All @@ -114,7 +115,7 @@ pub async fn read(env: &mut Env, is_raw: bool) -> Result<(Vec<AttrChar>, bool),
let c = read_char(env).await?;
if c == Some('\n') {
// Line continuation
// TODO Display $PS2
print_prompt(env).await;
continue;
}
result.push(quoting('\\'));
Expand Down Expand Up @@ -179,6 +180,36 @@ async fn read_char(env: &mut Env) -> Result<Option<char>, Error> {
}
}

/// Prints the prompt string for the continuation line.
///
/// This function prints the value of the `PS2` variable as a prompt for the
/// continuation line. If the shell is not interactive or the standard input
/// is not a terminal, this function does nothing.
async fn print_prompt(env: &mut Env) {
#[cfg(feature = "yash-prompt")]
{
use yash_env::System as _;
if !env.is_interactive() {
return;
}
if env.system.isatty(Fd::STDIN) != Ok(true) {
return;
}

// Obtain the prompt string
let mut context = yash_env::input::Context::default();
context.set_is_first_line(false);
let prompt = yash_prompt::fetch_posix(&env.variables, &context);
let prompt = yash_prompt::expand_posix(env, &prompt, false).await;
env.system.print_error(&prompt).await;
}

#[cfg(not(feature = "yash-prompt"))]
{
_ = env;
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -320,4 +351,6 @@ mod tests {
assert_eq!(result, Err(Errno::EILSEQ.into()));
});
}

// TODO Test PS2 prompt
}
1 change: 1 addition & 0 deletions yash-cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- The break and continue built-ins no longer allow exiting a trap.
- The read built-in now shows a prompt when reading a continued line.

## [0.1.0-beta.1] - 2024-06-09

Expand Down
1 change: 1 addition & 0 deletions yash-env/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `Env::is_interactive`
- `impl yash_system::alias::Glossary for Env`
- `input::Echo`
- This is a decorator of `Input` that implements the behavior of the verbose shell option.
Expand Down
13 changes: 12 additions & 1 deletion yash-env/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ use self::job::Pid;
use self::job::ProcessState;
use self::option::On;
use self::option::OptionSet;
use self::option::{AllExport, ErrExit, Monitor};
use self::option::{AllExport, ErrExit, Interactive, Monitor};
use self::semantics::Divert;
use self::semantics::ExitStatus;
use self::stack::Frame;
Expand Down Expand Up @@ -303,6 +303,17 @@ impl Env {
final_fd
}

/// Tests whether the current environment is an interactive shell.
///
/// This function returns true if and only if:
///
/// - the [`Interactive`] option is `On` in `self.options`, and
/// - the current context is not in a subshell (no `Frame::Subshell` in `self.stack`).
#[must_use]
pub fn is_interactive(&self) -> bool {
self.options.get(Interactive) == On && !self.stack.contains(&Frame::Subshell)
}

/// Tests whether the shell is performing job control.
///
/// This function returns true if and only if:
Expand Down
3 changes: 3 additions & 0 deletions yash-prompt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
//! used to create an interactive shell prompt. The prompter internally uses
//! the following functions to expand prompt strings:
//!
//! - [`fetch_posix`]: Fetches the value of a variable defined by POSIX for
//! a prompt string.
//! - [`expand_posix`]: Expands a prompt string in a POSIX-compliant manner.
//! - `expand_ex`: Expands a prompt string with yash-specific expansions.
//! (This function is not yet implemented.)
Expand Down Expand Up @@ -70,4 +72,5 @@ pub use expand_posix::expand_posix;
// TODO Yash-specific prompt expansion

mod prompter;
pub use prompter::fetch_posix;
pub use prompter::Prompter;
50 changes: 34 additions & 16 deletions yash-prompt/src/prompter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
use async_trait::async_trait;
use std::cell::RefCell;
use yash_env::input::{Context, Input, Result};
use yash_env::variable::{Expansion, PS1, PS2};
use yash_env::variable::{Expansion, VariableSet, PS1, PS2};
use yash_env::Env;
use yash_syntax::source::Location;

Expand Down Expand Up @@ -60,24 +60,40 @@ where
}

async fn print_prompt(env: &mut Env, context: &Context) {
let location = Location::dummy(""); // TODO context.location;

// TODO Yash-specific prompt variables
let var = if context.is_first_line() { PS1 } else { PS2 };
// https://github.com/rust-lang/rust-clippy/issues/13031
#[allow(clippy::manual_unwrap_or_default)]
let prompt = match env.variables.get(var).map(|v| v.expand(&location)) {
Some(Expansion::Scalar(s)) => s.into_owned(),
_ => Default::default(),
};
// Obtain the prompt string
let prompt = fetch_posix(&env.variables, context);

// Perform parameter expansion in the prompt string
let expanded_prompt = super::expand_posix(env, &prompt, true).await;
let expanded_prompt = super::expand_posix(env, &prompt, context.is_first_line()).await;

// Print the prompt to the standard error
env.system.print_error(&expanded_prompt).await;
}

/// Fetches the command prompt string from the variable set.
///
/// The return value is the raw value taken from the `PS1` or `PS2` variable
/// in the set. [`Context::is_first_line`] determines which variable is used.
/// An empty string is returned if the variable is not found.
///
/// The returned prompt string should be expanded before being shown to the
/// user.
///
/// This function does not consider yash-specific prompt variables.
pub fn fetch_posix(variables: &VariableSet, context: &Context) -> String {
// TODO context.location;
let location = Location::dummy("");

let var = if context.is_first_line() { PS1 } else { PS2 };
// https://github.com/rust-lang/rust-clippy/issues/13031
match variables.get(var).map(|v| v.expand(&location)) {
Some(Expansion::Scalar(s)) => s.into_owned(),
_ => Default::default(),
}
}

// TODO pub fn fetch_ex: yash-specific prompt variables

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -145,7 +161,7 @@ mod tests {
let system = Box::new(VirtualSystem::new());
let state = Rc::clone(&system.state);
let mut env = Env::with_system(system);
define_variable(&mut env, PS1, "my_custom_prompt>");
define_variable(&mut env, PS1, "my_custom_prompt !! >");
let ref_env = RefCell::new(&mut env);
let mut prompter = Prompter::new(Memory::new(""), &ref_env);

Expand All @@ -154,22 +170,24 @@ mod tests {
.now_or_never()
.unwrap()
.ok();
assert_stderr(&state, |stderr| assert_eq!(stderr, "my_custom_prompt>"));
assert_stderr(&state, |stderr| assert_eq!(stderr, "my_custom_prompt ! >"));
// Note that "!!" is expanded to "!" in the prompt string.
}

#[test]
fn ps2_variable_defines_continuation_prompt() {
let system = Box::new(VirtualSystem::new());
let state = Rc::clone(&system.state);
let mut env = Env::with_system(system);
define_variable(&mut env, PS2, "continuation_prompt>");
define_variable(&mut env, PS2, "continuation ! >");
let ref_env = RefCell::new(&mut env);
let mut prompter = Prompter::new(Memory::new(""), &ref_env);
let mut context = Context::default();
context.set_is_first_line(false);

prompter.next_line(&context).now_or_never().unwrap().ok();
assert_stderr(&state, |stderr| assert_eq!(stderr, "continuation_prompt>"));
assert_stderr(&state, |stderr| assert_eq!(stderr, "continuation ! >"));
// Note that "!" is not expanded in the prompt string.
}

#[test]
Expand Down

0 comments on commit 6fa7626

Please sign in to comment.