fixed the $0 parameter not being populated correctly
This commit is contained in:
@@ -554,8 +554,12 @@ pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
|
|||||||
pub fn expand_glob(raw: &str) -> ShResult<String> {
|
pub fn expand_glob(raw: &str) -> ShResult<String> {
|
||||||
let mut words = vec![];
|
let mut words = vec![];
|
||||||
|
|
||||||
|
let opts = glob::MatchOptions {
|
||||||
|
require_literal_leading_dot: !crate::state::read_shopts(|s| s.core.dotglob),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
for entry in
|
for entry in
|
||||||
glob::glob(raw).map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid glob pattern"))?
|
glob::glob_with(raw, opts).map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid glob pattern"))?
|
||||||
{
|
{
|
||||||
let entry =
|
let entry =
|
||||||
entry.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?;
|
entry.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?;
|
||||||
@@ -1926,7 +1930,8 @@ pub fn expand_prompt(raw: &str) -> ShResult<String> {
|
|||||||
let pathbuf = PathBuf::from(&path);
|
let pathbuf = PathBuf::from(&path);
|
||||||
let mut segments = pathbuf.iter().count();
|
let mut segments = pathbuf.iter().count();
|
||||||
let mut path_iter = pathbuf.iter();
|
let mut path_iter = pathbuf.iter();
|
||||||
while segments > 4 {
|
let max_segments = crate::state::read_shopts(|s| s.prompt.trunc_prompt_path);
|
||||||
|
while segments > max_segments {
|
||||||
path_iter.next();
|
path_iter.next();
|
||||||
segments -= 1;
|
segments -= 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ use super::{
|
|||||||
ParsedSrc, Redir, RedirType,
|
ParsedSrc, Redir, RedirType,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static RECURSE_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ScopeGuard;
|
pub struct ScopeGuard;
|
||||||
|
|
||||||
|
|
||||||
@@ -105,7 +109,12 @@ impl ExecArgs {
|
|||||||
pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool) -> ShResult<()> {
|
pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool) -> ShResult<()> {
|
||||||
let log_tab = read_logic(|l| l.clone());
|
let log_tab = read_logic(|l| l.clone());
|
||||||
let input = expand_aliases(input, HashSet::new(), &log_tab);
|
let input = expand_aliases(input, HashSet::new(), &log_tab);
|
||||||
let mut parser = ParsedSrc::new(Arc::new(input));
|
let lex_flags = if interactive {
|
||||||
|
super::lex::LexFlags::INTERACTIVE
|
||||||
|
} else {
|
||||||
|
super::lex::LexFlags::empty()
|
||||||
|
};
|
||||||
|
let mut parser = ParsedSrc::new(Arc::new(input)).with_lex_flags(lex_flags);
|
||||||
if let Err(errors) = parser.parse_src() {
|
if let Err(errors) = parser.parse_src() {
|
||||||
for error in errors {
|
for error in errors {
|
||||||
eprintln!("{error}");
|
eprintln!("{error}");
|
||||||
@@ -170,6 +179,10 @@ impl Dispatcher {
|
|||||||
self.exec_builtin(node)
|
self.exec_builtin(node)
|
||||||
} else if is_subsh(node.get_command().cloned()) {
|
} else if is_subsh(node.get_command().cloned()) {
|
||||||
self.exec_subsh(node)
|
self.exec_subsh(node)
|
||||||
|
} else if crate::state::read_shopts(|s| s.core.autocd) && Path::new(cmd.span.as_str()).is_dir() {
|
||||||
|
let dir = cmd.span.as_str().to_string();
|
||||||
|
let stack = IoStack { stack: self.io_stack.clone() };
|
||||||
|
exec_input(format!("cd {dir}"), Some(stack), self.interactive)
|
||||||
} else {
|
} else {
|
||||||
self.exec_cmd(node)
|
self.exec_cmd(node)
|
||||||
}
|
}
|
||||||
@@ -266,6 +279,21 @@ impl Dispatcher {
|
|||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let max_depth = crate::state::read_shopts(|s| s.core.max_recurse_depth);
|
||||||
|
let depth = RECURSE_DEPTH.with(|d| {
|
||||||
|
let cur = d.get();
|
||||||
|
d.set(cur + 1);
|
||||||
|
cur + 1
|
||||||
|
});
|
||||||
|
if depth > max_depth {
|
||||||
|
RECURSE_DEPTH.with(|d| d.set(d.get() - 1));
|
||||||
|
return Err(ShErr::full(
|
||||||
|
ShErrKind::InternalErr,
|
||||||
|
format!("maximum recursion depth ({max_depth}) exceeded"),
|
||||||
|
blame,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let env_vars = self.set_assignments(assignments, AssignBehavior::Export)?;
|
let env_vars = self.set_assignments(assignments, AssignBehavior::Export)?;
|
||||||
let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect());
|
let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect());
|
||||||
|
|
||||||
@@ -273,27 +301,30 @@ impl Dispatcher {
|
|||||||
|
|
||||||
let func_name = argv.remove(0).span.as_str().to_string();
|
let func_name = argv.remove(0).span.as_str().to_string();
|
||||||
let argv = prepare_argv(argv)?;
|
let argv = prepare_argv(argv)?;
|
||||||
if let Some(func) = read_logic(|l| l.get_func(&func_name)) {
|
let result = if let Some(func) = read_logic(|l| l.get_func(&func_name)) {
|
||||||
let _guard = ScopeGuard::exclusive_scope(Some(argv));
|
let _guard = ScopeGuard::exclusive_scope(Some(argv));
|
||||||
|
|
||||||
if let Err(e) = self.exec_brc_grp((*func).clone()) {
|
if let Err(e) = self.exec_brc_grp((*func).clone()) {
|
||||||
match e.kind() {
|
match e.kind() {
|
||||||
ShErrKind::FuncReturn(code) => {
|
ShErrKind::FuncReturn(code) => {
|
||||||
state::set_status(*code);
|
state::set_status(*code);
|
||||||
return Ok(());
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => return Err(e),
|
_ => Err(e),
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
} else {
|
} else {
|
||||||
Err(ShErr::full(
|
Err(ShErr::full(
|
||||||
ShErrKind::InternalErr,
|
ShErrKind::InternalErr,
|
||||||
format!("Failed to find function '{}'", func_name),
|
format!("Failed to find function '{}'", func_name),
|
||||||
blame,
|
blame,
|
||||||
))
|
))
|
||||||
}
|
};
|
||||||
|
|
||||||
|
RECURSE_DEPTH.with(|d| d.set(d.get() - 1));
|
||||||
|
result
|
||||||
}
|
}
|
||||||
fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> {
|
fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> {
|
||||||
let NdRule::BraceGrp { body } = brc_grp.class else {
|
let NdRule::BraceGrp { body } = brc_grp.class else {
|
||||||
|
|||||||
@@ -156,10 +156,10 @@ pub struct LexStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct LexFlags: u32 {
|
pub struct LexFlags: u32 {
|
||||||
/// Return comment tokens
|
/// The lexer is operating in interactive mode
|
||||||
const LEX_COMMENTS = 0b000000001;
|
const INTERACTIVE = 0b000000001;
|
||||||
/// Allow unfinished input
|
/// Allow unfinished input
|
||||||
const LEX_UNFINISHED = 0b000000010;
|
const LEX_UNFINISHED = 0b000000010;
|
||||||
/// The next string-type token is a command name
|
/// The next string-type token is a command name
|
||||||
@@ -740,7 +740,7 @@ impl Iterator for LexStream {
|
|||||||
}
|
}
|
||||||
self.get_token(ch_idx..self.cursor, TkRule::Sep)
|
self.get_token(ch_idx..self.cursor, TkRule::Sep)
|
||||||
}
|
}
|
||||||
'#' => {
|
'#' if !self.flags.contains(LexFlags::INTERACTIVE) || crate::state::read_shopts(|s| s.core.interactive_comments) => {
|
||||||
let ch_idx = self.cursor;
|
let ch_idx = self.cursor;
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ macro_rules! try_match {
|
|||||||
pub struct ParsedSrc {
|
pub struct ParsedSrc {
|
||||||
pub src: Arc<String>,
|
pub src: Arc<String>,
|
||||||
pub ast: Ast,
|
pub ast: Ast,
|
||||||
|
pub lex_flags: LexFlags,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParsedSrc {
|
impl ParsedSrc {
|
||||||
@@ -51,11 +52,16 @@ impl ParsedSrc {
|
|||||||
Self {
|
Self {
|
||||||
src,
|
src,
|
||||||
ast: Ast::new(vec![]),
|
ast: Ast::new(vec![]),
|
||||||
|
lex_flags: LexFlags::empty(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn with_lex_flags(mut self, flags: LexFlags) -> Self {
|
||||||
|
self.lex_flags = flags;
|
||||||
|
self
|
||||||
|
}
|
||||||
pub fn parse_src(&mut self) -> Result<(), Vec<ShErr>> {
|
pub fn parse_src(&mut self) -> Result<(), Vec<ShErr>> {
|
||||||
let mut tokens = vec![];
|
let mut tokens = vec![];
|
||||||
for lex_result in LexStream::new(self.src.clone(), LexFlags::empty()) {
|
for lex_result in LexStream::new(self.src.clone(), self.lex_flags) {
|
||||||
match lex_result {
|
match lex_result {
|
||||||
Ok(token) => tokens.push(token),
|
Ok(token) => tokens.push(token),
|
||||||
Err(error) => return Err(vec![error]),
|
Err(error) => return Err(vec![error]),
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ impl DerefMut for IoFrame {
|
|||||||
/// redirection
|
/// redirection
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct IoStack {
|
pub struct IoStack {
|
||||||
stack: Vec<IoFrame>,
|
pub stack: Vec<IoFrame>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IoStack {
|
impl IoStack {
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ impl Completer {
|
|||||||
ctx.push(markers::VAR_SUB);
|
ctx.push(markers::VAR_SUB);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
markers::ARG => {
|
markers::ARG | markers::ASSIGNMENT => {
|
||||||
log::debug!("Found argument marker at position {}", pos);
|
log::debug!("Found argument/assignment marker at position {}", pos);
|
||||||
if last_priority < 1 {
|
if last_priority < 1 {
|
||||||
ctx_start = pos;
|
ctx_start = pos;
|
||||||
ctx.push(markers::ARG);
|
ctx.push(markers::ARG);
|
||||||
@@ -328,6 +328,8 @@ impl Completer {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let limit = crate::state::read_shopts(|s| s.prompt.comp_limit);
|
||||||
|
candidates.truncate(limit);
|
||||||
|
|
||||||
Ok(CompResult::from_candidates(candidates))
|
Ok(CompResult::from_candidates(candidates))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::{env, path::{Path, PathBuf}};
|
use std::{env, os::unix::fs::PermissionsExt, path::{Path, PathBuf}};
|
||||||
|
|
||||||
use crate::{libsh::term::{Style, StyleSet, Styled}, prompt::readline::{annotate_input, markers}, state::read_logic};
|
use crate::{libsh::term::{Style, StyleSet, Styled}, prompt::readline::{annotate_input, markers}, state::{read_logic, read_shopts}};
|
||||||
|
|
||||||
/// Syntax highlighter for shell input using Unicode marker-based annotation
|
/// Syntax highlighter for shell input using Unicode marker-based annotation
|
||||||
///
|
///
|
||||||
@@ -214,16 +214,31 @@ impl Highlighter {
|
|||||||
fn is_valid(command: &str) -> bool {
|
fn is_valid(command: &str) -> bool {
|
||||||
let path = env::var("PATH").unwrap_or_default();
|
let path = env::var("PATH").unwrap_or_default();
|
||||||
let paths = path.split(':');
|
let paths = path.split(':');
|
||||||
if PathBuf::from(&command).exists() {
|
let cmd_path = PathBuf::from(&command);
|
||||||
return true;
|
|
||||||
|
if cmd_path.exists() {
|
||||||
|
// the user has given us an absolute path
|
||||||
|
if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) {
|
||||||
|
// this is a directory and autocd is enabled
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
let Ok(meta) = cmd_path.metadata() else { return false };
|
||||||
|
// this is a file that is executable by someone
|
||||||
|
return meta.permissions().mode() & 0o111 == 0
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// they gave us a command name
|
||||||
|
// now we must traverse the PATH env var
|
||||||
|
// and see if we find any matches
|
||||||
for path in paths {
|
for path in paths {
|
||||||
let path = PathBuf::from(path).join(command);
|
let path = PathBuf::from(path).join(command);
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
return true;
|
let Ok(meta) = path.metadata() else { continue };
|
||||||
|
return meta.permissions().mode() & 0o111 != 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// also check shell functions and aliases for any matches
|
||||||
let found = read_logic(|l| l.get_func(command).is_some() || l.get_alias(command).is_some());
|
let found = read_logic(|l| l.get_func(command).is_some() || l.get_alias(command).is_some());
|
||||||
if found {
|
if found {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -217,11 +217,17 @@ pub struct History {
|
|||||||
|
|
||||||
impl History {
|
impl History {
|
||||||
pub fn new() -> ShResult<Self> {
|
pub fn new() -> ShResult<Self> {
|
||||||
|
let ignore_dups = crate::state::read_shopts(|s| s.core.hist_ignore_dupes);
|
||||||
|
let max_hist = crate::state::read_shopts(|s| s.core.max_hist);
|
||||||
let path = PathBuf::from(env::var("FERNHIST").unwrap_or({
|
let path = PathBuf::from(env::var("FERNHIST").unwrap_or({
|
||||||
let home = env::var("HOME").unwrap();
|
let home = env::var("HOME").unwrap();
|
||||||
format!("{home}/.fern_history")
|
format!("{home}/.fern_history")
|
||||||
}));
|
}));
|
||||||
let mut entries = read_hist_file(&path)?;
|
let mut entries = read_hist_file(&path)?;
|
||||||
|
// Enforce max_hist limit on loaded entries
|
||||||
|
if entries.len() > max_hist {
|
||||||
|
entries = entries.split_off(entries.len() - max_hist);
|
||||||
|
}
|
||||||
// Create pending entry for current input
|
// Create pending entry for current input
|
||||||
let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0);
|
let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0);
|
||||||
entries.push(HistEntry {
|
entries.push(HistEntry {
|
||||||
@@ -238,8 +244,8 @@ impl History {
|
|||||||
search_mask,
|
search_mask,
|
||||||
cursor,
|
cursor,
|
||||||
search_direction: Direction::Backward,
|
search_direction: Direction::Backward,
|
||||||
ignore_dups: true,
|
ignore_dups,
|
||||||
max_size: None,
|
max_size: Some(max_hist as u32),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -212,7 +212,10 @@ impl FernVi {
|
|||||||
self.editor.set_hint(hint);
|
self.editor.set_hint(hint);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.writer.flush_write("\x07")?; // Bell character
|
match crate::state::read_shopts(|s| s.core.bell_style) {
|
||||||
|
crate::shopt::FernBellStyle::Audible => { self.writer.flush_write("\x07")?; }
|
||||||
|
crate::shopt::FernBellStyle::Visible | crate::shopt::FernBellStyle::Disable => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,10 +243,12 @@ impl FernVi {
|
|||||||
self.print_line()?;
|
self.print_line()?;
|
||||||
self.writer.flush_write("\n")?;
|
self.writer.flush_write("\n")?;
|
||||||
let buf = self.editor.take_buf();
|
let buf = self.editor.take_buf();
|
||||||
// Save command to history
|
// Save command to history if auto_hist is enabled
|
||||||
self.history.push(buf.clone());
|
if crate::state::read_shopts(|s| s.core.auto_hist) {
|
||||||
if let Err(e) = self.history.save() {
|
self.history.push(buf.clone());
|
||||||
eprintln!("Failed to save history: {e}");
|
if let Err(e) = self.history.save() {
|
||||||
|
eprintln!("Failed to save history: {e}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Ok(ReadlineEvent::Line(buf));
|
return Ok(ReadlineEvent::Line(buf));
|
||||||
}
|
}
|
||||||
@@ -283,7 +288,8 @@ impl FernVi {
|
|||||||
pub fn get_layout(&mut self, line: &str) -> Layout {
|
pub fn get_layout(&mut self, line: &str) -> Layout {
|
||||||
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
|
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
|
||||||
let (cols, _) = get_win_size(STDIN_FILENO);
|
let (cols, _) = get_win_size(STDIN_FILENO);
|
||||||
Layout::from_parts(/* tab_stop: */ 8, cols, &self.prompt, to_cursor, line)
|
let tab_stop = crate::state::read_shopts(|s| s.prompt.tab_stop) as u16;
|
||||||
|
Layout::from_parts(tab_stop, cols, &self.prompt, to_cursor, line)
|
||||||
}
|
}
|
||||||
pub fn scroll_history(&mut self, cmd: ViCmd) {
|
pub fn scroll_history(&mut self, cmd: ViCmd) {
|
||||||
/*
|
/*
|
||||||
@@ -360,15 +366,16 @@ impl FernVi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn line_text(&mut self) -> String {
|
pub fn line_text(&mut self) -> String {
|
||||||
let start = Instant::now();
|
|
||||||
let line = self.editor.to_string();
|
let line = self.editor.to_string();
|
||||||
self.highlighter.load_input(&line);
|
|
||||||
self.highlighter.highlight();
|
|
||||||
let highlighted = self.highlighter.take();
|
|
||||||
let hint = self.editor.get_hint_text();
|
let hint = self.editor.get_hint_text();
|
||||||
let complete = format!("{highlighted}{hint}");
|
if crate::state::read_shopts(|s| s.prompt.highlight) {
|
||||||
let end = start.elapsed();
|
self.highlighter.load_input(&line);
|
||||||
complete
|
self.highlighter.highlight();
|
||||||
|
let highlighted = self.highlighter.take();
|
||||||
|
format!("{highlighted}{hint}")
|
||||||
|
} else {
|
||||||
|
format!("{line}{hint}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_line(&mut self) -> ShResult<()> {
|
pub fn print_line(&mut self) -> ShResult<()> {
|
||||||
|
|||||||
54
src/shopt.rs
54
src/shopt.rs
@@ -111,7 +111,7 @@ impl ShOpts {
|
|||||||
return Err(
|
return Err(
|
||||||
ShErr::simple(
|
ShErr::simple(
|
||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
"shopt: Expected 'core' or 'prompt' in shopt key",
|
"shopt: expected 'core' or 'prompt' in shopt key",
|
||||||
)
|
)
|
||||||
.with_note(
|
.with_note(
|
||||||
Note::new("'shopt' takes arguments separated by periods to denote namespaces")
|
Note::new("'shopt' takes arguments separated by periods to denote namespaces")
|
||||||
@@ -384,9 +384,8 @@ pub struct ShOptPrompt {
|
|||||||
pub trunc_prompt_path: usize,
|
pub trunc_prompt_path: usize,
|
||||||
pub edit_mode: FernEditMode,
|
pub edit_mode: FernEditMode,
|
||||||
pub comp_limit: usize,
|
pub comp_limit: usize,
|
||||||
pub prompt_highlight: bool,
|
pub highlight: bool,
|
||||||
pub tab_stop: usize,
|
pub tab_stop: usize,
|
||||||
pub custom: HashMap<String, ShFunc>, // Contains functions for prompt modules
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShOptPrompt {
|
impl ShOptPrompt {
|
||||||
@@ -419,14 +418,14 @@ impl ShOptPrompt {
|
|||||||
};
|
};
|
||||||
self.comp_limit = val;
|
self.comp_limit = val;
|
||||||
}
|
}
|
||||||
"prompt_highlight" => {
|
"highlight" => {
|
||||||
let Ok(val) = val.parse::<bool>() else {
|
let Ok(val) = val.parse::<bool>() else {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
"shopt: expected 'true' or 'false' for prompt_highlight value",
|
"shopt: expected 'true' or 'false' for highlight value",
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
self.prompt_highlight = val;
|
self.highlight = val;
|
||||||
}
|
}
|
||||||
"tab_stop" => {
|
"tab_stop" => {
|
||||||
let Ok(val) = val.parse::<usize>() else {
|
let Ok(val) = val.parse::<usize>() else {
|
||||||
@@ -444,19 +443,17 @@ impl ShOptPrompt {
|
|||||||
return Err(
|
return Err(
|
||||||
ShErr::simple(
|
ShErr::simple(
|
||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
format!("shopt: Unexpected 'core' option '{opt}'"),
|
format!("shopt: Unexpected 'prompt' option '{opt}'"),
|
||||||
)
|
)
|
||||||
.with_note(Note::new("options can be accessed like 'core.option_name'"))
|
.with_note(Note::new("options can be accessed like 'prompt.option_name'"))
|
||||||
.with_note(
|
.with_note(
|
||||||
Note::new("'core' contains the following options").with_sub_notes(vec![
|
Note::new("'prompt' contains the following options").with_sub_notes(vec![
|
||||||
"dotglob",
|
"trunc_prompt_path",
|
||||||
"autocd",
|
"edit_mode",
|
||||||
"hist_ignore_dupes",
|
"comp_limit",
|
||||||
"max_hist",
|
"highlight",
|
||||||
"interactive_comments",
|
"tab_stop",
|
||||||
"auto_hist",
|
"custom",
|
||||||
"bell_style",
|
|
||||||
"max_recurse_depth",
|
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -489,10 +486,10 @@ impl ShOptPrompt {
|
|||||||
output.push_str(&format!("{}", self.comp_limit));
|
output.push_str(&format!("{}", self.comp_limit));
|
||||||
Ok(Some(output))
|
Ok(Some(output))
|
||||||
}
|
}
|
||||||
"prompt_highlight" => {
|
"highlight" => {
|
||||||
let mut output =
|
let mut output =
|
||||||
String::from("Whether to enable or disable syntax highlighting on the prompt\n");
|
String::from("Whether to enable or disable syntax highlighting on the prompt\n");
|
||||||
output.push_str(&format!("{}", self.prompt_highlight));
|
output.push_str(&format!("{}", self.highlight));
|
||||||
Ok(Some(output))
|
Ok(Some(output))
|
||||||
}
|
}
|
||||||
"tab_stop" => {
|
"tab_stop" => {
|
||||||
@@ -500,16 +497,6 @@ impl ShOptPrompt {
|
|||||||
output.push_str(&format!("{}", self.tab_stop));
|
output.push_str(&format!("{}", self.tab_stop));
|
||||||
Ok(Some(output))
|
Ok(Some(output))
|
||||||
}
|
}
|
||||||
"custom" => {
|
|
||||||
let mut output = String::from(
|
|
||||||
"A table of custom 'modules' executed as shell functions for prompt scripting\n",
|
|
||||||
);
|
|
||||||
output.push_str("Current modules: \n");
|
|
||||||
for key in self.custom.keys() {
|
|
||||||
output.push_str(&format!(" - {key}\n"));
|
|
||||||
}
|
|
||||||
Ok(Some(output.trim().to_string()))
|
|
||||||
}
|
|
||||||
_ => Err(
|
_ => Err(
|
||||||
ShErr::simple(
|
ShErr::simple(
|
||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
@@ -540,12 +527,8 @@ impl Display for ShOptPrompt {
|
|||||||
output.push(format!("trunc_prompt_path = {}", self.trunc_prompt_path));
|
output.push(format!("trunc_prompt_path = {}", self.trunc_prompt_path));
|
||||||
output.push(format!("edit_mode = {}", self.edit_mode));
|
output.push(format!("edit_mode = {}", self.edit_mode));
|
||||||
output.push(format!("comp_limit = {}", self.comp_limit));
|
output.push(format!("comp_limit = {}", self.comp_limit));
|
||||||
output.push(format!("prompt_highlight = {}", self.prompt_highlight));
|
output.push(format!("highlight = {}", self.highlight));
|
||||||
output.push(format!("tab_stop = {}", self.tab_stop));
|
output.push(format!("tab_stop = {}", self.tab_stop));
|
||||||
output.push(String::from("prompt modules: "));
|
|
||||||
for key in self.custom.keys() {
|
|
||||||
output.push(format!(" - {key}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let final_output = output.join("\n");
|
let final_output = output.join("\n");
|
||||||
|
|
||||||
@@ -559,9 +542,8 @@ impl Default for ShOptPrompt {
|
|||||||
trunc_prompt_path: 4,
|
trunc_prompt_path: 4,
|
||||||
edit_mode: FernEditMode::Vi,
|
edit_mode: FernEditMode::Vi,
|
||||||
comp_limit: 100,
|
comp_limit: 100,
|
||||||
prompt_highlight: true,
|
highlight: true,
|
||||||
tab_stop: 4,
|
tab_stop: 4,
|
||||||
custom: HashMap::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/state.rs
10
src/state.rs
@@ -121,6 +121,8 @@ impl ScopeStack {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let mut new = Self::default();
|
let mut new = Self::default();
|
||||||
new.scopes.push(VarTab::new());
|
new.scopes.push(VarTab::new());
|
||||||
|
let shell_name = std::env::args().next().unwrap_or_else(|| "fern".to_string());
|
||||||
|
new.global_params.insert(ShellParam::ShellName.to_string(), shell_name);
|
||||||
new
|
new
|
||||||
}
|
}
|
||||||
pub fn descend(&mut self, argv: Option<Vec<String>>) {
|
pub fn descend(&mut self, argv: Option<Vec<String>>) {
|
||||||
@@ -482,14 +484,6 @@ impl VarTab {
|
|||||||
fn init_params() -> HashMap<ShellParam, String> {
|
fn init_params() -> HashMap<ShellParam, String> {
|
||||||
let mut params = HashMap::new();
|
let mut params = HashMap::new();
|
||||||
params.insert(ShellParam::ArgCount, "0".into()); // Number of positional parameters
|
params.insert(ShellParam::ArgCount, "0".into()); // Number of positional parameters
|
||||||
params.insert(
|
|
||||||
ShellParam::Pos(0),
|
|
||||||
std::env::current_exe()
|
|
||||||
.unwrap()
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.to_string(),
|
|
||||||
); // Name of the shell
|
|
||||||
params.insert(ShellParam::ShPid, Pid::this().to_string()); // PID of the shell
|
params.insert(ShellParam::ShPid, Pid::this().to_string()); // PID of the shell
|
||||||
params.insert(ShellParam::LastJob, "".into()); // PID of the last background job (if any)
|
params.insert(ShellParam::LastJob, "".into()); // PID of the last background job (if any)
|
||||||
params
|
params
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use std::path::PathBuf;
|
|||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
use crate::prompt::readline::complete::Completer;
|
use crate::prompt::readline::complete::Completer;
|
||||||
|
use crate::prompt::readline::markers;
|
||||||
use crate::state::{write_logic, write_vars, VarFlags};
|
use crate::state::{write_logic, write_vars, VarFlags};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -320,12 +321,12 @@ fn context_detection_command_position() {
|
|||||||
let completer = Completer::new();
|
let completer = Completer::new();
|
||||||
|
|
||||||
// At the beginning - command context
|
// At the beginning - command context
|
||||||
let (in_cmd, _) = completer.get_completion_context("ech", 3);
|
let (ctx, _) = completer.get_completion_context("ech", 3);
|
||||||
assert!(in_cmd, "Should be in command context at start");
|
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context at start");
|
||||||
|
|
||||||
// After whitespace - still command if no command yet
|
// After whitespace - still command if no command yet
|
||||||
let (in_cmd, _) = completer.get_completion_context(" ech", 5);
|
let (ctx, _) = completer.get_completion_context(" ech", 5);
|
||||||
assert!(in_cmd, "Should be in command context after whitespace");
|
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after whitespace");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -333,11 +334,11 @@ fn context_detection_argument_position() {
|
|||||||
let completer = Completer::new();
|
let completer = Completer::new();
|
||||||
|
|
||||||
// After a complete command - argument context
|
// After a complete command - argument context
|
||||||
let (in_cmd, _) = completer.get_completion_context("echo hello", 10);
|
let (ctx, _) = completer.get_completion_context("echo hello", 10);
|
||||||
assert!(!in_cmd, "Should be in argument context after command");
|
assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context after command");
|
||||||
|
|
||||||
let (in_cmd, _) = completer.get_completion_context("ls -la /tmp", 11);
|
let (ctx, _) = completer.get_completion_context("ls -la /tmp", 11);
|
||||||
assert!(!in_cmd, "Should be in argument context");
|
assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -345,12 +346,12 @@ fn context_detection_nested_command_sub() {
|
|||||||
let completer = Completer::new();
|
let completer = Completer::new();
|
||||||
|
|
||||||
// Inside $() - should be command context
|
// Inside $() - should be command context
|
||||||
let (in_cmd, _) = completer.get_completion_context("echo \"$(ech", 11);
|
let (ctx, _) = completer.get_completion_context("echo \"$(ech", 11);
|
||||||
assert!(in_cmd, "Should be in command context inside $()");
|
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context inside $()");
|
||||||
|
|
||||||
// After command in $() - argument context
|
// After command in $() - argument context
|
||||||
let (in_cmd, _) = completer.get_completion_context("echo \"$(echo hell", 17);
|
let (ctx, _) = completer.get_completion_context("echo \"$(echo hell", 17);
|
||||||
assert!(!in_cmd, "Should be in argument context inside $()");
|
assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context inside $()");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -358,8 +359,8 @@ fn context_detection_pipe() {
|
|||||||
let completer = Completer::new();
|
let completer = Completer::new();
|
||||||
|
|
||||||
// After pipe - command context
|
// After pipe - command context
|
||||||
let (in_cmd, _) = completer.get_completion_context("ls | gre", 8);
|
let (ctx, _) = completer.get_completion_context("ls | gre", 8);
|
||||||
assert!(in_cmd, "Should be in command context after pipe");
|
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after pipe");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -367,12 +368,74 @@ fn context_detection_command_sep() {
|
|||||||
let completer = Completer::new();
|
let completer = Completer::new();
|
||||||
|
|
||||||
// After semicolon - command context
|
// After semicolon - command context
|
||||||
let (in_cmd, _) = completer.get_completion_context("echo foo; l", 11);
|
let (ctx, _) = completer.get_completion_context("echo foo; l", 11);
|
||||||
assert!(in_cmd, "Should be in command context after semicolon");
|
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after semicolon");
|
||||||
|
|
||||||
// After && - command context
|
// After && - command context
|
||||||
let (in_cmd, _) = completer.get_completion_context("true && l", 9);
|
let (ctx, _) = completer.get_completion_context("true && l", 9);
|
||||||
assert!(in_cmd, "Should be in command context after &&");
|
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after &&");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn context_detection_variable_substitution() {
|
||||||
|
let completer = Completer::new();
|
||||||
|
|
||||||
|
// $VAR at argument position - VAR_SUB should take priority over ARG
|
||||||
|
let (ctx, _) = completer.get_completion_context("echo $HOM", 9);
|
||||||
|
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "Should be in var_sub context for $HOM");
|
||||||
|
|
||||||
|
// $VAR at command position - VAR_SUB should take priority over COMMAND
|
||||||
|
let (ctx, _) = completer.get_completion_context("$HOM", 4);
|
||||||
|
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "Should be in var_sub context for bare $HOM");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn context_detection_variable_in_double_quotes() {
|
||||||
|
let completer = Completer::new();
|
||||||
|
|
||||||
|
// $VAR inside double quotes
|
||||||
|
let (ctx, _) = completer.get_completion_context("echo \"$HOM", 10);
|
||||||
|
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "Should be in var_sub context inside double quotes");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn context_detection_stack_base_is_null() {
|
||||||
|
let completer = Completer::new();
|
||||||
|
|
||||||
|
// Empty input - only NULL on the stack
|
||||||
|
let (ctx, _) = completer.get_completion_context("", 0);
|
||||||
|
assert_eq!(ctx, vec![markers::NULL], "Empty input should only have NULL marker");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn context_detection_context_start_position() {
|
||||||
|
let completer = Completer::new();
|
||||||
|
|
||||||
|
// Command at start - ctx_start should be 0
|
||||||
|
let (_, ctx_start) = completer.get_completion_context("ech", 3);
|
||||||
|
assert_eq!(ctx_start, 0, "Command at start should have ctx_start=0");
|
||||||
|
|
||||||
|
// Argument after command - ctx_start should be at arg position
|
||||||
|
let (_, ctx_start) = completer.get_completion_context("echo hel", 8);
|
||||||
|
assert_eq!(ctx_start, 5, "Argument ctx_start should point to arg start");
|
||||||
|
|
||||||
|
// Variable sub - ctx_start should point to the $
|
||||||
|
let (_, ctx_start) = completer.get_completion_context("echo $HOM", 9);
|
||||||
|
assert_eq!(ctx_start, 5, "Var sub ctx_start should point to the $");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn context_detection_priority_ordering() {
|
||||||
|
let completer = Completer::new();
|
||||||
|
|
||||||
|
// COMMAND (priority 2) should override ARG (priority 1)
|
||||||
|
// After a pipe, the next token is a command even though it looks like an arg
|
||||||
|
let (ctx, _) = completer.get_completion_context("echo foo | gr", 13);
|
||||||
|
assert_eq!(ctx.last(), Some(&markers::COMMAND), "COMMAND should win over ARG after pipe");
|
||||||
|
|
||||||
|
// VAR_SUB (priority 3) should override COMMAND (priority 2)
|
||||||
|
let (ctx, _) = completer.get_completion_context("$PA", 3);
|
||||||
|
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "VAR_SUB should win over COMMAND");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user