Added prompt escape code expansion flag to echo, -p

Added non-formatted runtime to prompt escape codes

Added prompt escape code that expands to the output of a shell function

Reworked internal logic for termios control
This commit is contained in:
2026-01-29 03:46:35 -05:00
parent 70f0e849ba
commit a4f48abd49
10 changed files with 381 additions and 178 deletions

View File

@@ -1,14 +1,7 @@
use std::sync::LazyLock; use std::sync::LazyLock;
use crate::{ use crate::{
builtin::setup_builtin, builtin::setup_builtin, expand::expand_prompt, getopt::{Opt, OptSet, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{IoStack, borrow_fd}, state
getopt::{get_opts_from_tokens, Opt, OptSet},
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node},
prelude::*,
procio::{borrow_fd, IoStack},
state,
}; };
pub static ECHO_OPTS: LazyLock<OptSet> = LazyLock::new(|| { pub static ECHO_OPTS: LazyLock<OptSet> = LazyLock::new(|| {
@@ -16,7 +9,7 @@ pub static ECHO_OPTS: LazyLock<OptSet> = LazyLock::new(|| {
Opt::Short('n'), Opt::Short('n'),
Opt::Short('E'), Opt::Short('E'),
Opt::Short('e'), Opt::Short('e'),
Opt::Short('r'), Opt::Short('p'),
] ]
.into() .into()
}); });
@@ -26,6 +19,7 @@ bitflags! {
const NO_NEWLINE = 0b000001; const NO_NEWLINE = 0b000001;
const USE_STDERR = 0b000010; const USE_STDERR = 0b000010;
const USE_ESCAPE = 0b000100; const USE_ESCAPE = 0b000100;
const USE_PROMPT = 0b001000;
} }
} }
@@ -49,11 +43,13 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
borrow_fd(STDOUT_FILENO) borrow_fd(STDOUT_FILENO)
}; };
let mut echo_output = argv let mut echo_output = prepare_echo_args(argv
.into_iter() .into_iter()
.map(|a| a.0) // Extract the String from the tuple of (String,Span) .map(|a| a.0) // Extract the String from the tuple of (String,Span)
.collect::<Vec<_>>() .collect::<Vec<_>>(),
.join(" "); flags.contains(EchoFlags::USE_ESCAPE),
flags.contains(EchoFlags::USE_PROMPT)
)?.join(" ");
if !flags.contains(EchoFlags::NO_NEWLINE) { if !flags.contains(EchoFlags::NO_NEWLINE) {
echo_output.push('\n') echo_output.push('\n')
@@ -66,6 +62,122 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
Ok(()) Ok(())
} }
pub fn prepare_echo_args(argv: Vec<String>, use_escape: bool, use_prompt: bool) -> ShResult<Vec<String>> {
if !use_escape {
if use_prompt {
let expanded: ShResult<Vec<String>> = argv
.into_iter()
.map(|s| expand_prompt(s.as_str()))
.collect();
return expanded
}
return Ok(argv);
}
let mut prepared_args = Vec::with_capacity(argv.len());
for arg in argv {
let mut prepared_arg = String::new();
if use_prompt {
prepared_arg = expand_prompt(&prepared_arg)?;
}
let mut chars = arg.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(&next_char) = chars.peek() {
match next_char {
'n' => {
prepared_arg.push('\n');
chars.next();
}
't' => {
prepared_arg.push('\t');
chars.next();
}
'r' => {
prepared_arg.push('\r');
chars.next();
}
'a' => {
prepared_arg.push('\x07');
chars.next();
}
'b' => {
prepared_arg.push('\x08');
chars.next();
}
'e' | 'E' => {
prepared_arg.push('\x1b');
chars.next();
}
'x' => {
chars.next(); // consume 'x'
let mut hex_digits = String::new();
for _ in 0..2 {
if let Some(&hex_char) = chars.peek() {
if hex_char.is_ascii_hexdigit() {
hex_digits.push(hex_char);
chars.next();
} else {
break;
}
} else {
break;
}
}
if let Ok(value) = u8::from_str_radix(&hex_digits, 16) {
prepared_arg.push(value as char);
} else {
prepared_arg.push('\\');
prepared_arg.push('x');
prepared_arg.push_str(&hex_digits);
}
}
'0' => {
chars.next(); // consume '0'
let mut octal_digits = String::new();
for _ in 0..3 {
if let Some(&octal_char) = chars.peek() {
if ('0'..='7').contains(&octal_char) {
octal_digits.push(octal_char);
chars.next();
} else {
break;
}
} else {
break;
}
}
if let Ok(value) = u8::from_str_radix(&octal_digits, 8) {
prepared_arg.push(value as char);
} else {
prepared_arg.push('\\');
prepared_arg.push('0');
prepared_arg.push_str(&octal_digits);
}
}
'\\' => {
prepared_arg.push('\\');
chars.next();
}
_ => prepared_arg.push(c),
}
} else {
prepared_arg.push(c);
}
} else {
prepared_arg.push(c);
}
}
prepared_args.push(prepared_arg);
}
Ok(prepared_args)
}
pub fn get_echo_flags(mut opts: Vec<Opt>) -> ShResult<EchoFlags> { pub fn get_echo_flags(mut opts: Vec<Opt>) -> ShResult<EchoFlags> {
let mut flags = EchoFlags::empty(); let mut flags = EchoFlags::empty();
@@ -82,6 +194,7 @@ pub fn get_echo_flags(mut opts: Vec<Opt>) -> ShResult<EchoFlags> {
'n' => flags |= EchoFlags::NO_NEWLINE, 'n' => flags |= EchoFlags::NO_NEWLINE,
'r' => flags |= EchoFlags::USE_STDERR, 'r' => flags |= EchoFlags::USE_STDERR,
'e' => flags |= EchoFlags::USE_ESCAPE, 'e' => flags |= EchoFlags::USE_ESCAPE,
'p' => flags |= EchoFlags::USE_PROMPT,
_ => unreachable!(), _ => unreachable!(),
} }
} }

View File

@@ -11,7 +11,7 @@ use crate::parse::lex::{is_field_sep, is_hard_sep, LexFlags, LexStream, Tk, TkFl
use crate::parse::{Redir, RedirType}; use crate::parse::{Redir, RedirType};
use crate::prelude::*; use crate::prelude::*;
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack}; use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
use crate::state::{LogTab, VarFlags, read_jobs, read_vars, write_jobs, write_meta, write_vars}; use crate::state::{LogTab, VarFlags, read_jobs, read_logic, read_vars, write_jobs, write_meta, write_vars};
const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0']; const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0'];
@@ -789,13 +789,15 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
io_stack.push_frame(cmd_sub_io_frame); io_stack.push_frame(cmd_sub_io_frame);
if let Err(e) = exec_input(raw.to_string(), Some(io_stack)) { if let Err(e) = exec_input(raw.to_string(), Some(io_stack)) {
eprintln!("{e}"); eprintln!("{e}");
exit(1); unsafe { libc::_exit(1) };
} }
exit(0); unsafe { libc::_exit(0) };
} }
ForkResult::Parent { child } => { ForkResult::Parent { child } => {
std::mem::drop(cmd_sub_io_frame); // Closes the write pipe std::mem::drop(cmd_sub_io_frame); // Closes the write pipe
let status = waitpid(child, Some(WtFlag::WSTOPPED))?; let status = waitpid(child, Some(WtFlag::WSTOPPED))?;
// Reclaim terminal foreground in case child changed it
crate::jobs::take_term()?;
match status { match status {
WtStat::Exited(_, _) => { WtStat::Exited(_, _) => {
flog!(DEBUG, "filling buffer"); flog!(DEBUG, "filling buffer");
@@ -1423,9 +1425,11 @@ pub enum PromptTk {
AsciiOct(i32), AsciiOct(i32),
Text(String), Text(String),
AnsiSeq(String), AnsiSeq(String),
Function(String), // Expands to the output of any defined shell function
VisGrp, VisGrp,
UserSeq, UserSeq,
Runtime, RuntimeMillis,
RuntimeFormatted,
Weekday, Weekday,
Dquote, Dquote,
Squote, Squote,
@@ -1442,6 +1446,8 @@ pub enum PromptTk {
SuccessSymbol, SuccessSymbol,
FailureSymbol, FailureSymbol,
JobCount, JobCount,
VisGroupOpen,
VisGroupClose,
} }
pub fn format_cmd_runtime(dur: std::time::Duration) -> String { pub fn format_cmd_runtime(dur: std::time::Duration) -> String {
@@ -1646,10 +1652,46 @@ fn tokenize_prompt(raw: &str) -> Vec<PromptTk> {
'$' => tokens.push(PromptTk::PromptSymbol), '$' => tokens.push(PromptTk::PromptSymbol),
'n' => tokens.push(PromptTk::Text("\n".into())), 'n' => tokens.push(PromptTk::Text("\n".into())),
'r' => tokens.push(PromptTk::Text("\r".into())), 'r' => tokens.push(PromptTk::Text("\r".into())),
'T' => tokens.push(PromptTk::Runtime), 't' => tokens.push(PromptTk::RuntimeMillis),
'T' => tokens.push(PromptTk::RuntimeFormatted),
'\\' => tokens.push(PromptTk::Text("\\".into())), '\\' => tokens.push(PromptTk::Text("\\".into())),
'"' => tokens.push(PromptTk::Text("\"".into())), '"' => tokens.push(PromptTk::Text("\"".into())),
'\'' => tokens.push(PromptTk::Text("'".into())), '\'' => tokens.push(PromptTk::Text("'".into())),
'(' => tokens.push(PromptTk::VisGroupOpen),
')' => tokens.push(PromptTk::VisGroupClose),
'!' => {
let mut func_name = String::new();
let is_braced = chars.peek() == Some(&'{');
while let Some(ch) = chars.peek() {
match ch {
'}' if is_braced => {
chars.next();
break;
}
'A'..='Z' | 'a'..='z' | '0'..='9' | '_' => {
func_name.push(*ch);
chars.next();
}
_ => {
if is_braced {
// Invalid character in braced function name
tokens.push(PromptTk::Text(format!("\\!{{{func_name}")));
break;
} else {
// End of unbraced function name
let func_exists = read_logic(|l| l.get_func(&func_name).is_some());
if func_exists {
tokens.push(PromptTk::Function(func_name));
} else {
tokens.push(PromptTk::Text(format!("\\!{func_name}")));
}
break;
}
}
}
}
}
'e' => { 'e' => {
if chars.next() == Some('[') { if chars.next() == Some('[') {
let mut params = String::new(); let mut params = String::new();
@@ -1733,65 +1775,84 @@ pub fn expand_prompt(raw: &str) -> ShResult<String> {
let mut result = String::new(); let mut result = String::new();
while let Some(token) = tokens.next() { while let Some(token) = tokens.next() {
match token { match token {
PromptTk::AsciiOct(_) => todo!(), PromptTk::AsciiOct(_) => todo!(),
PromptTk::Text(txt) => result.push_str(&txt), PromptTk::Text(txt) => result.push_str(&txt),
PromptTk::AnsiSeq(params) => result.push_str(&params), PromptTk::AnsiSeq(params) => result.push_str(&params),
PromptTk::Runtime => { PromptTk::RuntimeMillis => {
if let Some(runtime) = write_meta(|m| m.stop_timer()) { if let Some(runtime) = write_meta(|m| m.stop_timer()) {
let runtime_fmt = format_cmd_runtime(runtime); let runtime_millis = runtime.as_millis().to_string();
result.push_str(&runtime_fmt); result.push_str(&runtime_millis);
} }
} }
PromptTk::Pwd => { PromptTk::RuntimeFormatted => {
let mut pwd = std::env::var("PWD").unwrap(); if let Some(runtime) = write_meta(|m| m.stop_timer()) {
let home = std::env::var("HOME").unwrap(); let runtime_fmt = format_cmd_runtime(runtime);
if pwd.starts_with(&home) { result.push_str(&runtime_fmt);
pwd = pwd.replacen(&home, "~", 1); }
} }
result.push_str(&pwd); PromptTk::Pwd => {
} let mut pwd = std::env::var("PWD").unwrap();
PromptTk::PwdShort => { let home = std::env::var("HOME").unwrap();
let mut path = std::env::var("PWD").unwrap(); if pwd.starts_with(&home) {
let home = std::env::var("HOME").unwrap(); pwd = pwd.replacen(&home, "~", 1);
if path.starts_with(&home) { }
path = path.replacen(&home, "~", 1); result.push_str(&pwd);
} }
let pathbuf = PathBuf::from(&path); PromptTk::PwdShort => {
let mut segments = pathbuf.iter().count(); let mut path = std::env::var("PWD").unwrap();
let mut path_iter = pathbuf.iter(); let home = std::env::var("HOME").unwrap();
while segments > 4 { if path.starts_with(&home) {
path_iter.next(); path = path.replacen(&home, "~", 1);
segments -= 1; }
} let pathbuf = PathBuf::from(&path);
let path_rebuilt: PathBuf = path_iter.collect(); let mut segments = pathbuf.iter().count();
let mut path_rebuilt = path_rebuilt.to_str().unwrap().to_string(); let mut path_iter = pathbuf.iter();
if path_rebuilt.starts_with(&home) { while segments > 4 {
path_rebuilt = path_rebuilt.replacen(&home, "~", 1); path_iter.next();
} segments -= 1;
result.push_str(&path_rebuilt); }
} let path_rebuilt: PathBuf = path_iter.collect();
PromptTk::Hostname => { let mut path_rebuilt = path_rebuilt.to_str().unwrap().to_string();
let hostname = std::env::var("HOST").unwrap(); if path_rebuilt.starts_with(&home) {
result.push_str(&hostname); path_rebuilt = path_rebuilt.replacen(&home, "~", 1);
} }
PromptTk::HostnameShort => todo!(), result.push_str(&path_rebuilt);
PromptTk::ShellName => result.push_str("fern"), }
PromptTk::Username => { PromptTk::Hostname => {
let username = std::env::var("USER").unwrap(); let hostname = std::env::var("HOST").unwrap();
result.push_str(&username); result.push_str(&hostname);
} }
PromptTk::PromptSymbol => { PromptTk::HostnameShort => todo!(),
let uid = std::env::var("UID").unwrap(); PromptTk::ShellName => result.push_str("fern"),
let symbol = if &uid == "0" { '#' } else { '$' }; PromptTk::Username => {
result.push(symbol); let username = std::env::var("USER").unwrap();
} result.push_str(&username);
PromptTk::ExitCode => todo!(), }
PromptTk::SuccessSymbol => todo!(), PromptTk::PromptSymbol => {
PromptTk::FailureSymbol => todo!(), let uid = std::env::var("UID").unwrap();
PromptTk::JobCount => todo!(), let symbol = if &uid == "0" { '#' } else { '$' };
_ => unimplemented!(), result.push(symbol);
} }
PromptTk::ExitCode => todo!(),
PromptTk::SuccessSymbol => todo!(),
PromptTk::FailureSymbol => todo!(),
PromptTk::JobCount => todo!(),
PromptTk::Function(f) => {
flog!(DEBUG, "Expanding prompt function: {}", f);
let output = expand_cmd_sub(&f)?;
result.push_str(&output);
}
PromptTk::VisGrp => todo!(),
PromptTk::UserSeq => todo!(),
PromptTk::Weekday => todo!(),
PromptTk::Dquote => todo!(),
PromptTk::Squote => todo!(),
PromptTk::Return => todo!(),
PromptTk::Newline => todo!(),
PromptTk::VisGroupOpen => todo!(),
PromptTk::VisGroupClose => todo!(),
}
} }
Ok(result) Ok(result)

View File

@@ -1,6 +1,6 @@
use termios::{LocalFlags, Termios}; use termios::{LocalFlags, Termios};
use crate::{prelude::*, state::write_jobs}; use crate::{prelude::*};
/// ///
/// The previous state of the terminal options. /// The previous state of the terminal options.
/// ///
@@ -31,64 +31,46 @@ use crate::{prelude::*, state::write_jobs};
/// lifecycle could lead to undefined behavior. /// lifecycle could lead to undefined behavior.
pub(crate) static mut SAVED_TERMIOS: Option<Option<Termios>> = None; pub(crate) static mut SAVED_TERMIOS: Option<Option<Termios>> = None;
pub fn save_termios() { #[derive(Debug)]
unsafe { pub struct TermiosGuard {
SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() { saved_termios: Option<Termios>
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL;
termios::tcsetattr(
std::io::stdin(),
nix::sys::termios::SetArg::TCSANOW,
&termios,
)
.unwrap();
Some(termios)
} else {
None
});
}
}
#[allow(static_mut_refs)]
///Access the saved termios
///
///# Safety
///This function is unsafe because it accesses a public mutable static value.
/// This function should only ever be called after save_termios() has already
/// been called.
pub unsafe fn get_saved_termios() -> Option<Termios> { unsafe {
// SAVED_TERMIOS should *only ever* be set once and accessed once
// Set at the start of the program, and accessed during the exit of the program
// to reset the termios. Do not use this variable anywhere else
SAVED_TERMIOS.clone().flatten()
}}
/// Set termios to not echo control characters, like ^Z for instance
pub fn set_termios() {
if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL;
termios::tcsetattr(
std::io::stdin(),
nix::sys::termios::SetArg::TCSANOW,
&termios,
)
.unwrap();
}
} }
pub fn sh_quit(code: i32) -> ! { impl TermiosGuard {
write_jobs(|j| { pub fn new(new_termios: Termios) -> Self {
for job in j.jobs_mut().iter_mut().flatten() { let mut new = Self { saved_termios: None };
job.killpg(Signal::SIGTERM).ok();
} if isatty(std::io::stdin().as_raw_fd()).unwrap() {
}); let current_termios = termios::tcgetattr(std::io::stdin()).unwrap();
if let Some(termios) = unsafe { get_saved_termios() } { new.saved_termios = Some(current_termios);
termios::tcsetattr(std::io::stdin(), termios::SetArg::TCSANOW, &termios).unwrap();
} termios::tcsetattr(
if code == 0 { std::io::stdin(),
eprintln!("exit"); nix::sys::termios::SetArg::TCSANOW,
} else { &new_termios,
eprintln!("exit {code}"); ).unwrap();
} }
exit(code);
new
}
}
impl Default for TermiosGuard {
fn default() -> Self {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL;
Self::new(termios)
}
}
impl Drop for TermiosGuard {
fn drop(&mut self) {
if let Some(saved) = &self.saved_termios {
termios::tcsetattr(
std::io::stdin(),
nix::sys::termios::SetArg::TCSANOW,
saved,
).unwrap();
}
}
} }

View File

@@ -18,11 +18,14 @@ pub mod state;
#[cfg(test)] #[cfg(test)]
pub mod tests; pub mod tests;
use std::process::ExitCode;
use std::sync::atomic::Ordering;
use crate::libsh::error::ShErrKind; use crate::libsh::error::ShErrKind;
use crate::libsh::sys::{save_termios, set_termios}; use crate::libsh::sys::TermiosGuard;
use crate::parse::execute::exec_input; use crate::parse::execute::exec_input;
use crate::prelude::*; use crate::prelude::*;
use crate::signal::{check_signals, sig_setup, signals_pending}; use crate::signal::{QUIT_CODE, check_signals, sig_setup, signals_pending};
use crate::state::source_rc; use crate::state::source_rc;
use clap::Parser; use clap::Parser;
use shopt::FernEditMode; use shopt::FernEditMode;
@@ -53,12 +56,12 @@ fn kickstart_lazy_evals() {
read_vars(|_| {}); read_vars(|_| {});
} }
fn main() { fn main() -> ExitCode {
kickstart_lazy_evals(); kickstart_lazy_evals();
let args = FernArgs::parse(); let args = FernArgs::parse();
if args.version { if args.version {
println!("fern {}", env!("CARGO_PKG_VERSION")); println!("fern {}", env!("CARGO_PKG_VERSION"));
return; return ExitCode::SUCCESS;
} }
if let Some(path) = args.script { if let Some(path) = args.script {
@@ -66,17 +69,21 @@ fn main() {
} else { } else {
fern_interactive(); fern_interactive();
} }
ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8)
} }
fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) { fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) {
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());
exit(1); QUIT_CODE.store(1, Ordering::SeqCst);
return;
} }
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());
exit(1); QUIT_CODE.store(1, Ordering::SeqCst);
return;
}; };
write_vars(|v| v.cur_scope_mut().bpush_arg(path.to_string_lossy().to_string())); write_vars(|v| v.cur_scope_mut().bpush_arg(path.to_string_lossy().to_string()));
@@ -86,13 +93,19 @@ fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) {
if let Err(e) = exec_input(input, None) { if let Err(e) = exec_input(input, None) {
eprintln!("{e}"); eprintln!("{e}");
exit(1); match e.kind() {
ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst);
}
_ => {
QUIT_CODE.store(1, Ordering::SeqCst);
}
}
} }
} }
fn fern_interactive() { fn fern_interactive() {
save_termios(); let _termios_guard = TermiosGuard::default(); // sets raw mode, restores termios on drop
set_termios();
sig_setup(); sig_setup();
if let Err(e) = source_rc() { if let Err(e) = source_rc() {
@@ -129,37 +142,52 @@ fn fern_interactive() {
line line
} }
Err(e) => { Err(e) => {
if let ShErrKind::ReadlineIntr(partial) = e.kind() { match e.kind() {
// Did we get signaled? Check signal flags ShErrKind::ReadlineIntr(partial) => {
// If nothing to worry about, retry the readline // Did we get signaled? Check signal flags
while signals_pending() { // If nothing to worry about, retry the readline with the unfinished input
if let Err(e) = check_signals() { while signals_pending() {
if let ShErrKind::ClearReadline = e.kind() { if let Err(e) = check_signals() {
partial_input.clear(); if let ShErrKind::ClearReadline = e.kind() {
if !signals_pending() { partial_input.clear();
continue 'outer; if !signals_pending() {
} continue 'outer;
}; }
eprintln!("{e}"); };
eprintln!("{e}");
}
} }
} partial_input = partial.to_string();
partial_input = partial.to_string();
continue;
} else {
eprintln!("{e}");
readline_err_count += 1;
if readline_err_count == 20 {
eprintln!("reached maximum readline error count, exiting");
break;
} else {
continue; continue;
} }
ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst);
return;
}
_ => {
eprintln!("{e}");
readline_err_count += 1;
if readline_err_count == 20 {
eprintln!("reached maximum readline error count, exiting");
break;
} else {
continue;
}
}
} }
} }
}; };
if let Err(e) = exec_input(input, None) { if let Err(e) = exec_input(input, None) {
eprintln!("{e}"); match e.kind() {
ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst);
return;
}
_ => {
eprintln!("{e}");
}
}
} }
} }
} }

View File

@@ -1282,11 +1282,30 @@ impl ParseStream {
assignments.push(assign) assignments.push(assign)
} else if is_keyword { } else if is_keyword {
return Ok(None); return Ok(None);
} else if prefix_tk.class == TkRule::Sep {
// Separator ends the prefix section - add it so commit() consumes it
node_tks.push(prefix_tk.clone());
break;
} else {
// Other non-prefix token ends the prefix section
break;
} }
} }
if argv.is_empty() && assignments.is_empty() { if argv.is_empty() {
return Ok(None); if assignments.is_empty() {
return Ok(None);
} else {
// If we have assignments but no command word,
// return the assignment-only command without parsing more tokens
self.commit(node_tks.len());
return Ok(Some(Node {
class: NdRule::Command { assignments, argv },
tokens: node_tks,
flags: NdFlags::empty(),
redirs,
}));
}
} }
while let Some(tk) = tk_iter.next() { while let Some(tk) = tk_iter.next() {

View File

@@ -11,17 +11,19 @@ use crate::{
/// Initialize the line editor /// Initialize the line editor
fn get_prompt() -> ShResult<String> { fn get_prompt() -> ShResult<String> {
let Ok(prompt) = env::var("PS1") else { let Ok(prompt) = env::var("PS1") else {
// prompt expands to: // default prompt expands to:
// //
// username@hostname // username@hostname
// short/path/to/pwd/ // short/path/to/pwd/
// $ _ // $ _
let default = let default =
"\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m "; "\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ";
return expand_prompt(default); return expand_prompt(default);
}; };
let sanitized = format!("\\e[0m{prompt}");
flog!(DEBUG, "Using prompt: {}", sanitized.replace("\n", "\\n"));
expand_prompt(&prompt) expand_prompt(&sanitized)
} }
pub fn readline(edit_mode: FernEditMode, initial: Option<&str>) -> ShResult<String> { pub fn readline(edit_mode: FernEditMode, initial: Option<&str>) -> ShResult<String> {

View File

@@ -8,7 +8,6 @@ use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVis
use crate::libsh::{ use crate::libsh::{
error::{ShErr, ShErrKind, ShResult}, error::{ShErr, ShErrKind, ShResult},
sys::sh_quit,
term::{Style, Styled}, term::{Style, Styled},
}; };
use crate::prelude::*; use crate::prelude::*;
@@ -95,7 +94,7 @@ impl Readline for FernVi {
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) { if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
if self.editor.buffer.is_empty() { if self.editor.buffer.is_empty() {
std::mem::drop(raw_mode_guard); std::mem::drop(raw_mode_guard);
sh_quit(0); return Err(ShErr::simple(ShErrKind::CleanExit(0), "exit"));
} else { } else {
self.editor.buffer.clear(); self.editor.buffer.clear();
continue; continue;

View File

@@ -687,7 +687,7 @@ impl LineWriter for TermWriter {
for _ in 0..rows_to_clear { for _ in 0..rows_to_clear {
self.buffer.push_str("\x1b[2K\x1b[A"); self.buffer.push_str("\x1b[2K\x1b[A");
} }
self.buffer.push_str("\x1b[2K"); self.buffer.push_str("\x1b[2K\r"); // Clear line and return to column 0
write_all(self.out, self.buffer.as_str())?; write_all(self.out, self.buffer.as_str())?;
self.buffer.clear(); self.buffer.clear();
Ok(()) Ok(())

View File

@@ -15,8 +15,8 @@ static GOT_SIGTSTP: AtomicBool = AtomicBool::new(false);
static GOT_SIGCHLD: AtomicBool = AtomicBool::new(false); static GOT_SIGCHLD: AtomicBool = AtomicBool::new(false);
static REAPING_ENABLED: AtomicBool = AtomicBool::new(true); static REAPING_ENABLED: AtomicBool = AtomicBool::new(true);
static SHOULD_QUIT: AtomicBool = AtomicBool::new(false); pub static SHOULD_QUIT: AtomicBool = AtomicBool::new(false);
static QUIT_CODE: AtomicI32 = AtomicI32::new(0); pub static QUIT_CODE: AtomicI32 = AtomicI32::new(0);
pub fn signals_pending() -> bool { pub fn signals_pending() -> bool {
GOT_SIGINT.load(Ordering::SeqCst) GOT_SIGINT.load(Ordering::SeqCst)

View File

@@ -686,7 +686,6 @@ impl MetaTab {
pub fn stop_timer(&mut self) -> Option<Duration> { pub fn stop_timer(&mut self) -> Option<Duration> {
self self
.runtime_start .runtime_start
.take() // runtime_start returns to None
.map(|start| start.elapsed()) // return the duration, if any .map(|start| start.elapsed()) // return the duration, if any
} }
} }