implemented read command for ex mode

This commit is contained in:
2026-03-16 01:53:49 -04:00
parent bcc4a87e10
commit ec9795c781
15 changed files with 605 additions and 506 deletions

View File

@@ -4,14 +4,25 @@ use ariadne::Span as ASpan;
use nix::libc::STDIN_FILENO; use nix::libc::STDIN_FILENO;
use crate::{ use crate::{
libsh::{error::{ShErr, ShErrKind, ShResult}, guards::RawModeGuard}, parse::{NdRule, Node, Redir, RedirType, execute::{exec_input, prepare_argv}, lex::{QuoteState, Span}}, procio::{IoFrame, IoMode}, readline::{complete::ScoredCandidate, markers}, state libsh::{
error::{ShErr, ShErrKind, ShResult},
guards::RawModeGuard,
},
parse::{
NdRule, Node, Redir, RedirType,
execute::{exec_input, prepare_argv},
lex::{QuoteState, Span},
},
procio::{IoFrame, IoMode},
readline::{complete::ScoredCandidate, markers},
state,
}; };
const TAG_SEQ: &str = "\x1b[1;33m"; // bold yellow — searchable tags const TAG_SEQ: &str = "\x1b[1;33m"; // bold yellow — searchable tags
const REF_SEQ: &str = "\x1b[4;36m"; // underline cyan — cross-references const REF_SEQ: &str = "\x1b[4;36m"; // underline cyan — cross-references
const RESET_SEQ: &str = "\x1b[0m"; const RESET_SEQ: &str = "\x1b[0m";
const HEADER_SEQ: &str = "\x1b[1;35m"; // bold magenta — section headers const HEADER_SEQ: &str = "\x1b[1;35m"; // bold magenta — section headers
const CODE_SEQ: &str = "\x1b[32m"; // green — inline code const CODE_SEQ: &str = "\x1b[32m"; // green — inline code
const KEYWORD_2_SEQ: &str = "\x1b[1;32m"; // bold green — {keyword} const KEYWORD_2_SEQ: &str = "\x1b[1;32m"; // bold green — {keyword}
const KEYWORD_3_SEQ: &str = "\x1b[3;37m"; // italic white — [optional] const KEYWORD_3_SEQ: &str = "\x1b[3;37m"; // italic white — [optional]
@@ -27,265 +38,263 @@ pub fn help(node: Node) -> ShResult<()> {
let mut argv = prepare_argv(argv)?.into_iter().peekable(); let mut argv = prepare_argv(argv)?.into_iter().peekable();
let help = argv.next().unwrap(); // drop 'help' let help = argv.next().unwrap(); // drop 'help'
// Join all of the word-split arguments into a single string // Join all of the word-split arguments into a single string
// Preserve the span too // Preserve the span too
let (topic, span) = if argv.peek().is_none() { let (topic, span) = if argv.peek().is_none() {
("help.txt".to_string(), help.1) ("help.txt".to_string(), help.1)
} else { } else {
argv.fold((String::new(), Span::default()), |mut acc, arg| { argv.fold((String::new(), Span::default()), |mut acc, arg| {
if acc.1 == Span::default() { if acc.1 == Span::default() {
acc.1 = arg.1.clone(); acc.1 = arg.1.clone();
} else { } else {
let new_end = arg.1.end(); let new_end = arg.1.end();
let start = acc.1.start(); let start = acc.1.start();
acc.1.set_range(start..new_end); acc.1.set_range(start..new_end);
} }
if acc.0.is_empty() { if acc.0.is_empty() {
acc.0 = arg.0; acc.0 = arg.0;
} else { } else {
acc.0 = acc.0 + &format!(" {}",arg.0); acc.0 = acc.0 + &format!(" {}", arg.0);
} }
acc acc
}) })
}; };
let hpath = env::var("SHED_HPATH").unwrap_or_default(); let hpath = env::var("SHED_HPATH").unwrap_or_default();
for path in hpath.split(':') { for path in hpath.split(':') {
let path = Path::new(&path).join(&topic); let path = Path::new(&path).join(&topic);
if path.is_file() { if path.is_file() {
let Ok(contents) = std::fs::read_to_string(&path) else { let Ok(contents) = std::fs::read_to_string(&path) else {
continue; continue;
}; };
let filename = path.file_stem() let filename = path.file_stem().unwrap().to_string_lossy().to_string();
.unwrap()
.to_string_lossy()
.to_string();
let unescaped = unescape_help(&contents); let unescaped = unescape_help(&contents);
let expanded = expand_help(&unescaped); let expanded = expand_help(&unescaped);
open_help(&expanded, None, Some(filename))?; open_help(&expanded, None, Some(filename))?;
state::set_status(0); state::set_status(0);
return Ok(()); return Ok(());
} }
} }
// didn't find an exact filename match, its probably a tag search // didn't find an exact filename match, its probably a tag search
for path in hpath.split(':') { for path in hpath.split(':') {
let path = Path::new(path); let path = Path::new(path);
if let Ok(entries) = path.read_dir() { if let Ok(entries) = path.read_dir() {
for entry in entries { for entry in entries {
let Ok(entry) = entry else { continue }; let Ok(entry) = entry else { continue };
let path = entry.path(); let path = entry.path();
let filename = path.file_stem() let filename = path.file_stem().unwrap().to_string_lossy().to_string();
.unwrap()
.to_string_lossy()
.to_string();
if !path.is_file() { if !path.is_file() {
continue; continue;
} }
let Ok(contents) = std::fs::read_to_string(&path) else { let Ok(contents) = std::fs::read_to_string(&path) else {
continue; continue;
}; };
let unescaped = unescape_help(&contents); let unescaped = unescape_help(&contents);
let expanded = expand_help(&unescaped); let expanded = expand_help(&unescaped);
let tags = read_tags(&expanded); let tags = read_tags(&expanded);
for (tag, line) in &tags { for (tag, line) in &tags {}
}
if let Some((matched_tag, line)) = get_best_match(&topic, &tags) { if let Some((matched_tag, line)) = get_best_match(&topic, &tags) {
open_help(&expanded, Some(line), Some(filename))?; open_help(&expanded, Some(line), Some(filename))?;
state::set_status(0); state::set_status(0);
return Ok(()); return Ok(());
} else { } else {
} }
} }
} }
} }
state::set_status(1); state::set_status(1);
Err(ShErr::at( Err(ShErr::at(
ShErrKind::NotFound, ShErrKind::NotFound,
span, span,
"No relevant help page found for this topic", "No relevant help page found for this topic",
)) ))
} }
pub fn open_help(content: &str, line: Option<usize>, file_name: Option<String>) -> ShResult<()> { pub fn open_help(content: &str, line: Option<usize>, file_name: Option<String>) -> ShResult<()> {
let pager = env::var("PAGER").unwrap_or("less -R".into()); let pager = env::var("PAGER").unwrap_or("less -R".into());
let line_arg = line.map(|ln| format!("+{ln}")).unwrap_or_default(); let line_arg = line.map(|ln| format!("+{ln}")).unwrap_or_default();
let prompt_arg = file_name.map(|name| format!("-Ps'{name}'")).unwrap_or_default(); let prompt_arg = file_name
.map(|name| format!("-Ps'{name}'"))
.unwrap_or_default();
let mut tmp = tempfile::NamedTempFile::new()?; let mut tmp = tempfile::NamedTempFile::new()?;
let tmp_path = tmp.path().to_string_lossy().to_string(); let tmp_path = tmp.path().to_string_lossy().to_string();
tmp.write_all(content.as_bytes())?; tmp.write_all(content.as_bytes())?;
tmp.flush()?; tmp.flush()?;
RawModeGuard::with_cooked_mode(|| { RawModeGuard::with_cooked_mode(|| {
exec_input( exec_input(
format!("{pager} {line_arg} {prompt_arg} {tmp_path}"), format!("{pager} {line_arg} {prompt_arg} {tmp_path}"),
None, None,
true, true,
Some("help".into()), Some("help".into()),
) )
}) })
} }
pub fn get_best_match(topic: &str, tags: &[(String, usize)]) -> Option<(String, usize)> { pub fn get_best_match(topic: &str, tags: &[(String, usize)]) -> Option<(String, usize)> {
let mut candidates: Vec<_> = tags.iter() let mut candidates: Vec<_> = tags
.map(|(tag,line)| (ScoredCandidate::new(tag.to_string()), *line)) .iter()
.collect(); .map(|(tag, line)| (ScoredCandidate::new(tag.to_string()), *line))
.collect();
for (cand,_) in candidates.iter_mut() { for (cand, _) in candidates.iter_mut() {
cand.fuzzy_score(topic); cand.fuzzy_score(topic);
} }
candidates.retain(|(c,_)| c.score.unwrap_or(i32::MIN) > i32::MIN); candidates.retain(|(c, _)| c.score.unwrap_or(i32::MIN) > i32::MIN);
candidates.sort_by_key(|(c,_)| c.score.unwrap_or(i32::MIN)); candidates.sort_by_key(|(c, _)| c.score.unwrap_or(i32::MIN));
candidates.first().map(|(c,line)| (c.content.clone(), *line)) candidates
.first()
.map(|(c, line)| (c.content.clone(), *line))
} }
pub fn read_tags(raw: &str) -> Vec<(String, usize)> { pub fn read_tags(raw: &str) -> Vec<(String, usize)> {
let mut tags = vec![]; let mut tags = vec![];
for (line_num, line) in raw.lines().enumerate() { for (line_num, line) in raw.lines().enumerate() {
let mut rest = line; let mut rest = line;
while let Some(pos) = rest.find(TAG_SEQ) { while let Some(pos) = rest.find(TAG_SEQ) {
let after_seq = &rest[pos + TAG_SEQ.len()..]; let after_seq = &rest[pos + TAG_SEQ.len()..];
if let Some(end) = after_seq.find(RESET_SEQ) { if let Some(end) = after_seq.find(RESET_SEQ) {
let tag = &after_seq[..end]; let tag = &after_seq[..end];
tags.push((tag.to_string(), line_num + 1)); tags.push((tag.to_string(), line_num + 1));
rest = &after_seq[end + RESET_SEQ.len()..]; rest = &after_seq[end + RESET_SEQ.len()..];
} else { } else {
break; break;
} }
} }
} }
tags tags
} }
pub fn expand_help(raw: &str) -> String { pub fn expand_help(raw: &str) -> String {
let mut result = String::new(); let mut result = String::new();
let mut chars = raw.chars(); let mut chars = raw.chars();
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
markers::RESET => result.push_str(RESET_SEQ), markers::RESET => result.push_str(RESET_SEQ),
markers::TAG => result.push_str(TAG_SEQ), markers::TAG => result.push_str(TAG_SEQ),
markers::REFERENCE => result.push_str(REF_SEQ), markers::REFERENCE => result.push_str(REF_SEQ),
markers::HEADER => result.push_str(HEADER_SEQ), markers::HEADER => result.push_str(HEADER_SEQ),
markers::CODE => result.push_str(CODE_SEQ), markers::CODE => result.push_str(CODE_SEQ),
markers::KEYWORD_2 => result.push_str(KEYWORD_2_SEQ), markers::KEYWORD_2 => result.push_str(KEYWORD_2_SEQ),
markers::KEYWORD_3 => result.push_str(KEYWORD_3_SEQ), markers::KEYWORD_3 => result.push_str(KEYWORD_3_SEQ),
_ => result.push(ch), _ => result.push(ch),
} }
} }
result result
} }
pub fn unescape_help(raw: &str) -> String { pub fn unescape_help(raw: &str) -> String {
let mut result = String::new(); let mut result = String::new();
let mut chars = raw.chars().peekable(); let mut chars = raw.chars().peekable();
let mut qt_state = QuoteState::default(); let mut qt_state = QuoteState::default();
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
'\\' => { '\\' => {
if let Some(next_ch) = chars.next() { if let Some(next_ch) = chars.next() {
result.push(next_ch); result.push(next_ch);
} }
} }
'\n' => { '\n' => {
result.push(ch); result.push(ch);
qt_state = QuoteState::default(); qt_state = QuoteState::default();
} }
'"' => { '"' => {
result.push(ch); result.push(ch);
qt_state.toggle_double(); qt_state.toggle_double();
} }
'\'' => { '\'' => {
result.push(ch); result.push(ch);
qt_state.toggle_single(); qt_state.toggle_single();
} }
_ if qt_state.in_quote() || chars.peek().is_none_or(|ch| ch.is_whitespace()) => { _ if qt_state.in_quote() || chars.peek().is_none_or(|ch| ch.is_whitespace()) => {
result.push(ch); result.push(ch);
} }
'*' => { '*' => {
result.push(markers::TAG); result.push(markers::TAG);
while let Some(next_ch) = chars.next() { while let Some(next_ch) = chars.next() {
if next_ch == '*' { if next_ch == '*' {
result.push(markers::RESET); result.push(markers::RESET);
break; break;
} else { } else {
result.push(next_ch); result.push(next_ch);
} }
} }
} }
'|' => { '|' => {
result.push(markers::REFERENCE); result.push(markers::REFERENCE);
while let Some(next_ch) = chars.next() { while let Some(next_ch) = chars.next() {
if next_ch == '|' { if next_ch == '|' {
result.push(markers::RESET); result.push(markers::RESET);
break; break;
} else { } else {
result.push(next_ch); result.push(next_ch);
} }
} }
} }
'#' => { '#' => {
result.push(markers::HEADER); result.push(markers::HEADER);
while let Some(next_ch) = chars.next() { while let Some(next_ch) = chars.next() {
if next_ch == '#' { if next_ch == '#' {
result.push(markers::RESET); result.push(markers::RESET);
break; break;
} else { } else {
result.push(next_ch); result.push(next_ch);
} }
} }
} }
'`' => { '`' => {
result.push(markers::CODE); result.push(markers::CODE);
while let Some(next_ch) = chars.next() { while let Some(next_ch) = chars.next() {
if next_ch == '`' { if next_ch == '`' {
result.push(markers::RESET); result.push(markers::RESET);
break; break;
} else { } else {
result.push(next_ch); result.push(next_ch);
} }
} }
} }
'{' => { '{' => {
result.push(markers::KEYWORD_2); result.push(markers::KEYWORD_2);
while let Some(next_ch) = chars.next() { while let Some(next_ch) = chars.next() {
if next_ch == '}' { if next_ch == '}' {
result.push(markers::RESET); result.push(markers::RESET);
break; break;
} else { } else {
result.push(next_ch); result.push(next_ch);
} }
} }
} }
'[' => { '[' => {
result.push(markers::KEYWORD_3); result.push(markers::KEYWORD_3);
while let Some(next_ch) = chars.next() { while let Some(next_ch) = chars.next() {
if next_ch == ']' { if next_ch == ']' {
result.push(markers::RESET); result.push(markers::RESET);
break; break;
} else { } else {
result.push(next_ch); result.push(next_ch);
} }
} }
} }
_ => result.push(ch), _ => result.push(ch),
} }
} }
result result
} }

View File

@@ -11,6 +11,7 @@ pub mod eval;
pub mod exec; pub mod exec;
pub mod flowctl; pub mod flowctl;
pub mod getopts; pub mod getopts;
pub mod help;
pub mod intro; pub mod intro;
pub mod jobctl; pub mod jobctl;
pub mod keymap; pub mod keymap;
@@ -25,7 +26,6 @@ pub mod source;
pub mod test; // [[ ]] thing pub mod test; // [[ ]] thing
pub mod trap; pub mod trap;
pub mod varcmds; pub mod varcmds;
pub mod help;
pub const BUILTINS: [&str; 51] = [ pub const BUILTINS: [&str; 51] = [
"echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg", "echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg",

View File

@@ -474,32 +474,31 @@ pub fn expand_raw(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
markers::TILDE_SUB => { markers::TILDE_SUB => {
let mut username = String::new(); let mut username = String::new();
while chars.peek().is_some_and(|ch| *ch != '/') { while chars.peek().is_some_and(|ch| *ch != '/') {
let ch = chars.next().unwrap(); let ch = chars.next().unwrap();
username.push(ch); username.push(ch);
} }
let home = if username.is_empty() { let home = if username.is_empty() {
// standard '~' expansion // standard '~' expansion
env::var("HOME").unwrap_or_default() env::var("HOME").unwrap_or_default()
} } else if let Ok(result) = User::from_name(&username)
else if let Ok(result) = User::from_name(&username) && let Some(user) = result
&& let Some(user) = result { {
// username expansion like '~user' // username expansion like '~user'
user.dir.to_string_lossy().to_string() user.dir.to_string_lossy().to_string()
} } else if let Ok(id) = username.parse::<u32>()
else if let Ok(id) = username.parse::<u32>() && let Ok(result) = User::from_uid(Uid::from_raw(id))
&& let Ok(result) = User::from_uid(Uid::from_raw(id)) && let Some(user) = result
&& let Some(user) = result { {
// uid expansion like '~1000' // uid expansion like '~1000'
// shed only feature btw B) // shed only feature btw B)
user.dir.to_string_lossy().to_string() user.dir.to_string_lossy().to_string()
} } else {
else { // no match, use literal
// no match, use literal format!("~{username}")
format!("~{username}") };
};
result.push_str(&home); result.push_str(&home);
} }
@@ -1584,10 +1583,10 @@ pub fn unescape_math(raw: &str) -> String {
#[derive(Debug)] #[derive(Debug)]
pub enum ParamExp { pub enum ParamExp {
Len, // #var_name Len, // #var_name
ToUpperFirst, // ^var_name ToUpperFirst, // ^var_name
ToUpperAll, // ^^var_name ToUpperAll, // ^^var_name
ToLowerFirst, // ,var_name ToLowerFirst, // ,var_name
ToLowerAll, // ,,var_name ToLowerAll, // ,,var_name
DefaultUnsetOrNull(String), // :- DefaultUnsetOrNull(String), // :-
DefaultUnset(String), // - DefaultUnset(String), // -
SetDefaultUnsetOrNull(String), // := SetDefaultUnsetOrNull(String), // :=
@@ -1623,10 +1622,18 @@ impl FromStr for ParamExp {
)) ))
}; };
if s == "^^" { return Ok(ToUpperAll) } if s == "^^" {
if s == "^" { return Ok(ToUpperFirst) } return Ok(ToUpperAll);
if s == ",," { return Ok(ToLowerAll) } }
if s == "," { return Ok(ToLowerFirst) } if s == "^" {
return Ok(ToUpperFirst);
}
if s == ",," {
return Ok(ToLowerAll);
}
if s == "," {
return Ok(ToLowerFirst);
}
// Handle indirect var expansion: ${!var} // Handle indirect var expansion: ${!var}
if let Some(var) = s.strip_prefix('!') { if let Some(var) = s.strip_prefix('!') {
@@ -1745,32 +1752,32 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
if let Ok(expansion) = rest.parse::<ParamExp>() { if let Ok(expansion) = rest.parse::<ParamExp>() {
match expansion { match expansion {
ParamExp::Len => unreachable!(), ParamExp::Len => unreachable!(),
ParamExp::ToUpperAll => { ParamExp::ToUpperAll => {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
Ok(value.to_uppercase()) Ok(value.to_uppercase())
} }
ParamExp::ToUpperFirst => { ParamExp::ToUpperFirst => {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
let mut chars = value.chars(); let mut chars = value.chars();
let first = chars.next() let first = chars
.map(|c| c.to_uppercase() .next()
.to_string()) .map(|c| c.to_uppercase().to_string())
.unwrap_or_default(); .unwrap_or_default();
Ok(first + chars.as_str()) Ok(first + chars.as_str())
}
} ParamExp::ToLowerAll => {
ParamExp::ToLowerAll => { let value = vars.get_var(&var_name);
let value = vars.get_var(&var_name); Ok(value.to_lowercase())
Ok(value.to_lowercase()) }
} ParamExp::ToLowerFirst => {
ParamExp::ToLowerFirst => { let value = vars.get_var(&var_name);
let value = vars.get_var(&var_name); let mut chars = value.chars();
let mut chars = value.chars(); let first = chars
let first = chars.next() .next()
.map(|c| c.to_lowercase().to_string()) .map(|c| c.to_lowercase().to_string())
.unwrap_or_default(); .unwrap_or_default();
Ok(first + chars.as_str()) Ok(first + chars.as_str())
} }
ParamExp::DefaultUnsetOrNull(default) => { ParamExp::DefaultUnsetOrNull(default) => {
match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) { match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) {
Some(val) => Ok(val), Some(val) => Ok(val),

View File

@@ -598,26 +598,29 @@ impl Job {
.map(|chld| chld.stat()) .map(|chld| chld.stat())
.collect::<Vec<WtStat>>() .collect::<Vec<WtStat>>()
} }
pub fn pipe_status(stats: &[WtStat]) -> Option<Vec<i32>> { pub fn pipe_status(stats: &[WtStat]) -> Option<Vec<i32>> {
if stats.iter() if stats.iter().any(|stat| {
.any(|stat| matches!(stat, WtStat::StillAlive | WtStat::Continued(_) | WtStat::PtraceSyscall(_))) matches!(
|| stats.len() <= 1 { stat,
return None; WtStat::StillAlive | WtStat::Continued(_) | WtStat::PtraceSyscall(_)
} )
Some(stats.iter() }) || stats.len() <= 1
.map(|stat| { {
match stat { return None;
WtStat::Exited(_, code) => *code, }
WtStat::Signaled(_, signal, _) => SIG_EXIT_OFFSET + *signal as i32, Some(
WtStat::Stopped(_, signal) => SIG_EXIT_OFFSET + *signal as i32, stats
WtStat::PtraceEvent(_, signal, _) => SIG_EXIT_OFFSET + *signal as i32, .iter()
WtStat::PtraceSyscall(_) | .map(|stat| match stat {
WtStat::Continued(_) | WtStat::Exited(_, code) => *code,
WtStat::StillAlive => unreachable!() WtStat::Signaled(_, signal, _) => SIG_EXIT_OFFSET + *signal as i32,
} WtStat::Stopped(_, signal) => SIG_EXIT_OFFSET + *signal as i32,
}) WtStat::PtraceEvent(_, signal, _) => SIG_EXIT_OFFSET + *signal as i32,
.collect()) WtStat::PtraceSyscall(_) | WtStat::Continued(_) | WtStat::StillAlive => unreachable!(),
} })
.collect(),
)
}
pub fn get_pids(&self) -> Vec<Pid> { pub fn get_pids(&self) -> Vec<Pid> {
self self
.children .children
@@ -877,14 +880,14 @@ pub fn wait_fg(job: Job, interactive: bool) -> ShResult<()> {
_ => { /* Do nothing */ } _ => { /* Do nothing */ }
} }
} }
if let Some(pipe_status) = Job::pipe_status(&statuses) { if let Some(pipe_status) = Job::pipe_status(&statuses) {
let pipe_status = pipe_status let pipe_status = pipe_status
.into_iter() .into_iter()
.map(|s| s.to_string()) .map(|s| s.to_string())
.collect::<VecDeque<String>>(); .collect::<VecDeque<String>>();
write_vars(|v| v.set_var("PIPESTATUS", VarKind::Arr(pipe_status), VarFlags::NONE))?; write_vars(|v| v.set_var("PIPESTATUS", VarKind::Arr(pipe_status), VarFlags::NONE))?;
} }
// If job wasn't stopped (moved to bg), clear the fg slot // If job wasn't stopped (moved to bg), clear the fg slot
if !was_stopped { if !was_stopped {
write_jobs(|j| { write_jobs(|j| {

View File

@@ -40,7 +40,8 @@ use crate::readline::term::{LineWriter, RawModeGuard, raw_mode};
use crate::readline::{Prompt, ReadlineEvent, ShedVi}; use crate::readline::{Prompt, ReadlineEvent, ShedVi};
use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending}; use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending};
use crate::state::{ use crate::state::{
AutoCmdKind, read_logic, read_shopts, source_env, source_login, source_rc, write_jobs, write_meta, write_shopts AutoCmdKind, read_logic, read_shopts, source_env, source_login, source_rc, write_jobs,
write_meta, write_shopts,
}; };
use clap::Parser; use clap::Parser;
use state::write_vars; use state::write_vars;
@@ -59,8 +60,8 @@ struct ShedArgs {
#[arg(short)] #[arg(short)]
interactive: bool, interactive: bool,
#[arg(short)] #[arg(short)]
stdin: bool, stdin: bool,
#[arg(long, short)] #[arg(long, short)]
login_shell: bool, login_shell: bool,
@@ -128,16 +129,16 @@ fn main() -> ExitCode {
unsafe { env::set_var("SHLVL", "1") }; unsafe { env::set_var("SHLVL", "1") };
} }
if let Err(e) = source_env() { if let Err(e) = source_env() {
e.print_error(); e.print_error();
} }
if let Err(e) = if let Some(cmd) = args.command { if let Err(e) = if let Some(cmd) = args.command {
exec_dash_c(cmd) exec_dash_c(cmd)
} else if args.stdin || !isatty(STDIN_FILENO).unwrap_or(false) { } else if args.stdin || !isatty(STDIN_FILENO).unwrap_or(false) {
read_commands(args.script_args) read_commands(args.script_args)
} else if !args.script_args.is_empty() { } else if !args.script_args.is_empty() {
let path = args.script_args.remove(0); let path = args.script_args.remove(0);
run_script(path, args.script_args) run_script(path, args.script_args)
} else { } else {
let res = shed_interactive(args); let res = shed_interactive(args);
@@ -161,29 +162,29 @@ fn main() -> ExitCode {
} }
fn read_commands(args: Vec<String>) -> ShResult<()> { fn read_commands(args: Vec<String>) -> ShResult<()> {
let mut input = vec![]; let mut input = vec![];
let mut read_buf = [0u8;4096]; let mut read_buf = [0u8; 4096];
loop { loop {
match read(STDIN_FILENO, &mut read_buf) { match read(STDIN_FILENO, &mut read_buf) {
Ok(0) => break, Ok(0) => break,
Ok(n) => input.extend_from_slice(&read_buf[..n]), Ok(n) => input.extend_from_slice(&read_buf[..n]),
Err(Errno::EINTR) => continue, Err(Errno::EINTR) => continue,
Err(e) => { Err(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),
format!("error reading from stdin: {e}"), format!("error reading from stdin: {e}"),
)); ));
} }
} }
} }
let commands = String::from_utf8_lossy(&input).to_string(); let commands = String::from_utf8_lossy(&input).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(commands, None, false, None) exec_input(commands, None, false, None)
} }
fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> { fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> {
@@ -221,10 +222,11 @@ fn shed_interactive(args: ShedArgs) -> 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(args.login_shell); sig_setup(args.login_shell);
if args.login_shell if args.login_shell
&& let Err(e) = source_login() { && let Err(e) = source_login()
e.print_error(); {
} e.print_error();
}
if let Err(e) = source_rc() { if let Err(e) = source_rc() {
e.print_error(); e.print_error();

View File

@@ -8,7 +8,30 @@ use ariadne::Fmt;
use crate::{ use crate::{
builtin::{ builtin::{
alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, autocmd::autocmd, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, getopts::getopts, help::help, intro, jobctl::{self, JobBehavior, continue_job, disown, jobs}, keymap, map, pwd::pwd, read::{self, read_builtin}, resource::{ulimit, umask_builtin}, seek::seek, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset} alias::{alias, unalias},
arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate},
autocmd::autocmd,
cd::cd,
complete::{compgen_builtin, complete_builtin},
dirstack::{dirs, popd, pushd},
echo::echo,
eval, exec,
flowctl::flowctl,
getopts::getopts,
help::help,
intro,
jobctl::{self, JobBehavior, continue_job, disown, jobs},
keymap, map,
pwd::pwd,
read::{self, read_builtin},
resource::{ulimit, umask_builtin},
seek::seek,
shift::shift,
shopt::shopt,
source::source,
test::double_bracket_test,
trap::{TrapTarget, trap},
varcmds::{export, local, readonly, unset},
}, },
expand::{expand_aliases, expand_case_pattern, glob_to_regex}, expand::{expand_aliases, expand_case_pattern, glob_to_regex},
jobs::{ChildProc, JobStack, attach_tty, dispatch_job}, jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
@@ -995,7 +1018,7 @@ impl Dispatcher {
"ulimit" => ulimit(cmd), "ulimit" => ulimit(cmd),
"umask" => umask_builtin(cmd), "umask" => umask_builtin(cmd),
"seek" => seek(cmd), "seek" => seek(cmd),
"help" => help(cmd), "help" => help(cmd),
"true" | ":" => { "true" | ":" => {
state::set_status(0); state::set_status(0);
Ok(()) Ok(())

View File

@@ -1084,12 +1084,10 @@ impl ParseStream {
} }
} }
if !from_func_def { if !from_func_def {
self.parse_redir(&mut redirs, &mut node_tks)?; self.parse_redir(&mut redirs, &mut node_tks)?;
} }
let node = Node { let node = Node {
class: NdRule::BraceGrp { body }, class: NdRule::BraceGrp { body },
flags: NdFlags::empty(), flags: NdFlags::empty(),

View File

@@ -115,11 +115,14 @@ impl IoMode {
pub fn buffer(tgt_fd: RawFd, buf: String, flags: TkFlags) -> ShResult<Self> { pub fn buffer(tgt_fd: RawFd, buf: String, flags: TkFlags) -> ShResult<Self> {
Ok(Self::Buffer { tgt_fd, buf, flags }) Ok(Self::Buffer { tgt_fd, buf, flags })
} }
pub fn loaded_pipe(tgt_fd: RawFd, buf: &[u8]) -> ShResult<Self> { pub fn loaded_pipe(tgt_fd: RawFd, buf: &[u8]) -> ShResult<Self> {
let (rpipe, wpipe) = nix::unistd::pipe()?; let (rpipe, wpipe) = nix::unistd::pipe()?;
write(wpipe, buf)?; write(wpipe, buf)?;
Ok(Self::Pipe { tgt_fd, pipe: rpipe.into() }) Ok(Self::Pipe {
} tgt_fd,
pipe: rpipe.into(),
})
}
pub fn get_pipes() -> (Self, Self) { pub fn get_pipes() -> (Self, Self) {
let (rpipe, wpipe) = nix::unistd::pipe2(OFlag::O_CLOEXEC).unwrap(); let (rpipe, wpipe) = nix::unistd::pipe2(OFlag::O_CLOEXEC).unwrap();
( (

View File

@@ -203,7 +203,7 @@ fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
.collect() .collect()
} }
#[derive(Default,Clone,Debug)] #[derive(Default, Clone, Debug)]
pub struct History { pub struct History {
path: PathBuf, path: PathBuf,
pub pending: Option<LineBuf>, // command, cursor_pos pub pending: Option<LineBuf>, // command, cursor_pos
@@ -215,7 +215,7 @@ pub struct History {
//search_direction: Direction, //search_direction: Direction,
ignore_dups: bool, ignore_dups: bool,
max_size: Option<u32>, max_size: Option<u32>,
stateless: bool stateless: bool,
} }
impl History { impl History {
@@ -231,7 +231,7 @@ impl History {
//search_direction: Direction::Backward, //search_direction: Direction::Backward,
ignore_dups: false, ignore_dups: false,
max_size: None, max_size: None,
stateless: true, stateless: true,
} }
} }
pub fn new() -> ShResult<Self> { pub fn new() -> ShResult<Self> {
@@ -269,7 +269,7 @@ impl History {
//search_direction: Direction::Backward, //search_direction: Direction::Backward,
ignore_dups, ignore_dups,
max_size, max_size,
stateless: false, stateless: false,
}) })
} }
@@ -454,9 +454,9 @@ impl History {
} }
pub fn save(&mut self) -> ShResult<()> { pub fn save(&mut self) -> ShResult<()> {
if self.stateless { if self.stateless {
return Ok(()); return Ok(());
} }
let mut file = OpenOptions::new() let mut file = OpenOptions::new()
.create(true) .create(true)
.append(true) .append(true)

View File

@@ -12,6 +12,7 @@ use super::vicmd::{
ViCmd, Word, ViCmd, Word,
}; };
use crate::{ use crate::{
expand::expand_cmd_sub,
libsh::{error::ShResult, guards::var_ctx_guard}, libsh::{error::ShResult, guards::var_ctx_guard},
parse::{ parse::{
execute::exec_input, execute::exec_input,
@@ -23,6 +24,7 @@ use crate::{
markers, markers,
register::{RegisterContent, write_register}, register::{RegisterContent, write_register},
term::RawModeGuard, term::RawModeGuard,
vicmd::ReadSrc,
}, },
state::{VarFlags, VarKind, read_shopts, write_meta, write_vars}, state::{VarFlags, VarKind, read_shopts, write_meta, write_vars},
}; };
@@ -441,33 +443,30 @@ pub struct LineBuf {
} }
impl Default for LineBuf { impl Default for LineBuf {
fn default() -> Self { fn default() -> Self {
Self { Self {
buffer: String::new(), buffer: String::new(),
hint: None, hint: None,
grapheme_indices: Some(vec![]), grapheme_indices: Some(vec![]),
cursor: ClampedUsize::new(0, 0, false), cursor: ClampedUsize::new(0, 0, false),
select_mode: None, select_mode: None,
select_range: None, select_range: None,
last_selection: None, last_selection: None,
insert_mode_start_pos: None, insert_mode_start_pos: None,
saved_col: None, saved_col: None,
indent_ctx: IndentCtx::new(), indent_ctx: IndentCtx::new(),
undo_stack: vec![], undo_stack: vec![],
redo_stack: vec![], redo_stack: vec![],
} }
} }
} }
impl LineBuf { impl LineBuf {
pub fn new() -> Self { pub fn new() -> Self {
let mut new = Self { let mut new = Self::default();
grapheme_indices: Some(vec![]), // We know the buffer is empty, so this keeps us safe from unwrapping None
..Default::default()
};
new.update_graphemes(); new.update_graphemes();
new new
} }
@@ -3329,12 +3328,39 @@ impl LineBuf {
| Verb::CompleteBackward | Verb::CompleteBackward
| Verb::VisualModeSelectLast => self.apply_motion(motion), | Verb::VisualModeSelectLast => self.apply_motion(motion),
Verb::ShellCmd(cmd) => self.verb_shell_cmd(cmd)?, Verb::ShellCmd(cmd) => self.verb_shell_cmd(cmd)?,
Verb::Normal(_) Verb::Read(src) => match src {
| Verb::Read(_) ReadSrc::File(path_buf) => {
| Verb::Write(_) if !path_buf.is_file() {
| Verb::Substitute(..) write_meta(|m| m.post_system_message(format!("{} is not a file", path_buf.display())));
| Verb::RepeatSubstitute return Ok(());
| Verb::RepeatGlobal => {} }
let Ok(contents) = std::fs::read_to_string(&path_buf) else {
write_meta(|m| {
m.post_system_message(format!("Failed to read file {}", path_buf.display()))
});
return Ok(());
};
let grapheme_count = contents.graphemes(true).count();
self.insert_str_at_cursor(&contents);
self.cursor.add(grapheme_count);
}
ReadSrc::Cmd(cmd) => {
let output = match expand_cmd_sub(&cmd) {
Ok(out) => out,
Err(e) => {
e.print_error();
return Ok(());
}
};
let grapheme_count = output.graphemes(true).count();
self.insert_str_at_cursor(&output);
self.cursor.add(grapheme_count);
}
},
Verb::Write(dest) => {}
Verb::Edit(path) => {}
Verb::Normal(_) | Verb::Substitute(..) | Verb::RepeatSubstitute | Verb::RepeatGlobal => {}
} }
Ok(()) Ok(())
} }

View File

@@ -133,17 +133,17 @@ pub mod markers {
('\u{e000}'..'\u{efff}').contains(&c) ('\u{e000}'..'\u{efff}').contains(&c)
} }
// Help command formatting markers // Help command formatting markers
pub const TAG: Marker = '\u{e180}'; pub const TAG: Marker = '\u{e180}';
pub const REFERENCE: Marker = '\u{e181}'; pub const REFERENCE: Marker = '\u{e181}';
pub const HEADER: Marker = '\u{e182}'; pub const HEADER: Marker = '\u{e182}';
pub const CODE: Marker = '\u{e183}'; pub const CODE: Marker = '\u{e183}';
/// angle brackets /// angle brackets
pub const KEYWORD_1: Marker = '\u{e184}'; pub const KEYWORD_1: Marker = '\u{e184}';
/// curly brackets /// curly brackets
pub const KEYWORD_2: Marker = '\u{e185}'; pub const KEYWORD_2: Marker = '\u{e185}';
/// square brackets /// square brackets
pub const KEYWORD_3: Marker = '\u{e186}'; pub const KEYWORD_3: Marker = '\u{e186}';
} }
type Marker = char; type Marker = char;
@@ -268,7 +268,7 @@ pub struct ShedVi {
pub old_layout: Option<Layout>, pub old_layout: Option<Layout>,
pub history: History, pub history: History,
pub ex_history: History, pub ex_history: History,
pub needs_redraw: bool, pub needs_redraw: bool,
} }
@@ -290,7 +290,7 @@ impl ShedVi {
repeat_motion: None, repeat_motion: None,
editor: LineBuf::new(), editor: LineBuf::new(),
history: History::new()?, history: History::new()?,
ex_history: History::empty(), ex_history: History::empty(),
needs_redraw: true, needs_redraw: true,
}; };
write_vars(|v| { write_vars(|v| {
@@ -322,7 +322,7 @@ impl ShedVi {
repeat_motion: None, repeat_motion: None,
editor: LineBuf::new(), editor: LineBuf::new(),
history: History::empty(), history: History::empty(),
ex_history: History::empty(), ex_history: History::empty(),
needs_redraw: true, needs_redraw: true,
}; };
write_vars(|v| { write_vars(|v| {
@@ -861,16 +861,16 @@ impl ShedVi {
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit()); let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit());
let is_shell_cmd = cmd.verb().is_some_and(|v| matches!(v.1, Verb::ShellCmd(_))); let is_shell_cmd = cmd.verb().is_some_and(|v| matches!(v.1, Verb::ShellCmd(_)));
let is_ex_cmd = cmd.flags.contains(CmdFlags::IS_EX_CMD); let is_ex_cmd = cmd.flags.contains(CmdFlags::IS_EX_CMD);
log::debug!("is_ex_cmd: {is_ex_cmd}"); log::debug!("is_ex_cmd: {is_ex_cmd}");
if is_shell_cmd { if is_shell_cmd {
self.old_layout = None; self.old_layout = None;
} }
if is_ex_cmd { if is_ex_cmd {
self.ex_history.push(cmd.raw_seq.clone()); self.ex_history.push(cmd.raw_seq.clone());
self.ex_history.reset(); self.ex_history.reset();
log::debug!("ex_history: {:?}", self.ex_history.entries()); log::debug!("ex_history: {:?}", self.ex_history.entries());
} }
let before = self.editor.buffer.clone(); let before = self.editor.buffer.clone();

View File

@@ -1,5 +1,9 @@
use std::path::PathBuf;
use bitflags::bitflags; use bitflags::bitflags;
use crate::readline::vimode::ex::SubFlags;
use super::register::{RegisterContent, append_register, read_register, write_register}; use super::register::{RegisterContent, append_register, read_register, write_register};
//TODO: write tests that take edit results and cursor positions from actual //TODO: write tests that take edit results and cursor positions from actual
@@ -64,7 +68,7 @@ bitflags! {
const VISUAL_LINE = 1<<1; const VISUAL_LINE = 1<<1;
const VISUAL_BLOCK = 1<<2; const VISUAL_BLOCK = 1<<2;
const EXIT_CUR_MODE = 1<<3; const EXIT_CUR_MODE = 1<<3;
const IS_EX_CMD = 1<<4; const IS_EX_CMD = 1<<4;
} }
} }
@@ -256,7 +260,8 @@ pub enum Verb {
Normal(String), Normal(String),
Read(ReadSrc), Read(ReadSrc),
Write(WriteDest), Write(WriteDest),
Substitute(String, String, super::vimode::ex::SubFlags), Edit(PathBuf),
Substitute(String, String, SubFlags),
RepeatSubstitute, RepeatSubstitute,
RepeatGlobal, RepeatGlobal,
} }

View File

@@ -5,6 +5,7 @@ use std::str::Chars;
use itertools::Itertools; use itertools::Itertools;
use crate::bitflags; use crate::bitflags;
use crate::expand::{Expander, expand_raw};
use crate::libsh::error::{ShErr, ShErrKind, ShResult}; use crate::libsh::error::{ShErr, ShErrKind, ShResult};
use crate::readline::history::History; use crate::readline::history::History;
use crate::readline::keys::KeyEvent; use crate::readline::keys::KeyEvent;
@@ -14,7 +15,7 @@ use crate::readline::vicmd::{
WriteDest, WriteDest,
}; };
use crate::readline::vimode::{ModeReport, ViInsert, ViMode}; use crate::readline::vimode::{ModeReport, ViInsert, ViMode};
use crate::state::write_meta; use crate::state::{get_home, write_meta};
bitflags! { bitflags! {
#[derive(Debug,Clone,Copy,PartialEq,Eq)] #[derive(Debug,Clone,Copy,PartialEq,Eq)]
@@ -34,18 +35,18 @@ bitflags! {
struct ExEditor { struct ExEditor {
buf: LineBuf, buf: LineBuf,
mode: ViInsert, mode: ViInsert,
history: History history: History,
} }
impl ExEditor { impl ExEditor {
pub fn new(history: History) -> Self { pub fn new(history: History) -> Self {
let mut new = Self { let mut new = Self {
history, history,
..Default::default() ..Default::default()
}; };
new.buf.update_graphemes(); new.buf.update_graphemes();
new new
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
*self = Self::default() *self = Self::default()
} }
@@ -85,13 +86,13 @@ impl ExEditor {
let Some(mut cmd) = self.mode.handle_key(key) else { let Some(mut cmd) = self.mode.handle_key(key) else {
return Ok(()); return Ok(());
}; };
cmd.alter_line_motion_if_no_verb(); cmd.alter_line_motion_if_no_verb();
log::debug!("ExEditor got cmd: {:?}", cmd); log::debug!("ExEditor got cmd: {:?}", cmd);
if self.should_grab_history(&cmd) { if self.should_grab_history(&cmd) {
log::debug!("Grabbing history for cmd: {:?}", cmd); log::debug!("Grabbing history for cmd: {:?}", cmd);
self.scroll_history(cmd); self.scroll_history(cmd);
return Ok(()) return Ok(());
} }
self.buf.exec_cmd(cmd) self.buf.exec_cmd(cmd)
} }
} }
@@ -103,7 +104,9 @@ pub struct ViEx {
impl ViEx { impl ViEx {
pub fn new(history: History) -> Self { pub fn new(history: History) -> Self {
Self { pending_cmd: ExEditor::new(history) } Self {
pending_cmd: ExEditor::new(history),
}
} }
} }
@@ -115,9 +118,7 @@ impl ViMode for ViEx {
E(C::Char('\r'), M::NONE) | E(C::Enter, M::NONE) => { E(C::Char('\r'), M::NONE) | E(C::Enter, M::NONE) => {
let input = self.pending_cmd.buf.as_str(); let input = self.pending_cmd.buf.as_str();
match parse_ex_cmd(input) { match parse_ex_cmd(input) {
Ok(cmd) => { Ok(cmd) => Ok(cmd),
Ok(cmd)
}
Err(e) => { Err(e) => {
let msg = e.unwrap_or(format!("Not an editor command: {}", input)); let msg = e.unwrap_or(format!("Not an editor command: {}", input));
write_meta(|m| m.post_system_message(msg.clone())); write_meta(|m| m.post_system_message(msg.clone()));
@@ -129,18 +130,14 @@ impl ViMode for ViEx {
self.pending_cmd.clear(); self.pending_cmd.clear();
Ok(None) Ok(None)
} }
E(C::Esc, M::NONE) => { E(C::Esc, M::NONE) => Ok(Some(ViCmd {
Ok(Some(ViCmd { register: RegisterName::default(),
register: RegisterName::default(), verb: Some(VerbCmd(1, Verb::NormalMode)),
verb: Some(VerbCmd(1, Verb::NormalMode)), motion: None,
motion: None, flags: CmdFlags::empty(),
flags: CmdFlags::empty(), raw_seq: "".into(),
raw_seq: "".into(), })),
})) _ => self.pending_cmd.handle_key(key).map(|_| None),
}
_ => {
self.pending_cmd.handle_key(key).map(|_| None)
}
} }
} }
fn handle_key(&mut self, key: KeyEvent) -> Option<ViCmd> { fn handle_key(&mut self, key: KeyEvent) -> Option<ViCmd> {
@@ -248,7 +245,7 @@ fn parse_ex_command(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Opt
let mut cmd_name = String::new(); let mut cmd_name = String::new();
while let Some(ch) = chars.peek() { while let Some(ch) = chars.peek() {
if ch == &'!' { if cmd_name.is_empty() && ch == &'!' {
cmd_name.push(*ch); cmd_name.push(*ch);
chars.next(); chars.next();
break; break;
@@ -265,16 +262,17 @@ fn parse_ex_command(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Opt
let cmd = unescape_shell_cmd(&cmd); let cmd = unescape_shell_cmd(&cmd);
Ok(Some(Verb::ShellCmd(cmd))) Ok(Some(Verb::ShellCmd(cmd)))
} }
_ if "help".starts_with(&cmd_name) => { _ if "help".starts_with(&cmd_name) => {
let cmd = "help ".to_string() + chars.collect::<String>().trim(); let cmd = "help ".to_string() + chars.collect::<String>().trim();
Ok(Some(Verb::ShellCmd(cmd))) Ok(Some(Verb::ShellCmd(cmd)))
} }
"normal!" => parse_normal(chars), "normal!" => parse_normal(chars),
_ if "delete".starts_with(&cmd_name) => Ok(Some(Verb::Delete)), _ if "delete".starts_with(&cmd_name) => Ok(Some(Verb::Delete)),
_ if "yank".starts_with(&cmd_name) => Ok(Some(Verb::Yank)), _ if "yank".starts_with(&cmd_name) => Ok(Some(Verb::Yank)),
_ if "put".starts_with(&cmd_name) => Ok(Some(Verb::Put(Anchor::After))), _ if "put".starts_with(&cmd_name) => Ok(Some(Verb::Put(Anchor::After))),
_ if "read".starts_with(&cmd_name) => parse_read(chars), _ if "read".starts_with(&cmd_name) => parse_read(chars),
_ if "write".starts_with(&cmd_name) => parse_write(chars), _ if "write".starts_with(&cmd_name) => parse_write(chars),
_ if "edit".starts_with(&cmd_name) => parse_edit(chars),
_ if "substitute".starts_with(&cmd_name) => parse_substitute(chars), _ if "substitute".starts_with(&cmd_name) => parse_substitute(chars),
_ => Err(None), _ => Err(None),
} }
@@ -289,6 +287,19 @@ fn parse_normal(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<
Ok(Some(Verb::Normal(seq))) Ok(Some(Verb::Normal(seq)))
} }
fn parse_edit(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> {
chars
.peeking_take_while(|c| c.is_whitespace())
.for_each(drop);
let arg: String = chars.collect();
if arg.trim().is_empty() {
return Err(Some("Expected file path after ':edit'".into()));
}
let arg_path = get_path(arg.trim())?;
Ok(Some(Verb::Edit(arg_path)))
}
fn parse_read(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> { fn parse_read(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> {
chars chars
.peeking_take_while(|c| c.is_whitespace()) .peeking_take_while(|c| c.is_whitespace())
@@ -311,23 +322,15 @@ fn parse_read(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<St
if is_shell_read { if is_shell_read {
Ok(Some(Verb::Read(ReadSrc::Cmd(arg)))) Ok(Some(Verb::Read(ReadSrc::Cmd(arg))))
} else { } else {
let arg_path = get_path(arg.trim()); let arg_path = get_path(arg.trim())?;
Ok(Some(Verb::Read(ReadSrc::File(arg_path)))) Ok(Some(Verb::Read(ReadSrc::File(arg_path))))
} }
} }
fn get_path(path: &str) -> PathBuf { fn get_path(path: &str) -> Result<PathBuf, Option<String>> {
if let Some(stripped) = path.strip_prefix("~/") let expanded = expand_raw(&mut path.chars().peekable())
&& let Some(home) = std::env::var_os("HOME") .map_err(|e| Some(format!("Error expanding path: {}", e)))?;
{ Ok(PathBuf::from(&expanded))
return PathBuf::from(home).join(stripped);
}
if path == "~"
&& let Some(home) = std::env::var_os("HOME")
{
return PathBuf::from(home);
}
PathBuf::from(path)
} }
fn parse_write(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> { fn parse_write(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> {
@@ -350,7 +353,7 @@ fn parse_write(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<S
} }
let arg: String = chars.collect(); let arg: String = chars.collect();
let arg_path = get_path(arg.trim()); let arg_path = get_path(arg.trim())?;
let dest = if is_file_append { let dest = if is_file_append {
WriteDest::FileAppend(arg_path) WriteDest::FileAppend(arg_path)

View File

@@ -1,4 +1,7 @@
use std::{collections::VecDeque, sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering}}; use std::{
collections::VecDeque,
sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering},
};
use nix::sys::signal::{SaFlags, SigAction, sigaction}; use nix::sys::signal::{SaFlags, SigAction, sigaction};
@@ -8,7 +11,10 @@ use crate::{
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::execute::exec_input, parse::execute::exec_input,
prelude::*, prelude::*,
state::{AutoCmd, AutoCmdKind, VarFlags, VarKind, read_jobs, read_logic, write_jobs, write_meta, write_vars}, state::{
AutoCmd, AutoCmdKind, VarFlags, VarKind, read_jobs, read_logic, write_jobs, write_meta,
write_vars,
},
}; };
static SIGNALS: AtomicU64 = AtomicU64::new(0); static SIGNALS: AtomicU64 = AtomicU64::new(0);
@@ -316,16 +322,16 @@ pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> {
let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned()); let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned());
if let Some(job) = result { if let Some(job) = result {
let job_complete_msg = job.display(&job_order, JobCmdFlags::PIDS).to_string(); let job_complete_msg = job.display(&job_order, JobCmdFlags::PIDS).to_string();
let statuses = job.get_stats(); let statuses = job.get_stats();
if let Some(pipe_status) = Job::pipe_status(&statuses) { if let Some(pipe_status) = Job::pipe_status(&statuses) {
let pipe_status = pipe_status let pipe_status = pipe_status
.into_iter() .into_iter()
.map(|s| s.to_string()) .map(|s| s.to_string())
.collect::<VecDeque<String>>(); .collect::<VecDeque<String>>();
write_vars(|v| v.set_var("PIPESTATUS", VarKind::Arr(pipe_status), VarFlags::NONE))?; write_vars(|v| v.set_var("PIPESTATUS", VarKind::Arr(pipe_status), VarFlags::NONE))?;
} }
let post_job_hooks = read_logic(|l| l.get_autocmds(AutoCmdKind::OnJobFinish)); let post_job_hooks = read_logic(|l| l.get_autocmds(AutoCmdKind::OnJobFinish));
for cmd in post_job_hooks { for cmd in post_job_hooks {

View File

@@ -1098,7 +1098,7 @@ impl VarTab {
.map(|hname| hname.to_string_lossy().to_string()) .map(|hname| hname.to_string_lossy().to_string())
.unwrap_or_default(); .unwrap_or_default();
let help_paths = format!("/usr/share/shed/doc:{home}/.local/share/shed/doc"); let help_paths = format!("/usr/share/shed/doc:{home}/.local/share/shed/doc");
unsafe { unsafe {
env::set_var("IFS", " \t\n"); env::set_var("IFS", " \t\n");
@@ -1116,7 +1116,7 @@ impl VarTab {
env::set_var("SHELL", pathbuf_to_string(std::env::current_exe())); env::set_var("SHELL", pathbuf_to_string(std::env::current_exe()));
env::set_var("SHED_HIST", format!("{}/.shedhist", home)); env::set_var("SHED_HIST", format!("{}/.shedhist", home));
env::set_var("SHED_RC", format!("{}/.shedrc", home)); env::set_var("SHED_RC", format!("{}/.shedrc", home));
env::set_var("SHED_HPATH", help_paths); env::set_var("SHED_HPATH", help_paths);
} }
} }
pub fn init_sh_argv(&mut self) { pub fn init_sh_argv(&mut self) {
@@ -1873,36 +1873,41 @@ pub fn set_status(code: i32) {
} }
pub fn source_runtime_file(name: &str, env_var_name: Option<&str>) -> ShResult<()> { pub fn source_runtime_file(name: &str, env_var_name: Option<&str>) -> ShResult<()> {
let etc_path = PathBuf::from(format!("/etc/shed/{name}")); let etc_path = PathBuf::from(format!("/etc/shed/{name}"));
if etc_path.is_file() if etc_path.is_file()
&& let Err(e) = source_file(etc_path) { && let Err(e) = source_file(etc_path)
e.print_error(); {
} e.print_error();
}
let path = if let Some(name) = env_var_name let path = if let Some(name) = env_var_name
&& let Ok(path) = env::var(name) { && let Ok(path) = env::var(name)
{
PathBuf::from(&path) PathBuf::from(&path)
} else if let Some(home) = get_home() { } else if let Some(home) = get_home() {
home.join(format!(".{name}")) home.join(format!(".{name}"))
} else { } else {
return Err(ShErr::simple(ShErrKind::InternalErr, "could not determine home path")); return Err(ShErr::simple(
ShErrKind::InternalErr,
"could not determine home path",
));
}; };
if !path.is_file() { if !path.is_file() {
return Ok(()) return Ok(());
} }
source_file(path) source_file(path)
} }
pub fn source_rc() -> ShResult<()> { pub fn source_rc() -> ShResult<()> {
source_runtime_file("shedrc", Some("SHED_RC")) source_runtime_file("shedrc", Some("SHED_RC"))
} }
pub fn source_login() -> ShResult<()> { pub fn source_login() -> ShResult<()> {
source_runtime_file("shed_profile", Some("SHED_PROFILE")) source_runtime_file("shed_profile", Some("SHED_PROFILE"))
} }
pub fn source_env() -> ShResult<()> { pub fn source_env() -> ShResult<()> {
source_runtime_file("shedenv", Some("SHED_ENV")) source_runtime_file("shedenv", Some("SHED_ENV"))
} }
pub fn source_file(path: PathBuf) -> ShResult<()> { pub fn source_file(path: PathBuf) -> ShResult<()> {
@@ -1917,30 +1922,39 @@ pub fn source_file(path: PathBuf) -> ShResult<()> {
#[track_caller] #[track_caller]
pub fn get_home_unchecked() -> PathBuf { pub fn get_home_unchecked() -> PathBuf {
if let Some(home) = get_home() { if let Some(home) = get_home() {
home home
} else { } else {
let caller = std::panic::Location::caller(); let caller = std::panic::Location::caller();
panic!("get_home_unchecked: could not determine home directory (called from {}:{})", caller.file(), caller.line()) panic!(
} "get_home_unchecked: could not determine home directory (called from {}:{})",
caller.file(),
caller.line()
)
}
} }
#[track_caller] #[track_caller]
pub fn get_home_str_unchecked() -> String { pub fn get_home_str_unchecked() -> String {
if let Some(home) = get_home() { if let Some(home) = get_home() {
home.to_string_lossy().to_string() home.to_string_lossy().to_string()
} else { } else {
let caller = std::panic::Location::caller(); let caller = std::panic::Location::caller();
panic!("get_home_str_unchecked: could not determine home directory (called from {}:{})", caller.file(), caller.line()) panic!(
} "get_home_str_unchecked: could not determine home directory (called from {}:{})",
caller.file(),
caller.line()
)
}
} }
pub fn get_home() -> Option<PathBuf> { pub fn get_home() -> Option<PathBuf> {
env::var("HOME").ok().map(PathBuf::from).or_else(|| { env::var("HOME")
User::from_uid(getuid()).ok().flatten().map(|u| u.dir) .ok()
}) .map(PathBuf::from)
.or_else(|| User::from_uid(getuid()).ok().flatten().map(|u| u.dir))
} }
pub fn get_home_str() -> Option<String> { pub fn get_home_str() -> Option<String> {
get_home().map(|h| h.to_string_lossy().to_string()) get_home().map(|h| h.to_string_lossy().to_string())
} }