Fixed command substitutions not expanding when used as a command name

This commit is contained in:
2026-02-23 23:32:12 -05:00
parent 06a55734c9
commit fa49e2ef70
4 changed files with 201 additions and 185 deletions

View File

@@ -1,7 +1,7 @@
#![allow( #![allow(
clippy::derivable_impls, clippy::derivable_impls,
clippy::tabs_in_doc_comments, clippy::tabs_in_doc_comments,
clippy::while_let_on_iterator clippy::while_let_on_iterator
)] )]
pub mod builtin; pub mod builtin;
pub mod expand; pub mod expand;
@@ -23,7 +23,6 @@ use std::process::ExitCode;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use nix::errno::Errno; use nix::errno::Errno;
use nix::libc::STDIN_FILENO;
use nix::poll::{PollFd, PollFlags, PollTimeout, poll}; use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
use nix::unistd::read; use nix::unistd::read;
@@ -33,7 +32,7 @@ use crate::libsh::sys::TTY_FILENO;
use crate::parse::execute::exec_input; use crate::parse::execute::exec_input;
use crate::prelude::*; use crate::prelude::*;
use crate::prompt::get_prompt; use crate::prompt::get_prompt;
use crate::prompt::readline::term::raw_mode; use crate::prompt::readline::term::{RawModeGuard, raw_mode};
use crate::prompt::readline::{FernVi, ReadlineEvent}; use crate::prompt::readline::{FernVi, ReadlineEvent};
use crate::signal::{QUIT_CODE, check_signals, sig_setup, signals_pending}; use crate::signal::{QUIT_CODE, check_signals, sig_setup, signals_pending};
use crate::state::{read_logic, source_rc, write_jobs, write_meta}; use crate::state::{read_logic, source_rc, write_jobs, write_meta};
@@ -42,16 +41,16 @@ use state::{read_vars, write_vars};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct FernArgs { struct FernArgs {
script: Option<String>, script: Option<String>,
#[arg(short)] #[arg(short)]
command: Option<String>, command: Option<String>,
#[arg(trailing_var_arg = true)] #[arg(trailing_var_arg = true)]
script_args: Vec<String>, script_args: Vec<String>,
#[arg(long)] #[arg(long)]
version: bool, version: bool,
#[arg(short)] #[arg(short)]
interactive: bool, interactive: bool,
@@ -71,7 +70,7 @@ struct FernArgs {
/// closure, which forces access to the variable table and causes its `LazyLock` /// closure, which forces access to the variable table and causes its `LazyLock`
/// constructor to run. /// constructor to run.
fn kickstart_lazy_evals() { fn kickstart_lazy_evals() {
read_vars(|_| {}); read_vars(|_| {});
} }
/// We need to make sure that even if we panic, our child processes get sighup /// We need to make sure that even if we panic, our child processes get sighup
@@ -89,196 +88,192 @@ fn setup_panic_handler() {
} }
fn main() -> ExitCode { fn main() -> ExitCode {
env_logger::init(); env_logger::init();
kickstart_lazy_evals(); kickstart_lazy_evals();
setup_panic_handler(); setup_panic_handler();
let mut args = FernArgs::parse(); let mut args = FernArgs::parse();
if env::args().next().is_some_and(|a| a.starts_with('-')) { if env::args().next().is_some_and(|a| a.starts_with('-')) {
// first arg is '-fern' // first arg is '-fern'
// meaning we are in a login shell // meaning we are in a login shell
args.login_shell = true; args.login_shell = true;
} }
if args.version { if args.version {
println!("fern {} ({} {})", env!("CARGO_PKG_VERSION"), std::env::consts::ARCH, std::env::consts::OS); println!("fern {} ({} {})", env!("CARGO_PKG_VERSION"), std::env::consts::ARCH, std::env::consts::OS);
return ExitCode::SUCCESS; return ExitCode::SUCCESS;
} }
if let Err(e) = if let Some(path) = args.script { if let Err(e) = if let Some(path) = args.script {
run_script(path, args.script_args) run_script(path, args.script_args)
} else if let Some(cmd) = args.command { } else if let Some(cmd) = args.command {
exec_input(cmd, None, false) exec_input(cmd, None, false)
} else { } else {
fern_interactive() fern_interactive()
} { } {
eprintln!("fern: {e}"); eprintln!("fern: {e}");
}; };
if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit)) if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit))
&& let Err(e) = exec_input(trap, None, false) && let Err(e) = exec_input(trap, None, false) {
{ eprintln!("fern: error running EXIT trap: {e}");
eprintln!("fern: error running EXIT trap: {e}"); }
}
write_jobs(|j| j.hang_up()); write_jobs(|j| j.hang_up());
ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8) ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8)
} }
fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> { fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> {
let path = path.as_ref(); let path = path.as_ref();
if !path.is_file() { if !path.is_file() {
eprintln!("fern: Failed to open input file: {}", path.display()); eprintln!("fern: Failed to open input file: {}", path.display());
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::CleanExit(1), ShErrKind::CleanExit(1),
"input file not found", "input file not found",
)); ));
} }
let Ok(input) = fs::read_to_string(path) else { let Ok(input) = fs::read_to_string(path) else {
eprintln!("fern: Failed to read input file: {}", path.display()); eprintln!("fern: Failed to read input file: {}", path.display());
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::CleanExit(1), ShErrKind::CleanExit(1),
"failed to read input file", "failed to read input file",
)); ));
}; };
write_vars(|v| { write_vars(|v| {
v.cur_scope_mut() v.cur_scope_mut()
.bpush_arg(path.to_string_lossy().to_string()) .bpush_arg(path.to_string_lossy().to_string())
}); });
for arg in args { for arg in args {
write_vars(|v| v.cur_scope_mut().bpush_arg(arg)) write_vars(|v| v.cur_scope_mut().bpush_arg(arg))
} }
exec_input(input, None, false) exec_input(input, None, false)
} }
fn fern_interactive() -> ShResult<()> { fn fern_interactive() -> ShResult<()> {
let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop
sig_setup(); sig_setup();
if let Err(e) = source_rc() { if let Err(e) = source_rc() {
eprintln!("{e}"); eprintln!("{e}");
} }
// Create readline instance with initial prompt // Create readline instance with initial prompt
let mut readline = match FernVi::new(get_prompt().ok(), *TTY_FILENO) { let mut readline = match FernVi::new(get_prompt().ok(), *TTY_FILENO) {
Ok(rl) => rl, Ok(rl) => rl,
Err(e) => { Err(e) => {
eprintln!("Failed to initialize readline: {e}"); eprintln!("Failed to initialize readline: {e}");
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::CleanExit(1), ShErrKind::CleanExit(1),
"readline initialization failed", "readline initialization failed",
)); ));
} }
}; };
// Main poll loop // Main poll loop
loop { loop {
// Handle any pending signals // Handle any pending signals
while signals_pending() { while signals_pending() {
if let Err(e) = check_signals() { if let Err(e) = check_signals() {
match e.kind() { match e.kind() {
ShErrKind::ClearReadline => { ShErrKind::ClearReadline => {
// Ctrl+C - clear current input and show new prompt // Ctrl+C - clear current input and show new prompt
readline.reset(get_prompt().ok()); readline.reset(get_prompt().ok());
} }
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
_ => eprintln!("{e}"), _ => eprintln!("{e}"),
} }
} }
} }
readline.print_line()?; readline.print_line()?;
// Poll for stdin input // Poll for stdin input
let mut fds = [PollFd::new( let mut fds = [PollFd::new(
unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) }, unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) },
PollFlags::POLLIN, PollFlags::POLLIN,
)]; )];
match poll(&mut fds, PollTimeout::MAX) { match poll(&mut fds, PollTimeout::MAX) {
Ok(_) => {} Ok(_) => {}
Err(Errno::EINTR) => { Err(Errno::EINTR) => {
// Interrupted by signal, loop back to handle it // Interrupted by signal, loop back to handle it
continue; continue;
} }
Err(e) => { Err(e) => {
eprintln!("poll error: {e}"); eprintln!("poll error: {e}");
break; break;
} }
} }
// Check if stdin has data // Check if stdin has data
if fds[0] if fds[0].revents().is_some_and(|r| r.contains(PollFlags::POLLIN)) {
.revents() let mut buffer = [0u8; 1024];
.is_some_and(|r| r.contains(PollFlags::POLLIN)) match read(*TTY_FILENO, &mut buffer) {
{ Ok(0) => {
let mut buffer = [0u8; 1024]; // EOF
match read(*TTY_FILENO, &mut buffer) { break;
Ok(0) => { }
// EOF Ok(n) => {
break; readline.feed_bytes(&buffer[..n]);
} }
Ok(n) => { Err(Errno::EINTR) => {
readline.feed_bytes(&buffer[..n]); // Interrupted, continue to handle signals
} continue;
Err(Errno::EINTR) => { }
// Interrupted, continue to handle signals Err(e) => {
continue; eprintln!("read error: {e}");
} break;
Err(e) => { }
eprintln!("read error: {e}"); }
break; }
}
}
}
// Process any available input // Process any available input
match readline.process_input() { match readline.process_input() {
Ok(ReadlineEvent::Line(input)) => { Ok(ReadlineEvent::Line(input)) => {
let start = Instant::now(); let start = Instant::now();
write_meta(|m| m.start_timer()); write_meta(|m| m.start_timer());
if let Err(e) = exec_input(input, None, true) { if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input, None, true)) {
match e.kind() { match e.kind() {
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
_ => eprintln!("{e}"), _ => eprintln!("{e}"),
} }
} }
let command_run_time = start.elapsed(); let command_run_time = start.elapsed();
log::info!("Command executed in {:.2?}", command_run_time); log::info!("Command executed in {:.2?}", command_run_time);
write_meta(|m| m.stop_timer()); write_meta(|m| m.stop_timer());
// Reset for next command with fresh prompt // Reset for next command with fresh prompt
readline.reset(get_prompt().ok()); readline.reset(get_prompt().ok());
let real_end = start.elapsed(); let real_end = start.elapsed();
log::info!("Total round trip time: {:.2?}", real_end); log::info!("Total round trip time: {:.2?}", real_end);
} }
Ok(ReadlineEvent::Eof) => { Ok(ReadlineEvent::Eof) => {
// Ctrl+D on empty line // Ctrl+D on empty line
QUIT_CODE.store(0, Ordering::SeqCst); QUIT_CODE.store(0, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
Ok(ReadlineEvent::Pending) => { Ok(ReadlineEvent::Pending) => {
// No complete input yet, keep polling // No complete input yet, keep polling
} }
Err(e) => match e.kind() { Err(e) => match e.kind() {
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
_ => eprintln!("{e}"), _ => eprintln!("{e}"),
}, },
} }
} }
Ok(()) Ok(())
} }

View File

@@ -155,7 +155,9 @@ pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool) -
return Ok(()); return Ok(());
} }
let mut dispatcher = Dispatcher::new(parser.extract_nodes(), interactive); let nodes = parser.extract_nodes();
let mut dispatcher = Dispatcher::new(nodes, interactive);
if let Some(mut stack) = io_stack { if let Some(mut stack) = io_stack {
dispatcher.io_stack.extend(stack.drain(..)); dispatcher.io_stack.extend(stack.drain(..));
} }

View File

@@ -659,6 +659,9 @@ impl LexStream {
} }
_ if is_cmd_sub(text) => { _ if is_cmd_sub(text) => {
new_tk.mark(TkFlags::IS_CMDSUB); new_tk.mark(TkFlags::IS_CMDSUB);
if self.next_is_cmd() {
new_tk.mark(TkFlags::IS_CMD);
}
self.set_next_is_cmd(false); self.set_next_is_cmd(false);
} }
_ => { _ => {
@@ -846,11 +849,26 @@ pub fn is_field_sep(ch: char) -> bool {
} }
pub fn is_keyword(slice: &str) -> bool { pub fn is_keyword(slice: &str) -> bool {
KEYWORDS.contains(&slice) || (slice.ends_with("()") && !slice.ends_with("\\()")) KEYWORDS.contains(&slice) || ends_with_unescaped(slice, "()")
} }
pub fn is_cmd_sub(slice: &str) -> bool { pub fn is_cmd_sub(slice: &str) -> bool {
(slice.starts_with("$(") && slice.ends_with(')')) && !slice.ends_with("\\)") slice.starts_with("$(") && ends_with_unescaped(slice,")")
}
pub fn ends_with_unescaped(slice: &str, pat: &str) -> bool {
slice.ends_with(pat) && !pos_is_escaped(slice, slice.len() - pat.len())
}
pub fn pos_is_escaped(slice: &str, pos: usize) -> bool {
let bytes = slice.as_bytes();
let mut escaped = false;
let mut i = pos;
while i > 0 && bytes[i - 1] == b'\\' {
escaped = !escaped;
i -= 1;
}
escaped
} }
pub fn lookahead(pat: &str, mut chars: Chars) -> Option<usize> { pub fn lookahead(pat: &str, mut chars: Chars) -> Option<usize> {

View File

@@ -184,14 +184,16 @@ impl Highlighter {
input_chars.next(); input_chars.next();
} }
let inner_clean = Self::strip_markers(&inner);
// Determine prefix from content (handles both <( and >( for proc subs) // Determine prefix from content (handles both <( and >( for proc subs)
let prefix = match ch { let prefix = match ch {
markers::CMD_SUB => "$(", markers::CMD_SUB => "$(",
markers::SUBSH => "(", markers::SUBSH => "(",
markers::PROC_SUB => { markers::PROC_SUB => {
if inner.starts_with("<(") { if inner_clean.starts_with("<(") {
"<(" "<("
} else if inner.starts_with(">(") { } else if inner_clean.starts_with(">(") {
">(" ">("
} else { } else {
"<(" "<("
@@ -199,14 +201,13 @@ impl Highlighter {
} }
_ => unreachable!(), _ => unreachable!(),
}; };
let inner_content = if incomplete { let inner_content = if incomplete {
inner.strip_prefix(prefix).unwrap_or(&inner) inner_clean.strip_prefix(prefix).unwrap_or(&inner_clean)
} else { } else {
inner inner_clean
.strip_prefix(prefix) .strip_prefix(prefix)
.and_then(|s| s.strip_suffix(")")) .and_then(|s| s.strip_suffix(")"))
.unwrap_or(&inner) .unwrap_or(&inner_clean)
}; };
let mut recursive_highlighter = Self::new(); let mut recursive_highlighter = Self::new();