Fixed command substitutions not expanding when used as a command name
This commit is contained in:
347
src/main.rs
347
src/main.rs
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(..));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user