Skip to content
Open
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
3 changes: 2 additions & 1 deletion brush-builtins/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ thiserror = "2.0.18"
tracing = "0.1.44"

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

[target.'cfg(any(unix, windows))'.dependencies]
tokio = { version = "1.50.0", features = [
Expand All @@ -153,6 +153,7 @@ tokio = { version = "1.50.0", features = [
"rt-multi-thread",
"signal",
"sync",
"time",
] }
uucore = { version = "0.8.0", default-features = false, features = ["format"] }

Expand Down
22 changes: 19 additions & 3 deletions brush-builtins/src/alias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ impl builtins::Command for AliasCommand {
context: brush_core::ExecutionContext<'_, SE>,
) -> Result<brush_core::ExecutionResult, Self::Error> {
let mut exit_code = ExecutionResult::success();
let mut output = Vec::new();
let mut stderr_output = Vec::new();

if self.print || self.aliases.is_empty() {
for (name, value) in context.shell.aliases() {
writeln!(context.stdout(), "alias {name}='{value}'")?;
writeln!(output, "alias {name}='{value}'")?;
}
} else {
for alias in &self.aliases {
Expand All @@ -38,10 +40,10 @@ impl builtins::Command for AliasCommand {
.aliases_mut()
.insert(name.to_owned(), unexpanded_value.to_owned());
} else if let Some(value) = context.shell.aliases().get(alias) {
writeln!(context.stdout(), "alias {alias}='{value}'")?;
writeln!(output, "alias {alias}='{value}'")?;
} else {
writeln!(
context.stderr(),
stderr_output,
"{}: {alias}: not found",
context.command_name
)?;
Expand All @@ -50,6 +52,20 @@ impl builtins::Command for AliasCommand {
}
}

if !output.is_empty() {
if let Some(mut stdout) = context.stdout() {
stdout.write_all(&output).await?;
stdout.flush().await?;
}
}

if !stderr_output.is_empty() {
if let Some(mut stderr) = context.stderr() {
stderr.write_all(&stderr_output).await?;
stderr.flush().await?;
}
}

Ok(exit_code)
}
}
15 changes: 11 additions & 4 deletions brush-builtins/src/bg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@ impl builtins::Command for BgCommand {
context: brush_core::ExecutionContext<'_, SE>,
) -> Result<brush_core::ExecutionResult, Self::Error> {
let mut exit_code = ExecutionResult::success();
let mut stderr_output = Vec::new();

if !self.job_specs.is_empty() {
for job_spec in &self.job_specs {
if let Some(job) = context.shell.jobs_mut().resolve_job_spec(job_spec) {
job.move_to_background()?;
} else {
writeln!(
context.stderr(),
stderr_output,
"{}: {}: no such job",
context.command_name,
job_spec
context.command_name, job_spec
)?;
exit_code = ExecutionResult::general_error();
}
Expand All @@ -37,11 +37,18 @@ impl builtins::Command for BgCommand {
if let Some(job) = context.shell.jobs_mut().current_job_mut() {
job.move_to_background()?;
} else {
writeln!(context.stderr(), "{}: no current job", context.command_name)?;
writeln!(stderr_output, "{}: no current job", context.command_name)?;
exit_code = ExecutionResult::general_error();
}
}

if !stderr_output.is_empty() {
if let Some(mut stderr) = context.stderr() {
stderr.write_all(&stderr_output).await?;
stderr.flush().await?;
}
}

Ok(exit_code)
}
}
59 changes: 35 additions & 24 deletions brush-builtins/src/bind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,40 +148,40 @@ impl BindCommand {
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
) -> Result<ExecutionResult, BindError> {
let mut bindings = bindings.lock().await;
let mut output = Vec::new();

if self.list_funcs {
for func in interfaces::InputFunction::iter() {
writeln!(context.stdout(), "{func}")?;
writeln!(output, "{func}")?;
}
}

if self.list_funcs_and_bindings {
display_funcs_and_bindings(&*bindings, context, false /* reusable? */)?;
display_funcs_and_bindings(&*bindings, &mut output, false)?;
}

if self.list_funcs_and_bindings_reusable {
display_funcs_and_bindings(&*bindings, context, true /* reusable? */)?;
display_funcs_and_bindings(&*bindings, &mut output, true)?;
}

if self.list_key_seqs_that_invoke_macros {
display_macros(&*bindings, context, false /* reusable? */)?;
display_macros(&*bindings, &mut output, false)?;
}

if self.list_key_seqs_that_invoke_macros_reusable {
display_macros(&*bindings, context, true /* reusable? */)?;
display_macros(&*bindings, &mut output, true)?;
}

if self.list_vars {
let options = &context.shell.completion_config().fallback_options;

// For now we'll just display a few items and show defaults.
writeln!(
context.stdout(),
output,
"mark-directories is set to `{}'",
to_onoff(options.mark_directories)
)?;
writeln!(
context.stdout(),
output,
"mark-symlinked-directories is set to `{}'",
to_onoff(options.mark_symlinked_directories)
)?;
Expand All @@ -190,14 +190,13 @@ impl BindCommand {
if self.list_vars_reusable {
let options = &context.shell.completion_config().fallback_options;

// For now we'll just display a few items and show defaults.
writeln!(
context.stdout(),
output,
"set mark-directories {}",
to_onoff(options.mark_directories)
)?;
writeln!(
context.stdout(),
output,
"set mark-symlinked-directories {}",
to_onoff(options.mark_symlinked_directories)
)?;
Expand All @@ -208,12 +207,19 @@ impl BindCommand {

if !seqs.is_empty() {
writeln!(
context.stdout(),
output,
"{func_str} can be invoked via {}.",
seqs.iter().map(|seq| std::format!("\"{seq}\"")).join(", ")
)?;
} else {
writeln!(context.stdout(), "{func_str} is not bound to any keys.")?;
writeln!(output, "{func_str} is not bound to any keys.")?;
drop(bindings);
if !output.is_empty() {
if let Some(mut stdout) = context.stdout() {
stdout.write_all(&output).await?;
stdout.flush().await?;
}
}
return Ok(ExecutionResult::general_error());
}
}
Expand Down Expand Up @@ -241,13 +247,12 @@ impl BindCommand {
continue;
};

writeln!(context.stdout(), "\"{seq}\" \"{cmd}\"")?;
writeln!(output, "\"{seq}\" \"{cmd}\"")?;
}
}

if !self.key_seq_bindings.is_empty() {
if self.keymap.as_ref().is_some_and(|k| k.is_vi()) {
// NOTE(vi): Quietly ignore since we don't support vi mode.
return Ok(ExecutionResult::success());
}

Expand All @@ -259,7 +264,6 @@ impl BindCommand {

if let Some(key_sequence) = &self.key_sequence {
if self.keymap.as_ref().is_some_and(|k| k.is_vi()) {
// NOTE(vi): Quietly ignore since we don't support vi mode.
return Ok(ExecutionResult::success());
}

Expand All @@ -269,6 +273,13 @@ impl BindCommand {

drop(bindings);

if !output.is_empty() {
if let Some(mut stdout) = context.stdout() {
stdout.write_all(&output).await?;
stdout.flush().await?;
}
}

Ok(ExecutionResult::success())
}
}
Expand Down Expand Up @@ -443,7 +454,7 @@ const fn to_onoff(value: bool) -> &'static str {

fn display_funcs_and_bindings(
bindings: &dyn interfaces::KeyBindings,
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
output: &mut Vec<u8>,
reusable: bool,
) -> Result<(), BindError> {
let mut sequences_by_func: HashMap<InputFunction, Vec<KeySequence>> = HashMap::new();
Expand All @@ -464,20 +475,20 @@ fn display_funcs_and_bindings(
if let Some(seqs) = sequences_by_func.get(&func) {
if reusable {
for seq in seqs {
writeln!(context.stdout(), "\"{seq}\": {func}")?;
writeln!(output, "\"{seq}\": {func}")?;
}
} else {
writeln!(
context.stdout(),
output,
"{func} can be found on {}.",
seqs.iter().map(|seq| std::format!("\"{seq}\"")).join(", ")
)?;
}
} else {
if reusable {
writeln!(context.stdout(), "# {func} (not bound)")?;
writeln!(output, "# {func} (not bound)")?;
} else {
writeln!(context.stdout(), "{func} is not bound to any keys")?;
writeln!(output, "{func} is not bound to any keys")?;
}
}
}
Expand All @@ -487,14 +498,14 @@ fn display_funcs_and_bindings(

fn display_macros(
bindings: &dyn interfaces::KeyBindings,
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
output: &mut Vec<u8>,
reusable: bool,
) -> Result<(), BindError> {
for (left, right) in bindings.get_macros() {
if reusable {
writeln!(context.stdout(), "\"{left}\": \"{right}\"")?;
writeln!(output, "\"{left}\": \"{right}\"")?;
} else {
writeln!(context.stdout(), "{left} outputs {right}")?;
writeln!(output, "{left} outputs {right}")?;
}
}

Expand Down
17 changes: 9 additions & 8 deletions brush-builtins/src/caller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,36 +18,37 @@ impl builtins::Command for CallerCommand {
) -> Result<ExecutionResult, Self::Error> {
let stack = context.shell.call_stack();

// See how far back we need to look. Frame N represents the Nth caller
// (e.g., 0 = immediate caller, 1 = caller's caller, etc.).
let expr = self.expr.unwrap_or(0);

// Get all frames into a vector we can easily index into.
let frames: Vec<_> = stack
.iter()
.filter(|frame| frame.frame_type.is_function() || frame.frame_type.is_script())
.collect();

// Look for the last-known location in the parent of frame N.
let Some(calling_frame) = frames.get(expr + 1) else {
return Ok(ExecutionResult::general_error());
};

let line = calling_frame.current_line().unwrap_or(1);
let filename = &calling_frame.source_info.source;

// When the expr is provided, we display "LINE FUNCTION_NAME FILENAME"
// When the expr is omitted, we only display "LINE FILENAME"
let mut output = Vec::new();

if self.expr.is_some() {
let function_name = match &calling_frame.frame_type {
callstack::FrameType::Function(func_call) => func_call.name(),
callstack::FrameType::Script(..) => "source".into(),
_ => "".into(),
};

writeln!(context.stdout(), "{line} {function_name} {filename}")?;
writeln!(output, "{line} {function_name} {filename}")?;
} else {
writeln!(context.stdout(), "{line} {filename}")?;
writeln!(output, "{line} {filename}")?;
}

if let Some(mut stdout) = context.stdout() {
stdout.write_all(&output).await?;
stdout.flush().await?;
}

Ok(ExecutionResult::success())
Expand Down
29 changes: 16 additions & 13 deletions brush-builtins/src/cd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,32 +37,36 @@ impl builtins::Command for CdCommand {
&self,
context: brush_core::ExecutionContext<'_, SE>,
) -> Result<ExecutionResult, Self::Error> {
// TODO(cd): implement 'cd -@'
if self.file_with_xattr_as_dir {
return error::unimp("cd -@");
}

let mut should_print = false;
let mut target_dir = if let Some(target_dir) = &self.target_dir {
// `cd -', equivalent to `cd $OLDPWD'
if target_dir.as_os_str() == "-" {
should_print = true;
if let Some(oldpwd) = context.shell.env_str("OLDPWD") {
PathBuf::from(oldpwd.to_string())
} else {
writeln!(context.stderr(), "OLDPWD not set")?;
let mut stderr_output = Vec::new();
writeln!(stderr_output, "OLDPWD not set")?;
if let Some(mut stderr) = context.stderr() {
stderr.write_all(&stderr_output).await?;
}
return Ok(ExecutionResult::general_error());
}
} else {
// TODO(cd): remove clone, and use temporary lifetime extension after rust 1.75
target_dir.clone()
}
// `cd' without arguments is equivalent to `cd $HOME'
} else {
if let Some(home_var) = context.shell.env_str("HOME") {
PathBuf::from(home_var.to_string())
} else {
writeln!(context.stderr(), "HOME not set")?;
let mut stderr_output = Vec::new();
writeln!(stderr_output, "HOME not set")?;
if let Some(mut stderr) = context.stderr() {
stderr.write_all(&stderr_output).await?;
}
return Ok(ExecutionResult::general_error());
}
};
Expand All @@ -73,7 +77,6 @@ impl builtins::Command for CdCommand {
.options()
.do_not_resolve_symlinks_when_changing_dir
{
// -e is only relevant in physical mode.
if self.exit_on_failed_cwd_resolution {
return error::unimp("cd -e");
}
Expand All @@ -83,13 +86,13 @@ impl builtins::Command for CdCommand {

context.shell.set_working_dir(&target_dir)?;

// Bash compatibility
// https://www.gnu.org/software/bash/manual/bash.html#index-cd
// If a non-empty directory name from CDPATH is used, or if '-' is the first argument, and
// the directory change is successful, the absolute pathname of the new working
// directory is written to the standard output.
if should_print {
writeln!(context.stdout(), "{}", target_dir.display())?;
let mut output = Vec::new();
writeln!(output, "{}", target_dir.display())?;
if let Some(mut stdout) = context.stdout() {
stdout.write_all(&output).await?;
stdout.flush().await?;
}
}

Ok(ExecutionResult::success())
Expand Down
Loading