Skip to content

Commit

Permalink
chore: extract InteractiveShell as trait + refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
reubeno committed Sep 9, 2024
1 parent e410ae8 commit fd57d6b
Show file tree
Hide file tree
Showing 16 changed files with 758 additions and 528 deletions.
231 changes: 124 additions & 107 deletions Cargo.lock

Large diffs are not rendered by default.

16 changes: 9 additions & 7 deletions brush-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ async-trait = "0.1.82"
brush-parser = { version = "^0.2.6", path = "../brush-parser" }
cached = "0.53.0"
cfg-if = "1.0.0"
clap = { version = "4.5.11", features = ["derive", "wrap_help"] }
clap = { version = "4.5.17", features = ["derive", "wrap_help"] }
fancy-regex = "0.13.0"
futures = "0.3.30"
itertools = "0.13.0"
Expand All @@ -32,11 +32,7 @@ thiserror = "1.0.62"
tracing = "0.1.40"

[target.'cfg(target_family = "wasm")'.dependencies]
tokio = { version = "1.40.0", features = [
"io-util",
"macros",
"rt",
] }
tokio = { version = "1.40.0", features = ["io-util", "macros", "rt"] }

[target.'cfg(any(windows, unix))'.dependencies]
hostname = "0.4.0"
Expand All @@ -56,7 +52,13 @@ whoami = "1.5.2"

[target.'cfg(unix)'.dependencies]
command-fds = "0.3.0"
nix = { version = "0.29.0", features = ["fs", "process", "signal", "term", "user"] }
nix = { version = "0.29.0", features = [
"fs",
"process",
"signal",
"term",
"user",
] }
uzers = "0.12.1"

[target.'cfg(target_os = "linux")'.dependencies]
Expand Down
12 changes: 12 additions & 0 deletions brush-core/src/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ impl Clone for Shell {
}
}

impl AsRef<Shell> for Shell {
fn as_ref(&self) -> &Shell {
self
}
}

impl AsMut<Shell> for Shell {
fn as_mut(&mut self) -> &mut Shell {
self
}
}

/// Options for creating a new shell.
#[derive(Debug, Default)]
pub struct CreateOptions {
Expand Down
11 changes: 8 additions & 3 deletions brush-interactive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,22 @@ rust-version.workspace = true
[lib]
bench = false

[features]
basic = []
rustyline = ["dep:rustyline"]

[lints]
workspace = true

[dependencies]
async-trait = "0.1.82"
brush-parser = { version = "^0.2.6", path = "../brush-parser" }
brush-core = { version = "^0.2.7", path = "../brush-core" }
rustyline = { package = "brush-rustyline-fork", version = "14.0.1", optional = true, features = [
"derive",
] }
thiserror = "1.0.62"
tracing = "0.1.40"

[target.'cfg(any(windows, unix))'.dependencies]
rustyline = { package = "brush-rustyline-fork", version = "14.0.1", features = [
"derive",
] }
tokio = { version = "1.40.0", features = ["macros", "signal"] }
101 changes: 23 additions & 78 deletions brush-interactive/src/basic_shell.rs
Original file line number Diff line number Diff line change
@@ -1,112 +1,57 @@
use std::io::Write;

use crate::{
interactive_shell::{InteractiveShell, ReadResult},
ShellError,
};

/// Represents a minimal shell capable of taking commands from standard input
/// and reporting results to standard output and standard error streams.
pub struct InteractiveShell {
pub struct BasicShell {
shell: brush_core::Shell,
}

impl InteractiveShell {
impl BasicShell {
/// Returns a new interactive shell instance, created with the provided options.
///
/// # Arguments
///
/// * `options` - Options for creating the interactive shell.
pub async fn new(options: &crate::Options) -> Result<InteractiveShell, ShellError> {
pub async fn new(options: &crate::Options) -> Result<Self, ShellError> {
let shell = brush_core::Shell::new(&options.shell).await?;
Ok(InteractiveShell { shell })
Ok(Self { shell })
}
}

impl InteractiveShell for BasicShell {
/// Returns an immutable reference to the inner shell object.
pub fn shell(&self) -> &brush_core::Shell {
&self.shell
fn shell(&self) -> impl AsRef<brush_core::Shell> {
self.shell.as_ref()
}

/// Returns a mutable reference to the inner shell object.
pub fn shell_mut(&mut self) -> &mut brush_core::Shell {
&mut self.shell
}

/// Runs the interactive shell loop, reading commands from standard input and writing
/// results to standard output and standard error. Continues until the shell
/// normally exits or until a fatal error occurs.
pub async fn run_interactively(&mut self) -> Result<(), ShellError> {
loop {
// Check for any completed jobs.
self.shell_mut().check_for_completed_jobs()?;

let result = self.run_interactively_once().await;
match result {
Ok(Some(brush_core::ExecutionResult {
exit_shell,
return_from_function_or_script,
..
})) => {
if exit_shell {
break;
}

if return_from_function_or_script {
tracing::error!("return from non-function/script");
}
}
Ok(None) => {
break;
}
Err(e) => {
// Report the error, but continue to execute.
tracing::error!("error: {:#}", e);
}
}
}

Ok(())
fn shell_mut(&mut self) -> impl AsMut<brush_core::Shell> {
self.shell.as_mut()
}

async fn run_interactively_once(
&mut self,
) -> Result<Option<brush_core::ExecutionResult>, ShellError> {
// Compose the prompt.
let prompt = self.shell_mut().compose_prompt().await?;

match self.readline(prompt.as_str()) {
Some(read_result) => {
let params = self.shell().default_exec_params();
match self.shell_mut().run_string(read_result, &params).await {
Ok(result) => Ok(Some(result)),
Err(e) => Err(e.into()),
}
}
None => Ok(None),
}
}

fn readline(&mut self, prompt: &str) -> Option<String> {
let _ = print!("{prompt}");
fn read_line(&mut self, prompt: &str) -> Result<ReadResult, ShellError> {
print!("{prompt}");
let _ = std::io::stdout().flush();

let mut buffer = String::new();
let stdin = std::io::stdin(); // We get `Stdin` here.
if let Ok(bytes_read) = stdin.read_line(&mut buffer) {
if bytes_read > 0 {
Some(buffer)
Ok(ReadResult::Input(buffer))
} else {
None
Ok(ReadResult::Eof)
}
} else {
None
Err(ShellError::InputError)
}
}
}

/// Represents an error encountered while running or otherwise managing an interactive shell.
#[derive(thiserror::Error, Debug)]
pub enum ShellError {
/// An error occurred with the embedded shell.
#[error("{0}")]
ShellError(#[from] brush_core::Error),

/// A generic I/O error occurred.
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
fn update_history(&mut self) -> Result<(), ShellError> {
Ok(())
}
}
21 changes: 21 additions & 0 deletions brush-interactive/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// Represents an error encountered while running or otherwise managing an interactive shell.
#[allow(clippy::module_name_repetitions)]
#[allow(clippy::enum_variant_names)]
#[derive(thiserror::Error, Debug)]
pub enum ShellError {
/// An error occurred with the embedded shell.
#[error("{0}")]
ShellError(#[from] brush_core::Error),

/// A generic I/O error occurred.
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),

/// An error occurred while reading input.
#[error("input error occurred")]
InputError,

/// The requested input backend type is not supported.
#[error("requested input backend type not supported")]
InputBackendNotSupported,
}
Loading

0 comments on commit fd57d6b

Please sign in to comment.