From ec9795c7818c64785d52a1f2cf76004878a1002c Mon Sep 17 00:00:00 2001 From: pagedmov Date: Mon, 16 Mar 2026 01:53:49 -0400 Subject: [PATCH] implemented read command for ex mode --- src/builtin/help.rs | 469 +++++++++++++++++++------------------- src/builtin/mod.rs | 2 +- src/expand.rs | 125 +++++----- src/jobs.rs | 57 ++--- src/main.rs | 64 +++--- src/parse/execute.rs | 27 ++- src/parse/mod.rs | 2 - src/procio.rs | 13 +- src/readline/history.rs | 14 +- src/readline/linebuf.rs | 78 ++++--- src/readline/mod.rs | 42 ++-- src/readline/vicmd.rs | 9 +- src/readline/vimode/ex.rs | 107 ++++----- src/signal.rs | 26 ++- src/state.rs | 76 +++--- 15 files changed, 605 insertions(+), 506 deletions(-) diff --git a/src/builtin/help.rs b/src/builtin/help.rs index 9377c07..4bc282d 100644 --- a/src/builtin/help.rs +++ b/src/builtin/help.rs @@ -4,14 +4,25 @@ use ariadne::Span as ASpan; use nix::libc::STDIN_FILENO; 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 REF_SEQ: &str = "\x1b[4;36m"; // underline cyan — cross-references +const TAG_SEQ: &str = "\x1b[1;33m"; // bold yellow — searchable tags +const REF_SEQ: &str = "\x1b[4;36m"; // underline cyan — cross-references const RESET_SEQ: &str = "\x1b[0m"; -const HEADER_SEQ: &str = "\x1b[1;35m"; // bold magenta — section headers -const CODE_SEQ: &str = "\x1b[32m"; // green — inline code +const HEADER_SEQ: &str = "\x1b[1;35m"; // bold magenta — section headers +const CODE_SEQ: &str = "\x1b[32m"; // green — inline code const KEYWORD_2_SEQ: &str = "\x1b[1;32m"; // bold green — {keyword} 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 help = argv.next().unwrap(); // drop 'help' - // Join all of the word-split arguments into a single string - // Preserve the span too - let (topic, span) = if argv.peek().is_none() { - ("help.txt".to_string(), help.1) - } else { - argv.fold((String::new(), Span::default()), |mut acc, arg| { - if acc.1 == Span::default() { - acc.1 = arg.1.clone(); - } else { - let new_end = arg.1.end(); - let start = acc.1.start(); - acc.1.set_range(start..new_end); - } + // Join all of the word-split arguments into a single string + // Preserve the span too + let (topic, span) = if argv.peek().is_none() { + ("help.txt".to_string(), help.1) + } else { + argv.fold((String::new(), Span::default()), |mut acc, arg| { + if acc.1 == Span::default() { + acc.1 = arg.1.clone(); + } else { + let new_end = arg.1.end(); + let start = acc.1.start(); + acc.1.set_range(start..new_end); + } - if acc.0.is_empty() { - acc.0 = arg.0; - } else { - acc.0 = acc.0 + &format!(" {}",arg.0); - } - acc - }) - }; + if acc.0.is_empty() { + acc.0 = arg.0; + } else { + acc.0 = acc.0 + &format!(" {}", arg.0); + } + acc + }) + }; - let hpath = env::var("SHED_HPATH").unwrap_or_default(); + let hpath = env::var("SHED_HPATH").unwrap_or_default(); - for path in hpath.split(':') { - let path = Path::new(&path).join(&topic); - if path.is_file() { - let Ok(contents) = std::fs::read_to_string(&path) else { - continue; - }; - let filename = path.file_stem() - .unwrap() - .to_string_lossy() - .to_string(); + for path in hpath.split(':') { + let path = Path::new(&path).join(&topic); + if path.is_file() { + let Ok(contents) = std::fs::read_to_string(&path) else { + continue; + }; + let filename = path.file_stem().unwrap().to_string_lossy().to_string(); - let unescaped = unescape_help(&contents); - let expanded = expand_help(&unescaped); - open_help(&expanded, None, Some(filename))?; - state::set_status(0); - return Ok(()); - } - } + let unescaped = unescape_help(&contents); + let expanded = expand_help(&unescaped); + open_help(&expanded, None, Some(filename))?; + state::set_status(0); + return Ok(()); + } + } - // didn't find an exact filename match, its probably a tag search - for path in hpath.split(':') { - let path = Path::new(path); - if let Ok(entries) = path.read_dir() { - for entry in entries { - let Ok(entry) = entry else { continue }; - let path = entry.path(); - let filename = path.file_stem() - .unwrap() - .to_string_lossy() - .to_string(); + // didn't find an exact filename match, its probably a tag search + for path in hpath.split(':') { + let path = Path::new(path); + if let Ok(entries) = path.read_dir() { + for entry in entries { + let Ok(entry) = entry else { continue }; + let path = entry.path(); + let filename = path.file_stem().unwrap().to_string_lossy().to_string(); - if !path.is_file() { - continue; - } + if !path.is_file() { + continue; + } - let Ok(contents) = std::fs::read_to_string(&path) else { - continue; - }; + let Ok(contents) = std::fs::read_to_string(&path) else { + continue; + }; - let unescaped = unescape_help(&contents); - let expanded = expand_help(&unescaped); - let tags = read_tags(&expanded); + let unescaped = unescape_help(&contents); + let expanded = expand_help(&unescaped); + 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) { - open_help(&expanded, Some(line), Some(filename))?; - state::set_status(0); - return Ok(()); - } else { - } - } - } - } + if let Some((matched_tag, line)) = get_best_match(&topic, &tags) { + open_help(&expanded, Some(line), Some(filename))?; + state::set_status(0); + return Ok(()); + } else { + } + } + } + } - state::set_status(1); - Err(ShErr::at( - ShErrKind::NotFound, - span, - "No relevant help page found for this topic", - )) + state::set_status(1); + Err(ShErr::at( + ShErrKind::NotFound, + span, + "No relevant help page found for this topic", + )) } pub fn open_help(content: &str, line: Option, file_name: Option) -> ShResult<()> { - let pager = env::var("PAGER").unwrap_or("less -R".into()); - 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 pager = env::var("PAGER").unwrap_or("less -R".into()); + 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 mut tmp = tempfile::NamedTempFile::new()?; - let tmp_path = tmp.path().to_string_lossy().to_string(); - tmp.write_all(content.as_bytes())?; - tmp.flush()?; + let mut tmp = tempfile::NamedTempFile::new()?; + let tmp_path = tmp.path().to_string_lossy().to_string(); + tmp.write_all(content.as_bytes())?; + tmp.flush()?; - RawModeGuard::with_cooked_mode(|| { - exec_input( - format!("{pager} {line_arg} {prompt_arg} {tmp_path}"), - None, - true, - Some("help".into()), - ) - }) + RawModeGuard::with_cooked_mode(|| { + exec_input( + format!("{pager} {line_arg} {prompt_arg} {tmp_path}"), + None, + true, + Some("help".into()), + ) + }) } pub fn get_best_match(topic: &str, tags: &[(String, usize)]) -> Option<(String, usize)> { - let mut candidates: Vec<_> = tags.iter() - .map(|(tag,line)| (ScoredCandidate::new(tag.to_string()), *line)) - .collect(); + let mut candidates: Vec<_> = tags + .iter() + .map(|(tag, line)| (ScoredCandidate::new(tag.to_string()), *line)) + .collect(); - for (cand,_) in candidates.iter_mut() { - cand.fuzzy_score(topic); - } + for (cand, _) in candidates.iter_mut() { + cand.fuzzy_score(topic); + } - candidates.retain(|(c,_)| c.score.unwrap_or(i32::MIN) > i32::MIN); - candidates.sort_by_key(|(c,_)| c.score.unwrap_or(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.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)> { - let mut tags = vec![]; + let mut tags = vec![]; - for (line_num, line) in raw.lines().enumerate() { - let mut rest = line; + for (line_num, line) in raw.lines().enumerate() { + let mut rest = line; - while let Some(pos) = rest.find(TAG_SEQ) { - let after_seq = &rest[pos + TAG_SEQ.len()..]; - if let Some(end) = after_seq.find(RESET_SEQ) { - let tag = &after_seq[..end]; - tags.push((tag.to_string(), line_num + 1)); - rest = &after_seq[end + RESET_SEQ.len()..]; - } else { - break; - } - } - } + while let Some(pos) = rest.find(TAG_SEQ) { + let after_seq = &rest[pos + TAG_SEQ.len()..]; + if let Some(end) = after_seq.find(RESET_SEQ) { + let tag = &after_seq[..end]; + tags.push((tag.to_string(), line_num + 1)); + rest = &after_seq[end + RESET_SEQ.len()..]; + } else { + break; + } + } + } - tags + tags } pub fn expand_help(raw: &str) -> String { - let mut result = String::new(); - let mut chars = raw.chars(); + let mut result = String::new(); + let mut chars = raw.chars(); - while let Some(ch) = chars.next() { - match ch { - markers::RESET => result.push_str(RESET_SEQ), - markers::TAG => result.push_str(TAG_SEQ), - markers::REFERENCE => result.push_str(REF_SEQ), - markers::HEADER => result.push_str(HEADER_SEQ), - markers::CODE => result.push_str(CODE_SEQ), - markers::KEYWORD_2 => result.push_str(KEYWORD_2_SEQ), - markers::KEYWORD_3 => result.push_str(KEYWORD_3_SEQ), - _ => result.push(ch), - } - } - result + while let Some(ch) = chars.next() { + match ch { + markers::RESET => result.push_str(RESET_SEQ), + markers::TAG => result.push_str(TAG_SEQ), + markers::REFERENCE => result.push_str(REF_SEQ), + markers::HEADER => result.push_str(HEADER_SEQ), + markers::CODE => result.push_str(CODE_SEQ), + markers::KEYWORD_2 => result.push_str(KEYWORD_2_SEQ), + markers::KEYWORD_3 => result.push_str(KEYWORD_3_SEQ), + _ => result.push(ch), + } + } + result } pub fn unescape_help(raw: &str) -> String { - let mut result = String::new(); - let mut chars = raw.chars().peekable(); - let mut qt_state = QuoteState::default(); + let mut result = String::new(); + let mut chars = raw.chars().peekable(); + let mut qt_state = QuoteState::default(); - while let Some(ch) = chars.next() { - match ch { - '\\' => { - if let Some(next_ch) = chars.next() { - result.push(next_ch); - } - } - '\n' => { - result.push(ch); - qt_state = QuoteState::default(); - } - '"' => { - result.push(ch); - qt_state.toggle_double(); - } - '\'' => { - result.push(ch); - qt_state.toggle_single(); - } - _ if qt_state.in_quote() || chars.peek().is_none_or(|ch| ch.is_whitespace()) => { - result.push(ch); - } - '*' => { - result.push(markers::TAG); - while let Some(next_ch) = chars.next() { - if next_ch == '*' { - result.push(markers::RESET); - break; - } else { - result.push(next_ch); - } - } - } - '|' => { - result.push(markers::REFERENCE); - while let Some(next_ch) = chars.next() { - if next_ch == '|' { - result.push(markers::RESET); - break; - } else { - result.push(next_ch); - } - } - } - '#' => { - result.push(markers::HEADER); - while let Some(next_ch) = chars.next() { - if next_ch == '#' { - result.push(markers::RESET); - break; - } else { - result.push(next_ch); - } - } - } - '`' => { - result.push(markers::CODE); - while let Some(next_ch) = chars.next() { - if next_ch == '`' { - result.push(markers::RESET); - break; - } else { - result.push(next_ch); - } - } - } - '{' => { - result.push(markers::KEYWORD_2); - while let Some(next_ch) = chars.next() { - if next_ch == '}' { - result.push(markers::RESET); - break; - } else { - result.push(next_ch); - } - } - } - '[' => { - result.push(markers::KEYWORD_3); - while let Some(next_ch) = chars.next() { - if next_ch == ']' { - result.push(markers::RESET); - break; - } else { - result.push(next_ch); - } - } - } - _ => result.push(ch), - } - } - result + while let Some(ch) = chars.next() { + match ch { + '\\' => { + if let Some(next_ch) = chars.next() { + result.push(next_ch); + } + } + '\n' => { + result.push(ch); + qt_state = QuoteState::default(); + } + '"' => { + result.push(ch); + qt_state.toggle_double(); + } + '\'' => { + result.push(ch); + qt_state.toggle_single(); + } + _ if qt_state.in_quote() || chars.peek().is_none_or(|ch| ch.is_whitespace()) => { + result.push(ch); + } + '*' => { + result.push(markers::TAG); + while let Some(next_ch) = chars.next() { + if next_ch == '*' { + result.push(markers::RESET); + break; + } else { + result.push(next_ch); + } + } + } + '|' => { + result.push(markers::REFERENCE); + while let Some(next_ch) = chars.next() { + if next_ch == '|' { + result.push(markers::RESET); + break; + } else { + result.push(next_ch); + } + } + } + '#' => { + result.push(markers::HEADER); + while let Some(next_ch) = chars.next() { + if next_ch == '#' { + result.push(markers::RESET); + break; + } else { + result.push(next_ch); + } + } + } + '`' => { + result.push(markers::CODE); + while let Some(next_ch) = chars.next() { + if next_ch == '`' { + result.push(markers::RESET); + break; + } else { + result.push(next_ch); + } + } + } + '{' => { + result.push(markers::KEYWORD_2); + while let Some(next_ch) = chars.next() { + if next_ch == '}' { + result.push(markers::RESET); + break; + } else { + result.push(next_ch); + } + } + } + '[' => { + result.push(markers::KEYWORD_3); + while let Some(next_ch) = chars.next() { + if next_ch == ']' { + result.push(markers::RESET); + break; + } else { + result.push(next_ch); + } + } + } + _ => result.push(ch), + } + } + result } diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index a17f930..290c92f 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -11,6 +11,7 @@ pub mod eval; pub mod exec; pub mod flowctl; pub mod getopts; +pub mod help; pub mod intro; pub mod jobctl; pub mod keymap; @@ -25,7 +26,6 @@ pub mod source; pub mod test; // [[ ]] thing pub mod trap; pub mod varcmds; -pub mod help; pub const BUILTINS: [&str; 51] = [ "echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg", diff --git a/src/expand.rs b/src/expand.rs index d576f32..d03f7a3 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -474,32 +474,31 @@ pub fn expand_raw(chars: &mut Peekable>) -> ShResult { while let Some(ch) = chars.next() { match ch { markers::TILDE_SUB => { - let mut username = String::new(); - while chars.peek().is_some_and(|ch| *ch != '/') { - let ch = chars.next().unwrap(); - username.push(ch); - } + let mut username = String::new(); + while chars.peek().is_some_and(|ch| *ch != '/') { + let ch = chars.next().unwrap(); + username.push(ch); + } - let home = if username.is_empty() { - // standard '~' expansion - env::var("HOME").unwrap_or_default() - } - else if let Ok(result) = User::from_name(&username) - && let Some(user) = result { - // username expansion like '~user' - user.dir.to_string_lossy().to_string() - } - else if let Ok(id) = username.parse::() - && let Ok(result) = User::from_uid(Uid::from_raw(id)) - && let Some(user) = result { - // uid expansion like '~1000' - // shed only feature btw B) - user.dir.to_string_lossy().to_string() - } - else { - // no match, use literal - format!("~{username}") - }; + let home = if username.is_empty() { + // standard '~' expansion + env::var("HOME").unwrap_or_default() + } else if let Ok(result) = User::from_name(&username) + && let Some(user) = result + { + // username expansion like '~user' + user.dir.to_string_lossy().to_string() + } else if let Ok(id) = username.parse::() + && let Ok(result) = User::from_uid(Uid::from_raw(id)) + && let Some(user) = result + { + // uid expansion like '~1000' + // shed only feature btw B) + user.dir.to_string_lossy().to_string() + } else { + // no match, use literal + format!("~{username}") + }; result.push_str(&home); } @@ -1584,10 +1583,10 @@ pub fn unescape_math(raw: &str) -> String { #[derive(Debug)] pub enum ParamExp { Len, // #var_name - ToUpperFirst, // ^var_name - ToUpperAll, // ^^var_name - ToLowerFirst, // ,var_name - ToLowerAll, // ,,var_name + ToUpperFirst, // ^var_name + ToUpperAll, // ^^var_name + ToLowerFirst, // ,var_name + ToLowerAll, // ,,var_name DefaultUnsetOrNull(String), // :- DefaultUnset(String), // - SetDefaultUnsetOrNull(String), // := @@ -1623,10 +1622,18 @@ impl FromStr for ParamExp { )) }; - if s == "^^" { return Ok(ToUpperAll) } - if s == "^" { return Ok(ToUpperFirst) } - if s == ",," { return Ok(ToLowerAll) } - if s == "," { return Ok(ToLowerFirst) } + if s == "^^" { + return Ok(ToUpperAll); + } + if s == "^" { + return Ok(ToUpperFirst); + } + if s == ",," { + return Ok(ToLowerAll); + } + if s == "," { + return Ok(ToLowerFirst); + } // Handle indirect var expansion: ${!var} if let Some(var) = s.strip_prefix('!') { @@ -1745,32 +1752,32 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { if let Ok(expansion) = rest.parse::() { match expansion { ParamExp::Len => unreachable!(), - ParamExp::ToUpperAll => { - let value = vars.get_var(&var_name); - Ok(value.to_uppercase()) - } - ParamExp::ToUpperFirst => { - let value = vars.get_var(&var_name); - let mut chars = value.chars(); - let first = chars.next() - .map(|c| c.to_uppercase() - .to_string()) - .unwrap_or_default(); - Ok(first + chars.as_str()) - - } - ParamExp::ToLowerAll => { - let value = vars.get_var(&var_name); - Ok(value.to_lowercase()) - } - ParamExp::ToLowerFirst => { - let value = vars.get_var(&var_name); - let mut chars = value.chars(); - let first = chars.next() - .map(|c| c.to_lowercase().to_string()) - .unwrap_or_default(); - Ok(first + chars.as_str()) - } + ParamExp::ToUpperAll => { + let value = vars.get_var(&var_name); + Ok(value.to_uppercase()) + } + ParamExp::ToUpperFirst => { + let value = vars.get_var(&var_name); + let mut chars = value.chars(); + let first = chars + .next() + .map(|c| c.to_uppercase().to_string()) + .unwrap_or_default(); + Ok(first + chars.as_str()) + } + ParamExp::ToLowerAll => { + let value = vars.get_var(&var_name); + Ok(value.to_lowercase()) + } + ParamExp::ToLowerFirst => { + let value = vars.get_var(&var_name); + let mut chars = value.chars(); + let first = chars + .next() + .map(|c| c.to_lowercase().to_string()) + .unwrap_or_default(); + Ok(first + chars.as_str()) + } ParamExp::DefaultUnsetOrNull(default) => { match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) { Some(val) => Ok(val), diff --git a/src/jobs.rs b/src/jobs.rs index 962a60e..cca4252 100644 --- a/src/jobs.rs +++ b/src/jobs.rs @@ -598,26 +598,29 @@ impl Job { .map(|chld| chld.stat()) .collect::>() } - pub fn pipe_status(stats: &[WtStat]) -> Option> { - if stats.iter() - .any(|stat| matches!(stat, WtStat::StillAlive | WtStat::Continued(_) | WtStat::PtraceSyscall(_))) - || stats.len() <= 1 { - return None; - } - Some(stats.iter() - .map(|stat| { - match stat { - WtStat::Exited(_, code) => *code, - 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, - WtStat::PtraceSyscall(_) | - WtStat::Continued(_) | - WtStat::StillAlive => unreachable!() - } - }) - .collect()) - } + pub fn pipe_status(stats: &[WtStat]) -> Option> { + if stats.iter().any(|stat| { + matches!( + stat, + WtStat::StillAlive | WtStat::Continued(_) | WtStat::PtraceSyscall(_) + ) + }) || stats.len() <= 1 + { + return None; + } + Some( + stats + .iter() + .map(|stat| match stat { + WtStat::Exited(_, code) => *code, + 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, + WtStat::PtraceSyscall(_) | WtStat::Continued(_) | WtStat::StillAlive => unreachable!(), + }) + .collect(), + ) + } pub fn get_pids(&self) -> Vec { self .children @@ -877,14 +880,14 @@ pub fn wait_fg(job: Job, interactive: bool) -> ShResult<()> { _ => { /* Do nothing */ } } } - if let Some(pipe_status) = Job::pipe_status(&statuses) { - let pipe_status = pipe_status - .into_iter() - .map(|s| s.to_string()) - .collect::>(); + if let Some(pipe_status) = Job::pipe_status(&statuses) { + let pipe_status = pipe_status + .into_iter() + .map(|s| s.to_string()) + .collect::>(); - 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 !was_stopped { write_jobs(|j| { diff --git a/src/main.rs b/src/main.rs index d06c7a3..fb84b2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,7 +40,8 @@ use crate::readline::term::{LineWriter, RawModeGuard, raw_mode}; use crate::readline::{Prompt, ReadlineEvent, ShedVi}; use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending}; 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 state::write_vars; @@ -59,8 +60,8 @@ struct ShedArgs { #[arg(short)] interactive: bool, - #[arg(short)] - stdin: bool, + #[arg(short)] + stdin: bool, #[arg(long, short)] login_shell: bool, @@ -128,16 +129,16 @@ fn main() -> ExitCode { unsafe { env::set_var("SHLVL", "1") }; } - if let Err(e) = source_env() { - e.print_error(); - } + if let Err(e) = source_env() { + e.print_error(); + } if let Err(e) = if let Some(cmd) = args.command { exec_dash_c(cmd) } else if args.stdin || !isatty(STDIN_FILENO).unwrap_or(false) { - read_commands(args.script_args) - } else if !args.script_args.is_empty() { - let path = args.script_args.remove(0); + read_commands(args.script_args) + } else if !args.script_args.is_empty() { + let path = args.script_args.remove(0); run_script(path, args.script_args) } else { let res = shed_interactive(args); @@ -161,29 +162,29 @@ fn main() -> ExitCode { } fn read_commands(args: Vec) -> ShResult<()> { - let mut input = vec![]; - let mut read_buf = [0u8;4096]; - loop { - match read(STDIN_FILENO, &mut read_buf) { - Ok(0) => break, - Ok(n) => input.extend_from_slice(&read_buf[..n]), - Err(Errno::EINTR) => continue, - Err(e) => { - QUIT_CODE.store(1, Ordering::SeqCst); - return Err(ShErr::simple( - ShErrKind::CleanExit(1), - format!("error reading from stdin: {e}"), - )); - } - } - } + let mut input = vec![]; + let mut read_buf = [0u8; 4096]; + loop { + match read(STDIN_FILENO, &mut read_buf) { + Ok(0) => break, + Ok(n) => input.extend_from_slice(&read_buf[..n]), + Err(Errno::EINTR) => continue, + Err(e) => { + QUIT_CODE.store(1, Ordering::SeqCst); + return Err(ShErr::simple( + ShErrKind::CleanExit(1), + 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 { 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>(path: P, args: Vec) -> ShResult<()> { @@ -221,10 +222,11 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> { let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop sig_setup(args.login_shell); - if args.login_shell - && let Err(e) = source_login() { - e.print_error(); - } + if args.login_shell + && let Err(e) = source_login() + { + e.print_error(); + } if let Err(e) = source_rc() { e.print_error(); diff --git a/src/parse/execute.rs b/src/parse/execute.rs index db1d0a6..d006248 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -8,7 +8,30 @@ use ariadne::Fmt; use crate::{ 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}, jobs::{ChildProc, JobStack, attach_tty, dispatch_job}, @@ -995,7 +1018,7 @@ impl Dispatcher { "ulimit" => ulimit(cmd), "umask" => umask_builtin(cmd), "seek" => seek(cmd), - "help" => help(cmd), + "help" => help(cmd), "true" | ":" => { state::set_status(0); Ok(()) diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 46063b9..f26575c 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -1084,12 +1084,10 @@ impl ParseStream { } } - if !from_func_def { self.parse_redir(&mut redirs, &mut node_tks)?; } - let node = Node { class: NdRule::BraceGrp { body }, flags: NdFlags::empty(), diff --git a/src/procio.rs b/src/procio.rs index b6b3fe4..5b4eeb7 100644 --- a/src/procio.rs +++ b/src/procio.rs @@ -115,11 +115,14 @@ impl IoMode { pub fn buffer(tgt_fd: RawFd, buf: String, flags: TkFlags) -> ShResult { Ok(Self::Buffer { tgt_fd, buf, flags }) } - pub fn loaded_pipe(tgt_fd: RawFd, buf: &[u8]) -> ShResult { - let (rpipe, wpipe) = nix::unistd::pipe()?; - write(wpipe, buf)?; - Ok(Self::Pipe { tgt_fd, pipe: rpipe.into() }) - } + pub fn loaded_pipe(tgt_fd: RawFd, buf: &[u8]) -> ShResult { + let (rpipe, wpipe) = nix::unistd::pipe()?; + write(wpipe, buf)?; + Ok(Self::Pipe { + tgt_fd, + pipe: rpipe.into(), + }) + } pub fn get_pipes() -> (Self, Self) { let (rpipe, wpipe) = nix::unistd::pipe2(OFlag::O_CLOEXEC).unwrap(); ( diff --git a/src/readline/history.rs b/src/readline/history.rs index ba1fbe3..06d6778 100644 --- a/src/readline/history.rs +++ b/src/readline/history.rs @@ -203,7 +203,7 @@ fn dedupe_entries(entries: &[HistEntry]) -> Vec { .collect() } -#[derive(Default,Clone,Debug)] +#[derive(Default, Clone, Debug)] pub struct History { path: PathBuf, pub pending: Option, // command, cursor_pos @@ -215,7 +215,7 @@ pub struct History { //search_direction: Direction, ignore_dups: bool, max_size: Option, - stateless: bool + stateless: bool, } impl History { @@ -231,7 +231,7 @@ impl History { //search_direction: Direction::Backward, ignore_dups: false, max_size: None, - stateless: true, + stateless: true, } } pub fn new() -> ShResult { @@ -269,7 +269,7 @@ impl History { //search_direction: Direction::Backward, ignore_dups, max_size, - stateless: false, + stateless: false, }) } @@ -454,9 +454,9 @@ impl History { } pub fn save(&mut self) -> ShResult<()> { - if self.stateless { - return Ok(()); - } + if self.stateless { + return Ok(()); + } let mut file = OpenOptions::new() .create(true) .append(true) diff --git a/src/readline/linebuf.rs b/src/readline/linebuf.rs index d86d64c..8252f68 100644 --- a/src/readline/linebuf.rs +++ b/src/readline/linebuf.rs @@ -12,6 +12,7 @@ use super::vicmd::{ ViCmd, Word, }; use crate::{ + expand::expand_cmd_sub, libsh::{error::ShResult, guards::var_ctx_guard}, parse::{ execute::exec_input, @@ -23,6 +24,7 @@ use crate::{ markers, register::{RegisterContent, write_register}, term::RawModeGuard, + vicmd::ReadSrc, }, state::{VarFlags, VarKind, read_shopts, write_meta, write_vars}, }; @@ -441,33 +443,30 @@ pub struct LineBuf { } impl Default for LineBuf { - fn default() -> Self { - Self { - buffer: String::new(), - hint: None, - grapheme_indices: Some(vec![]), - cursor: ClampedUsize::new(0, 0, false), + fn default() -> Self { + Self { + buffer: String::new(), + hint: None, + grapheme_indices: Some(vec![]), + cursor: ClampedUsize::new(0, 0, false), - select_mode: None, - select_range: None, - last_selection: None, + select_mode: None, + select_range: None, + last_selection: None, - insert_mode_start_pos: None, - saved_col: None, - indent_ctx: IndentCtx::new(), + insert_mode_start_pos: None, + saved_col: None, + indent_ctx: IndentCtx::new(), - undo_stack: vec![], - redo_stack: vec![], - } - } + undo_stack: vec![], + redo_stack: vec![], + } + } } impl LineBuf { pub fn new() -> Self { - let mut new = Self { - grapheme_indices: Some(vec![]), // We know the buffer is empty, so this keeps us safe from unwrapping None - ..Default::default() - }; + let mut new = Self::default(); new.update_graphemes(); new } @@ -3329,12 +3328,39 @@ impl LineBuf { | Verb::CompleteBackward | Verb::VisualModeSelectLast => self.apply_motion(motion), Verb::ShellCmd(cmd) => self.verb_shell_cmd(cmd)?, - Verb::Normal(_) - | Verb::Read(_) - | Verb::Write(_) - | Verb::Substitute(..) - | Verb::RepeatSubstitute - | Verb::RepeatGlobal => {} + Verb::Read(src) => match src { + ReadSrc::File(path_buf) => { + if !path_buf.is_file() { + write_meta(|m| m.post_system_message(format!("{} is not a file", path_buf.display()))); + return Ok(()); + } + 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(()) } diff --git a/src/readline/mod.rs b/src/readline/mod.rs index 9ce0816..30b0c52 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -133,17 +133,17 @@ pub mod markers { ('\u{e000}'..'\u{efff}').contains(&c) } - // Help command formatting markers - pub const TAG: Marker = '\u{e180}'; - pub const REFERENCE: Marker = '\u{e181}'; - pub const HEADER: Marker = '\u{e182}'; - pub const CODE: Marker = '\u{e183}'; - /// angle brackets - pub const KEYWORD_1: Marker = '\u{e184}'; - /// curly brackets - pub const KEYWORD_2: Marker = '\u{e185}'; - /// square brackets - pub const KEYWORD_3: Marker = '\u{e186}'; + // Help command formatting markers + pub const TAG: Marker = '\u{e180}'; + pub const REFERENCE: Marker = '\u{e181}'; + pub const HEADER: Marker = '\u{e182}'; + pub const CODE: Marker = '\u{e183}'; + /// angle brackets + pub const KEYWORD_1: Marker = '\u{e184}'; + /// curly brackets + pub const KEYWORD_2: Marker = '\u{e185}'; + /// square brackets + pub const KEYWORD_3: Marker = '\u{e186}'; } type Marker = char; @@ -268,7 +268,7 @@ pub struct ShedVi { pub old_layout: Option, pub history: History, - pub ex_history: History, + pub ex_history: History, pub needs_redraw: bool, } @@ -290,7 +290,7 @@ impl ShedVi { repeat_motion: None, editor: LineBuf::new(), history: History::new()?, - ex_history: History::empty(), + ex_history: History::empty(), needs_redraw: true, }; write_vars(|v| { @@ -322,7 +322,7 @@ impl ShedVi { repeat_motion: None, editor: LineBuf::new(), history: History::empty(), - ex_history: History::empty(), + ex_history: History::empty(), needs_redraw: true, }; write_vars(|v| { @@ -861,16 +861,16 @@ impl ShedVi { 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_ex_cmd = cmd.flags.contains(CmdFlags::IS_EX_CMD); - log::debug!("is_ex_cmd: {is_ex_cmd}"); + let is_ex_cmd = cmd.flags.contains(CmdFlags::IS_EX_CMD); + log::debug!("is_ex_cmd: {is_ex_cmd}"); if is_shell_cmd { self.old_layout = None; } - if is_ex_cmd { - self.ex_history.push(cmd.raw_seq.clone()); - self.ex_history.reset(); - log::debug!("ex_history: {:?}", self.ex_history.entries()); - } + if is_ex_cmd { + self.ex_history.push(cmd.raw_seq.clone()); + self.ex_history.reset(); + log::debug!("ex_history: {:?}", self.ex_history.entries()); + } let before = self.editor.buffer.clone(); diff --git a/src/readline/vicmd.rs b/src/readline/vicmd.rs index 3deb968..1c66f75 100644 --- a/src/readline/vicmd.rs +++ b/src/readline/vicmd.rs @@ -1,5 +1,9 @@ +use std::path::PathBuf; + use bitflags::bitflags; +use crate::readline::vimode::ex::SubFlags; + use super::register::{RegisterContent, append_register, read_register, write_register}; //TODO: write tests that take edit results and cursor positions from actual @@ -64,7 +68,7 @@ bitflags! { const VISUAL_LINE = 1<<1; const VISUAL_BLOCK = 1<<2; 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), Read(ReadSrc), Write(WriteDest), - Substitute(String, String, super::vimode::ex::SubFlags), + Edit(PathBuf), + Substitute(String, String, SubFlags), RepeatSubstitute, RepeatGlobal, } diff --git a/src/readline/vimode/ex.rs b/src/readline/vimode/ex.rs index bb2f883..56618d0 100644 --- a/src/readline/vimode/ex.rs +++ b/src/readline/vimode/ex.rs @@ -5,6 +5,7 @@ use std::str::Chars; use itertools::Itertools; use crate::bitflags; +use crate::expand::{Expander, expand_raw}; use crate::libsh::error::{ShErr, ShErrKind, ShResult}; use crate::readline::history::History; use crate::readline::keys::KeyEvent; @@ -14,7 +15,7 @@ use crate::readline::vicmd::{ WriteDest, }; use crate::readline::vimode::{ModeReport, ViInsert, ViMode}; -use crate::state::write_meta; +use crate::state::{get_home, write_meta}; bitflags! { #[derive(Debug,Clone,Copy,PartialEq,Eq)] @@ -34,18 +35,18 @@ bitflags! { struct ExEditor { buf: LineBuf, mode: ViInsert, - history: History + history: History, } impl ExEditor { - pub fn new(history: History) -> Self { - let mut new = Self { - history, - ..Default::default() - }; - new.buf.update_graphemes(); - new - } + pub fn new(history: History) -> Self { + let mut new = Self { + history, + ..Default::default() + }; + new.buf.update_graphemes(); + new + } pub fn clear(&mut self) { *self = Self::default() } @@ -85,13 +86,13 @@ impl ExEditor { let Some(mut cmd) = self.mode.handle_key(key) else { return Ok(()); }; - cmd.alter_line_motion_if_no_verb(); - log::debug!("ExEditor got cmd: {:?}", cmd); - if self.should_grab_history(&cmd) { - log::debug!("Grabbing history for cmd: {:?}", cmd); - self.scroll_history(cmd); - return Ok(()) - } + cmd.alter_line_motion_if_no_verb(); + log::debug!("ExEditor got cmd: {:?}", cmd); + if self.should_grab_history(&cmd) { + log::debug!("Grabbing history for cmd: {:?}", cmd); + self.scroll_history(cmd); + return Ok(()); + } self.buf.exec_cmd(cmd) } } @@ -103,7 +104,9 @@ pub struct ViEx { impl ViEx { 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) => { let input = self.pending_cmd.buf.as_str(); match parse_ex_cmd(input) { - Ok(cmd) => { - Ok(cmd) - } + Ok(cmd) => Ok(cmd), Err(e) => { let msg = e.unwrap_or(format!("Not an editor command: {}", input)); write_meta(|m| m.post_system_message(msg.clone())); @@ -129,18 +130,14 @@ impl ViMode for ViEx { self.pending_cmd.clear(); Ok(None) } - E(C::Esc, M::NONE) => { - Ok(Some(ViCmd { - register: RegisterName::default(), - verb: Some(VerbCmd(1, Verb::NormalMode)), - motion: None, - flags: CmdFlags::empty(), - raw_seq: "".into(), - })) - } - _ => { - self.pending_cmd.handle_key(key).map(|_| None) - } + E(C::Esc, M::NONE) => Ok(Some(ViCmd { + register: RegisterName::default(), + verb: Some(VerbCmd(1, Verb::NormalMode)), + motion: None, + flags: CmdFlags::empty(), + raw_seq: "".into(), + })), + _ => self.pending_cmd.handle_key(key).map(|_| None), } } fn handle_key(&mut self, key: KeyEvent) -> Option { @@ -248,7 +245,7 @@ fn parse_ex_command(chars: &mut Peekable>) -> Result, Opt let mut cmd_name = String::new(); while let Some(ch) = chars.peek() { - if ch == &'!' { + if cmd_name.is_empty() && ch == &'!' { cmd_name.push(*ch); chars.next(); break; @@ -265,16 +262,17 @@ fn parse_ex_command(chars: &mut Peekable>) -> Result, Opt let cmd = unescape_shell_cmd(&cmd); Ok(Some(Verb::ShellCmd(cmd))) } - _ if "help".starts_with(&cmd_name) => { - let cmd = "help ".to_string() + chars.collect::().trim(); - Ok(Some(Verb::ShellCmd(cmd))) - } + _ if "help".starts_with(&cmd_name) => { + let cmd = "help ".to_string() + chars.collect::().trim(); + Ok(Some(Verb::ShellCmd(cmd))) + } "normal!" => parse_normal(chars), _ if "delete".starts_with(&cmd_name) => Ok(Some(Verb::Delete)), _ if "yank".starts_with(&cmd_name) => Ok(Some(Verb::Yank)), _ if "put".starts_with(&cmd_name) => Ok(Some(Verb::Put(Anchor::After))), _ if "read".starts_with(&cmd_name) => parse_read(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), _ => Err(None), } @@ -289,6 +287,19 @@ fn parse_normal(chars: &mut Peekable>) -> Result, Option< Ok(Some(Verb::Normal(seq))) } +fn parse_edit(chars: &mut Peekable>) -> Result, Option> { + 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>) -> Result, Option> { chars .peeking_take_while(|c| c.is_whitespace()) @@ -311,23 +322,15 @@ fn parse_read(chars: &mut Peekable>) -> Result, Option PathBuf { - if let Some(stripped) = path.strip_prefix("~/") - && let Some(home) = std::env::var_os("HOME") - { - return PathBuf::from(home).join(stripped); - } - if path == "~" - && let Some(home) = std::env::var_os("HOME") - { - return PathBuf::from(home); - } - PathBuf::from(path) +fn get_path(path: &str) -> Result> { + let expanded = expand_raw(&mut path.chars().peekable()) + .map_err(|e| Some(format!("Error expanding path: {}", e)))?; + Ok(PathBuf::from(&expanded)) } fn parse_write(chars: &mut Peekable>) -> Result, Option> { @@ -350,7 +353,7 @@ fn parse_write(chars: &mut Peekable>) -> Result, Option ShResult<()> { let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned()); if let Some(job) = result { 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) { - let pipe_status = pipe_status - .into_iter() - .map(|s| s.to_string()) - .collect::>(); + if let Some(pipe_status) = Job::pipe_status(&statuses) { + let pipe_status = pipe_status + .into_iter() + .map(|s| s.to_string()) + .collect::>(); - 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)); for cmd in post_job_hooks { diff --git a/src/state.rs b/src/state.rs index a9c6bb9..44624bc 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1098,7 +1098,7 @@ impl VarTab { .map(|hname| hname.to_string_lossy().to_string()) .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 { 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("SHED_HIST", format!("{}/.shedhist", 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) { @@ -1873,36 +1873,41 @@ pub fn set_status(code: i32) { } pub fn source_runtime_file(name: &str, env_var_name: Option<&str>) -> ShResult<()> { - let etc_path = PathBuf::from(format!("/etc/shed/{name}")); - if etc_path.is_file() - && let Err(e) = source_file(etc_path) { - e.print_error(); - } + let etc_path = PathBuf::from(format!("/etc/shed/{name}")); + if etc_path.is_file() + && let Err(e) = source_file(etc_path) + { + e.print_error(); + } let path = if let Some(name) = env_var_name - && let Ok(path) = env::var(name) { + && let Ok(path) = env::var(name) + { PathBuf::from(&path) - } else if let Some(home) = get_home() { - home.join(format!(".{name}")) + } else if let Some(home) = get_home() { + home.join(format!(".{name}")) } 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() { - return Ok(()) + return Ok(()); } source_file(path) } pub fn source_rc() -> ShResult<()> { - source_runtime_file("shedrc", Some("SHED_RC")) + source_runtime_file("shedrc", Some("SHED_RC")) } 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<()> { - source_runtime_file("shedenv", Some("SHED_ENV")) + source_runtime_file("shedenv", Some("SHED_ENV")) } pub fn source_file(path: PathBuf) -> ShResult<()> { @@ -1917,30 +1922,39 @@ pub fn source_file(path: PathBuf) -> ShResult<()> { #[track_caller] pub fn get_home_unchecked() -> PathBuf { - if let Some(home) = get_home() { - home - } else { - let caller = std::panic::Location::caller(); - panic!("get_home_unchecked: could not determine home directory (called from {}:{})", caller.file(), caller.line()) - } + if let Some(home) = get_home() { + home + } else { + let caller = std::panic::Location::caller(); + panic!( + "get_home_unchecked: could not determine home directory (called from {}:{})", + caller.file(), + caller.line() + ) + } } #[track_caller] pub fn get_home_str_unchecked() -> String { - if let Some(home) = get_home() { - home.to_string_lossy().to_string() - } else { - let caller = std::panic::Location::caller(); - panic!("get_home_str_unchecked: could not determine home directory (called from {}:{})", caller.file(), caller.line()) - } + if let Some(home) = get_home() { + home.to_string_lossy().to_string() + } else { + let caller = std::panic::Location::caller(); + panic!( + "get_home_str_unchecked: could not determine home directory (called from {}:{})", + caller.file(), + caller.line() + ) + } } pub fn get_home() -> Option { - env::var("HOME").ok().map(PathBuf::from).or_else(|| { - User::from_uid(getuid()).ok().flatten().map(|u| u.dir) - }) + env::var("HOME") + .ok() + .map(PathBuf::from) + .or_else(|| User::from_uid(getuid()).ok().flatten().map(|u| u.dir)) } pub fn get_home_str() -> Option { - get_home().map(|h| h.to_string_lossy().to_string()) + get_home().map(|h| h.to_string_lossy().to_string()) }