Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: allow interrupting read builtin, run pipeline cmds in subshell #81

Merged
merged 1 commit into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 75 additions & 22 deletions brush-core/src/builtins/read.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use clap::Parser;
use std::{collections::VecDeque, io::Read};
use std::collections::VecDeque;
use std::io::Read;

use crate::{builtin, commands, env, error, openfiles, variables};

Expand Down Expand Up @@ -72,19 +73,17 @@ impl builtin::Command for ReadCommand {
if self.raw_mode {
tracing::debug!("read -r is not implemented");
}
if self.silent {
return error::unimp("read -s");
}
if self.timeout_in_seconds.is_some() {
return error::unimp("read -t");
}
if self.fd_num_to_read.is_some() {
return error::unimp("read -u");
}

let input_line = read_line(context.stdin());
let input_line = self.read_line(context.stdin())?;
if let Some(input_line) = input_line {
let mut variable_names: VecDeque<String> = self.variable_names.clone().into();
let mut spillover: Option<String> = None;
for field in input_line.split_ascii_whitespace() {
if let Some(variable_name) = variable_names.pop_front() {
context.shell.env.update_or_add(
Expand All @@ -95,36 +94,90 @@ impl builtin::Command for ReadCommand {
env::EnvironmentScope::Global,
)?;
} else {
return error::unimp("too few variable names");
match &mut spillover {
Some(s) => {
s.push(' ');
s.push_str(field);
}
None => spillover = Some(field.to_owned()),
}
}
}

if let Some(spillover) = spillover {
context.shell.env.update_or_add(
"REPLY",
variables::ShellValueLiteral::Scalar(spillover),
|_| Ok(()),
env::EnvironmentLookup::Anywhere,
env::EnvironmentScope::Global,
)?;
}

Ok(crate::builtin::ExitCode::Success)
} else {
Ok(crate::builtin::ExitCode::Custom(1))
}
}
}

fn read_line(mut file: openfiles::OpenFile) -> Option<String> {
let mut line = String::new();
let mut buffer = [0; 1]; // 1-byte buffer
impl ReadCommand {
fn read_line(&self, mut file: openfiles::OpenFile) -> Result<Option<String>, error::Error> {
let orig_term_attr = file.get_term_attr()?;
if let Some(orig_term_attr) = &orig_term_attr {
let mut updated_term_attr = orig_term_attr.to_owned();

updated_term_attr
.local_flags
.remove(nix::sys::termios::LocalFlags::ICANON);
updated_term_attr
.local_flags
.remove(nix::sys::termios::LocalFlags::ISIG);

if self.silent {
updated_term_attr
.local_flags
.remove(nix::sys::termios::LocalFlags::ECHO);
}

file.set_term_attr(nix::sys::termios::SetArg::TCSANOW, &updated_term_attr)?;
}

let mut line = String::new();
let mut buffer = [0; 1]; // 1-byte buffer

// TODO: Look at ignoring errors here.
while let Ok(n) = file.read(&mut buffer) {
if n == 0 {
break; // EOF reached.
}

let ch = buffer[0] as char;

if ch == '\x03' {
return Ok(None); // Ctrl+C aborts.
}

if ch == '\n' {
break; // End of line reached
}

// Ignore other control characters.
if ch.is_ascii_control() {
continue;
}

// TODO: Look at ignoring errors here.
while let Ok(n) = file.read(&mut buffer) {
if n == 0 {
break; // EOF reached
line.push(ch);
}
let ch = buffer[0] as char;

if ch == '\n' {
break; // End of line reached
if let Some(orig_term_attr) = &orig_term_attr {
file.set_term_attr(nix::sys::termios::SetArg::TCSANOW, orig_term_attr)?;
}
line.push(ch);
}

if line.is_empty() {
None
} else {
Some(line)
if line.is_empty() {
Ok(None)
} else {
Ok(Some(line))
}
}
}
10 changes: 9 additions & 1 deletion brush-core/src/extendedtests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,15 @@ pub(crate) fn apply_unary_predicate_to_str(
}
}
ast::UnaryPredicate::FdIsOpenTerminal => {
error::unimp("unary extended test predicate: FdIsOpenTerminal")
if let Ok(fd) = operand.parse::<u32>() {
if let Some(open_file) = shell.open_files.files.get(&fd) {
Ok(open_file.is_term())
} else {
Ok(false)
}
} else {
Ok(false)
}
}
ast::UnaryPredicate::FileExistsAndIsSetuid => {
let path = Path::new(operand);
Expand Down
39 changes: 27 additions & 12 deletions brush-core/src/interp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ struct PipelineExecutionContext<'a> {

current_pipeline_index: usize,
pipeline_len: usize,
output_pipes: Vec<os_pipe::PipeReader>,
output_pipes: &'a mut Vec<os_pipe::PipeReader>,

params: ExecutionParameters,
}
Expand Down Expand Up @@ -301,20 +301,35 @@ impl Execute for ast::Pipeline {
shell: &mut Shell,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
let mut pipeline_context = PipelineExecutionContext {
shell,
current_pipeline_index: 0,
pipeline_len: self.seq.len(),
output_pipes: vec![],
params: params.clone(),
};

let pipeline_len = self.seq.len();
let mut output_pipes = vec![];
let mut spawn_results = VecDeque::new();

for command in &self.seq {
let spawn_result = command.execute_in_pipeline(&mut pipeline_context).await?;
for (current_pipeline_index, command) in self.seq.iter().enumerate() {
// If there's only one command in the pipeline, then we run directly in the current shell.
// Otherwise, we spawn a separate subshell for each command in the pipeline.
let spawn_result = if pipeline_len > 1 {
let mut subshell = shell.clone();
let mut pipeline_context = PipelineExecutionContext {
shell: &mut subshell,
current_pipeline_index,
pipeline_len,
output_pipes: &mut output_pipes,
params: params.clone(),
};
command.execute_in_pipeline(&mut pipeline_context).await?
} else {
let mut pipeline_context = PipelineExecutionContext {
shell,
current_pipeline_index,
pipeline_len,
output_pipes: &mut output_pipes,
params: params.clone(),
};
command.execute_in_pipeline(&mut pipeline_context).await?
};

spawn_results.push_back(spawn_result);
pipeline_context.current_pipeline_index += 1;
}

let mut result = ExecutionResult::success();
Expand Down
50 changes: 50 additions & 0 deletions brush-core/src/openfiles.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::io::IsTerminal;
#[cfg(unix)]
use std::os::fd::AsFd;
#[cfg(unix)]
Expand Down Expand Up @@ -76,6 +77,55 @@ impl OpenFile {
OpenFile::HereDocument(_) => error::unimp("as_raw_fd for here doc"),
}
}

pub(crate) fn is_term(&self) -> bool {
match self {
OpenFile::Stdin => std::io::stdin().is_terminal(),
OpenFile::Stdout => std::io::stdout().is_terminal(),
OpenFile::Stderr => std::io::stderr().is_terminal(),
OpenFile::Null => false,
OpenFile::File(f) => f.is_terminal(),
OpenFile::PipeReader(_) => false,
OpenFile::PipeWriter(_) => false,
OpenFile::HereDocument(_) => false,
}
}

pub(crate) fn get_term_attr(&self) -> Result<Option<nix::sys::termios::Termios>, error::Error> {
if !self.is_term() {
return Ok(None);
}

let result = match self {
OpenFile::Stdin => Some(nix::sys::termios::tcgetattr(std::io::stdin())?),
OpenFile::Stdout => Some(nix::sys::termios::tcgetattr(std::io::stdout())?),
OpenFile::Stderr => Some(nix::sys::termios::tcgetattr(std::io::stderr())?),
OpenFile::Null => None,
OpenFile::File(f) => Some(nix::sys::termios::tcgetattr(f)?),
OpenFile::PipeReader(_) => None,
OpenFile::PipeWriter(_) => None,
OpenFile::HereDocument(_) => None,
};
Ok(result)
}

pub(crate) fn set_term_attr(
&self,
action: nix::sys::termios::SetArg,
termios: &nix::sys::termios::Termios,
) -> Result<(), error::Error> {
match self {
OpenFile::Stdin => nix::sys::termios::tcsetattr(std::io::stdin(), action, termios)?,
OpenFile::Stdout => nix::sys::termios::tcsetattr(std::io::stdout(), action, termios)?,
OpenFile::Stderr => nix::sys::termios::tcsetattr(std::io::stderr(), action, termios)?,
OpenFile::Null => (),
OpenFile::File(f) => nix::sys::termios::tcsetattr(f, action, termios)?,
OpenFile::PipeReader(_) => (),
OpenFile::PipeWriter(_) => (),
OpenFile::HereDocument(_) => (),
}
Ok(())
}
}

impl From<OpenFile> for Stdio {
Expand Down
18 changes: 18 additions & 0 deletions brush-shell/tests/cases/builtins/read.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,21 @@ cases:
b
stdin: |
(echo a; echo b) | while read name; do echo "Hello, $name"; done

- name: "read from here string"
known_failure: true
stdin: |
read myvar <<< "hello"
echo "myvar: ${myvar}"

- name: "read from process substitution"
known_failure: true
stdin: |
read myvar < <(echo hello)
echo "myvar: ${myvar}"

- name: "read from command substitution"
known_failure: true
stdin: |
read myvar <<< "$(echo hello)"
echo "myvar: ${myvar}"
11 changes: 11 additions & 0 deletions brush-shell/tests/cases/pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,14 @@ cases:
exit $1
stdin: |
./script.sh 10 | ./script.sh 0 | ./script.sh 33

- name: "Side effects in pipe commands"
stdin: |
var=0
echo "var: ${var}"
{ var=1; }
echo "var: ${var}"
{ var=2; echo hi; } | cat
echo "var: ${var}"
echo hi | { var=3; cat; }
echo "var: ${var}"
Loading