Added rustfmt.toml, formatted codebase
This commit is contained in:
7
rustfmt.toml
Normal file
7
rustfmt.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
max_width = 100
|
||||
tab_spaces = 2
|
||||
edition = "2021"
|
||||
|
||||
newline_style = "Unix"
|
||||
|
||||
wrap_comments = true
|
||||
@@ -1,95 +1,103 @@
|
||||
use crate::{jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state::{self, read_logic, write_logic}};
|
||||
use crate::{
|
||||
jobs::JobBldr,
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
parse::{NdRule, Node},
|
||||
prelude::*,
|
||||
procio::{borrow_fd, IoStack},
|
||||
state::{self, read_logic, write_logic},
|
||||
};
|
||||
|
||||
use super::setup_builtin;
|
||||
|
||||
pub fn alias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
|
||||
let NdRule::Command { assignments: _, argv } = node.class else {
|
||||
unreachable!()
|
||||
};
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
argv,
|
||||
} = node.class
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let (argv,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?;
|
||||
let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||
|
||||
if argv.is_empty() {
|
||||
// Display the environment variables
|
||||
let mut alias_output = read_logic(|l| {
|
||||
l.aliases()
|
||||
.iter()
|
||||
.map(|ent| format!("{} = \"{}\"", ent.0, ent.1))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
alias_output.sort(); // Sort them alphabetically
|
||||
let mut alias_output = alias_output.join("\n"); // Join them with newlines
|
||||
alias_output.push('\n'); // Push a final newline
|
||||
if argv.is_empty() {
|
||||
// Display the environment variables
|
||||
let mut alias_output = read_logic(|l| {
|
||||
l.aliases()
|
||||
.iter()
|
||||
.map(|ent| format!("{} = \"{}\"", ent.0, ent.1))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
alias_output.sort(); // Sort them alphabetically
|
||||
let mut alias_output = alias_output.join("\n"); // Join them with newlines
|
||||
alias_output.push('\n'); // Push a final newline
|
||||
|
||||
let stdout = borrow_fd(STDOUT_FILENO);
|
||||
write(stdout, alias_output.as_bytes())?; // Write it
|
||||
} else {
|
||||
for (arg,span) in argv {
|
||||
if arg == "command" || arg == "builtin" {
|
||||
return Err(
|
||||
ShErr::full(
|
||||
ShErrKind::ExecFail,
|
||||
format!("alias: Cannot assign alias to reserved name '{arg}'"),
|
||||
span
|
||||
)
|
||||
)
|
||||
}
|
||||
let stdout = borrow_fd(STDOUT_FILENO);
|
||||
write(stdout, alias_output.as_bytes())?; // Write it
|
||||
} else {
|
||||
for (arg, span) in argv {
|
||||
if arg == "command" || arg == "builtin" {
|
||||
return Err(ShErr::full(
|
||||
ShErrKind::ExecFail,
|
||||
format!("alias: Cannot assign alias to reserved name '{arg}'"),
|
||||
span,
|
||||
));
|
||||
}
|
||||
|
||||
let Some((name,body)) = arg.split_once('=') else {
|
||||
return Err(
|
||||
ShErr::full(
|
||||
ShErrKind::SyntaxErr,
|
||||
"alias: Expected an assignment in alias args",
|
||||
span
|
||||
)
|
||||
)
|
||||
};
|
||||
write_logic(|l| l.insert_alias(name, body));
|
||||
}
|
||||
}
|
||||
io_frame.unwrap().restore()?;
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
let Some((name, body)) = arg.split_once('=') else {
|
||||
return Err(ShErr::full(
|
||||
ShErrKind::SyntaxErr,
|
||||
"alias: Expected an assignment in alias args",
|
||||
span,
|
||||
));
|
||||
};
|
||||
write_logic(|l| l.insert_alias(name, body));
|
||||
}
|
||||
}
|
||||
io_frame.unwrap().restore()?;
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unalias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
|
||||
let NdRule::Command { assignments: _, argv } = node.class else {
|
||||
unreachable!()
|
||||
};
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
argv,
|
||||
} = node.class
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||
let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||
|
||||
if argv.is_empty() {
|
||||
// Display the environment variables
|
||||
let mut alias_output = read_logic(|l| {
|
||||
l.aliases()
|
||||
.iter()
|
||||
.map(|ent| format!("{} = \"{}\"", ent.0, ent.1))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
alias_output.sort(); // Sort them alphabetically
|
||||
let mut alias_output = alias_output.join("\n"); // Join them with newlines
|
||||
alias_output.push('\n'); // Push a final newline
|
||||
|
||||
if argv.is_empty() {
|
||||
// Display the environment variables
|
||||
let mut alias_output = read_logic(|l| {
|
||||
l.aliases()
|
||||
.iter()
|
||||
.map(|ent| format!("{} = \"{}\"", ent.0, ent.1))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
alias_output.sort(); // Sort them alphabetically
|
||||
let mut alias_output = alias_output.join("\n"); // Join them with newlines
|
||||
alias_output.push('\n'); // Push a final newline
|
||||
|
||||
let stdout = borrow_fd(STDOUT_FILENO);
|
||||
write(stdout, alias_output.as_bytes())?; // Write it
|
||||
} else {
|
||||
for (arg,span) in argv {
|
||||
flog!(DEBUG, arg);
|
||||
if read_logic(|l| l.get_alias(&arg)).is_none() {
|
||||
return Err(
|
||||
ShErr::full(
|
||||
ShErrKind::SyntaxErr,
|
||||
format!("unalias: alias '{arg}' not found"),
|
||||
span
|
||||
)
|
||||
)
|
||||
};
|
||||
write_logic(|l| l.remove_alias(&arg))
|
||||
}
|
||||
}
|
||||
io_frame.unwrap().restore()?;
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
let stdout = borrow_fd(STDOUT_FILENO);
|
||||
write(stdout, alias_output.as_bytes())?; // Write it
|
||||
} else {
|
||||
for (arg, span) in argv {
|
||||
flog!(DEBUG, arg);
|
||||
if read_logic(|l| l.get_alias(&arg)).is_none() {
|
||||
return Err(ShErr::full(
|
||||
ShErrKind::SyntaxErr,
|
||||
format!("unalias: alias '{arg}' not found"),
|
||||
span,
|
||||
));
|
||||
};
|
||||
write_logic(|l| l.remove_alias(&arg))
|
||||
}
|
||||
}
|
||||
io_frame.unwrap().restore()?;
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,45 +1,51 @@
|
||||
use crate::{jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, prelude::*, state::{self}};
|
||||
use crate::{
|
||||
jobs::JobBldr,
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
parse::{NdRule, Node},
|
||||
prelude::*,
|
||||
state::{self},
|
||||
};
|
||||
|
||||
use super::setup_builtin;
|
||||
|
||||
pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
|
||||
let span = node.get_span();
|
||||
let NdRule::Command { assignments: _, argv } = node.class else {
|
||||
unreachable!()
|
||||
};
|
||||
let span = node.get_span();
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
argv,
|
||||
} = node.class
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let (argv,_) = setup_builtin(argv,job,None)?;
|
||||
let (argv, _) = setup_builtin(argv, job, None)?;
|
||||
|
||||
let new_dir = if let Some((arg,_)) = argv.into_iter().next() {
|
||||
PathBuf::from(arg)
|
||||
} else {
|
||||
PathBuf::from(env::var("HOME").unwrap())
|
||||
};
|
||||
let new_dir = if let Some((arg, _)) = argv.into_iter().next() {
|
||||
PathBuf::from(arg)
|
||||
} else {
|
||||
PathBuf::from(env::var("HOME").unwrap())
|
||||
};
|
||||
|
||||
if !new_dir.exists() {
|
||||
return Err(
|
||||
ShErr::full(
|
||||
ShErrKind::ExecFail,
|
||||
format!("cd: No such file or directory '{}'",new_dir.display()),
|
||||
span,
|
||||
)
|
||||
)
|
||||
}
|
||||
if !new_dir.exists() {
|
||||
return Err(ShErr::full(
|
||||
ShErrKind::ExecFail,
|
||||
format!("cd: No such file or directory '{}'", new_dir.display()),
|
||||
span,
|
||||
));
|
||||
}
|
||||
|
||||
if !new_dir.is_dir() {
|
||||
return Err(
|
||||
ShErr::full(
|
||||
ShErrKind::ExecFail,
|
||||
format!("cd: Not a directory '{}'",new_dir.display()),
|
||||
span,
|
||||
)
|
||||
)
|
||||
}
|
||||
if !new_dir.is_dir() {
|
||||
return Err(ShErr::full(
|
||||
ShErrKind::ExecFail,
|
||||
format!("cd: Not a directory '{}'", new_dir.display()),
|
||||
span,
|
||||
));
|
||||
}
|
||||
|
||||
env::set_current_dir(new_dir).unwrap();
|
||||
let new_dir = env::current_dir().unwrap();
|
||||
env::set_var("PWD", new_dir);
|
||||
env::set_current_dir(new_dir).unwrap();
|
||||
let new_dir = env::current_dir().unwrap();
|
||||
env::set_var("PWD", new_dir);
|
||||
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,77 +1,90 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use crate::{builtin::setup_builtin, getopt::{get_opts_from_tokens, Opt, OptSet}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state};
|
||||
use crate::{
|
||||
builtin::setup_builtin,
|
||||
getopt::{get_opts_from_tokens, Opt, OptSet},
|
||||
jobs::JobBldr,
|
||||
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
||||
parse::{NdRule, Node},
|
||||
prelude::*,
|
||||
procio::{borrow_fd, IoStack},
|
||||
state,
|
||||
};
|
||||
|
||||
pub static ECHO_OPTS: LazyLock<OptSet> = LazyLock::new(|| {[
|
||||
Opt::Short('n'),
|
||||
Opt::Short('E'),
|
||||
Opt::Short('e'),
|
||||
Opt::Short('r'),
|
||||
].into()});
|
||||
pub static ECHO_OPTS: LazyLock<OptSet> = LazyLock::new(|| {
|
||||
[
|
||||
Opt::Short('n'),
|
||||
Opt::Short('E'),
|
||||
Opt::Short('e'),
|
||||
Opt::Short('r'),
|
||||
]
|
||||
.into()
|
||||
});
|
||||
|
||||
bitflags! {
|
||||
pub struct EchoFlags: u32 {
|
||||
const NO_NEWLINE = 0b000001;
|
||||
const USE_STDERR = 0b000010;
|
||||
const USE_ESCAPE = 0b000100;
|
||||
}
|
||||
pub struct EchoFlags: u32 {
|
||||
const NO_NEWLINE = 0b000001;
|
||||
const USE_STDERR = 0b000010;
|
||||
const USE_ESCAPE = 0b000100;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
|
||||
let blame = node.get_span().clone();
|
||||
let NdRule::Command { assignments: _, argv } = node.class else {
|
||||
unreachable!()
|
||||
};
|
||||
assert!(!argv.is_empty());
|
||||
let (argv,opts) = get_opts_from_tokens(argv);
|
||||
let flags = get_echo_flags(opts).blame(blame)?;
|
||||
let (argv,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?;
|
||||
let blame = node.get_span().clone();
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
argv,
|
||||
} = node.class
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
assert!(!argv.is_empty());
|
||||
let (argv, opts) = get_opts_from_tokens(argv);
|
||||
let flags = get_echo_flags(opts).blame(blame)?;
|
||||
let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||
|
||||
let output_channel = if flags.contains(EchoFlags::USE_STDERR) {
|
||||
borrow_fd(STDERR_FILENO)
|
||||
} else {
|
||||
borrow_fd(STDOUT_FILENO)
|
||||
};
|
||||
let output_channel = if flags.contains(EchoFlags::USE_STDERR) {
|
||||
borrow_fd(STDERR_FILENO)
|
||||
} else {
|
||||
borrow_fd(STDOUT_FILENO)
|
||||
};
|
||||
|
||||
let mut echo_output = argv.into_iter()
|
||||
.map(|a| a.0) // Extract the String from the tuple of (String,Span)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let mut echo_output = argv
|
||||
.into_iter()
|
||||
.map(|a| a.0) // Extract the String from the tuple of (String,Span)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
if !flags.contains(EchoFlags::NO_NEWLINE) {
|
||||
echo_output.push('\n')
|
||||
}
|
||||
if !flags.contains(EchoFlags::NO_NEWLINE) {
|
||||
echo_output.push('\n')
|
||||
}
|
||||
|
||||
write(output_channel, echo_output.as_bytes())?;
|
||||
write(output_channel, echo_output.as_bytes())?;
|
||||
|
||||
io_frame.unwrap().restore()?;
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
io_frame.unwrap().restore()?;
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_echo_flags(mut opts: Vec<Opt>) -> ShResult<EchoFlags> {
|
||||
let mut flags = EchoFlags::empty();
|
||||
let mut flags = EchoFlags::empty();
|
||||
|
||||
while let Some(opt) = opts.pop() {
|
||||
if !ECHO_OPTS.contains(&opt) {
|
||||
return Err(
|
||||
ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
format!("echo: Unexpected flag '{opt}'"),
|
||||
)
|
||||
)
|
||||
}
|
||||
let Opt::Short(opt) = opt else {
|
||||
unreachable!()
|
||||
};
|
||||
while let Some(opt) = opts.pop() {
|
||||
if !ECHO_OPTS.contains(&opt) {
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
format!("echo: Unexpected flag '{opt}'"),
|
||||
));
|
||||
}
|
||||
let Opt::Short(opt) = opt else { unreachable!() };
|
||||
|
||||
match opt {
|
||||
'n' => flags |= EchoFlags::NO_NEWLINE,
|
||||
'r' => flags |= EchoFlags::USE_STDERR,
|
||||
'e' => flags |= EchoFlags::USE_ESCAPE,
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
match opt {
|
||||
'n' => flags |= EchoFlags::NO_NEWLINE,
|
||||
'r' => flags |= EchoFlags::USE_STDERR,
|
||||
'e' => flags |= EchoFlags::USE_ESCAPE,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(flags)
|
||||
Ok(flags)
|
||||
}
|
||||
|
||||
@@ -1,35 +1,48 @@
|
||||
use crate::{jobs::JobBldr, libsh::error::ShResult, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state::{self, write_vars}};
|
||||
use crate::{
|
||||
jobs::JobBldr,
|
||||
libsh::error::ShResult,
|
||||
parse::{NdRule, Node},
|
||||
prelude::*,
|
||||
procio::{borrow_fd, IoStack},
|
||||
state::{self, write_vars},
|
||||
};
|
||||
|
||||
use super::setup_builtin;
|
||||
|
||||
pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
|
||||
let NdRule::Command { assignments: _, argv } = node.class else {
|
||||
unreachable!()
|
||||
};
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
argv,
|
||||
} = node.class
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let (argv,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?;
|
||||
let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||
|
||||
if argv.is_empty() {
|
||||
// Display the environment variables
|
||||
let mut env_output = env::vars()
|
||||
.map(|var| format!("{}={}",var.0,var.1)) // Get all of them, zip them into one string
|
||||
.collect::<Vec<_>>();
|
||||
env_output.sort(); // Sort them alphabetically
|
||||
let mut env_output = env_output.join("\n"); // Join them with newlines
|
||||
env_output.push('\n'); // Push a final newline
|
||||
if argv.is_empty() {
|
||||
// Display the environment variables
|
||||
let mut env_output = env::vars()
|
||||
.map(|var| format!("{}={}", var.0, var.1)) // Get all of them, zip them into one string
|
||||
.collect::<Vec<_>>();
|
||||
env_output.sort(); // Sort them alphabetically
|
||||
let mut env_output = env_output.join("\n"); // Join them with newlines
|
||||
env_output.push('\n'); // Push a final newline
|
||||
|
||||
let stdout = borrow_fd(STDOUT_FILENO);
|
||||
write(stdout, env_output.as_bytes())?; // Write it
|
||||
} else {
|
||||
for (arg,_) in argv {
|
||||
if let Some((var,val)) = arg.split_once('=') {
|
||||
write_vars(|v| v.set_var(var, val, true)); // Export an assignment like 'foo=bar'
|
||||
} else {
|
||||
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if any
|
||||
}
|
||||
}
|
||||
}
|
||||
io_frame.unwrap().restore()?;
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
let stdout = borrow_fd(STDOUT_FILENO);
|
||||
write(stdout, env_output.as_bytes())?; // Write it
|
||||
} else {
|
||||
for (arg, _) in argv {
|
||||
if let Some((var, val)) = arg.split_once('=') {
|
||||
write_vars(|v| v.set_var(var, val, true)); // Export an assignment like
|
||||
// 'foo=bar'
|
||||
} else {
|
||||
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if
|
||||
// any
|
||||
}
|
||||
}
|
||||
}
|
||||
io_frame.unwrap().restore()?;
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,39 +1,46 @@
|
||||
use crate::{libsh::error::{ShErr, ShErrKind, ShResult}, parse::{execute::prepare_argv, NdRule, Node}, prelude::*};
|
||||
use crate::{
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
parse::{execute::prepare_argv, NdRule, Node},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
|
||||
use ShErrKind::*;
|
||||
let NdRule::Command { assignments: _, argv } = node.class else {
|
||||
unreachable!()
|
||||
};
|
||||
let mut code = 0;
|
||||
use ShErrKind::*;
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
argv,
|
||||
} = node.class
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let mut code = 0;
|
||||
|
||||
let mut argv = prepare_argv(argv)?;
|
||||
let cmd = argv.remove(0).0;
|
||||
let mut argv = prepare_argv(argv)?;
|
||||
let cmd = argv.remove(0).0;
|
||||
|
||||
if !argv.is_empty() {
|
||||
let (arg,span) = argv
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
if !argv.is_empty() {
|
||||
let (arg, span) = argv.into_iter().next().unwrap();
|
||||
|
||||
let Ok(status) = arg.parse::<i32>() else {
|
||||
return Err(
|
||||
ShErr::full(ShErrKind::SyntaxErr, format!("{cmd}: Expected a number"), span)
|
||||
)
|
||||
};
|
||||
let Ok(status) = arg.parse::<i32>() else {
|
||||
return Err(ShErr::full(
|
||||
ShErrKind::SyntaxErr,
|
||||
format!("{cmd}: Expected a number"),
|
||||
span,
|
||||
));
|
||||
};
|
||||
|
||||
code = status;
|
||||
}
|
||||
code = status;
|
||||
}
|
||||
|
||||
flog!(DEBUG,code);
|
||||
flog!(DEBUG, code);
|
||||
|
||||
let kind = match kind {
|
||||
LoopContinue(_) => LoopContinue(code),
|
||||
LoopBreak(_) => LoopBreak(code),
|
||||
FuncReturn(_) => FuncReturn(code),
|
||||
CleanExit(_) => CleanExit(code),
|
||||
_ => unreachable!()
|
||||
};
|
||||
let kind = match kind {
|
||||
LoopContinue(_) => LoopContinue(code),
|
||||
LoopBreak(_) => LoopBreak(code),
|
||||
FuncReturn(_) => FuncReturn(code),
|
||||
CleanExit(_) => CleanExit(code),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Err(ShErr::simple(kind, ""))
|
||||
Err(ShErr::simple(kind, ""))
|
||||
}
|
||||
|
||||
@@ -1,183 +1,182 @@
|
||||
use crate::{jobs::{JobBldr, JobCmdFlags, JobID}, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{lex::Span, NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state::{self, read_jobs, write_jobs}};
|
||||
use crate::{
|
||||
jobs::{JobBldr, JobCmdFlags, JobID},
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
parse::{lex::Span, NdRule, Node},
|
||||
prelude::*,
|
||||
procio::{borrow_fd, IoStack},
|
||||
state::{self, read_jobs, write_jobs},
|
||||
};
|
||||
|
||||
use super::setup_builtin;
|
||||
|
||||
pub enum JobBehavior {
|
||||
Foregound,
|
||||
Background
|
||||
Foregound,
|
||||
Background,
|
||||
}
|
||||
|
||||
pub fn continue_job(node: Node, job: &mut JobBldr, behavior: JobBehavior) -> ShResult<()> {
|
||||
let blame = node.get_span().clone();
|
||||
let cmd = match behavior {
|
||||
JobBehavior::Foregound => "fg",
|
||||
JobBehavior::Background => "bg"
|
||||
};
|
||||
let NdRule::Command { assignments: _, argv } = node.class else {
|
||||
unreachable!()
|
||||
};
|
||||
let blame = node.get_span().clone();
|
||||
let cmd = match behavior {
|
||||
JobBehavior::Foregound => "fg",
|
||||
JobBehavior::Background => "bg",
|
||||
};
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
argv,
|
||||
} = node.class
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let (argv,_) = setup_builtin(argv, job, None)?;
|
||||
let mut argv = argv.into_iter();
|
||||
let (argv, _) = setup_builtin(argv, job, None)?;
|
||||
let mut argv = argv.into_iter();
|
||||
|
||||
if read_jobs(|j| j.get_fg().is_some()) {
|
||||
return Err(
|
||||
ShErr::full(
|
||||
ShErrKind::InternalErr,
|
||||
format!("Somehow called '{}' with an existing foreground job",cmd),
|
||||
blame
|
||||
)
|
||||
)
|
||||
}
|
||||
if read_jobs(|j| j.get_fg().is_some()) {
|
||||
return Err(ShErr::full(
|
||||
ShErrKind::InternalErr,
|
||||
format!("Somehow called '{}' with an existing foreground job", cmd),
|
||||
blame,
|
||||
));
|
||||
}
|
||||
|
||||
let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) {
|
||||
id
|
||||
} else {
|
||||
return Err(
|
||||
ShErr::full(
|
||||
ShErrKind::ExecFail,
|
||||
"No jobs found",
|
||||
blame
|
||||
)
|
||||
)
|
||||
};
|
||||
let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) {
|
||||
id
|
||||
} else {
|
||||
return Err(ShErr::full(ShErrKind::ExecFail, "No jobs found", blame));
|
||||
};
|
||||
|
||||
let tabid = match argv.next() {
|
||||
Some((arg,blame)) => parse_job_id(&arg, blame)?,
|
||||
None => curr_job_id
|
||||
};
|
||||
let tabid = match argv.next() {
|
||||
Some((arg, blame)) => parse_job_id(&arg, blame)?,
|
||||
None => curr_job_id,
|
||||
};
|
||||
|
||||
let mut job = write_jobs(|j| {
|
||||
let id = JobID::TableID(tabid);
|
||||
let query_result = j.query(id.clone());
|
||||
if query_result.is_some() {
|
||||
Ok(j.remove_job(id.clone()).unwrap())
|
||||
} else {
|
||||
Err(
|
||||
ShErr::full(
|
||||
ShErrKind::ExecFail,
|
||||
format!("Job id `{}' not found", tabid),
|
||||
blame
|
||||
)
|
||||
)
|
||||
}
|
||||
})?;
|
||||
let mut job = write_jobs(|j| {
|
||||
let id = JobID::TableID(tabid);
|
||||
let query_result = j.query(id.clone());
|
||||
if query_result.is_some() {
|
||||
Ok(j.remove_job(id.clone()).unwrap())
|
||||
} else {
|
||||
Err(ShErr::full(
|
||||
ShErrKind::ExecFail,
|
||||
format!("Job id `{}' not found", tabid),
|
||||
blame,
|
||||
))
|
||||
}
|
||||
})?;
|
||||
|
||||
job.killpg(Signal::SIGCONT)?;
|
||||
job.killpg(Signal::SIGCONT)?;
|
||||
|
||||
match behavior {
|
||||
JobBehavior::Foregound => {
|
||||
write_jobs(|j| j.new_fg(job))?;
|
||||
}
|
||||
JobBehavior::Background => {
|
||||
let job_order = read_jobs(|j| j.order().to_vec());
|
||||
write(borrow_fd(1), job.display(&job_order, JobCmdFlags::PIDS).as_bytes())?;
|
||||
write_jobs(|j| j.insert_job(job, true))?;
|
||||
}
|
||||
}
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
match behavior {
|
||||
JobBehavior::Foregound => {
|
||||
write_jobs(|j| j.new_fg(job))?;
|
||||
}
|
||||
JobBehavior::Background => {
|
||||
let job_order = read_jobs(|j| j.order().to_vec());
|
||||
write(
|
||||
borrow_fd(1),
|
||||
job.display(&job_order, JobCmdFlags::PIDS).as_bytes(),
|
||||
)?;
|
||||
write_jobs(|j| j.insert_job(job, true))?;
|
||||
}
|
||||
}
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_job_id(arg: &str, blame: Span) -> ShResult<usize> {
|
||||
if arg.starts_with('%') {
|
||||
let arg = arg.strip_prefix('%').unwrap();
|
||||
if arg.chars().all(|ch| ch.is_ascii_digit()) {
|
||||
Ok(arg.parse::<usize>().unwrap())
|
||||
} else {
|
||||
let result = write_jobs(|j| {
|
||||
let query_result = j.query(JobID::Command(arg.into()));
|
||||
query_result.map(|job| job.tabid().unwrap())
|
||||
});
|
||||
match result {
|
||||
Some(id) => Ok(id),
|
||||
None => Err(
|
||||
ShErr::full(
|
||||
ShErrKind::InternalErr,
|
||||
"Found a job but no table id in parse_job_id()",
|
||||
blame
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if arg.chars().all(|ch| ch.is_ascii_digit()) {
|
||||
let result = write_jobs(|j| {
|
||||
let pgid_query_result = j.query(JobID::Pgid(Pid::from_raw(arg.parse::<i32>().unwrap())));
|
||||
if let Some(job) = pgid_query_result {
|
||||
return Some(job.tabid().unwrap())
|
||||
}
|
||||
if arg.starts_with('%') {
|
||||
let arg = arg.strip_prefix('%').unwrap();
|
||||
if arg.chars().all(|ch| ch.is_ascii_digit()) {
|
||||
Ok(arg.parse::<usize>().unwrap())
|
||||
} else {
|
||||
let result = write_jobs(|j| {
|
||||
let query_result = j.query(JobID::Command(arg.into()));
|
||||
query_result.map(|job| job.tabid().unwrap())
|
||||
});
|
||||
match result {
|
||||
Some(id) => Ok(id),
|
||||
None => Err(ShErr::full(
|
||||
ShErrKind::InternalErr,
|
||||
"Found a job but no table id in parse_job_id()",
|
||||
blame,
|
||||
)),
|
||||
}
|
||||
}
|
||||
} else if arg.chars().all(|ch| ch.is_ascii_digit()) {
|
||||
let result = write_jobs(|j| {
|
||||
let pgid_query_result = j.query(JobID::Pgid(Pid::from_raw(arg.parse::<i32>().unwrap())));
|
||||
if let Some(job) = pgid_query_result {
|
||||
return Some(job.tabid().unwrap());
|
||||
}
|
||||
|
||||
if arg.parse::<i32>().unwrap() > 0 {
|
||||
let table_id_query_result = j.query(JobID::TableID(arg.parse::<usize>().unwrap()));
|
||||
return table_id_query_result.map(|job| job.tabid().unwrap());
|
||||
}
|
||||
if arg.parse::<i32>().unwrap() > 0 {
|
||||
let table_id_query_result = j.query(JobID::TableID(arg.parse::<usize>().unwrap()));
|
||||
return table_id_query_result.map(|job| job.tabid().unwrap());
|
||||
}
|
||||
|
||||
None
|
||||
});
|
||||
None
|
||||
});
|
||||
|
||||
match result {
|
||||
Some(id) => Ok(id),
|
||||
None => Err(
|
||||
ShErr::full(
|
||||
ShErrKind::InternalErr,
|
||||
"Found a job but no table id in parse_job_id()",
|
||||
blame
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Err(
|
||||
ShErr::full(
|
||||
ShErrKind::SyntaxErr,
|
||||
format!("Invalid fd arg: {}", arg),
|
||||
blame
|
||||
)
|
||||
)
|
||||
}
|
||||
match result {
|
||||
Some(id) => Ok(id),
|
||||
None => Err(ShErr::full(
|
||||
ShErrKind::InternalErr,
|
||||
"Found a job but no table id in parse_job_id()",
|
||||
blame,
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Err(ShErr::full(
|
||||
ShErrKind::SyntaxErr,
|
||||
format!("Invalid fd arg: {}", arg),
|
||||
blame,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
|
||||
let NdRule::Command { assignments: _, argv } = node.class else {
|
||||
unreachable!()
|
||||
};
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
argv,
|
||||
} = node.class
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let (argv,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?;
|
||||
let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||
|
||||
let mut flags = JobCmdFlags::empty();
|
||||
for (arg,span) in argv {
|
||||
let mut chars = arg.chars().peekable();
|
||||
if chars.peek().is_none_or(|ch| *ch != '-') {
|
||||
return Err(
|
||||
ShErr::full(
|
||||
ShErrKind::SyntaxErr,
|
||||
"Invalid flag in jobs call",
|
||||
span
|
||||
)
|
||||
)
|
||||
}
|
||||
chars.next();
|
||||
for ch in chars {
|
||||
let flag = match ch {
|
||||
'l' => JobCmdFlags::LONG,
|
||||
'p' => JobCmdFlags::PIDS,
|
||||
'n' => JobCmdFlags::NEW_ONLY,
|
||||
'r' => JobCmdFlags::RUNNING,
|
||||
's' => JobCmdFlags::STOPPED,
|
||||
_ => return Err(
|
||||
ShErr::full(
|
||||
ShErrKind::SyntaxErr,
|
||||
"Invalid flag in jobs call",
|
||||
span
|
||||
)
|
||||
)
|
||||
let mut flags = JobCmdFlags::empty();
|
||||
for (arg, span) in argv {
|
||||
let mut chars = arg.chars().peekable();
|
||||
if chars.peek().is_none_or(|ch| *ch != '-') {
|
||||
return Err(ShErr::full(
|
||||
ShErrKind::SyntaxErr,
|
||||
"Invalid flag in jobs call",
|
||||
span,
|
||||
));
|
||||
}
|
||||
chars.next();
|
||||
for ch in chars {
|
||||
let flag = match ch {
|
||||
'l' => JobCmdFlags::LONG,
|
||||
'p' => JobCmdFlags::PIDS,
|
||||
'n' => JobCmdFlags::NEW_ONLY,
|
||||
'r' => JobCmdFlags::RUNNING,
|
||||
's' => JobCmdFlags::STOPPED,
|
||||
_ => {
|
||||
return Err(ShErr::full(
|
||||
ShErrKind::SyntaxErr,
|
||||
"Invalid flag in jobs call",
|
||||
span,
|
||||
))
|
||||
}
|
||||
};
|
||||
flags |= flag
|
||||
}
|
||||
}
|
||||
write_jobs(|j| j.print_jobs(flags))?;
|
||||
io_frame.unwrap().restore()?;
|
||||
state::set_status(0);
|
||||
|
||||
};
|
||||
flags |= flag
|
||||
}
|
||||
}
|
||||
write_jobs(|j| j.print_jobs(flags))?;
|
||||
io_frame.unwrap().restore()?;
|
||||
state::set_status(0);
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,50 +1,45 @@
|
||||
use nix::unistd::Pid;
|
||||
|
||||
use crate::{jobs::{ChildProc, JobBldr}, libsh::error::ShResult, parse::{execute::prepare_argv, lex::{Span, Tk}, Redir}, procio::{IoFrame, IoStack}};
|
||||
use crate::{
|
||||
jobs::{ChildProc, JobBldr},
|
||||
libsh::error::ShResult,
|
||||
parse::{
|
||||
execute::prepare_argv,
|
||||
lex::{Span, Tk},
|
||||
Redir,
|
||||
},
|
||||
procio::{IoFrame, IoStack},
|
||||
};
|
||||
|
||||
pub mod echo;
|
||||
pub mod cd;
|
||||
pub mod export;
|
||||
pub mod pwd;
|
||||
pub mod source;
|
||||
pub mod shift;
|
||||
pub mod jobctl;
|
||||
pub mod alias;
|
||||
pub mod cd;
|
||||
pub mod echo;
|
||||
pub mod export;
|
||||
pub mod flowctl;
|
||||
pub mod zoltraak;
|
||||
pub mod jobctl;
|
||||
pub mod pwd;
|
||||
pub mod shift;
|
||||
pub mod shopt;
|
||||
pub mod test; // [[ ]] thing
|
||||
pub mod source;
|
||||
pub mod test;
|
||||
pub mod zoltraak; // [[ ]] thing
|
||||
|
||||
pub const BUILTINS: [&str;19] = [
|
||||
"echo",
|
||||
"cd",
|
||||
"export",
|
||||
"pwd",
|
||||
"source",
|
||||
"shift",
|
||||
"jobs",
|
||||
"fg",
|
||||
"bg",
|
||||
"alias",
|
||||
"unalias",
|
||||
"return",
|
||||
"break",
|
||||
"continue",
|
||||
"exit",
|
||||
"zoltraak",
|
||||
"shopt",
|
||||
"builtin",
|
||||
"command",
|
||||
pub const BUILTINS: [&str; 19] = [
|
||||
"echo", "cd", "export", "pwd", "source", "shift", "jobs", "fg", "bg", "alias", "unalias",
|
||||
"return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command",
|
||||
];
|
||||
|
||||
/// Sets up a builtin command
|
||||
///
|
||||
/// Prepares a builtin for execution by processing arguments, setting up redirections, and registering the command as a child process in the given `JobBldr`
|
||||
/// Prepares a builtin for execution by processing arguments, setting up
|
||||
/// redirections, and registering the command as a child process in the given
|
||||
/// `JobBldr`
|
||||
///
|
||||
/// # Parameters
|
||||
/// * argv - The vector of raw argument tokens
|
||||
/// * job - A mutable reference to a `JobBldr`
|
||||
/// * io_mode - An optional 2-tuple consisting of a mutable reference to an `IoStack` and a vector of `Redirs`
|
||||
/// * io_mode - An optional 2-tuple consisting of a mutable reference to an
|
||||
/// `IoStack` and a vector of `Redirs`
|
||||
///
|
||||
/// # Behavior
|
||||
/// * Cleans, expands, and word splits the arg vector
|
||||
@@ -56,36 +51,39 @@ pub const BUILTINS: [&str;19] = [
|
||||
/// * The popped `IoFrame`, if any
|
||||
///
|
||||
/// # Notes
|
||||
/// * If redirections are given to this function, the caller must call `IoFrame.restore()` on the returned `IoFrame`
|
||||
/// * If redirections are given, the second field of the resulting tuple will *always* be `Some()`
|
||||
/// * If redirections are given to this function, the caller must call
|
||||
/// `IoFrame.restore()` on the returned `IoFrame`
|
||||
/// * If redirections are given, the second field of the resulting tuple will
|
||||
/// *always* be `Some()`
|
||||
/// * If no redirections are given, the second field will *always* be `None`
|
||||
type SetupReturns = ShResult<(Vec<(String,Span)>, Option<IoFrame>)>;
|
||||
type SetupReturns = ShResult<(Vec<(String, Span)>, Option<IoFrame>)>;
|
||||
pub fn setup_builtin(
|
||||
argv: Vec<Tk>,
|
||||
job: &mut JobBldr,
|
||||
io_mode: Option<(&mut IoStack,Vec<Redir>)>,
|
||||
argv: Vec<Tk>,
|
||||
job: &mut JobBldr,
|
||||
io_mode: Option<(&mut IoStack, Vec<Redir>)>,
|
||||
) -> SetupReturns {
|
||||
let mut argv: Vec<(String,Span)> = prepare_argv(argv)?;
|
||||
let mut argv: Vec<(String, Span)> = prepare_argv(argv)?;
|
||||
|
||||
let child_pgid = if let Some(pgid) = job.pgid() {
|
||||
pgid
|
||||
} else {
|
||||
job.set_pgid(Pid::this());
|
||||
Pid::this()
|
||||
};
|
||||
let cmd_name = argv.remove(0).0;
|
||||
let child = ChildProc::new(Pid::this(), Some(&cmd_name), Some(child_pgid))?;
|
||||
job.push_child(child);
|
||||
let child_pgid = if let Some(pgid) = job.pgid() {
|
||||
pgid
|
||||
} else {
|
||||
job.set_pgid(Pid::this());
|
||||
Pid::this()
|
||||
};
|
||||
let cmd_name = argv.remove(0).0;
|
||||
let child = ChildProc::new(Pid::this(), Some(&cmd_name), Some(child_pgid))?;
|
||||
job.push_child(child);
|
||||
|
||||
let io_frame = if let Some((io_stack,redirs)) = io_mode {
|
||||
io_stack.append_to_frame(redirs);
|
||||
let mut io_frame = io_stack.pop_frame();
|
||||
io_frame.redirect()?;
|
||||
Some(io_frame)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let io_frame = if let Some((io_stack, redirs)) = io_mode {
|
||||
io_stack.append_to_frame(redirs);
|
||||
let mut io_frame = io_stack.pop_frame();
|
||||
io_frame.redirect()?;
|
||||
Some(io_frame)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// We return the io_frame because the caller needs to also call io_frame.restore()
|
||||
Ok((argv,io_frame))
|
||||
// We return the io_frame because the caller needs to also call
|
||||
// io_frame.restore()
|
||||
Ok((argv, io_frame))
|
||||
}
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
use crate::{jobs::JobBldr, libsh::error::ShResult, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state};
|
||||
use crate::{
|
||||
jobs::JobBldr,
|
||||
libsh::error::ShResult,
|
||||
parse::{NdRule, Node},
|
||||
prelude::*,
|
||||
procio::{borrow_fd, IoStack},
|
||||
state,
|
||||
};
|
||||
|
||||
use super::setup_builtin;
|
||||
|
||||
pub fn pwd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
|
||||
let NdRule::Command { assignments: _, argv } = node.class else {
|
||||
unreachable!()
|
||||
};
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
argv,
|
||||
} = node.class
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let (_,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?;
|
||||
let (_, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||
|
||||
let stdout = borrow_fd(STDOUT_FILENO);
|
||||
let stdout = borrow_fd(STDOUT_FILENO);
|
||||
|
||||
let mut curr_dir = env::current_dir().unwrap().to_str().unwrap().to_string();
|
||||
curr_dir.push('\n');
|
||||
write(stdout, curr_dir.as_bytes())?;
|
||||
let mut curr_dir = env::current_dir().unwrap().to_str().unwrap().to_string();
|
||||
curr_dir.push('\n');
|
||||
write(stdout, curr_dir.as_bytes())?;
|
||||
|
||||
io_frame.unwrap().restore().unwrap();
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
io_frame.unwrap().restore().unwrap();
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
use crate::{jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, state::{self, write_vars}};
|
||||
use crate::{
|
||||
jobs::JobBldr,
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
parse::{NdRule, Node},
|
||||
state::{self, write_vars},
|
||||
};
|
||||
|
||||
use super::setup_builtin;
|
||||
|
||||
pub fn shift(node: Node, job: &mut JobBldr) -> ShResult<()> {
|
||||
let NdRule::Command { assignments: _, argv } = node.class else {
|
||||
unreachable!()
|
||||
};
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
argv,
|
||||
} = node.class
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let (argv,_) = setup_builtin(argv, job, None)?;
|
||||
let mut argv = argv.into_iter();
|
||||
let (argv, _) = setup_builtin(argv, job, None)?;
|
||||
let mut argv = argv.into_iter();
|
||||
|
||||
if let Some((arg,span)) = argv.next() {
|
||||
let Ok(count) = arg.parse::<usize>() else {
|
||||
return Err(
|
||||
ShErr::full(
|
||||
ShErrKind::ExecFail,
|
||||
"Expected a number in shift args",
|
||||
span
|
||||
)
|
||||
)
|
||||
};
|
||||
for _ in 0..count {
|
||||
write_vars(|v| v.fpop_arg());
|
||||
}
|
||||
}
|
||||
if let Some((arg, span)) = argv.next() {
|
||||
let Ok(count) = arg.parse::<usize>() else {
|
||||
return Err(ShErr::full(
|
||||
ShErrKind::ExecFail,
|
||||
"Expected a number in shift args",
|
||||
span,
|
||||
));
|
||||
};
|
||||
for _ in 0..count {
|
||||
write_vars(|v| v.fpop_arg());
|
||||
}
|
||||
}
|
||||
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,30 +1,41 @@
|
||||
use crate::{jobs::JobBldr, libsh::error::{ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state::write_shopts};
|
||||
use crate::{
|
||||
jobs::JobBldr,
|
||||
libsh::error::{ShResult, ShResultExt},
|
||||
parse::{NdRule, Node},
|
||||
prelude::*,
|
||||
procio::{borrow_fd, IoStack},
|
||||
state::write_shopts,
|
||||
};
|
||||
|
||||
use super::setup_builtin;
|
||||
|
||||
pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
|
||||
let NdRule::Command { assignments: _, argv } = node.class else {
|
||||
unreachable!()
|
||||
};
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
argv,
|
||||
} = node.class
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let (argv,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?;
|
||||
let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||
|
||||
let mut io_frame = io_frame.unwrap();
|
||||
io_frame.redirect()?;
|
||||
for (arg,span) in argv {
|
||||
let Some(mut output) = write_shopts(|s| s.query(&arg)).blame(span)? else {
|
||||
continue
|
||||
};
|
||||
let mut io_frame = io_frame.unwrap();
|
||||
io_frame.redirect()?;
|
||||
for (arg, span) in argv {
|
||||
let Some(mut output) = write_shopts(|s| s.query(&arg)).blame(span)? else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let output_channel = borrow_fd(STDOUT_FILENO);
|
||||
output.push('\n');
|
||||
let output_channel = borrow_fd(STDOUT_FILENO);
|
||||
output.push('\n');
|
||||
|
||||
if let Err(e) = write(output_channel, output.as_bytes()) {
|
||||
io_frame.restore()?;
|
||||
return Err(e.into())
|
||||
}
|
||||
}
|
||||
io_frame.restore()?;
|
||||
if let Err(e) = write(output_channel, output.as_bytes()) {
|
||||
io_frame.restore()?;
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
io_frame.restore()?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,37 +1,43 @@
|
||||
use crate::{jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, prelude::*, state::{self, source_file}};
|
||||
use crate::{
|
||||
jobs::JobBldr,
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
parse::{NdRule, Node},
|
||||
prelude::*,
|
||||
state::{self, source_file},
|
||||
};
|
||||
|
||||
use super::setup_builtin;
|
||||
|
||||
pub fn source(node: Node, job: &mut JobBldr) -> ShResult<()> {
|
||||
let NdRule::Command { assignments: _, argv } = node.class else {
|
||||
unreachable!()
|
||||
};
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
argv,
|
||||
} = node.class
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let (argv,_) = setup_builtin(argv, job, None)?;
|
||||
let (argv, _) = setup_builtin(argv, job, None)?;
|
||||
|
||||
for (arg,span) in argv {
|
||||
let path = PathBuf::from(arg);
|
||||
if !path.exists() {
|
||||
return Err(
|
||||
ShErr::full(
|
||||
ShErrKind::ExecFail,
|
||||
format!("source: File '{}' not found",path.display()),
|
||||
span
|
||||
)
|
||||
);
|
||||
}
|
||||
if !path.is_file() {
|
||||
return Err(
|
||||
ShErr::full(
|
||||
ShErrKind::ExecFail,
|
||||
format!("source: Given path '{}' is not a file",path.display()),
|
||||
span
|
||||
)
|
||||
);
|
||||
}
|
||||
source_file(path)?;
|
||||
}
|
||||
for (arg, span) in argv {
|
||||
let path = PathBuf::from(arg);
|
||||
if !path.exists() {
|
||||
return Err(ShErr::full(
|
||||
ShErrKind::ExecFail,
|
||||
format!("source: File '{}' not found", path.display()),
|
||||
span,
|
||||
));
|
||||
}
|
||||
if !path.is_file() {
|
||||
return Err(ShErr::full(
|
||||
ShErrKind::ExecFail,
|
||||
format!("source: Given path '{}' is not a file", path.display()),
|
||||
span,
|
||||
));
|
||||
}
|
||||
source_file(path)?;
|
||||
}
|
||||
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,328 +1,321 @@
|
||||
use std::{fs::metadata, path::PathBuf, str::FromStr};
|
||||
|
||||
use nix::{sys::stat::{self, SFlag}, unistd::AccessFlags};
|
||||
use nix::{
|
||||
sys::stat::{self, SFlag},
|
||||
unistd::AccessFlags,
|
||||
};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::{libsh::error::{ShErr, ShErrKind, ShResult},prelude::*, parse::{ConjunctOp, NdRule, Node, TestCase, TEST_UNARY_OPS}};
|
||||
use crate::{
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
parse::{ConjunctOp, NdRule, Node, TestCase, TEST_UNARY_OPS},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum UnaryOp {
|
||||
Exists, // -e
|
||||
Directory, // -d
|
||||
File, // -f
|
||||
Symlink, // -h or -L
|
||||
Readable, // -r
|
||||
Writable, // -w
|
||||
Executable, // -x
|
||||
NonEmpty, // -s
|
||||
NamedPipe, // -p
|
||||
Socket, // -S
|
||||
BlockSpecial, // -b
|
||||
CharSpecial, // -c
|
||||
Sticky, // -k
|
||||
UIDOwner, // -O
|
||||
GIDOwner, // -G
|
||||
ModifiedSinceStatusChange, // -N
|
||||
SetUID, // -u
|
||||
SetGID, // -g
|
||||
Terminal, // -t
|
||||
NonNull, // -n
|
||||
Null, // -z
|
||||
Exists, // -e
|
||||
Directory, // -d
|
||||
File, // -f
|
||||
Symlink, // -h or -L
|
||||
Readable, // -r
|
||||
Writable, // -w
|
||||
Executable, // -x
|
||||
NonEmpty, // -s
|
||||
NamedPipe, // -p
|
||||
Socket, // -S
|
||||
BlockSpecial, // -b
|
||||
CharSpecial, // -c
|
||||
Sticky, // -k
|
||||
UIDOwner, // -O
|
||||
GIDOwner, // -G
|
||||
ModifiedSinceStatusChange, // -N
|
||||
SetUID, // -u
|
||||
SetGID, // -g
|
||||
Terminal, // -t
|
||||
NonNull, // -n
|
||||
Null, // -z
|
||||
}
|
||||
|
||||
impl FromStr for UnaryOp {
|
||||
type Err = ShErr;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"-e" => Ok(Self::Exists),
|
||||
"-d" => Ok(Self::Directory),
|
||||
"-f" => Ok(Self::File),
|
||||
"-h" | "-L" => Ok(Self::Symlink), // -h or -L
|
||||
"-r" => Ok(Self::Readable),
|
||||
"-w" => Ok(Self::Writable),
|
||||
"-x" => Ok(Self::Executable),
|
||||
"-s" => Ok(Self::NonEmpty),
|
||||
"-p" => Ok(Self::NamedPipe),
|
||||
"-S" => Ok(Self::Socket),
|
||||
"-b" => Ok(Self::BlockSpecial),
|
||||
"-c" => Ok(Self::CharSpecial),
|
||||
"-k" => Ok(Self::Sticky),
|
||||
"-O" => Ok(Self::UIDOwner),
|
||||
"-G" => Ok(Self::GIDOwner),
|
||||
"-N" => Ok(Self::ModifiedSinceStatusChange),
|
||||
"-u" => Ok(Self::SetUID),
|
||||
"-g" => Ok(Self::SetGID),
|
||||
"-t" => Ok(Self::Terminal),
|
||||
"-n" => Ok(Self::NonNull),
|
||||
"-z" => Ok(Self::Null),
|
||||
_ => Err(ShErr::Simple { kind: ShErrKind::SyntaxErr, msg: "Invalid test operator".into(), notes: vec![] })
|
||||
}
|
||||
}
|
||||
type Err = ShErr;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"-e" => Ok(Self::Exists),
|
||||
"-d" => Ok(Self::Directory),
|
||||
"-f" => Ok(Self::File),
|
||||
"-h" | "-L" => Ok(Self::Symlink), // -h or -L
|
||||
"-r" => Ok(Self::Readable),
|
||||
"-w" => Ok(Self::Writable),
|
||||
"-x" => Ok(Self::Executable),
|
||||
"-s" => Ok(Self::NonEmpty),
|
||||
"-p" => Ok(Self::NamedPipe),
|
||||
"-S" => Ok(Self::Socket),
|
||||
"-b" => Ok(Self::BlockSpecial),
|
||||
"-c" => Ok(Self::CharSpecial),
|
||||
"-k" => Ok(Self::Sticky),
|
||||
"-O" => Ok(Self::UIDOwner),
|
||||
"-G" => Ok(Self::GIDOwner),
|
||||
"-N" => Ok(Self::ModifiedSinceStatusChange),
|
||||
"-u" => Ok(Self::SetUID),
|
||||
"-g" => Ok(Self::SetGID),
|
||||
"-t" => Ok(Self::Terminal),
|
||||
"-n" => Ok(Self::NonNull),
|
||||
"-z" => Ok(Self::Null),
|
||||
_ => Err(ShErr::Simple {
|
||||
kind: ShErrKind::SyntaxErr,
|
||||
msg: "Invalid test operator".into(),
|
||||
notes: vec![],
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TestOp {
|
||||
Unary(UnaryOp),
|
||||
StringEq, // ==
|
||||
StringNeq, // !=
|
||||
IntEq, // -eq
|
||||
IntNeq, // -ne
|
||||
IntGt, // -gt
|
||||
IntLt, // -lt
|
||||
IntGe, // -ge
|
||||
IntLe, // -le
|
||||
RegexMatch, // =~
|
||||
Unary(UnaryOp),
|
||||
StringEq, // ==
|
||||
StringNeq, // !=
|
||||
IntEq, // -eq
|
||||
IntNeq, // -ne
|
||||
IntGt, // -gt
|
||||
IntLt, // -lt
|
||||
IntGe, // -ge
|
||||
IntLe, // -le
|
||||
RegexMatch, // =~
|
||||
}
|
||||
|
||||
impl FromStr for TestOp {
|
||||
type Err = ShErr;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"==" => Ok(Self::StringEq),
|
||||
"!=" => Ok(Self::StringNeq),
|
||||
"=~" => Ok(Self::RegexMatch),
|
||||
"-eq" => Ok(Self::IntEq),
|
||||
"-ne" => Ok(Self::IntNeq),
|
||||
"-gt" => Ok(Self::IntGt),
|
||||
"-lt" => Ok(Self::IntLt),
|
||||
"-ge" => Ok(Self::IntGe),
|
||||
"-le" => Ok(Self::IntLe),
|
||||
_ if TEST_UNARY_OPS.contains(&s) => {
|
||||
Ok(Self::Unary(s.parse::<UnaryOp>()?))
|
||||
}
|
||||
_ => Err(ShErr::Simple { kind: ShErrKind::SyntaxErr, msg: "Invalid test operator".into(), notes: vec![] })
|
||||
}
|
||||
}
|
||||
type Err = ShErr;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"==" => Ok(Self::StringEq),
|
||||
"!=" => Ok(Self::StringNeq),
|
||||
"=~" => Ok(Self::RegexMatch),
|
||||
"-eq" => Ok(Self::IntEq),
|
||||
"-ne" => Ok(Self::IntNeq),
|
||||
"-gt" => Ok(Self::IntGt),
|
||||
"-lt" => Ok(Self::IntLt),
|
||||
"-ge" => Ok(Self::IntGe),
|
||||
"-le" => Ok(Self::IntLe),
|
||||
_ if TEST_UNARY_OPS.contains(&s) => Ok(Self::Unary(s.parse::<UnaryOp>()?)),
|
||||
_ => Err(ShErr::Simple {
|
||||
kind: ShErrKind::SyntaxErr,
|
||||
msg: "Invalid test operator".into(),
|
||||
notes: vec![],
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_posix_classes(pat: &str) -> String {
|
||||
pat.replace("[[:alnum:]]", r"[A-Za-z0-9]")
|
||||
.replace("[[:alpha:]]", r"[A-Za-z]")
|
||||
.replace("[[:blank:]]", r"[ \t]")
|
||||
.replace("[[:cntrl:]]", r"[\x00-\x1F\x7F]")
|
||||
.replace("[[:digit:]]", r"[0-9]")
|
||||
.replace("[[:graph:]]", r"[!-~]")
|
||||
.replace("[[:lower:]]", r"[a-z]")
|
||||
.replace("[[:print:]]", r"[\x20-\x7E]")
|
||||
.replace("[[:space:]]", r"[ \t\r\n\x0B\x0C]") // vertical tab (\x0B), form feed (\x0C)
|
||||
.replace("[[:upper:]]", r"[A-Z]")
|
||||
.replace("[[:xdigit:]]", r"[0-9A-Fa-f]")
|
||||
pat
|
||||
.replace("[[:alnum:]]", r"[A-Za-z0-9]")
|
||||
.replace("[[:alpha:]]", r"[A-Za-z]")
|
||||
.replace("[[:blank:]]", r"[ \t]")
|
||||
.replace("[[:cntrl:]]", r"[\x00-\x1F\x7F]")
|
||||
.replace("[[:digit:]]", r"[0-9]")
|
||||
.replace("[[:graph:]]", r"[!-~]")
|
||||
.replace("[[:lower:]]", r"[a-z]")
|
||||
.replace("[[:print:]]", r"[\x20-\x7E]")
|
||||
.replace("[[:space:]]", r"[ \t\r\n\x0B\x0C]") // vertical tab (\x0B), form feed (\x0C)
|
||||
.replace("[[:upper:]]", r"[A-Z]")
|
||||
.replace("[[:xdigit:]]", r"[0-9A-Fa-f]")
|
||||
}
|
||||
|
||||
pub fn double_bracket_test(node: Node) -> ShResult<bool> {
|
||||
let err_span = node.get_span();
|
||||
let NdRule::Test { cases } = node.class else {
|
||||
unreachable!()
|
||||
};
|
||||
let mut last_result = false;
|
||||
let mut conjunct_op: Option<ConjunctOp>;
|
||||
let err_span = node.get_span();
|
||||
let NdRule::Test { cases } = node.class else {
|
||||
unreachable!()
|
||||
};
|
||||
let mut last_result = false;
|
||||
let mut conjunct_op: Option<ConjunctOp>;
|
||||
|
||||
for case in cases {
|
||||
let result = match case {
|
||||
TestCase::Unary { operator, operand, conjunct } => {
|
||||
let operand = operand.expand()?.get_words().join(" ");
|
||||
conjunct_op = conjunct;
|
||||
let TestOp::Unary(op) = TestOp::from_str(operator.as_str())? else {
|
||||
return Err(
|
||||
ShErr::Full { kind: ShErrKind::SyntaxErr, msg: "Invalid unary operator".into(), notes: vec![], span: err_span }
|
||||
)
|
||||
};
|
||||
match op {
|
||||
UnaryOp::Exists => {
|
||||
let path = PathBuf::from(operand.as_str());
|
||||
path.exists()
|
||||
}
|
||||
UnaryOp::Directory => {
|
||||
let path = PathBuf::from(operand.as_str());
|
||||
if path.exists() {
|
||||
path.metadata()
|
||||
.unwrap()
|
||||
.is_dir()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
UnaryOp::File => {
|
||||
let path = PathBuf::from(operand.as_str());
|
||||
if path.exists() {
|
||||
path.metadata()
|
||||
.unwrap()
|
||||
.is_file()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
UnaryOp::Symlink => {
|
||||
let path = PathBuf::from(operand.as_str());
|
||||
if path.exists() {
|
||||
path.metadata()
|
||||
.unwrap()
|
||||
.file_type()
|
||||
.is_symlink()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
UnaryOp::Readable => nix::unistd::access(operand.as_str(), AccessFlags::R_OK).is_ok(),
|
||||
UnaryOp::Writable => nix::unistd::access(operand.as_str(), AccessFlags::W_OK).is_ok(),
|
||||
UnaryOp::Executable => nix::unistd::access(operand.as_str(), AccessFlags::X_OK).is_ok(),
|
||||
UnaryOp::NonEmpty => {
|
||||
match metadata(operand.as_str()) {
|
||||
Ok(meta) => meta.len() > 0,
|
||||
Err(_) => false
|
||||
}
|
||||
}
|
||||
UnaryOp::NamedPipe => {
|
||||
match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFIFO),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
UnaryOp::Socket => {
|
||||
match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFSOCK),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
UnaryOp::BlockSpecial => {
|
||||
match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFBLK),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
UnaryOp::CharSpecial => {
|
||||
match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFCHR),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
UnaryOp::Sticky => {
|
||||
match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => stat.st_mode & nix::libc::S_ISVTX != 0,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
UnaryOp::UIDOwner => {
|
||||
match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => stat.st_uid == nix::unistd::geteuid().as_raw(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
for case in cases {
|
||||
let result = match case {
|
||||
TestCase::Unary {
|
||||
operator,
|
||||
operand,
|
||||
conjunct,
|
||||
} => {
|
||||
let operand = operand.expand()?.get_words().join(" ");
|
||||
conjunct_op = conjunct;
|
||||
let TestOp::Unary(op) = TestOp::from_str(operator.as_str())? else {
|
||||
return Err(ShErr::Full {
|
||||
kind: ShErrKind::SyntaxErr,
|
||||
msg: "Invalid unary operator".into(),
|
||||
notes: vec![],
|
||||
span: err_span,
|
||||
});
|
||||
};
|
||||
match op {
|
||||
UnaryOp::Exists => {
|
||||
let path = PathBuf::from(operand.as_str());
|
||||
path.exists()
|
||||
}
|
||||
UnaryOp::Directory => {
|
||||
let path = PathBuf::from(operand.as_str());
|
||||
if path.exists() {
|
||||
path.metadata().unwrap().is_dir()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
UnaryOp::File => {
|
||||
let path = PathBuf::from(operand.as_str());
|
||||
if path.exists() {
|
||||
path.metadata().unwrap().is_file()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
UnaryOp::Symlink => {
|
||||
let path = PathBuf::from(operand.as_str());
|
||||
if path.exists() {
|
||||
path.metadata().unwrap().file_type().is_symlink()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
UnaryOp::Readable => nix::unistd::access(operand.as_str(), AccessFlags::R_OK).is_ok(),
|
||||
UnaryOp::Writable => nix::unistd::access(operand.as_str(), AccessFlags::W_OK).is_ok(),
|
||||
UnaryOp::Executable => nix::unistd::access(operand.as_str(), AccessFlags::X_OK).is_ok(),
|
||||
UnaryOp::NonEmpty => match metadata(operand.as_str()) {
|
||||
Ok(meta) => meta.len() > 0,
|
||||
Err(_) => false,
|
||||
},
|
||||
UnaryOp::NamedPipe => match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFIFO),
|
||||
Err(_) => false,
|
||||
},
|
||||
UnaryOp::Socket => match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFSOCK),
|
||||
Err(_) => false,
|
||||
},
|
||||
UnaryOp::BlockSpecial => match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFBLK),
|
||||
Err(_) => false,
|
||||
},
|
||||
UnaryOp::CharSpecial => match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFCHR),
|
||||
Err(_) => false,
|
||||
},
|
||||
UnaryOp::Sticky => match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => stat.st_mode & nix::libc::S_ISVTX != 0,
|
||||
Err(_) => false,
|
||||
},
|
||||
UnaryOp::UIDOwner => match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => stat.st_uid == nix::unistd::geteuid().as_raw(),
|
||||
Err(_) => false,
|
||||
},
|
||||
|
||||
UnaryOp::GIDOwner => {
|
||||
match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => stat.st_gid == nix::unistd::getegid().as_raw(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
UnaryOp::GIDOwner => match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => stat.st_gid == nix::unistd::getegid().as_raw(),
|
||||
Err(_) => false,
|
||||
},
|
||||
|
||||
UnaryOp::ModifiedSinceStatusChange => {
|
||||
match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => stat.st_mtime > stat.st_ctime,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
UnaryOp::ModifiedSinceStatusChange => match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => stat.st_mtime > stat.st_ctime,
|
||||
Err(_) => false,
|
||||
},
|
||||
|
||||
UnaryOp::SetUID => {
|
||||
match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => stat.st_mode & nix::libc::S_ISUID != 0,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
UnaryOp::SetUID => match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => stat.st_mode & nix::libc::S_ISUID != 0,
|
||||
Err(_) => false,
|
||||
},
|
||||
|
||||
UnaryOp::SetGID => {
|
||||
match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => stat.st_mode & nix::libc::S_ISGID != 0,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
UnaryOp::SetGID => match stat::stat(operand.as_str()) {
|
||||
Ok(stat) => stat.st_mode & nix::libc::S_ISGID != 0,
|
||||
Err(_) => false,
|
||||
},
|
||||
|
||||
UnaryOp::Terminal => {
|
||||
match operand.as_str().parse::<nix::libc::c_int>() {
|
||||
Ok(fd) => unsafe { nix::libc::isatty(fd) == 1 },
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
UnaryOp::NonNull => !operand.is_empty(),
|
||||
UnaryOp::Null => operand.is_empty(),
|
||||
}
|
||||
}
|
||||
TestCase::Binary { lhs, operator, rhs, conjunct } => {
|
||||
let lhs = lhs.expand()?.get_words().join(" ");
|
||||
let rhs = rhs.expand()?.get_words().join(" ");
|
||||
conjunct_op = conjunct;
|
||||
let test_op = operator.as_str().parse::<TestOp>()?;
|
||||
flog!(DEBUG, lhs);
|
||||
flog!(DEBUG, rhs);
|
||||
flog!(DEBUG, test_op);
|
||||
match test_op {
|
||||
TestOp::Unary(_) => {
|
||||
return Err(
|
||||
ShErr::Full {
|
||||
kind: ShErrKind::SyntaxErr,
|
||||
msg: "Expected a binary operator in this test call; found a unary operator".into(),
|
||||
notes: vec![],
|
||||
span: err_span
|
||||
}
|
||||
)
|
||||
}
|
||||
TestOp::StringEq => rhs.trim() == lhs.trim(),
|
||||
TestOp::StringNeq => rhs.trim() != lhs.trim(),
|
||||
TestOp::IntNeq |
|
||||
TestOp::IntGt |
|
||||
TestOp::IntLt |
|
||||
TestOp::IntGe |
|
||||
TestOp::IntLe |
|
||||
TestOp::IntEq => {
|
||||
let err = ShErr::Full {
|
||||
kind: ShErrKind::SyntaxErr,
|
||||
msg: format!("Expected an integer with '{}' operator", operator.as_str()),
|
||||
notes: vec![],
|
||||
span: err_span.clone()
|
||||
};
|
||||
let Ok(lhs) = lhs.trim().parse::<i32>() else {
|
||||
return Err(err)
|
||||
};
|
||||
let Ok(rhs) = rhs.trim().parse::<i32>() else {
|
||||
return Err(err)
|
||||
};
|
||||
match test_op {
|
||||
TestOp::IntNeq => lhs != rhs,
|
||||
TestOp::IntGt => lhs > rhs,
|
||||
TestOp::IntLt => lhs < rhs,
|
||||
TestOp::IntGe => lhs >= rhs,
|
||||
TestOp::IntLe => lhs <= rhs,
|
||||
TestOp::IntEq => lhs == rhs,
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
TestOp::RegexMatch => {
|
||||
// FIXME: Imagine doing all of this in every single iteration of a loop
|
||||
let cleaned = replace_posix_classes(&rhs);
|
||||
let regex = Regex::new(&cleaned).unwrap();
|
||||
regex.is_match(&lhs)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
flog!(DEBUG, last_result);
|
||||
UnaryOp::Terminal => match operand.as_str().parse::<nix::libc::c_int>() {
|
||||
Ok(fd) => unsafe { nix::libc::isatty(fd) == 1 },
|
||||
Err(_) => false,
|
||||
},
|
||||
UnaryOp::NonNull => !operand.is_empty(),
|
||||
UnaryOp::Null => operand.is_empty(),
|
||||
}
|
||||
}
|
||||
TestCase::Binary {
|
||||
lhs,
|
||||
operator,
|
||||
rhs,
|
||||
conjunct,
|
||||
} => {
|
||||
let lhs = lhs.expand()?.get_words().join(" ");
|
||||
let rhs = rhs.expand()?.get_words().join(" ");
|
||||
conjunct_op = conjunct;
|
||||
let test_op = operator.as_str().parse::<TestOp>()?;
|
||||
flog!(DEBUG, lhs);
|
||||
flog!(DEBUG, rhs);
|
||||
flog!(DEBUG, test_op);
|
||||
match test_op {
|
||||
TestOp::Unary(_) => {
|
||||
return Err(ShErr::Full {
|
||||
kind: ShErrKind::SyntaxErr,
|
||||
msg: "Expected a binary operator in this test call; found a unary operator".into(),
|
||||
notes: vec![],
|
||||
span: err_span,
|
||||
})
|
||||
}
|
||||
TestOp::StringEq => rhs.trim() == lhs.trim(),
|
||||
TestOp::StringNeq => rhs.trim() != lhs.trim(),
|
||||
TestOp::IntNeq
|
||||
| TestOp::IntGt
|
||||
| TestOp::IntLt
|
||||
| TestOp::IntGe
|
||||
| TestOp::IntLe
|
||||
| TestOp::IntEq => {
|
||||
let err = ShErr::Full {
|
||||
kind: ShErrKind::SyntaxErr,
|
||||
msg: format!("Expected an integer with '{}' operator", operator.as_str()),
|
||||
notes: vec![],
|
||||
span: err_span.clone(),
|
||||
};
|
||||
let Ok(lhs) = lhs.trim().parse::<i32>() else {
|
||||
return Err(err);
|
||||
};
|
||||
let Ok(rhs) = rhs.trim().parse::<i32>() else {
|
||||
return Err(err);
|
||||
};
|
||||
match test_op {
|
||||
TestOp::IntNeq => lhs != rhs,
|
||||
TestOp::IntGt => lhs > rhs,
|
||||
TestOp::IntLt => lhs < rhs,
|
||||
TestOp::IntGe => lhs >= rhs,
|
||||
TestOp::IntLe => lhs <= rhs,
|
||||
TestOp::IntEq => lhs == rhs,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
TestOp::RegexMatch => {
|
||||
// FIXME: Imagine doing all of this in every single iteration of a loop
|
||||
let cleaned = replace_posix_classes(&rhs);
|
||||
let regex = Regex::new(&cleaned).unwrap();
|
||||
regex.is_match(&lhs)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
flog!(DEBUG, last_result);
|
||||
|
||||
if let Some(op) = conjunct_op {
|
||||
match op {
|
||||
ConjunctOp::And if !last_result => {
|
||||
last_result = result;
|
||||
break
|
||||
}
|
||||
ConjunctOp::Or if last_result => {
|
||||
last_result = result;
|
||||
break
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
last_result = result;
|
||||
}
|
||||
}
|
||||
flog!(DEBUG, last_result);
|
||||
Ok(last_result)
|
||||
if let Some(op) = conjunct_op {
|
||||
match op {
|
||||
ConjunctOp::And if !last_result => {
|
||||
last_result = result;
|
||||
break;
|
||||
}
|
||||
ConjunctOp::Or if last_result => {
|
||||
last_result = result;
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
last_result = result;
|
||||
}
|
||||
}
|
||||
flog!(DEBUG, last_result);
|
||||
Ok(last_result)
|
||||
}
|
||||
|
||||
@@ -1,190 +1,191 @@
|
||||
use std::{os::unix::fs::OpenOptionsExt, sync::LazyLock};
|
||||
|
||||
use crate::{getopt::{get_opts_from_tokens, Opt, OptSet}, jobs::JobBldr, libsh::error::{Note, ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}};
|
||||
use crate::{
|
||||
getopt::{get_opts_from_tokens, Opt, OptSet},
|
||||
jobs::JobBldr,
|
||||
libsh::error::{Note, ShErr, ShErrKind, ShResult, ShResultExt},
|
||||
parse::{NdRule, Node},
|
||||
prelude::*,
|
||||
procio::{borrow_fd, IoStack},
|
||||
};
|
||||
|
||||
use super::setup_builtin;
|
||||
|
||||
pub static ZOLTRAAK_OPTS: LazyLock<OptSet> = LazyLock::new(|| {
|
||||
[
|
||||
Opt::Long("dry-run".into()),
|
||||
Opt::Long("confirm".into()),
|
||||
Opt::Long("no-preserve-root".into()),
|
||||
Opt::Short('r'),
|
||||
Opt::Short('f'),
|
||||
Opt::Short('v')
|
||||
].into()
|
||||
[
|
||||
Opt::Long("dry-run".into()),
|
||||
Opt::Long("confirm".into()),
|
||||
Opt::Long("no-preserve-root".into()),
|
||||
Opt::Short('r'),
|
||||
Opt::Short('f'),
|
||||
Opt::Short('v'),
|
||||
]
|
||||
.into()
|
||||
});
|
||||
|
||||
bitflags! {
|
||||
#[derive(Clone,Copy,Debug,PartialEq,Eq)]
|
||||
struct ZoltFlags: u32 {
|
||||
const DRY = 0b000001;
|
||||
const CONFIRM = 0b000010;
|
||||
const NO_PRESERVE_ROOT = 0b000100;
|
||||
const RECURSIVE = 0b001000;
|
||||
const FORCE = 0b010000;
|
||||
const VERBOSE = 0b100000;
|
||||
}
|
||||
#[derive(Clone,Copy,Debug,PartialEq,Eq)]
|
||||
struct ZoltFlags: u32 {
|
||||
const DRY = 0b000001;
|
||||
const CONFIRM = 0b000010;
|
||||
const NO_PRESERVE_ROOT = 0b000100;
|
||||
const RECURSIVE = 0b001000;
|
||||
const FORCE = 0b010000;
|
||||
const VERBOSE = 0b100000;
|
||||
}
|
||||
}
|
||||
|
||||
/// Annihilate a file
|
||||
///
|
||||
/// This command works similarly to 'rm', but behaves more destructively.
|
||||
/// The file given as an argument is completely destroyed. The command works by shredding all of the data contained in the file, before truncating the length of the file to 0 to ensure that not even any metadata remains.
|
||||
/// The file given as an argument is completely destroyed. The command works by
|
||||
/// shredding all of the data contained in the file, before truncating the
|
||||
/// length of the file to 0 to ensure that not even any metadata remains.
|
||||
pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
|
||||
let NdRule::Command { assignments: _, argv } = node.class else {
|
||||
unreachable!()
|
||||
};
|
||||
let mut flags = ZoltFlags::empty();
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
argv,
|
||||
} = node.class
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let mut flags = ZoltFlags::empty();
|
||||
|
||||
let (argv,opts) = get_opts_from_tokens(argv);
|
||||
let (argv, opts) = get_opts_from_tokens(argv);
|
||||
|
||||
for opt in opts {
|
||||
if !ZOLTRAAK_OPTS.contains(&opt) {
|
||||
return Err(
|
||||
ShErr::simple(
|
||||
ShErrKind::SyntaxErr,
|
||||
format!("zoltraak: unrecognized option '{opt}'")
|
||||
)
|
||||
)
|
||||
}
|
||||
match opt {
|
||||
Opt::Long(flag) => {
|
||||
match flag.as_str() {
|
||||
"no-preserve-root" => flags |= ZoltFlags::NO_PRESERVE_ROOT,
|
||||
"confirm" => flags |= ZoltFlags::CONFIRM,
|
||||
"dry-run" => flags |= ZoltFlags::DRY,
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
Opt::Short(flag) => {
|
||||
match flag {
|
||||
'r' => flags |= ZoltFlags::RECURSIVE,
|
||||
'f' => flags |= ZoltFlags::FORCE,
|
||||
'v' => flags |= ZoltFlags::VERBOSE,
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for opt in opts {
|
||||
if !ZOLTRAAK_OPTS.contains(&opt) {
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::SyntaxErr,
|
||||
format!("zoltraak: unrecognized option '{opt}'"),
|
||||
));
|
||||
}
|
||||
match opt {
|
||||
Opt::Long(flag) => match flag.as_str() {
|
||||
"no-preserve-root" => flags |= ZoltFlags::NO_PRESERVE_ROOT,
|
||||
"confirm" => flags |= ZoltFlags::CONFIRM,
|
||||
"dry-run" => flags |= ZoltFlags::DRY,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Opt::Short(flag) => match flag {
|
||||
'r' => flags |= ZoltFlags::RECURSIVE,
|
||||
'f' => flags |= ZoltFlags::FORCE,
|
||||
'v' => flags |= ZoltFlags::VERBOSE,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||
let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||
|
||||
let mut io_frame = io_frame.unwrap();
|
||||
io_frame.redirect()?;
|
||||
let mut io_frame = io_frame.unwrap();
|
||||
io_frame.redirect()?;
|
||||
|
||||
for (arg,span) in argv {
|
||||
if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) {
|
||||
return Err(
|
||||
ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
"zoltraak: Attempted to destroy root directory '/'"
|
||||
)
|
||||
.with_note(
|
||||
Note::new("If you really want to do this, you can use the --no-preserve-root flag")
|
||||
.with_sub_notes(vec![
|
||||
"Example: 'zoltraak --no-preserve-root /'"
|
||||
])
|
||||
)
|
||||
)
|
||||
}
|
||||
if let Err(e) = annihilate(&arg, flags).blame(span) {
|
||||
io_frame.restore()?;
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
for (arg, span) in argv {
|
||||
if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) {
|
||||
return Err(
|
||||
ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
"zoltraak: Attempted to destroy root directory '/'",
|
||||
)
|
||||
.with_note(
|
||||
Note::new("If you really want to do this, you can use the --no-preserve-root flag")
|
||||
.with_sub_notes(vec!["Example: 'zoltraak --no-preserve-root /'"]),
|
||||
),
|
||||
);
|
||||
}
|
||||
if let Err(e) = annihilate(&arg, flags).blame(span) {
|
||||
io_frame.restore()?;
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
io_frame.restore()?;
|
||||
io_frame.restore()?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn annihilate(path: &str, flags: ZoltFlags) -> ShResult<()> {
|
||||
let path_buf = PathBuf::from(path);
|
||||
let is_recursive = flags.contains(ZoltFlags::RECURSIVE);
|
||||
let is_verbose = flags.contains(ZoltFlags::VERBOSE);
|
||||
let path_buf = PathBuf::from(path);
|
||||
let is_recursive = flags.contains(ZoltFlags::RECURSIVE);
|
||||
let is_verbose = flags.contains(ZoltFlags::VERBOSE);
|
||||
|
||||
const BLOCK_SIZE: u64 = 4096;
|
||||
const BLOCK_SIZE: u64 = 4096;
|
||||
|
||||
if !path_buf.exists() {
|
||||
return Err(
|
||||
ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
format!("zoltraak: File '{path}' not found")
|
||||
)
|
||||
)
|
||||
}
|
||||
if !path_buf.exists() {
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
format!("zoltraak: File '{path}' not found"),
|
||||
));
|
||||
}
|
||||
|
||||
if path_buf.is_file() {
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.custom_flags(libc::O_DIRECT)
|
||||
.open(path_buf)?;
|
||||
if path_buf.is_file() {
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.custom_flags(libc::O_DIRECT)
|
||||
.open(path_buf)?;
|
||||
|
||||
let meta = file.metadata()?;
|
||||
let file_size = meta.len();
|
||||
let full_blocks = file_size / BLOCK_SIZE;
|
||||
let byte_remainder = file_size % BLOCK_SIZE;
|
||||
let meta = file.metadata()?;
|
||||
let file_size = meta.len();
|
||||
let full_blocks = file_size / BLOCK_SIZE;
|
||||
let byte_remainder = file_size % BLOCK_SIZE;
|
||||
|
||||
let full_buf = vec![0; BLOCK_SIZE as usize];
|
||||
let remainder_buf = vec![0; byte_remainder as usize];
|
||||
let full_buf = vec![0; BLOCK_SIZE as usize];
|
||||
let remainder_buf = vec![0; byte_remainder as usize];
|
||||
|
||||
for _ in 0..full_blocks {
|
||||
file.write_all(&full_buf)?;
|
||||
}
|
||||
for _ in 0..full_blocks {
|
||||
file.write_all(&full_buf)?;
|
||||
}
|
||||
|
||||
if byte_remainder > 0 {
|
||||
file.write_all(&remainder_buf)?;
|
||||
}
|
||||
if byte_remainder > 0 {
|
||||
file.write_all(&remainder_buf)?;
|
||||
}
|
||||
|
||||
file.set_len(0)?;
|
||||
mem::drop(file);
|
||||
fs::remove_file(path)?;
|
||||
if is_verbose {
|
||||
let stderr = borrow_fd(STDERR_FILENO);
|
||||
write(stderr, format!("shredded file '{path}'\n").as_bytes())?;
|
||||
}
|
||||
file.set_len(0)?;
|
||||
mem::drop(file);
|
||||
fs::remove_file(path)?;
|
||||
if is_verbose {
|
||||
let stderr = borrow_fd(STDERR_FILENO);
|
||||
write(stderr, format!("shredded file '{path}'\n").as_bytes())?;
|
||||
}
|
||||
} else if path_buf.is_dir() {
|
||||
if is_recursive {
|
||||
annihilate_recursive(path, flags)?; // scary
|
||||
} else {
|
||||
return Err(
|
||||
ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
format!("zoltraak: '{path}' is a directory"),
|
||||
)
|
||||
.with_note(
|
||||
Note::new("Use the '-r' flag to recursively shred directories")
|
||||
.with_sub_notes(vec!["Example: 'zoltraak -r directory'"]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
} else if path_buf.is_dir() {
|
||||
if is_recursive {
|
||||
annihilate_recursive(path, flags)?; // scary
|
||||
} else {
|
||||
return Err(
|
||||
ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
format!("zoltraak: '{path}' is a directory")
|
||||
)
|
||||
.with_note(
|
||||
Note::new("Use the '-r' flag to recursively shred directories")
|
||||
.with_sub_notes(vec![
|
||||
"Example: 'zoltraak -r directory'"
|
||||
])
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn annihilate_recursive(dir: &str, flags: ZoltFlags) -> ShResult<()> {
|
||||
let dir_path = PathBuf::from(dir);
|
||||
let is_verbose = flags.contains(ZoltFlags::VERBOSE);
|
||||
let dir_path = PathBuf::from(dir);
|
||||
let is_verbose = flags.contains(ZoltFlags::VERBOSE);
|
||||
|
||||
for dir_entry in fs::read_dir(&dir_path)? {
|
||||
let entry = dir_entry?.path();
|
||||
let file = entry.to_str().unwrap();
|
||||
for dir_entry in fs::read_dir(&dir_path)? {
|
||||
let entry = dir_entry?.path();
|
||||
let file = entry.to_str().unwrap();
|
||||
|
||||
if entry.is_file() {
|
||||
annihilate(file, flags)?;
|
||||
} else if entry.is_dir() {
|
||||
annihilate_recursive(file, flags)?;
|
||||
}
|
||||
}
|
||||
fs::remove_dir(dir)?;
|
||||
if is_verbose {
|
||||
let stderr = borrow_fd(STDERR_FILENO);
|
||||
write(stderr, format!("shredded directory '{dir}'\n").as_bytes())?;
|
||||
}
|
||||
Ok(())
|
||||
if entry.is_file() {
|
||||
annihilate(file, flags)?;
|
||||
} else if entry.is_dir() {
|
||||
annihilate_recursive(file, flags)?;
|
||||
}
|
||||
}
|
||||
fs::remove_dir(dir)?;
|
||||
if is_verbose {
|
||||
let stderr = borrow_fd(STDERR_FILENO);
|
||||
write(stderr, format!("shredded directory '{dir}'\n").as_bytes())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
2704
src/expand.rs
2704
src/expand.rs
File diff suppressed because it is too large
Load Diff
175
src/fern.rs
175
src/fern.rs
@@ -1,127 +1,130 @@
|
||||
#![allow(
|
||||
clippy::derivable_impls,
|
||||
clippy::tabs_in_doc_comments,
|
||||
clippy::while_let_on_iterator
|
||||
clippy::derivable_impls,
|
||||
clippy::tabs_in_doc_comments,
|
||||
clippy::while_let_on_iterator
|
||||
)]
|
||||
pub mod prelude;
|
||||
pub mod libsh;
|
||||
pub mod prompt;
|
||||
pub mod procio;
|
||||
pub mod parse;
|
||||
pub mod expand;
|
||||
pub mod state;
|
||||
pub mod builtin;
|
||||
pub mod jobs;
|
||||
pub mod signal;
|
||||
pub mod expand;
|
||||
pub mod getopt;
|
||||
pub mod jobs;
|
||||
pub mod libsh;
|
||||
pub mod parse;
|
||||
pub mod prelude;
|
||||
pub mod procio;
|
||||
pub mod prompt;
|
||||
pub mod shopt;
|
||||
pub mod signal;
|
||||
pub mod state;
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
use crate::libsh::sys::{save_termios, set_termios};
|
||||
use crate::parse::execute::exec_input;
|
||||
use crate::prelude::*;
|
||||
use crate::signal::sig_setup;
|
||||
use crate::state::source_rc;
|
||||
use crate::prelude::*;
|
||||
use clap::Parser;
|
||||
use shopt::FernEditMode;
|
||||
use state::{read_shopts, read_vars, write_shopts, write_vars};
|
||||
use state::{read_vars, write_shopts, write_vars};
|
||||
|
||||
#[derive(Parser,Debug)]
|
||||
#[derive(Parser, Debug)]
|
||||
struct FernArgs {
|
||||
script: Option<String>,
|
||||
script: Option<String>,
|
||||
|
||||
#[arg(trailing_var_arg = true)]
|
||||
script_args: Vec<String>,
|
||||
#[arg(trailing_var_arg = true)]
|
||||
script_args: Vec<String>,
|
||||
|
||||
#[arg(long)]
|
||||
version: bool
|
||||
#[arg(long)]
|
||||
version: bool,
|
||||
}
|
||||
|
||||
/// Force evaluation of lazily-initialized values early in shell startup.
|
||||
///
|
||||
/// In particular, this ensures that the variable table is initialized, which populates
|
||||
/// environment variables from the system. If this initialization is deferred too long,
|
||||
/// features like prompt expansion may fail due to missing environment variables.
|
||||
/// In particular, this ensures that the variable table is initialized, which
|
||||
/// populates environment variables from the system. If this initialization is
|
||||
/// deferred too long, features like prompt expansion may fail due to missing
|
||||
/// environment variables.
|
||||
///
|
||||
/// This function triggers initialization by calling `read_vars` with a no-op closure,
|
||||
/// which forces access to the variable table and causes its `LazyLock` constructor to run.
|
||||
/// This function triggers initialization by calling `read_vars` with a no-op
|
||||
/// closure, which forces access to the variable table and causes its `LazyLock`
|
||||
/// constructor to run.
|
||||
fn kickstart_lazy_evals() {
|
||||
read_vars(|_| {});
|
||||
read_vars(|_| {});
|
||||
}
|
||||
|
||||
fn main() {
|
||||
kickstart_lazy_evals();
|
||||
let args = FernArgs::parse();
|
||||
if args.version {
|
||||
println!("fern {}", env!("CARGO_PKG_VERSION"));
|
||||
return;
|
||||
}
|
||||
kickstart_lazy_evals();
|
||||
let args = FernArgs::parse();
|
||||
if args.version {
|
||||
println!("fern {}", env!("CARGO_PKG_VERSION"));
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(path) = args.script {
|
||||
run_script(path, args.script_args);
|
||||
} else {
|
||||
fern_interactive();
|
||||
}
|
||||
if let Some(path) = args.script {
|
||||
run_script(path, args.script_args);
|
||||
} else {
|
||||
fern_interactive();
|
||||
}
|
||||
}
|
||||
|
||||
fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) {
|
||||
let path = path.as_ref();
|
||||
if !path.is_file() {
|
||||
eprintln!("fern: Failed to open input file: {}", path.display());
|
||||
exit(1);
|
||||
}
|
||||
let Ok(input) = fs::read_to_string(path) else {
|
||||
eprintln!("fern: Failed to read input file: {}", path.display());
|
||||
exit(1);
|
||||
};
|
||||
let path = path.as_ref();
|
||||
if !path.is_file() {
|
||||
eprintln!("fern: Failed to open input file: {}", path.display());
|
||||
exit(1);
|
||||
}
|
||||
let Ok(input) = fs::read_to_string(path) else {
|
||||
eprintln!("fern: Failed to read input file: {}", path.display());
|
||||
exit(1);
|
||||
};
|
||||
|
||||
write_vars(|v| v.bpush_arg(path.to_string_lossy().to_string()));
|
||||
for arg in args {
|
||||
write_vars(|v| v.bpush_arg(arg))
|
||||
}
|
||||
write_vars(|v| v.bpush_arg(path.to_string_lossy().to_string()));
|
||||
for arg in args {
|
||||
write_vars(|v| v.bpush_arg(arg))
|
||||
}
|
||||
|
||||
if let Err(e) = exec_input(input,None) {
|
||||
eprintln!("{e}");
|
||||
exit(1);
|
||||
}
|
||||
if let Err(e) = exec_input(input, None) {
|
||||
eprintln!("{e}");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn fern_interactive() {
|
||||
save_termios();
|
||||
set_termios();
|
||||
sig_setup();
|
||||
save_termios();
|
||||
set_termios();
|
||||
sig_setup();
|
||||
|
||||
if let Err(e) = source_rc() {
|
||||
eprintln!("{e}");
|
||||
}
|
||||
if let Err(e) = source_rc() {
|
||||
eprintln!("{e}");
|
||||
}
|
||||
|
||||
let mut readline_err_count: u32 = 0;
|
||||
let mut readline_err_count: u32 = 0;
|
||||
|
||||
loop { // Main loop
|
||||
let edit_mode = write_shopts(|opt| opt.query("prompt.edit_mode"))
|
||||
.unwrap()
|
||||
.map(|mode| mode.parse::<FernEditMode>().unwrap_or_default())
|
||||
.unwrap();
|
||||
let input = match prompt::readline(edit_mode) {
|
||||
Ok(line) => {
|
||||
readline_err_count = 0;
|
||||
line
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
readline_err_count += 1;
|
||||
if readline_err_count == 20 {
|
||||
eprintln!("reached maximum readline error count, exiting");
|
||||
break
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
};
|
||||
loop {
|
||||
// Main loop
|
||||
let edit_mode = write_shopts(|opt| opt.query("prompt.edit_mode"))
|
||||
.unwrap()
|
||||
.map(|mode| mode.parse::<FernEditMode>().unwrap_or_default())
|
||||
.unwrap();
|
||||
let input = match prompt::readline(edit_mode) {
|
||||
Ok(line) => {
|
||||
readline_err_count = 0;
|
||||
line
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
readline_err_count += 1;
|
||||
if readline_err_count == 20 {
|
||||
eprintln!("reached maximum readline error count, exiting");
|
||||
break;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = exec_input(input,None) {
|
||||
eprintln!("{e}");
|
||||
}
|
||||
}
|
||||
if let Err(e) = exec_input(input, None) {
|
||||
eprintln!("{e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
109
src/getopt.rs
109
src/getopt.rs
@@ -6,75 +6,74 @@ use crate::{parse::lex::Tk, prelude::*};
|
||||
|
||||
pub type OptSet = Arc<[Opt]>;
|
||||
|
||||
|
||||
#[derive(Clone,PartialEq,Eq,Debug)]
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub enum Opt {
|
||||
Long(String),
|
||||
Short(char)
|
||||
Long(String),
|
||||
Short(char),
|
||||
}
|
||||
|
||||
impl Opt {
|
||||
pub fn parse(s: &str) -> Vec<Self> {
|
||||
let mut opts = vec![];
|
||||
pub fn parse(s: &str) -> Vec<Self> {
|
||||
let mut opts = vec![];
|
||||
|
||||
if s.starts_with("--") {
|
||||
opts.push(Opt::Long(s.trim_start_matches('-').to_string()))
|
||||
} else if s.starts_with('-') {
|
||||
let mut chars = s.trim_start_matches('-').chars();
|
||||
while let Some(ch) = chars.next() {
|
||||
opts.push(Self::Short(ch))
|
||||
}
|
||||
}
|
||||
if s.starts_with("--") {
|
||||
opts.push(Opt::Long(s.trim_start_matches('-').to_string()))
|
||||
} else if s.starts_with('-') {
|
||||
let mut chars = s.trim_start_matches('-').chars();
|
||||
while let Some(ch) = chars.next() {
|
||||
opts.push(Self::Short(ch))
|
||||
}
|
||||
}
|
||||
|
||||
opts
|
||||
}
|
||||
opts
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Opt {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Long(opt) => write!(f,"--{}",opt),
|
||||
Self::Short(opt) => write!(f,"-{}",opt),
|
||||
}
|
||||
}
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Long(opt) => write!(f, "--{}", opt),
|
||||
Self::Short(opt) => write!(f, "-{}", opt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_opts(words: Vec<String>) -> (Vec<String>,Vec<Opt>) {
|
||||
let mut words_iter = words.into_iter();
|
||||
let mut opts = vec![];
|
||||
let mut non_opts = vec![];
|
||||
pub fn get_opts(words: Vec<String>) -> (Vec<String>, Vec<Opt>) {
|
||||
let mut words_iter = words.into_iter();
|
||||
let mut opts = vec![];
|
||||
let mut non_opts = vec![];
|
||||
|
||||
while let Some(word) = words_iter.next() {
|
||||
if &word == "--" {
|
||||
non_opts.extend(words_iter);
|
||||
break
|
||||
}
|
||||
let parsed_opts = Opt::parse(&word);
|
||||
if parsed_opts.is_empty() {
|
||||
non_opts.push(word)
|
||||
} else {
|
||||
opts.extend(parsed_opts);
|
||||
}
|
||||
}
|
||||
(non_opts,opts)
|
||||
while let Some(word) = words_iter.next() {
|
||||
if &word == "--" {
|
||||
non_opts.extend(words_iter);
|
||||
break;
|
||||
}
|
||||
let parsed_opts = Opt::parse(&word);
|
||||
if parsed_opts.is_empty() {
|
||||
non_opts.push(word)
|
||||
} else {
|
||||
opts.extend(parsed_opts);
|
||||
}
|
||||
}
|
||||
(non_opts, opts)
|
||||
}
|
||||
|
||||
pub fn get_opts_from_tokens(tokens: Vec<Tk>) -> (Vec<Tk>, Vec<Opt>) {
|
||||
let mut tokens_iter = tokens.into_iter();
|
||||
let mut opts = vec![];
|
||||
let mut non_opts = vec![];
|
||||
let mut tokens_iter = tokens.into_iter();
|
||||
let mut opts = vec![];
|
||||
let mut non_opts = vec![];
|
||||
|
||||
while let Some(token) = tokens_iter.next() {
|
||||
if &token.to_string() == "--" {
|
||||
non_opts.extend(tokens_iter);
|
||||
break
|
||||
}
|
||||
let parsed_opts = Opt::parse(&token.to_string());
|
||||
if parsed_opts.is_empty() {
|
||||
non_opts.push(token)
|
||||
} else {
|
||||
opts.extend(parsed_opts);
|
||||
}
|
||||
}
|
||||
(non_opts,opts)
|
||||
while let Some(token) = tokens_iter.next() {
|
||||
if &token.to_string() == "--" {
|
||||
non_opts.extend(tokens_iter);
|
||||
break;
|
||||
}
|
||||
let parsed_opts = Opt::parse(&token.to_string());
|
||||
if parsed_opts.is_empty() {
|
||||
non_opts.push(token)
|
||||
} else {
|
||||
opts.extend(parsed_opts);
|
||||
}
|
||||
}
|
||||
(non_opts, opts)
|
||||
}
|
||||
|
||||
1293
src/jobs.rs
1293
src/jobs.rs
File diff suppressed because it is too large
Load Diff
@@ -1,356 +1,439 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::{
|
||||
libsh::term::{Style, Styled},
|
||||
parse::lex::Span,
|
||||
prelude::*
|
||||
libsh::term::{Style, Styled},
|
||||
parse::lex::Span,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
pub type ShResult<T> = Result<T,ShErr>;
|
||||
pub type ShResult<T> = Result<T, ShErr>;
|
||||
|
||||
pub trait ShResultExt {
|
||||
fn blame(self, span: Span) -> Self;
|
||||
fn try_blame(self, span: Span) -> Self;
|
||||
fn blame(self, span: Span) -> Self;
|
||||
fn try_blame(self, span: Span) -> Self;
|
||||
}
|
||||
|
||||
impl<T> ShResultExt for Result<T,ShErr> {
|
||||
/// Blame a span for an error
|
||||
fn blame(self, new_span: Span) -> Self {
|
||||
let Err(e) = self else {
|
||||
return self
|
||||
};
|
||||
match e {
|
||||
ShErr::Simple { kind, msg, notes } |
|
||||
ShErr::Full { kind, msg, notes, span: _ } => Err(ShErr::Full { kind: kind.clone(), msg: msg.clone(), notes: notes.clone(), span: new_span }),
|
||||
}
|
||||
}
|
||||
/// Blame a span if no blame has been assigned yet
|
||||
fn try_blame(self, new_span: Span) -> Self {
|
||||
let Err(e) = &self else {
|
||||
return self
|
||||
};
|
||||
match e {
|
||||
ShErr::Simple { kind, msg, notes } => Err(ShErr::Full { kind: kind.clone(), msg: msg.clone(), notes: notes.clone(), span: new_span }),
|
||||
ShErr::Full { kind: _, msg: _, span: _, notes: _ } => self
|
||||
}
|
||||
}
|
||||
impl<T> ShResultExt for Result<T, ShErr> {
|
||||
/// Blame a span for an error
|
||||
fn blame(self, new_span: Span) -> Self {
|
||||
let Err(e) = self else { return self };
|
||||
match e {
|
||||
ShErr::Simple { kind, msg, notes }
|
||||
| ShErr::Full {
|
||||
kind,
|
||||
msg,
|
||||
notes,
|
||||
span: _,
|
||||
} => Err(ShErr::Full {
|
||||
kind: kind.clone(),
|
||||
msg: msg.clone(),
|
||||
notes: notes.clone(),
|
||||
span: new_span,
|
||||
}),
|
||||
}
|
||||
}
|
||||
/// Blame a span if no blame has been assigned yet
|
||||
fn try_blame(self, new_span: Span) -> Self {
|
||||
let Err(e) = &self else { return self };
|
||||
match e {
|
||||
ShErr::Simple { kind, msg, notes } => Err(ShErr::Full {
|
||||
kind: kind.clone(),
|
||||
msg: msg.clone(),
|
||||
notes: notes.clone(),
|
||||
span: new_span,
|
||||
}),
|
||||
ShErr::Full {
|
||||
kind: _,
|
||||
msg: _,
|
||||
span: _,
|
||||
notes: _,
|
||||
} => self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Note {
|
||||
main: String,
|
||||
sub_notes: Vec<Note>,
|
||||
depth: usize
|
||||
main: String,
|
||||
sub_notes: Vec<Note>,
|
||||
depth: usize,
|
||||
}
|
||||
|
||||
impl Note {
|
||||
pub fn new(main: impl Into<String>) -> Self {
|
||||
Self {
|
||||
main: main.into(),
|
||||
sub_notes: vec![],
|
||||
depth: 0
|
||||
}
|
||||
}
|
||||
pub fn new(main: impl Into<String>) -> Self {
|
||||
Self {
|
||||
main: main.into(),
|
||||
sub_notes: vec![],
|
||||
depth: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_sub_notes(self, new_sub_notes: Vec<impl Into<String>>) -> Self {
|
||||
let Self { main, mut sub_notes, depth } = self;
|
||||
for raw_note in new_sub_notes {
|
||||
let mut note = Note::new(raw_note);
|
||||
note.depth = self.depth + 1;
|
||||
sub_notes.push(note);
|
||||
}
|
||||
Self { main, sub_notes, depth }
|
||||
}
|
||||
pub fn with_sub_notes(self, new_sub_notes: Vec<impl Into<String>>) -> Self {
|
||||
let Self {
|
||||
main,
|
||||
mut sub_notes,
|
||||
depth,
|
||||
} = self;
|
||||
for raw_note in new_sub_notes {
|
||||
let mut note = Note::new(raw_note);
|
||||
note.depth = self.depth + 1;
|
||||
sub_notes.push(note);
|
||||
}
|
||||
Self {
|
||||
main,
|
||||
sub_notes,
|
||||
depth,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Note {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let note = "note".styled(Style::Green);
|
||||
let main = &self.main;
|
||||
if self.depth == 0 {
|
||||
writeln!(f, "{note}: {main}")?;
|
||||
} else {
|
||||
let bar_break = "-".styled(Style::Cyan | Style::Bold);
|
||||
let indent = " ".repeat(self.depth);
|
||||
writeln!(f, " {indent}{bar_break} {main}")?;
|
||||
}
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let note = "note".styled(Style::Green);
|
||||
let main = &self.main;
|
||||
if self.depth == 0 {
|
||||
writeln!(f, "{note}: {main}")?;
|
||||
} else {
|
||||
let bar_break = "-".styled(Style::Cyan | Style::Bold);
|
||||
let indent = " ".repeat(self.depth);
|
||||
writeln!(f, " {indent}{bar_break} {main}")?;
|
||||
}
|
||||
|
||||
for sub_note in &self.sub_notes {
|
||||
write!(f, "{sub_note}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
for sub_note in &self.sub_notes {
|
||||
write!(f, "{sub_note}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ShErr {
|
||||
Simple { kind: ShErrKind, msg: String, notes: Vec<Note> },
|
||||
Full { kind: ShErrKind, msg: String, notes: Vec<Note>, span: Span }
|
||||
Simple {
|
||||
kind: ShErrKind,
|
||||
msg: String,
|
||||
notes: Vec<Note>,
|
||||
},
|
||||
Full {
|
||||
kind: ShErrKind,
|
||||
msg: String,
|
||||
notes: Vec<Note>,
|
||||
span: Span,
|
||||
},
|
||||
}
|
||||
|
||||
impl ShErr {
|
||||
pub fn simple(kind: ShErrKind, msg: impl Into<String>) -> Self {
|
||||
let msg = msg.into();
|
||||
Self::Simple { kind, msg, notes: vec![] }
|
||||
}
|
||||
pub fn full(kind: ShErrKind, msg: impl Into<String>, span: Span) -> Self {
|
||||
let msg = msg.into();
|
||||
Self::Full { kind, msg, span, notes: vec![] }
|
||||
}
|
||||
pub fn unpack(self) -> (ShErrKind,String,Vec<Note>,Option<Span>) {
|
||||
match self {
|
||||
ShErr::Simple { kind, msg, notes } => (kind,msg,notes,None),
|
||||
ShErr::Full { kind, msg, notes, span } => (kind,msg,notes,Some(span))
|
||||
}
|
||||
}
|
||||
pub fn with_note(self, note: Note) -> Self {
|
||||
let (kind,msg,mut notes,span) = self.unpack();
|
||||
notes.push(note);
|
||||
if let Some(span) = span {
|
||||
Self::Full { kind, msg, notes, span }
|
||||
} else {
|
||||
Self::Simple { kind, msg, notes }
|
||||
}
|
||||
}
|
||||
pub fn with_span(sherr: ShErr, span: Span) -> Self {
|
||||
let (kind,msg,notes,_) = sherr.unpack();
|
||||
Self::Full { kind, msg, notes, span }
|
||||
}
|
||||
pub fn kind(&self) -> &ShErrKind {
|
||||
match self {
|
||||
ShErr::Simple { kind, msg: _, notes: _ } |
|
||||
ShErr::Full { kind, msg: _, notes: _, span: _ } => kind
|
||||
}
|
||||
}
|
||||
pub fn get_window(&self) -> Vec<(usize,String)> {
|
||||
let ShErr::Full { kind: _, msg: _, notes: _, span } = self else {
|
||||
unreachable!()
|
||||
};
|
||||
let mut total_len: usize = 0;
|
||||
let mut total_lines: usize = 1;
|
||||
let mut lines = vec![];
|
||||
let mut cur_line = String::new();
|
||||
pub fn simple(kind: ShErrKind, msg: impl Into<String>) -> Self {
|
||||
let msg = msg.into();
|
||||
Self::Simple {
|
||||
kind,
|
||||
msg,
|
||||
notes: vec![],
|
||||
}
|
||||
}
|
||||
pub fn full(kind: ShErrKind, msg: impl Into<String>, span: Span) -> Self {
|
||||
let msg = msg.into();
|
||||
Self::Full {
|
||||
kind,
|
||||
msg,
|
||||
span,
|
||||
notes: vec![],
|
||||
}
|
||||
}
|
||||
pub fn unpack(self) -> (ShErrKind, String, Vec<Note>, Option<Span>) {
|
||||
match self {
|
||||
ShErr::Simple { kind, msg, notes } => (kind, msg, notes, None),
|
||||
ShErr::Full {
|
||||
kind,
|
||||
msg,
|
||||
notes,
|
||||
span,
|
||||
} => (kind, msg, notes, Some(span)),
|
||||
}
|
||||
}
|
||||
pub fn with_note(self, note: Note) -> Self {
|
||||
let (kind, msg, mut notes, span) = self.unpack();
|
||||
notes.push(note);
|
||||
if let Some(span) = span {
|
||||
Self::Full {
|
||||
kind,
|
||||
msg,
|
||||
notes,
|
||||
span,
|
||||
}
|
||||
} else {
|
||||
Self::Simple { kind, msg, notes }
|
||||
}
|
||||
}
|
||||
pub fn with_span(sherr: ShErr, span: Span) -> Self {
|
||||
let (kind, msg, notes, _) = sherr.unpack();
|
||||
Self::Full {
|
||||
kind,
|
||||
msg,
|
||||
notes,
|
||||
span,
|
||||
}
|
||||
}
|
||||
pub fn kind(&self) -> &ShErrKind {
|
||||
match self {
|
||||
ShErr::Simple {
|
||||
kind,
|
||||
msg: _,
|
||||
notes: _,
|
||||
}
|
||||
| ShErr::Full {
|
||||
kind,
|
||||
msg: _,
|
||||
notes: _,
|
||||
span: _,
|
||||
} => kind,
|
||||
}
|
||||
}
|
||||
pub fn get_window(&self) -> Vec<(usize, String)> {
|
||||
let ShErr::Full {
|
||||
kind: _,
|
||||
msg: _,
|
||||
notes: _,
|
||||
span,
|
||||
} = self
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let mut total_len: usize = 0;
|
||||
let mut total_lines: usize = 1;
|
||||
let mut lines = vec![];
|
||||
let mut cur_line = String::new();
|
||||
|
||||
let src = span.get_source();
|
||||
let mut chars = src.chars();
|
||||
let src = span.get_source();
|
||||
let mut chars = src.chars();
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
total_len += ch.len_utf8();
|
||||
cur_line.push(ch);
|
||||
if ch == '\n' {
|
||||
if total_len > span.start {
|
||||
let line = (
|
||||
total_lines,
|
||||
mem::take(&mut cur_line)
|
||||
);
|
||||
lines.push(line);
|
||||
}
|
||||
if total_len >= span.end {
|
||||
break
|
||||
}
|
||||
total_lines += 1;
|
||||
while let Some(ch) = chars.next() {
|
||||
total_len += ch.len_utf8();
|
||||
cur_line.push(ch);
|
||||
if ch == '\n' {
|
||||
if total_len > span.start {
|
||||
let line = (total_lines, mem::take(&mut cur_line));
|
||||
lines.push(line);
|
||||
}
|
||||
if total_len >= span.end {
|
||||
break;
|
||||
}
|
||||
total_lines += 1;
|
||||
|
||||
cur_line.clear();
|
||||
}
|
||||
}
|
||||
cur_line.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if !cur_line.is_empty() {
|
||||
let line = (
|
||||
total_lines,
|
||||
mem::take(&mut cur_line)
|
||||
);
|
||||
lines.push(line);
|
||||
}
|
||||
if !cur_line.is_empty() {
|
||||
let line = (total_lines, mem::take(&mut cur_line));
|
||||
lines.push(line);
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
pub fn get_line_col(&self) -> (usize,usize) {
|
||||
let ShErr::Full { kind: _, msg: _, notes: _, span } = self else {
|
||||
unreachable!()
|
||||
};
|
||||
lines
|
||||
}
|
||||
pub fn get_line_col(&self) -> (usize, usize) {
|
||||
let ShErr::Full {
|
||||
kind: _,
|
||||
msg: _,
|
||||
notes: _,
|
||||
span,
|
||||
} = self
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let mut lineno = 1;
|
||||
let mut colno = 1;
|
||||
let src = span.get_source();
|
||||
let mut chars = src.chars().enumerate();
|
||||
while let Some((pos,ch)) = chars.next() {
|
||||
if pos >= span.start {
|
||||
break
|
||||
}
|
||||
if ch == '\n' {
|
||||
lineno += 1;
|
||||
colno = 1;
|
||||
} else {
|
||||
colno += 1;
|
||||
}
|
||||
}
|
||||
(lineno,colno)
|
||||
}
|
||||
pub fn get_indicator_lines(&self) -> Option<Vec<String>> {
|
||||
match self {
|
||||
ShErr::Simple { kind: _, msg: _, notes: _ } => None,
|
||||
ShErr::Full { kind: _, msg: _, notes: _, span } => {
|
||||
let text = span.as_str();
|
||||
let lines = text.lines();
|
||||
let mut indicator_lines = vec![];
|
||||
let mut lineno = 1;
|
||||
let mut colno = 1;
|
||||
let src = span.get_source();
|
||||
let mut chars = src.chars().enumerate();
|
||||
while let Some((pos, ch)) = chars.next() {
|
||||
if pos >= span.start {
|
||||
break;
|
||||
}
|
||||
if ch == '\n' {
|
||||
lineno += 1;
|
||||
colno = 1;
|
||||
} else {
|
||||
colno += 1;
|
||||
}
|
||||
}
|
||||
(lineno, colno)
|
||||
}
|
||||
pub fn get_indicator_lines(&self) -> Option<Vec<String>> {
|
||||
match self {
|
||||
ShErr::Simple {
|
||||
kind: _,
|
||||
msg: _,
|
||||
notes: _,
|
||||
} => None,
|
||||
ShErr::Full {
|
||||
kind: _,
|
||||
msg: _,
|
||||
notes: _,
|
||||
span,
|
||||
} => {
|
||||
let text = span.as_str();
|
||||
let lines = text.lines();
|
||||
let mut indicator_lines = vec![];
|
||||
|
||||
for line in lines {
|
||||
let indicator_line = "^".repeat(line.trim().len()).styled(Style::Red | Style::Bold);
|
||||
indicator_lines.push(indicator_line);
|
||||
}
|
||||
for line in lines {
|
||||
let indicator_line = "^"
|
||||
.repeat(line.trim().len())
|
||||
.styled(Style::Red | Style::Bold);
|
||||
indicator_lines.push(indicator_line);
|
||||
}
|
||||
|
||||
Some(indicator_lines)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(indicator_lines)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ShErr {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Simple { msg, kind: _, notes } => {
|
||||
let mut all_strings = vec![msg.to_string()];
|
||||
let mut notes_fmt = vec![];
|
||||
for note in notes {
|
||||
let fmt = format!("{note}");
|
||||
notes_fmt.push(fmt);
|
||||
}
|
||||
all_strings.append(&mut notes_fmt);
|
||||
let mut output = all_strings.join("\n");
|
||||
output.push('\n');
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Simple {
|
||||
msg,
|
||||
kind: _,
|
||||
notes,
|
||||
} => {
|
||||
let mut all_strings = vec![msg.to_string()];
|
||||
let mut notes_fmt = vec![];
|
||||
for note in notes {
|
||||
let fmt = format!("{note}");
|
||||
notes_fmt.push(fmt);
|
||||
}
|
||||
all_strings.append(&mut notes_fmt);
|
||||
let mut output = all_strings.join("\n");
|
||||
output.push('\n');
|
||||
|
||||
writeln!(f, "{}", output)
|
||||
}
|
||||
writeln!(f, "{}", output)
|
||||
}
|
||||
|
||||
Self::Full { msg, kind, notes, span: _ } => {
|
||||
let window = self.get_window();
|
||||
let mut indicator_lines = self.get_indicator_lines().unwrap().into_iter();
|
||||
let mut lineno_pad_count = 0;
|
||||
for (lineno,_) in window.clone() {
|
||||
if lineno.to_string().len() > lineno_pad_count {
|
||||
lineno_pad_count = lineno.to_string().len() + 1
|
||||
}
|
||||
}
|
||||
let padding = " ".repeat(lineno_pad_count);
|
||||
writeln!(f)?;
|
||||
Self::Full {
|
||||
msg,
|
||||
kind,
|
||||
notes,
|
||||
span: _,
|
||||
} => {
|
||||
let window = self.get_window();
|
||||
let mut indicator_lines = self.get_indicator_lines().unwrap().into_iter();
|
||||
let mut lineno_pad_count = 0;
|
||||
for (lineno, _) in window.clone() {
|
||||
if lineno.to_string().len() > lineno_pad_count {
|
||||
lineno_pad_count = lineno.to_string().len() + 1
|
||||
}
|
||||
}
|
||||
let padding = " ".repeat(lineno_pad_count);
|
||||
writeln!(f)?;
|
||||
|
||||
let (line, col) = self.get_line_col();
|
||||
let line_fmt = line.styled(Style::Cyan | Style::Bold);
|
||||
let col_fmt = col.styled(Style::Cyan | Style::Bold);
|
||||
let kind = kind.styled(Style::Red | Style::Bold);
|
||||
let arrow = "->".styled(Style::Cyan | Style::Bold);
|
||||
writeln!(f, "{kind} - {msg}",)?;
|
||||
writeln!(f, "{padding}{arrow} [{line_fmt};{col_fmt}]",)?;
|
||||
|
||||
let (line,col) = self.get_line_col();
|
||||
let line_fmt = line.styled(Style::Cyan | Style::Bold);
|
||||
let col_fmt = col.styled(Style::Cyan | Style::Bold);
|
||||
let kind = kind.styled(Style::Red | Style::Bold);
|
||||
let arrow = "->".styled(Style::Cyan | Style::Bold);
|
||||
writeln!(f,
|
||||
"{kind} - {msg}",
|
||||
)?;
|
||||
writeln!(f,
|
||||
"{padding}{arrow} [{line_fmt};{col_fmt}]",
|
||||
)?;
|
||||
let bar = format!("{padding}|").styled(Style::Cyan | Style::Bold);
|
||||
writeln!(f, "{bar}")?;
|
||||
|
||||
let bar = format!("{padding}|").styled(Style::Cyan | Style::Bold);
|
||||
writeln!(f,"{bar}")?;
|
||||
let mut first_ind_ln = true;
|
||||
for (lineno, line) in window {
|
||||
let lineno = lineno.to_string();
|
||||
let line = line.trim();
|
||||
let mut prefix = format!("{padding}|");
|
||||
prefix.replace_range(0..lineno.len(), &lineno);
|
||||
prefix = prefix.styled(Style::Cyan | Style::Bold);
|
||||
writeln!(f, "{prefix} {line}")?;
|
||||
|
||||
let mut first_ind_ln = true;
|
||||
for (lineno,line) in window {
|
||||
let lineno = lineno.to_string();
|
||||
let line = line.trim();
|
||||
let mut prefix = format!("{padding}|");
|
||||
prefix.replace_range(0..lineno.len(), &lineno);
|
||||
prefix = prefix.styled(Style::Cyan | Style::Bold);
|
||||
writeln!(f,"{prefix} {line}")?;
|
||||
if let Some(ind_ln) = indicator_lines.next() {
|
||||
if first_ind_ln {
|
||||
let ind_ln_padding = " ".repeat(col);
|
||||
let ind_ln = format!("{ind_ln_padding}{ind_ln}");
|
||||
writeln!(f, "{bar}{ind_ln}")?;
|
||||
first_ind_ln = false;
|
||||
} else {
|
||||
writeln!(f, "{bar} {ind_ln}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ind_ln) = indicator_lines.next() {
|
||||
if first_ind_ln {
|
||||
let ind_ln_padding = " ".repeat(col);
|
||||
let ind_ln = format!("{ind_ln_padding}{ind_ln}");
|
||||
writeln!(f, "{bar}{ind_ln}")?;
|
||||
first_ind_ln = false;
|
||||
} else {
|
||||
writeln!(f, "{bar} {ind_ln}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
write!(f, "{bar}")?;
|
||||
|
||||
write!(f,"{bar}")?;
|
||||
|
||||
|
||||
let bar_break = "-".styled(Style::Cyan | Style::Bold);
|
||||
if !notes.is_empty() {
|
||||
writeln!(f)?;
|
||||
}
|
||||
for note in notes {
|
||||
|
||||
write!(f,
|
||||
"{padding}{bar_break} {note}"
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
let bar_break = "-".styled(Style::Cyan | Style::Bold);
|
||||
if !notes.is_empty() {
|
||||
writeln!(f)?;
|
||||
}
|
||||
for note in notes {
|
||||
write!(f, "{padding}{bar_break} {note}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for ShErr {
|
||||
fn from(_: std::io::Error) -> Self {
|
||||
let msg = std::io::Error::last_os_error();
|
||||
ShErr::simple(ShErrKind::IoErr, msg.to_string())
|
||||
}
|
||||
fn from(_: std::io::Error) -> Self {
|
||||
let msg = std::io::Error::last_os_error();
|
||||
ShErr::simple(ShErrKind::IoErr, msg.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::env::VarError> for ShErr {
|
||||
fn from(value: std::env::VarError) -> Self {
|
||||
ShErr::simple(ShErrKind::InternalErr, value.to_string())
|
||||
}
|
||||
fn from(value: std::env::VarError) -> Self {
|
||||
ShErr::simple(ShErrKind::InternalErr, value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Errno> for ShErr {
|
||||
fn from(value: Errno) -> Self {
|
||||
ShErr::simple(ShErrKind::Errno, value.to_string())
|
||||
}
|
||||
fn from(value: Errno) -> Self {
|
||||
ShErr::simple(ShErrKind::Errno, value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ShErrKind {
|
||||
IoErr,
|
||||
SyntaxErr,
|
||||
ParseErr,
|
||||
InternalErr,
|
||||
ExecFail,
|
||||
HistoryReadErr,
|
||||
ResourceLimitExceeded,
|
||||
BadPermission,
|
||||
Errno,
|
||||
FileNotFound(String),
|
||||
CmdNotFound(String),
|
||||
CleanExit(i32),
|
||||
FuncReturn(i32),
|
||||
LoopContinue(i32),
|
||||
LoopBreak(i32),
|
||||
ReadlineErr,
|
||||
Null
|
||||
IoErr,
|
||||
SyntaxErr,
|
||||
ParseErr,
|
||||
InternalErr,
|
||||
ExecFail,
|
||||
HistoryReadErr,
|
||||
ResourceLimitExceeded,
|
||||
BadPermission,
|
||||
Errno,
|
||||
FileNotFound(String),
|
||||
CmdNotFound(String),
|
||||
CleanExit(i32),
|
||||
FuncReturn(i32),
|
||||
LoopContinue(i32),
|
||||
LoopBreak(i32),
|
||||
ReadlineErr,
|
||||
Null,
|
||||
}
|
||||
|
||||
impl Display for ShErrKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let output = match self {
|
||||
Self::IoErr => "I/O Error",
|
||||
Self::SyntaxErr => "Syntax Error",
|
||||
Self::ParseErr => "Parse Error",
|
||||
Self::InternalErr => "Internal Error",
|
||||
Self::HistoryReadErr => "History Parse Error",
|
||||
Self::ExecFail => "Execution Failed",
|
||||
Self::ResourceLimitExceeded => "Resource Limit Exceeded",
|
||||
Self::BadPermission => "Bad Permissions",
|
||||
Self::Errno => "ERRNO",
|
||||
Self::FileNotFound(file) => &format!("File not found: {file}"),
|
||||
Self::CmdNotFound(cmd) => &format!("Command not found: {cmd}"),
|
||||
Self::CleanExit(_) => "",
|
||||
Self::FuncReturn(_) => "",
|
||||
Self::LoopContinue(_) => "",
|
||||
Self::LoopBreak(_) => "",
|
||||
Self::ReadlineErr => "Line Read Error",
|
||||
Self::Null => "",
|
||||
};
|
||||
write!(f,"{output}")
|
||||
}
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let output = match self {
|
||||
Self::IoErr => "I/O Error",
|
||||
Self::SyntaxErr => "Syntax Error",
|
||||
Self::ParseErr => "Parse Error",
|
||||
Self::InternalErr => "Internal Error",
|
||||
Self::HistoryReadErr => "History Parse Error",
|
||||
Self::ExecFail => "Execution Failed",
|
||||
Self::ResourceLimitExceeded => "Resource Limit Exceeded",
|
||||
Self::BadPermission => "Bad Permissions",
|
||||
Self::Errno => "ERRNO",
|
||||
Self::FileNotFound(file) => &format!("File not found: {file}"),
|
||||
Self::CmdNotFound(cmd) => &format!("Command not found: {cmd}"),
|
||||
Self::CleanExit(_) => "",
|
||||
Self::FuncReturn(_) => "",
|
||||
Self::LoopContinue(_) => "",
|
||||
Self::LoopBreak(_) => "",
|
||||
Self::ReadlineErr => "Line Read Error",
|
||||
Self::Null => "",
|
||||
};
|
||||
write!(f, "{output}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,55 +2,57 @@ use std::fmt::Display;
|
||||
|
||||
use super::term::{Style, Styled};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, PartialOrd, Ord, Eq , Debug)]
|
||||
#[derive(Clone, Copy, PartialEq, PartialOrd, Ord, Eq, Debug)]
|
||||
#[repr(u8)]
|
||||
pub enum FernLogLevel {
|
||||
NONE = 0,
|
||||
ERROR = 1,
|
||||
WARN = 2,
|
||||
INFO = 3,
|
||||
DEBUG = 4,
|
||||
TRACE = 5
|
||||
NONE = 0,
|
||||
ERROR = 1,
|
||||
WARN = 2,
|
||||
INFO = 3,
|
||||
DEBUG = 4,
|
||||
TRACE = 5,
|
||||
}
|
||||
|
||||
impl Display for FernLogLevel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use FernLogLevel::*;
|
||||
match self {
|
||||
ERROR => write!(f,"{}","ERROR".styled(Style::Red | Style::Bold)),
|
||||
WARN => write!(f,"{}","WARN".styled(Style::Yellow | Style::Bold)),
|
||||
INFO => write!(f,"{}","INFO".styled(Style::Green | Style::Bold)),
|
||||
DEBUG => write!(f,"{}","DEBUG".styled(Style::Magenta | Style::Bold)),
|
||||
TRACE => write!(f,"{}","TRACE".styled(Style::Blue | Style::Bold)),
|
||||
NONE => write!(f,"")
|
||||
}
|
||||
}
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use FernLogLevel::*;
|
||||
match self {
|
||||
ERROR => write!(f, "{}", "ERROR".styled(Style::Red | Style::Bold)),
|
||||
WARN => write!(f, "{}", "WARN".styled(Style::Yellow | Style::Bold)),
|
||||
INFO => write!(f, "{}", "INFO".styled(Style::Green | Style::Bold)),
|
||||
DEBUG => write!(f, "{}", "DEBUG".styled(Style::Magenta | Style::Bold)),
|
||||
TRACE => write!(f, "{}", "TRACE".styled(Style::Blue | Style::Bold)),
|
||||
NONE => write!(f, ""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_level() -> FernLogLevel {
|
||||
use FernLogLevel::*;
|
||||
let level = std::env::var("FERN_LOG_LEVEL").unwrap_or_default();
|
||||
match level.to_lowercase().as_str() {
|
||||
"error" => ERROR,
|
||||
"warn" => WARN,
|
||||
"info" => INFO,
|
||||
"debug" => DEBUG,
|
||||
"trace" => TRACE,
|
||||
_ => NONE
|
||||
}
|
||||
use FernLogLevel::*;
|
||||
let level = std::env::var("FERN_LOG_LEVEL").unwrap_or_default();
|
||||
match level.to_lowercase().as_str() {
|
||||
"error" => ERROR,
|
||||
"warn" => WARN,
|
||||
"info" => INFO,
|
||||
"debug" => DEBUG,
|
||||
"trace" => TRACE,
|
||||
_ => NONE,
|
||||
}
|
||||
}
|
||||
|
||||
/// A structured logging macro designed for `fern`.
|
||||
///
|
||||
/// `flog!` was implemented because `rustyline` uses `env_logger`, which clutters the debug output.
|
||||
/// This macro prints log messages in a structured format, including the log level, filename, and line number.
|
||||
/// `flog!` was implemented because `rustyline` uses `env_logger`, which
|
||||
/// clutters the debug output. This macro prints log messages in a structured
|
||||
/// format, including the log level, filename, and line number.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// The macro supports three types of arguments:
|
||||
///
|
||||
/// ## 1. **Formatted Messages**
|
||||
/// Similar to `println!` or `format!`, allows embedding values inside a formatted string.
|
||||
/// Similar to `println!` or `format!`, allows embedding values inside a
|
||||
/// formatted string.
|
||||
///
|
||||
/// ```rust
|
||||
/// flog!(ERROR, "foo is {}", foo);
|
||||
@@ -73,7 +75,8 @@ pub fn log_level() -> FernLogLevel {
|
||||
/// ```
|
||||
///
|
||||
/// ## 3. **Expressions**
|
||||
/// Logs the evaluated result of each given expression, displaying both the expression and its value.
|
||||
/// Logs the evaluated result of each given expression, displaying both the
|
||||
/// expression and its value.
|
||||
///
|
||||
/// ```rust
|
||||
/// flog!(INFO, 1.min(2));
|
||||
@@ -84,8 +87,10 @@ pub fn log_level() -> FernLogLevel {
|
||||
/// ```
|
||||
///
|
||||
/// # Considerations
|
||||
/// - This macro uses `eprintln!()` internally, so its formatting rules must be followed.
|
||||
/// - **Literals and formatted messages** require arguments that implement [`std::fmt::Display`].
|
||||
/// - This macro uses `eprintln!()` internally, so its formatting rules must be
|
||||
/// followed.
|
||||
/// - **Literals and formatted messages** require arguments that implement
|
||||
/// [`std::fmt::Display`].
|
||||
/// - **Expressions** require arguments that implement [`std::fmt::Debug`].
|
||||
#[macro_export]
|
||||
macro_rules! flog {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pub mod error;
|
||||
pub mod term;
|
||||
pub mod flog;
|
||||
pub mod sys;
|
||||
pub mod term;
|
||||
pub mod utils;
|
||||
|
||||
109
src/libsh/sys.rs
109
src/libsh/sys.rs
@@ -4,70 +4,91 @@ use crate::{prelude::*, state::write_jobs};
|
||||
///
|
||||
/// The previous state of the terminal options.
|
||||
///
|
||||
/// This variable stores the terminal settings at the start of the program and restores them when the program exits.
|
||||
/// It is initialized exactly once at the start of the program and accessed exactly once at the end of the program.
|
||||
/// It will not be mutated or accessed under any other circumstances.
|
||||
/// This variable stores the terminal settings at the start of the program and
|
||||
/// restores them when the program exits. It is initialized exactly once at the
|
||||
/// start of the program and accessed exactly once at the end of the program. It
|
||||
/// will not be mutated or accessed under any other circumstances.
|
||||
///
|
||||
/// This ended up being necessary because wrapping Termios in a thread-safe way was unreasonably tricky.
|
||||
/// This ended up being necessary because wrapping Termios in a thread-safe way
|
||||
/// was unreasonably tricky.
|
||||
///
|
||||
/// The possible states of this variable are:
|
||||
/// - `None`: The terminal options have not been set yet (before initialization).
|
||||
/// - `Some(None)`: There were no terminal options to save (i.e., no terminal input detected).
|
||||
/// - `Some(Some(Termios))`: The terminal options (as `Termios`) have been saved.
|
||||
/// - `None`: The terminal options have not been set yet (before
|
||||
/// initialization).
|
||||
/// - `Some(None)`: There were no terminal options to save (i.e., no terminal
|
||||
/// input detected).
|
||||
/// - `Some(Some(Termios))`: The terminal options (as `Termios`) have been
|
||||
/// saved.
|
||||
///
|
||||
/// **Important:** This static variable is mutable and accessed via unsafe code. It is only safe to use because:
|
||||
/// - It is set once during program startup and accessed once during program exit.
|
||||
/// **Important:** This static variable is mutable and accessed via unsafe code.
|
||||
/// It is only safe to use because:
|
||||
/// - It is set once during program startup and accessed once during program
|
||||
/// exit.
|
||||
/// - It is not mutated or accessed after the initial setup and final read.
|
||||
///
|
||||
/// **Caution:** Future changes to this code should respect these constraints to ensure safety. Modifying or accessing this variable outside the defined lifecycle could lead to undefined behavior.
|
||||
/// **Caution:** Future changes to this code should respect these constraints to
|
||||
/// ensure safety. Modifying or accessing this variable outside the defined
|
||||
/// lifecycle could lead to undefined behavior.
|
||||
pub(crate) static mut SAVED_TERMIOS: Option<Option<Termios>> = None;
|
||||
|
||||
pub fn save_termios() {
|
||||
unsafe {
|
||||
SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() {
|
||||
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
|
||||
termios.local_flags &= !LocalFlags::ECHOCTL;
|
||||
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap();
|
||||
Some(termios)
|
||||
} else {
|
||||
None
|
||||
});
|
||||
}
|
||||
unsafe {
|
||||
SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() {
|
||||
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
|
||||
termios.local_flags &= !LocalFlags::ECHOCTL;
|
||||
termios::tcsetattr(
|
||||
std::io::stdin(),
|
||||
nix::sys::termios::SetArg::TCSANOW,
|
||||
&termios,
|
||||
)
|
||||
.unwrap();
|
||||
Some(termios)
|
||||
} else {
|
||||
None
|
||||
});
|
||||
}
|
||||
}
|
||||
#[allow(static_mut_refs)]
|
||||
///Access the saved termios
|
||||
///
|
||||
///# Safety
|
||||
///This function is unsafe because it accesses a public mutable static value. This function should only ever be called after save_termios() has already been called.
|
||||
///This function is unsafe because it accesses a public mutable static value.
|
||||
/// This function should only ever be called after save_termios() has already
|
||||
/// been called.
|
||||
pub unsafe fn get_saved_termios() -> Option<Termios> {
|
||||
// SAVED_TERMIOS should *only ever* be set once and accessed once
|
||||
// Set at the start of the program, and accessed during the exit of the program to reset the termios.
|
||||
// Do not use this variable anywhere else
|
||||
SAVED_TERMIOS.clone().flatten()
|
||||
// SAVED_TERMIOS should *only ever* be set once and accessed once
|
||||
// Set at the start of the program, and accessed during the exit of the program
|
||||
// to reset the termios. Do not use this variable anywhere else
|
||||
SAVED_TERMIOS.clone().flatten()
|
||||
}
|
||||
|
||||
/// Set termios to not echo control characters, like ^Z for instance
|
||||
pub fn set_termios() {
|
||||
if isatty(std::io::stdin().as_raw_fd()).unwrap() {
|
||||
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
|
||||
termios.local_flags &= !LocalFlags::ECHOCTL;
|
||||
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap();
|
||||
}
|
||||
if isatty(std::io::stdin().as_raw_fd()).unwrap() {
|
||||
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
|
||||
termios.local_flags &= !LocalFlags::ECHOCTL;
|
||||
termios::tcsetattr(
|
||||
std::io::stdin(),
|
||||
nix::sys::termios::SetArg::TCSANOW,
|
||||
&termios,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sh_quit(code: i32) -> ! {
|
||||
write_jobs(|j| {
|
||||
for job in j.jobs_mut().iter_mut().flatten() {
|
||||
job.killpg(Signal::SIGTERM).ok();
|
||||
}
|
||||
});
|
||||
if let Some(termios) = unsafe { get_saved_termios() } {
|
||||
termios::tcsetattr(std::io::stdin(), termios::SetArg::TCSANOW, &termios).unwrap();
|
||||
}
|
||||
if code == 0 {
|
||||
eprintln!("exit");
|
||||
} else {
|
||||
eprintln!("exit {code}");
|
||||
}
|
||||
exit(code);
|
||||
write_jobs(|j| {
|
||||
for job in j.jobs_mut().iter_mut().flatten() {
|
||||
job.killpg(Signal::SIGTERM).ok();
|
||||
}
|
||||
});
|
||||
if let Some(termios) = unsafe { get_saved_termios() } {
|
||||
termios::tcsetattr(std::io::stdin(), termios::SetArg::TCSANOW, &termios).unwrap();
|
||||
}
|
||||
if code == 0 {
|
||||
eprintln!("exit");
|
||||
} else {
|
||||
eprintln!("exit {code}");
|
||||
}
|
||||
exit(code);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::{fmt::Display, ops::BitOr};
|
||||
|
||||
pub trait Styled: Sized + Display {
|
||||
fn styled<S: Into<StyleSet>>(self, style: S) -> String {
|
||||
let styles: StyleSet = style.into();
|
||||
let reset = Style::Reset;
|
||||
format!("{styles}{self}{reset}")
|
||||
}
|
||||
fn styled<S: Into<StyleSet>>(self, style: S) -> String {
|
||||
let styles: StyleSet = style.into();
|
||||
let reset = Style::Reset;
|
||||
format!("{styles}{self}{reset}")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Display> Styled for T {}
|
||||
@@ -13,157 +13,157 @@ impl<T: Display> Styled for T {}
|
||||
/// Enum representing a single ANSI style
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Style {
|
||||
// Undoes all styles
|
||||
Reset,
|
||||
// Foreground Colors
|
||||
Black,
|
||||
Red,
|
||||
Green,
|
||||
Yellow,
|
||||
Blue,
|
||||
Magenta,
|
||||
Cyan,
|
||||
White,
|
||||
BrightBlack,
|
||||
BrightRed,
|
||||
BrightGreen,
|
||||
BrightYellow,
|
||||
BrightBlue,
|
||||
BrightMagenta,
|
||||
BrightCyan,
|
||||
BrightWhite,
|
||||
RGB(u8, u8, u8), // Custom foreground color
|
||||
// Undoes all styles
|
||||
Reset,
|
||||
// Foreground Colors
|
||||
Black,
|
||||
Red,
|
||||
Green,
|
||||
Yellow,
|
||||
Blue,
|
||||
Magenta,
|
||||
Cyan,
|
||||
White,
|
||||
BrightBlack,
|
||||
BrightRed,
|
||||
BrightGreen,
|
||||
BrightYellow,
|
||||
BrightBlue,
|
||||
BrightMagenta,
|
||||
BrightCyan,
|
||||
BrightWhite,
|
||||
RGB(u8, u8, u8), // Custom foreground color
|
||||
|
||||
// Background Colors
|
||||
BgBlack,
|
||||
BgRed,
|
||||
BgGreen,
|
||||
BgYellow,
|
||||
BgBlue,
|
||||
BgMagenta,
|
||||
BgCyan,
|
||||
BgWhite,
|
||||
BgBrightBlack,
|
||||
BgBrightRed,
|
||||
BgBrightGreen,
|
||||
BgBrightYellow,
|
||||
BgBrightBlue,
|
||||
BgBrightMagenta,
|
||||
BgBrightCyan,
|
||||
BgBrightWhite,
|
||||
BgRGB(u8, u8, u8), // Custom background color
|
||||
// Background Colors
|
||||
BgBlack,
|
||||
BgRed,
|
||||
BgGreen,
|
||||
BgYellow,
|
||||
BgBlue,
|
||||
BgMagenta,
|
||||
BgCyan,
|
||||
BgWhite,
|
||||
BgBrightBlack,
|
||||
BgBrightRed,
|
||||
BgBrightGreen,
|
||||
BgBrightYellow,
|
||||
BgBrightBlue,
|
||||
BgBrightMagenta,
|
||||
BgBrightCyan,
|
||||
BgBrightWhite,
|
||||
BgRGB(u8, u8, u8), // Custom background color
|
||||
|
||||
// Text Attributes
|
||||
Bold,
|
||||
Dim,
|
||||
Italic,
|
||||
Underline,
|
||||
Strikethrough,
|
||||
Reversed,
|
||||
// Text Attributes
|
||||
Bold,
|
||||
Dim,
|
||||
Italic,
|
||||
Underline,
|
||||
Strikethrough,
|
||||
Reversed,
|
||||
}
|
||||
|
||||
impl Display for Style {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Style::Reset => write!(f, "\x1b[0m"),
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Style::Reset => write!(f, "\x1b[0m"),
|
||||
|
||||
// Foreground colors
|
||||
Style::Black => write!(f, "\x1b[30m"),
|
||||
Style::Red => write!(f, "\x1b[31m"),
|
||||
Style::Green => write!(f, "\x1b[32m"),
|
||||
Style::Yellow => write!(f, "\x1b[33m"),
|
||||
Style::Blue => write!(f, "\x1b[34m"),
|
||||
Style::Magenta => write!(f, "\x1b[35m"),
|
||||
Style::Cyan => write!(f, "\x1b[36m"),
|
||||
Style::White => write!(f, "\x1b[37m"),
|
||||
Style::BrightBlack => write!(f, "\x1b[90m"),
|
||||
Style::BrightRed => write!(f, "\x1b[91m"),
|
||||
Style::BrightGreen => write!(f, "\x1b[92m"),
|
||||
Style::BrightYellow => write!(f, "\x1b[93m"),
|
||||
Style::BrightBlue => write!(f, "\x1b[94m"),
|
||||
Style::BrightMagenta => write!(f, "\x1b[95m"),
|
||||
Style::BrightCyan => write!(f, "\x1b[96m"),
|
||||
Style::BrightWhite => write!(f, "\x1b[97m"),
|
||||
Style::RGB(r, g, b) => write!(f, "\x1b[38;2;{r};{g};{b}m"),
|
||||
// Foreground colors
|
||||
Style::Black => write!(f, "\x1b[30m"),
|
||||
Style::Red => write!(f, "\x1b[31m"),
|
||||
Style::Green => write!(f, "\x1b[32m"),
|
||||
Style::Yellow => write!(f, "\x1b[33m"),
|
||||
Style::Blue => write!(f, "\x1b[34m"),
|
||||
Style::Magenta => write!(f, "\x1b[35m"),
|
||||
Style::Cyan => write!(f, "\x1b[36m"),
|
||||
Style::White => write!(f, "\x1b[37m"),
|
||||
Style::BrightBlack => write!(f, "\x1b[90m"),
|
||||
Style::BrightRed => write!(f, "\x1b[91m"),
|
||||
Style::BrightGreen => write!(f, "\x1b[92m"),
|
||||
Style::BrightYellow => write!(f, "\x1b[93m"),
|
||||
Style::BrightBlue => write!(f, "\x1b[94m"),
|
||||
Style::BrightMagenta => write!(f, "\x1b[95m"),
|
||||
Style::BrightCyan => write!(f, "\x1b[96m"),
|
||||
Style::BrightWhite => write!(f, "\x1b[97m"),
|
||||
Style::RGB(r, g, b) => write!(f, "\x1b[38;2;{r};{g};{b}m"),
|
||||
|
||||
// Background colors
|
||||
Style::BgBlack => write!(f, "\x1b[40m"),
|
||||
Style::BgRed => write!(f, "\x1b[41m"),
|
||||
Style::BgGreen => write!(f, "\x1b[42m"),
|
||||
Style::BgYellow => write!(f, "\x1b[43m"),
|
||||
Style::BgBlue => write!(f, "\x1b[44m"),
|
||||
Style::BgMagenta => write!(f, "\x1b[45m"),
|
||||
Style::BgCyan => write!(f, "\x1b[46m"),
|
||||
Style::BgWhite => write!(f, "\x1b[47m"),
|
||||
Style::BgBrightBlack => write!(f, "\x1b[100m"),
|
||||
Style::BgBrightRed => write!(f, "\x1b[101m"),
|
||||
Style::BgBrightGreen => write!(f, "\x1b[102m"),
|
||||
Style::BgBrightYellow => write!(f, "\x1b[103m"),
|
||||
Style::BgBrightBlue => write!(f, "\x1b[104m"),
|
||||
Style::BgBrightMagenta => write!(f, "\x1b[105m"),
|
||||
Style::BgBrightCyan => write!(f, "\x1b[106m"),
|
||||
Style::BgBrightWhite => write!(f, "\x1b[107m"),
|
||||
Style::BgRGB(r, g, b) => write!(f, "\x1b[48;2;{r};{g};{b}m"),
|
||||
// Background colors
|
||||
Style::BgBlack => write!(f, "\x1b[40m"),
|
||||
Style::BgRed => write!(f, "\x1b[41m"),
|
||||
Style::BgGreen => write!(f, "\x1b[42m"),
|
||||
Style::BgYellow => write!(f, "\x1b[43m"),
|
||||
Style::BgBlue => write!(f, "\x1b[44m"),
|
||||
Style::BgMagenta => write!(f, "\x1b[45m"),
|
||||
Style::BgCyan => write!(f, "\x1b[46m"),
|
||||
Style::BgWhite => write!(f, "\x1b[47m"),
|
||||
Style::BgBrightBlack => write!(f, "\x1b[100m"),
|
||||
Style::BgBrightRed => write!(f, "\x1b[101m"),
|
||||
Style::BgBrightGreen => write!(f, "\x1b[102m"),
|
||||
Style::BgBrightYellow => write!(f, "\x1b[103m"),
|
||||
Style::BgBrightBlue => write!(f, "\x1b[104m"),
|
||||
Style::BgBrightMagenta => write!(f, "\x1b[105m"),
|
||||
Style::BgBrightCyan => write!(f, "\x1b[106m"),
|
||||
Style::BgBrightWhite => write!(f, "\x1b[107m"),
|
||||
Style::BgRGB(r, g, b) => write!(f, "\x1b[48;2;{r};{g};{b}m"),
|
||||
|
||||
// Text attributes
|
||||
Style::Bold => write!(f, "\x1b[1m"),
|
||||
Style::Dim => write!(f, "\x1b[2m"), // New
|
||||
Style::Italic => write!(f, "\x1b[3m"),
|
||||
Style::Underline => write!(f, "\x1b[4m"),
|
||||
Style::Strikethrough => write!(f, "\x1b[9m"), // New
|
||||
Style::Reversed => write!(f, "\x1b[7m"),
|
||||
}
|
||||
}
|
||||
// Text attributes
|
||||
Style::Bold => write!(f, "\x1b[1m"),
|
||||
Style::Dim => write!(f, "\x1b[2m"), // New
|
||||
Style::Italic => write!(f, "\x1b[3m"),
|
||||
Style::Underline => write!(f, "\x1b[4m"),
|
||||
Style::Strikethrough => write!(f, "\x1b[9m"), // New
|
||||
Style::Reversed => write!(f, "\x1b[7m"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct representing a **set** of styles
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct StyleSet {
|
||||
styles: Vec<Style>,
|
||||
styles: Vec<Style>,
|
||||
}
|
||||
|
||||
impl StyleSet {
|
||||
pub fn new() -> Self {
|
||||
Self { styles: vec![] }
|
||||
}
|
||||
pub fn new() -> Self {
|
||||
Self { styles: vec![] }
|
||||
}
|
||||
|
||||
pub fn add_style(mut self, style: Style) -> Self {
|
||||
if !self.styles.contains(&style) {
|
||||
self.styles.push(style);
|
||||
}
|
||||
self
|
||||
}
|
||||
pub fn add_style(mut self, style: Style) -> Self {
|
||||
if !self.styles.contains(&style) {
|
||||
self.styles.push(style);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for StyleSet {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for style in &self.styles {
|
||||
style.fmt(f)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for style in &self.styles {
|
||||
style.fmt(f)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Allow OR (`|`) operator to combine multiple `Style` values into a `StyleSet`
|
||||
impl BitOr for Style {
|
||||
type Output = StyleSet;
|
||||
type Output = StyleSet;
|
||||
|
||||
fn bitor(self, rhs: Self) -> Self::Output {
|
||||
StyleSet::new().add_style(self).add_style(rhs)
|
||||
}
|
||||
fn bitor(self, rhs: Self) -> Self::Output {
|
||||
StyleSet::new().add_style(self).add_style(rhs)
|
||||
}
|
||||
}
|
||||
|
||||
/// Allow OR (`|`) operator to combine `StyleSet` with `Style`
|
||||
impl BitOr<Style> for StyleSet {
|
||||
type Output = StyleSet;
|
||||
type Output = StyleSet;
|
||||
|
||||
fn bitor(self, rhs: Style) -> Self::Output {
|
||||
self.add_style(rhs)
|
||||
}
|
||||
fn bitor(self, rhs: Style) -> Self::Output {
|
||||
self.add_style(rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Style> for StyleSet {
|
||||
fn from(style: Style) -> Self {
|
||||
StyleSet::new().add_style(style)
|
||||
}
|
||||
fn from(style: Style) -> Self {
|
||||
StyleSet::new().add_style(style)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,107 +5,105 @@ use crate::parse::{Redir, RedirType};
|
||||
use crate::prelude::*;
|
||||
|
||||
pub trait VecDequeExt<T> {
|
||||
fn to_vec(self) -> Vec<T>;
|
||||
fn to_vec(self) -> Vec<T>;
|
||||
}
|
||||
|
||||
pub trait CharDequeUtils {
|
||||
fn to_string(self) -> String;
|
||||
fn ends_with(&self, pat: &str) -> bool;
|
||||
fn starts_with(&self, pat: &str) -> bool;
|
||||
fn to_string(self) -> String;
|
||||
fn ends_with(&self, pat: &str) -> bool;
|
||||
fn starts_with(&self, pat: &str) -> bool;
|
||||
}
|
||||
|
||||
pub trait TkVecUtils<Tk> {
|
||||
fn get_span(&self) -> Option<Span>;
|
||||
fn debug_tokens(&self);
|
||||
fn get_span(&self) -> Option<Span>;
|
||||
fn debug_tokens(&self);
|
||||
}
|
||||
|
||||
pub trait RedirVecUtils<Redir> {
|
||||
/// Splits the vector of redirections into two vectors
|
||||
///
|
||||
/// One vector contains input redirs, the other contains output redirs
|
||||
fn split_by_channel(self) -> (Vec<Redir>,Vec<Redir>);
|
||||
/// Splits the vector of redirections into two vectors
|
||||
///
|
||||
/// One vector contains input redirs, the other contains output redirs
|
||||
fn split_by_channel(self) -> (Vec<Redir>, Vec<Redir>);
|
||||
}
|
||||
|
||||
impl<T> VecDequeExt<T> for VecDeque<T> {
|
||||
fn to_vec(self) -> Vec<T> {
|
||||
self.into_iter().collect::<Vec<T>>()
|
||||
}
|
||||
fn to_vec(self) -> Vec<T> {
|
||||
self.into_iter().collect::<Vec<T>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl CharDequeUtils for VecDeque<char> {
|
||||
fn to_string(mut self) -> String {
|
||||
let mut result = String::with_capacity(self.len());
|
||||
while let Some(ch) = self.pop_front() {
|
||||
result.push(ch);
|
||||
}
|
||||
result
|
||||
}
|
||||
fn to_string(mut self) -> String {
|
||||
let mut result = String::with_capacity(self.len());
|
||||
while let Some(ch) = self.pop_front() {
|
||||
result.push(ch);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn ends_with(&self, pat: &str) -> bool {
|
||||
let pat_chars = pat.chars();
|
||||
let self_len = self.len();
|
||||
fn ends_with(&self, pat: &str) -> bool {
|
||||
let pat_chars = pat.chars();
|
||||
let self_len = self.len();
|
||||
|
||||
// If pattern is longer than self, return false
|
||||
if pat_chars.clone().count() > self_len {
|
||||
return false;
|
||||
}
|
||||
// If pattern is longer than self, return false
|
||||
if pat_chars.clone().count() > self_len {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare from the back
|
||||
self.iter().rev().zip(pat_chars.rev()).all(|(c1, c2)| c1 == &c2)
|
||||
}
|
||||
// Compare from the back
|
||||
self
|
||||
.iter()
|
||||
.rev()
|
||||
.zip(pat_chars.rev())
|
||||
.all(|(c1, c2)| c1 == &c2)
|
||||
}
|
||||
|
||||
fn starts_with(&self, pat: &str) -> bool {
|
||||
let pat_chars = pat.chars();
|
||||
let self_len = self.len();
|
||||
fn starts_with(&self, pat: &str) -> bool {
|
||||
let pat_chars = pat.chars();
|
||||
let self_len = self.len();
|
||||
|
||||
// If pattern is longer than self, return false
|
||||
if pat_chars.clone().count() > self_len {
|
||||
return false;
|
||||
}
|
||||
// If pattern is longer than self, return false
|
||||
if pat_chars.clone().count() > self_len {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare from the front
|
||||
self.iter().zip(pat_chars).all(|(c1, c2)| c1 == &c2)
|
||||
}
|
||||
// Compare from the front
|
||||
self.iter().zip(pat_chars).all(|(c1, c2)| c1 == &c2)
|
||||
}
|
||||
}
|
||||
|
||||
impl TkVecUtils<Tk> for Vec<Tk> {
|
||||
fn get_span(&self) -> Option<Span> {
|
||||
if let Some(first_tk) = self.first() {
|
||||
self.last().map(|last_tk| {
|
||||
Span::new(
|
||||
first_tk.span.start..last_tk.span.end,
|
||||
first_tk.source()
|
||||
)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn debug_tokens(&self) {
|
||||
for token in self {
|
||||
flog!(DEBUG, "token: {}",token)
|
||||
}
|
||||
}
|
||||
fn get_span(&self) -> Option<Span> {
|
||||
if let Some(first_tk) = self.first() {
|
||||
self
|
||||
.last()
|
||||
.map(|last_tk| Span::new(first_tk.span.start..last_tk.span.end, first_tk.source()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn debug_tokens(&self) {
|
||||
for token in self {
|
||||
flog!(DEBUG, "token: {}", token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RedirVecUtils<Redir> for Vec<Redir> {
|
||||
fn split_by_channel(self) -> (Vec<Redir>,Vec<Redir>) {
|
||||
let mut input = vec![];
|
||||
let mut output = vec![];
|
||||
for redir in self {
|
||||
match redir.class {
|
||||
RedirType::Input => input.push(redir),
|
||||
RedirType::Pipe => {
|
||||
match redir.io_mode.tgt_fd() {
|
||||
STDIN_FILENO => input.push(redir),
|
||||
STDOUT_FILENO |
|
||||
STDERR_FILENO => output.push(redir),
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
_ => output.push(redir)
|
||||
}
|
||||
}
|
||||
(input,output)
|
||||
}
|
||||
fn split_by_channel(self) -> (Vec<Redir>, Vec<Redir>) {
|
||||
let mut input = vec![];
|
||||
let mut output = vec![];
|
||||
for redir in self {
|
||||
match redir.class {
|
||||
RedirType::Input => input.push(redir),
|
||||
RedirType::Pipe => match redir.io_mode.tgt_fd() {
|
||||
STDIN_FILENO => input.push(redir),
|
||||
STDOUT_FILENO | STDERR_FILENO => output.push(redir),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
_ => output.push(redir),
|
||||
}
|
||||
}
|
||||
(input, output)
|
||||
}
|
||||
}
|
||||
|
||||
1243
src/parse/execute.rs
1243
src/parse/execute.rs
File diff suppressed because it is too large
Load Diff
1596
src/parse/lex.rs
1596
src/parse/lex.rs
File diff suppressed because it is too large
Load Diff
2810
src/parse/mod.rs
2810
src/parse/mod.rs
File diff suppressed because it is too large
Load Diff
@@ -1,46 +1,37 @@
|
||||
// Standard Library Common IO and FS Abstractions
|
||||
pub use std::io::{
|
||||
self,
|
||||
BufRead,
|
||||
BufReader,
|
||||
BufWriter,
|
||||
Error,
|
||||
ErrorKind,
|
||||
Read,
|
||||
Seek,
|
||||
SeekFrom,
|
||||
Write,
|
||||
};
|
||||
pub use std::fs::{ self, File, OpenOptions };
|
||||
pub use std::path::{ Path, PathBuf };
|
||||
pub use std::ffi::{ CStr, CString };
|
||||
pub use std::process::exit;
|
||||
pub use std::time::Instant;
|
||||
pub use std::sync::Arc;
|
||||
pub use std::mem;
|
||||
pub use std::env;
|
||||
pub use std::ffi::{CStr, CString};
|
||||
pub use std::fmt;
|
||||
pub use std::fs::{self, File, OpenOptions};
|
||||
pub use std::io::{
|
||||
self, BufRead, BufReader, BufWriter, Error, ErrorKind, Read, Seek, SeekFrom, Write,
|
||||
};
|
||||
pub use std::mem;
|
||||
pub use std::path::{Path, PathBuf};
|
||||
pub use std::process::exit;
|
||||
pub use std::sync::Arc;
|
||||
pub use std::time::Instant;
|
||||
|
||||
// Unix-specific IO abstractions
|
||||
pub use std::os::unix::io::{ AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd, };
|
||||
pub use std::os::unix::io::{AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
|
||||
|
||||
// Nix crate for POSIX APIs
|
||||
pub use nix::{
|
||||
errno::Errno,
|
||||
fcntl::{ open, OFlag },
|
||||
sys::{
|
||||
termios::{ self },
|
||||
signal::{ self, signal, kill, killpg, pthread_sigmask, SigSet, SigmaskHow, SigHandler, Signal },
|
||||
stat::Mode,
|
||||
wait::{ waitpid, WaitPidFlag as WtFlag, WaitStatus as WtStat },
|
||||
},
|
||||
libc::{ self, STDIN_FILENO, STDERR_FILENO, STDOUT_FILENO },
|
||||
unistd::{
|
||||
dup, read, isatty, write, close, setpgid, dup2, getpgrp, getpgid,
|
||||
execvpe, tcgetpgrp, tcsetpgrp, fork, pipe, Pid, ForkResult
|
||||
},
|
||||
};
|
||||
pub use bitflags::bitflags;
|
||||
pub use nix::{
|
||||
errno::Errno,
|
||||
fcntl::{open, OFlag},
|
||||
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
|
||||
sys::{
|
||||
signal::{self, kill, killpg, pthread_sigmask, signal, SigHandler, SigSet, SigmaskHow, Signal},
|
||||
stat::Mode,
|
||||
termios::{self},
|
||||
wait::{waitpid, WaitPidFlag as WtFlag, WaitStatus as WtStat},
|
||||
},
|
||||
unistd::{
|
||||
close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read, setpgid, tcgetpgrp,
|
||||
tcsetpgrp, write, ForkResult, Pid,
|
||||
},
|
||||
};
|
||||
|
||||
pub use crate::flog;
|
||||
pub use crate::libsh::flog::FernLogLevel::*;
|
||||
|
||||
470
src/procio.rs
470
src/procio.rs
@@ -1,266 +1,312 @@
|
||||
use std::{fmt::Debug, ops::{Deref, DerefMut}};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
use crate::{libsh::{error::{ShErr, ShErrKind, ShResult}, utils::RedirVecUtils}, parse::{get_redir_file, Redir, RedirType}, prelude::*};
|
||||
use crate::{
|
||||
libsh::{
|
||||
error::{ShErr, ShErrKind, ShResult},
|
||||
utils::RedirVecUtils,
|
||||
},
|
||||
parse::{get_redir_file, Redir, RedirType},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
// Credit to fish-shell for many of the implementation ideas present in this module
|
||||
// https://fishshell.com/
|
||||
// Credit to fish-shell for many of the implementation ideas present in this
|
||||
// module https://fishshell.com/
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum IoMode {
|
||||
Fd { tgt_fd: RawFd, src_fd: Arc<OwnedFd> },
|
||||
File { tgt_fd: RawFd, path: PathBuf, mode: RedirType },
|
||||
Pipe { tgt_fd: RawFd, pipe: Arc<OwnedFd> },
|
||||
Buffer { buf: String, pipe: Arc<OwnedFd> }
|
||||
Fd {
|
||||
tgt_fd: RawFd,
|
||||
src_fd: Arc<OwnedFd>,
|
||||
},
|
||||
File {
|
||||
tgt_fd: RawFd,
|
||||
path: PathBuf,
|
||||
mode: RedirType,
|
||||
},
|
||||
Pipe {
|
||||
tgt_fd: RawFd,
|
||||
pipe: Arc<OwnedFd>,
|
||||
},
|
||||
Buffer {
|
||||
buf: String,
|
||||
pipe: Arc<OwnedFd>,
|
||||
},
|
||||
}
|
||||
|
||||
impl IoMode {
|
||||
pub fn fd(tgt_fd: RawFd, src_fd: RawFd) -> Self {
|
||||
let src_fd = unsafe { OwnedFd::from_raw_fd(src_fd).into() };
|
||||
Self::Fd { tgt_fd, src_fd }
|
||||
}
|
||||
pub fn file(tgt_fd: RawFd, path: PathBuf, mode: RedirType) -> Self {
|
||||
Self::File { tgt_fd, path, mode }
|
||||
}
|
||||
pub fn pipe(tgt_fd: RawFd, pipe: OwnedFd) -> Self {
|
||||
let pipe = pipe.into();
|
||||
Self::Pipe { tgt_fd, pipe }
|
||||
}
|
||||
pub fn tgt_fd(&self) -> RawFd {
|
||||
match self {
|
||||
IoMode::Fd { tgt_fd, .. } |
|
||||
IoMode::File { tgt_fd, .. } |
|
||||
IoMode::Pipe { tgt_fd, .. } => *tgt_fd,
|
||||
_ => panic!()
|
||||
}
|
||||
}
|
||||
pub fn src_fd(&self) -> RawFd {
|
||||
match self {
|
||||
IoMode::Fd { tgt_fd: _, src_fd } => src_fd.as_raw_fd(),
|
||||
IoMode::File {..} => panic!("Attempted to obtain src_fd from file before opening"),
|
||||
IoMode::Pipe { tgt_fd: _, pipe } => pipe.as_raw_fd(),
|
||||
_ => panic!()
|
||||
}
|
||||
}
|
||||
pub fn open_file(mut self) -> ShResult<Self> {
|
||||
if let IoMode::File { tgt_fd, path, mode } = self {
|
||||
let file = get_redir_file(mode, path)?;
|
||||
self = IoMode::Fd { tgt_fd, src_fd: Arc::new(OwnedFd::from(file)) }
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
pub fn get_pipes() -> (Self,Self) {
|
||||
let (rpipe,wpipe) = pipe().unwrap();
|
||||
(
|
||||
Self::Pipe { tgt_fd: STDIN_FILENO, pipe: rpipe.into() },
|
||||
Self::Pipe { tgt_fd: STDOUT_FILENO, pipe: wpipe.into() }
|
||||
)
|
||||
}
|
||||
pub fn fd(tgt_fd: RawFd, src_fd: RawFd) -> Self {
|
||||
let src_fd = unsafe { OwnedFd::from_raw_fd(src_fd).into() };
|
||||
Self::Fd { tgt_fd, src_fd }
|
||||
}
|
||||
pub fn file(tgt_fd: RawFd, path: PathBuf, mode: RedirType) -> Self {
|
||||
Self::File { tgt_fd, path, mode }
|
||||
}
|
||||
pub fn pipe(tgt_fd: RawFd, pipe: OwnedFd) -> Self {
|
||||
let pipe = pipe.into();
|
||||
Self::Pipe { tgt_fd, pipe }
|
||||
}
|
||||
pub fn tgt_fd(&self) -> RawFd {
|
||||
match self {
|
||||
IoMode::Fd { tgt_fd, .. } | IoMode::File { tgt_fd, .. } | IoMode::Pipe { tgt_fd, .. } => {
|
||||
*tgt_fd
|
||||
}
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
pub fn src_fd(&self) -> RawFd {
|
||||
match self {
|
||||
IoMode::Fd { tgt_fd: _, src_fd } => src_fd.as_raw_fd(),
|
||||
IoMode::File { .. } => panic!("Attempted to obtain src_fd from file before opening"),
|
||||
IoMode::Pipe { tgt_fd: _, pipe } => pipe.as_raw_fd(),
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
pub fn open_file(mut self) -> ShResult<Self> {
|
||||
if let IoMode::File { tgt_fd, path, mode } = self {
|
||||
let file = get_redir_file(mode, path)?;
|
||||
self = IoMode::Fd {
|
||||
tgt_fd,
|
||||
src_fd: Arc::new(OwnedFd::from(file)),
|
||||
}
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
pub fn get_pipes() -> (Self, Self) {
|
||||
let (rpipe, wpipe) = pipe().unwrap();
|
||||
(
|
||||
Self::Pipe {
|
||||
tgt_fd: STDIN_FILENO,
|
||||
pipe: rpipe.into(),
|
||||
},
|
||||
Self::Pipe {
|
||||
tgt_fd: STDOUT_FILENO,
|
||||
pipe: wpipe.into(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for IoMode {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let src_fd = self.src_fd();
|
||||
Ok(read(src_fd, buf)?)
|
||||
}
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let src_fd = self.src_fd();
|
||||
Ok(read(src_fd, buf)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IoBuf<R: Read> {
|
||||
buf: Vec<u8>,
|
||||
reader: R,
|
||||
buf: Vec<u8>,
|
||||
reader: R,
|
||||
}
|
||||
|
||||
impl<R: Read> IoBuf<R> {
|
||||
pub fn new(reader: R) -> Self {
|
||||
Self {
|
||||
buf: Vec::new(),
|
||||
reader,
|
||||
}
|
||||
}
|
||||
pub fn new(reader: R) -> Self {
|
||||
Self {
|
||||
buf: Vec::new(),
|
||||
reader,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads exactly `size` bytes (or fewer if EOF) into the buffer
|
||||
pub fn read_buffer(&mut self, size: usize) -> io::Result<()> {
|
||||
let mut temp_buf = vec![0; size]; // Temporary buffer
|
||||
let bytes_read = self.reader.read(&mut temp_buf)?;
|
||||
self.buf.extend_from_slice(&temp_buf[..bytes_read]); // Append only what was read
|
||||
Ok(())
|
||||
}
|
||||
/// Reads exactly `size` bytes (or fewer if EOF) into the buffer
|
||||
pub fn read_buffer(&mut self, size: usize) -> io::Result<()> {
|
||||
let mut temp_buf = vec![0; size]; // Temporary buffer
|
||||
let bytes_read = self.reader.read(&mut temp_buf)?;
|
||||
self.buf.extend_from_slice(&temp_buf[..bytes_read]); // Append only what was read
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Continuously reads until EOF
|
||||
pub fn fill_buffer(&mut self) -> io::Result<()> {
|
||||
let mut temp_buf = vec![0; 1024]; // Read in chunks
|
||||
loop {
|
||||
flog!(DEBUG, "reading bytes");
|
||||
let bytes_read = self.reader.read(&mut temp_buf)?;
|
||||
flog!(DEBUG, bytes_read);
|
||||
if bytes_read == 0 {
|
||||
break; // EOF reached
|
||||
}
|
||||
self.buf.extend_from_slice(&temp_buf[..bytes_read]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Continuously reads until EOF
|
||||
pub fn fill_buffer(&mut self) -> io::Result<()> {
|
||||
let mut temp_buf = vec![0; 1024]; // Read in chunks
|
||||
loop {
|
||||
flog!(DEBUG, "reading bytes");
|
||||
let bytes_read = self.reader.read(&mut temp_buf)?;
|
||||
flog!(DEBUG, bytes_read);
|
||||
if bytes_read == 0 {
|
||||
break; // EOF reached
|
||||
}
|
||||
self.buf.extend_from_slice(&temp_buf[..bytes_read]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get current buffer contents as a string (if valid UTF-8)
|
||||
pub fn as_str(&self) -> ShResult<&str> {
|
||||
std::str::from_utf8(&self.buf).map_err(|_| {
|
||||
ShErr::simple(ShErrKind::InternalErr, "Invalid utf-8 in IoBuf")
|
||||
})
|
||||
}
|
||||
/// Get current buffer contents as a string (if valid UTF-8)
|
||||
pub fn as_str(&self) -> ShResult<&str> {
|
||||
std::str::from_utf8(&self.buf)
|
||||
.map_err(|_| ShErr::simple(ShErrKind::InternalErr, "Invalid utf-8 in IoBuf"))
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct wrapping three fildescs representing `stdin`, `stdout`, and `stderr` respectively
|
||||
#[derive(Debug,Clone)]
|
||||
pub struct IoGroup(RawFd,RawFd,RawFd);
|
||||
/// A struct wrapping three fildescs representing `stdin`, `stdout`, and
|
||||
/// `stderr` respectively
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IoGroup(RawFd, RawFd, RawFd);
|
||||
|
||||
/// A single stack frame used with the IoStack
|
||||
/// Each stack frame represents the redirections of a single command
|
||||
#[derive(Default,Clone,Debug)]
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct IoFrame {
|
||||
redirs: Vec<Redir>,
|
||||
saved_io: Option<IoGroup>,
|
||||
redirs: Vec<Redir>,
|
||||
saved_io: Option<IoGroup>,
|
||||
}
|
||||
|
||||
impl<'e> IoFrame {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
pub fn from_redirs(redirs: Vec<Redir>) -> Self {
|
||||
Self { redirs, saved_io: None }
|
||||
}
|
||||
pub fn from_redir(redir: Redir) -> Self {
|
||||
Self { redirs: vec![redir], saved_io: None }
|
||||
}
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
pub fn from_redirs(redirs: Vec<Redir>) -> Self {
|
||||
Self {
|
||||
redirs,
|
||||
saved_io: None,
|
||||
}
|
||||
}
|
||||
pub fn from_redir(redir: Redir) -> Self {
|
||||
Self {
|
||||
redirs: vec![redir],
|
||||
saved_io: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits the frame into two frames
|
||||
///
|
||||
/// One frame contains input redirections, the other contains output redirections
|
||||
/// This is used in shell structures to route redirections either *to* the condition, or *from* the body
|
||||
/// The first field of the tuple contains input redirections (used for the condition)
|
||||
/// The second field contains output redirections (used for the body)
|
||||
pub fn split_frame(self) -> (Self,Self) {
|
||||
let Self { redirs, saved_io: _ } = self;
|
||||
let (input_redirs,output_redirs) = redirs.split_by_channel();
|
||||
(
|
||||
Self::from_redirs(input_redirs),
|
||||
Self::from_redirs(output_redirs)
|
||||
)
|
||||
}
|
||||
pub fn save(&'e mut self) {
|
||||
let saved_in = dup(STDIN_FILENO).unwrap();
|
||||
let saved_out = dup(STDOUT_FILENO).unwrap();
|
||||
let saved_err = dup(STDERR_FILENO).unwrap();
|
||||
self.saved_io = Some(IoGroup(saved_in,saved_out,saved_err));
|
||||
}
|
||||
pub fn redirect(&mut self) -> ShResult<()> {
|
||||
self.save();
|
||||
for redir in &mut self.redirs {
|
||||
let io_mode = &mut redir.io_mode;
|
||||
flog!(DEBUG, io_mode);
|
||||
if let IoMode::File {..} = io_mode {
|
||||
*io_mode = io_mode.clone().open_file()?;
|
||||
};
|
||||
flog!(DEBUG, io_mode);
|
||||
let tgt_fd = io_mode.tgt_fd();
|
||||
let src_fd = io_mode.src_fd();
|
||||
dup2(src_fd, tgt_fd)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn restore(&mut self) -> ShResult<()> {
|
||||
if let Some(saved) = self.saved_io.take() {
|
||||
dup2(saved.0, STDIN_FILENO)?;
|
||||
close(saved.0)?;
|
||||
dup2(saved.1, STDOUT_FILENO)?;
|
||||
close(saved.1)?;
|
||||
dup2(saved.2, STDERR_FILENO)?;
|
||||
close(saved.2)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Splits the frame into two frames
|
||||
///
|
||||
/// One frame contains input redirections, the other contains output
|
||||
/// redirections This is used in shell structures to route redirections
|
||||
/// either *to* the condition, or *from* the body The first field of the
|
||||
/// tuple contains input redirections (used for the condition) The second
|
||||
/// field contains output redirections (used for the body)
|
||||
pub fn split_frame(self) -> (Self, Self) {
|
||||
let Self {
|
||||
redirs,
|
||||
saved_io: _,
|
||||
} = self;
|
||||
let (input_redirs, output_redirs) = redirs.split_by_channel();
|
||||
(
|
||||
Self::from_redirs(input_redirs),
|
||||
Self::from_redirs(output_redirs),
|
||||
)
|
||||
}
|
||||
pub fn save(&'e mut self) {
|
||||
let saved_in = dup(STDIN_FILENO).unwrap();
|
||||
let saved_out = dup(STDOUT_FILENO).unwrap();
|
||||
let saved_err = dup(STDERR_FILENO).unwrap();
|
||||
self.saved_io = Some(IoGroup(saved_in, saved_out, saved_err));
|
||||
}
|
||||
pub fn redirect(&mut self) -> ShResult<()> {
|
||||
self.save();
|
||||
for redir in &mut self.redirs {
|
||||
let io_mode = &mut redir.io_mode;
|
||||
flog!(DEBUG, io_mode);
|
||||
if let IoMode::File { .. } = io_mode {
|
||||
*io_mode = io_mode.clone().open_file()?;
|
||||
};
|
||||
flog!(DEBUG, io_mode);
|
||||
let tgt_fd = io_mode.tgt_fd();
|
||||
let src_fd = io_mode.src_fd();
|
||||
dup2(src_fd, tgt_fd)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn restore(&mut self) -> ShResult<()> {
|
||||
if let Some(saved) = self.saved_io.take() {
|
||||
dup2(saved.0, STDIN_FILENO)?;
|
||||
close(saved.0)?;
|
||||
dup2(saved.1, STDOUT_FILENO)?;
|
||||
close(saved.1)?;
|
||||
dup2(saved.2, STDERR_FILENO)?;
|
||||
close(saved.2)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for IoFrame {
|
||||
type Target = Vec<Redir>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.redirs
|
||||
}
|
||||
type Target = Vec<Redir>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.redirs
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for IoFrame {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.redirs
|
||||
}
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.redirs
|
||||
}
|
||||
}
|
||||
|
||||
/// A stack that maintains the current state of I/O for commands
|
||||
///
|
||||
/// This struct maintains the current state of I/O for the `Dispatcher` struct
|
||||
/// Each executed command requires an `IoFrame` in order to perform redirections.
|
||||
/// As nodes are walked through by the `Dispatcher`, it pushes new frames in certain contexts, and pops frames in others.
|
||||
/// Each command calls pop_frame() in order to get the current IoFrame in order to perform redirection
|
||||
#[derive(Debug,Default)]
|
||||
/// Each executed command requires an `IoFrame` in order to perform
|
||||
/// redirections. As nodes are walked through by the `Dispatcher`, it pushes new
|
||||
/// frames in certain contexts, and pops frames in others. Each command calls
|
||||
/// pop_frame() in order to get the current IoFrame in order to perform
|
||||
/// redirection
|
||||
#[derive(Debug, Default)]
|
||||
pub struct IoStack {
|
||||
stack: Vec<IoFrame>,
|
||||
stack: Vec<IoFrame>,
|
||||
}
|
||||
|
||||
impl IoStack {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
stack: vec![IoFrame::new()],
|
||||
}
|
||||
}
|
||||
pub fn curr_frame(&self) -> &IoFrame {
|
||||
self.stack.last().unwrap()
|
||||
}
|
||||
pub fn curr_frame_mut(&mut self) -> &mut IoFrame {
|
||||
self.stack.last_mut().unwrap()
|
||||
}
|
||||
pub fn push_to_frame(&mut self, redir: Redir) {
|
||||
self.curr_frame_mut().push(redir)
|
||||
}
|
||||
pub fn append_to_frame(&mut self, mut other: Vec<Redir>) {
|
||||
self.curr_frame_mut().append(&mut other)
|
||||
}
|
||||
/// Pop the current stack frame
|
||||
/// This differs from using `pop()` because it always returns a stack frame
|
||||
/// If `self.pop()` would empty the `IoStack`, it instead uses `std::mem::take()` to take the last frame
|
||||
/// There will always be at least one frame in the `IoStack`.
|
||||
pub fn pop_frame(&mut self) -> IoFrame {
|
||||
if self.stack.len() > 1 {
|
||||
self.pop().unwrap()
|
||||
} else {
|
||||
std::mem::take(self.curr_frame_mut())
|
||||
}
|
||||
}
|
||||
/// Push a new stack frame.
|
||||
pub fn push_frame(&mut self, frame: IoFrame) {
|
||||
self.push(frame)
|
||||
}
|
||||
/// Flatten the `IoStack`
|
||||
/// All of the current stack frames will be flattened into a single one
|
||||
/// Not sure what use this will serve, but my gut said this was worthy of writing
|
||||
pub fn flatten(&mut self) {
|
||||
let mut flat_frame = IoFrame::new();
|
||||
while let Some(mut frame) = self.pop() {
|
||||
flat_frame.append(&mut frame)
|
||||
}
|
||||
self.push(flat_frame);
|
||||
}
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
stack: vec![IoFrame::new()],
|
||||
}
|
||||
}
|
||||
pub fn curr_frame(&self) -> &IoFrame {
|
||||
self.stack.last().unwrap()
|
||||
}
|
||||
pub fn curr_frame_mut(&mut self) -> &mut IoFrame {
|
||||
self.stack.last_mut().unwrap()
|
||||
}
|
||||
pub fn push_to_frame(&mut self, redir: Redir) {
|
||||
self.curr_frame_mut().push(redir)
|
||||
}
|
||||
pub fn append_to_frame(&mut self, mut other: Vec<Redir>) {
|
||||
self.curr_frame_mut().append(&mut other)
|
||||
}
|
||||
/// Pop the current stack frame
|
||||
/// This differs from using `pop()` because it always returns a stack frame
|
||||
/// If `self.pop()` would empty the `IoStack`, it instead uses
|
||||
/// `std::mem::take()` to take the last frame There will always be at least
|
||||
/// one frame in the `IoStack`.
|
||||
pub fn pop_frame(&mut self) -> IoFrame {
|
||||
if self.stack.len() > 1 {
|
||||
self.pop().unwrap()
|
||||
} else {
|
||||
std::mem::take(self.curr_frame_mut())
|
||||
}
|
||||
}
|
||||
/// Push a new stack frame.
|
||||
pub fn push_frame(&mut self, frame: IoFrame) {
|
||||
self.push(frame)
|
||||
}
|
||||
/// Flatten the `IoStack`
|
||||
/// All of the current stack frames will be flattened into a single one
|
||||
/// Not sure what use this will serve, but my gut said this was worthy of
|
||||
/// writing
|
||||
pub fn flatten(&mut self) {
|
||||
let mut flat_frame = IoFrame::new();
|
||||
while let Some(mut frame) = self.pop() {
|
||||
flat_frame.append(&mut frame)
|
||||
}
|
||||
self.push(flat_frame);
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for IoStack {
|
||||
type Target = Vec<IoFrame>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.stack
|
||||
}
|
||||
type Target = Vec<IoFrame>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.stack
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for IoStack {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.stack
|
||||
}
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.stack
|
||||
}
|
||||
}
|
||||
|
||||
pub fn borrow_fd<'f>(fd: i32) -> BorrowedFd<'f> {
|
||||
unsafe { BorrowedFd::borrow_raw(fd) }
|
||||
unsafe { BorrowedFd::borrow_raw(fd) }
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
pub mod readline;
|
||||
pub mod highlight;
|
||||
pub mod readline;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use readline::{FernVi, Readline};
|
||||
|
||||
use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*, shopt::FernEditMode, state::read_shopts};
|
||||
use crate::{
|
||||
expand::expand_prompt, libsh::error::ShResult, prelude::*, shopt::FernEditMode,
|
||||
state::read_shopts,
|
||||
};
|
||||
|
||||
/// Initialize the line editor
|
||||
fn get_prompt() -> ShResult<String> {
|
||||
let Ok(prompt) = env::var("PS1") else {
|
||||
// prompt expands to:
|
||||
//
|
||||
// username@hostname
|
||||
// short/path/to/pwd/
|
||||
// $ _
|
||||
let default = "\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ";
|
||||
return expand_prompt(default)
|
||||
};
|
||||
let Ok(prompt) = env::var("PS1") else {
|
||||
// prompt expands to:
|
||||
//
|
||||
// username@hostname
|
||||
// short/path/to/pwd/
|
||||
// $ _
|
||||
let default =
|
||||
"\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ";
|
||||
return expand_prompt(default);
|
||||
};
|
||||
|
||||
expand_prompt(&prompt)
|
||||
expand_prompt(&prompt)
|
||||
}
|
||||
|
||||
pub fn readline(edit_mode: FernEditMode) -> ShResult<String> {
|
||||
let prompt = get_prompt()?;
|
||||
let mut reader: Box<dyn Readline> = match edit_mode {
|
||||
FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))?),
|
||||
FernEditMode::Emacs => todo!()
|
||||
};
|
||||
reader.readline()
|
||||
let prompt = get_prompt()?;
|
||||
let mut reader: Box<dyn Readline> = match edit_mode {
|
||||
FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))?),
|
||||
FernEditMode::Emacs => todo!(),
|
||||
};
|
||||
reader.readline()
|
||||
}
|
||||
|
||||
@@ -1,350 +1,393 @@
|
||||
use std::{env, fmt::{Write,Display}, fs::{self, OpenOptions}, io::Write as IoWrite, path::{Path, PathBuf}, str::FromStr, time::{Duration, SystemTime, UNIX_EPOCH}};
|
||||
use std::{
|
||||
env,
|
||||
fmt::{Display, Write},
|
||||
fs::{self, OpenOptions},
|
||||
io::Write as IoWrite,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
||||
use crate::prelude::*;
|
||||
|
||||
use super::vicmd::Direction; // surprisingly useful
|
||||
|
||||
#[derive(Default,Clone,Copy,Debug)]
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
pub enum SearchKind {
|
||||
Fuzzy,
|
||||
#[default]
|
||||
Prefix
|
||||
Fuzzy,
|
||||
#[default]
|
||||
Prefix,
|
||||
}
|
||||
|
||||
#[derive(Default,Clone,Debug)]
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct SearchConstraint {
|
||||
kind: SearchKind,
|
||||
term: String,
|
||||
kind: SearchKind,
|
||||
term: String,
|
||||
}
|
||||
|
||||
impl SearchConstraint {
|
||||
pub fn new(kind: SearchKind, term: String) -> Self {
|
||||
Self { kind, term }
|
||||
}
|
||||
pub fn new(kind: SearchKind, term: String) -> Self {
|
||||
Self { kind, term }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HistEntry {
|
||||
id: u32,
|
||||
timestamp: SystemTime,
|
||||
command: String,
|
||||
new: bool
|
||||
id: u32,
|
||||
timestamp: SystemTime,
|
||||
command: String,
|
||||
new: bool,
|
||||
}
|
||||
|
||||
impl HistEntry {
|
||||
pub fn id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
pub fn timestamp(&self) -> &SystemTime {
|
||||
&self.timestamp
|
||||
}
|
||||
pub fn command(&self) -> &str {
|
||||
&self.command
|
||||
}
|
||||
fn with_escaped_newlines(&self) -> String {
|
||||
let mut escaped = String::new();
|
||||
let mut chars = self.command.chars();
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'\\' => {
|
||||
escaped.push(ch);
|
||||
if let Some(ch) = chars.next() {
|
||||
escaped.push(ch)
|
||||
}
|
||||
}
|
||||
'\n' => {
|
||||
escaped.push_str("\\\n");
|
||||
}
|
||||
_ => escaped.push(ch),
|
||||
}
|
||||
}
|
||||
escaped
|
||||
}
|
||||
pub fn is_new(&self) -> bool {
|
||||
self.new
|
||||
}
|
||||
pub fn id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
pub fn timestamp(&self) -> &SystemTime {
|
||||
&self.timestamp
|
||||
}
|
||||
pub fn command(&self) -> &str {
|
||||
&self.command
|
||||
}
|
||||
fn with_escaped_newlines(&self) -> String {
|
||||
let mut escaped = String::new();
|
||||
let mut chars = self.command.chars();
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'\\' => {
|
||||
escaped.push(ch);
|
||||
if let Some(ch) = chars.next() {
|
||||
escaped.push(ch)
|
||||
}
|
||||
}
|
||||
'\n' => {
|
||||
escaped.push_str("\\\n");
|
||||
}
|
||||
_ => escaped.push(ch),
|
||||
}
|
||||
}
|
||||
escaped
|
||||
}
|
||||
pub fn is_new(&self) -> bool {
|
||||
self.new
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for HistEntry {
|
||||
type Err = ShErr;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let err = Err(
|
||||
ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on history entry '{s}'"), notes: vec![] }
|
||||
);
|
||||
type Err = ShErr;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let err = Err(ShErr::Simple {
|
||||
kind: ShErrKind::HistoryReadErr,
|
||||
msg: format!("Bad formatting on history entry '{s}'"),
|
||||
notes: vec![],
|
||||
});
|
||||
|
||||
//: 248972349;148;echo foo; echo bar
|
||||
let Some(cleaned) = s.strip_prefix(": ") else { return err };
|
||||
//248972349;148;echo foo; echo bar
|
||||
let Some((timestamp,id_and_command)) = cleaned.split_once(';') else { return err };
|
||||
//("248972349","148;echo foo; echo bar")
|
||||
let Some((id,command)) = id_and_command.split_once(';') else { return err };
|
||||
//("148","echo foo; echo bar")
|
||||
let Ok(ts_seconds) = timestamp.parse::<u64>() else { return err };
|
||||
let Ok(id) = id.parse::<u32>() else { return err };
|
||||
let timestamp = UNIX_EPOCH + Duration::from_secs(ts_seconds);
|
||||
let command = command.to_string();
|
||||
Ok(Self { id, timestamp, command, new: false })
|
||||
}
|
||||
//: 248972349;148;echo foo; echo bar
|
||||
let Some(cleaned) = s.strip_prefix(": ") else {
|
||||
return err;
|
||||
};
|
||||
//248972349;148;echo foo; echo bar
|
||||
let Some((timestamp, id_and_command)) = cleaned.split_once(';') else {
|
||||
return err;
|
||||
};
|
||||
//("248972349","148;echo foo; echo bar")
|
||||
let Some((id, command)) = id_and_command.split_once(';') else {
|
||||
return err;
|
||||
};
|
||||
//("148","echo foo; echo bar")
|
||||
let Ok(ts_seconds) = timestamp.parse::<u64>() else {
|
||||
return err;
|
||||
};
|
||||
let Ok(id) = id.parse::<u32>() else {
|
||||
return err;
|
||||
};
|
||||
let timestamp = UNIX_EPOCH + Duration::from_secs(ts_seconds);
|
||||
let command = command.to_string();
|
||||
Ok(Self {
|
||||
id,
|
||||
timestamp,
|
||||
command,
|
||||
new: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HistEntry {
|
||||
/// Similar to zsh's history format, but not entirely
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let command = self.with_escaped_newlines();
|
||||
let HistEntry { id, timestamp, command: _, new: _ } = self;
|
||||
let timestamp = timestamp.duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
writeln!(f, ": {timestamp};{id};{command}")
|
||||
}
|
||||
/// Similar to zsh's history format, but not entirely
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let command = self.with_escaped_newlines();
|
||||
let HistEntry {
|
||||
id,
|
||||
timestamp,
|
||||
command: _,
|
||||
new: _,
|
||||
} = self;
|
||||
let timestamp = timestamp.duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
writeln!(f, ": {timestamp};{id};{command}")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HistEntries(Vec<HistEntry>);
|
||||
|
||||
|
||||
impl FromStr for HistEntries {
|
||||
type Err = ShErr;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut entries = vec![];
|
||||
type Err = ShErr;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut entries = vec![];
|
||||
|
||||
let mut lines = s.lines().enumerate().peekable();
|
||||
let mut cur_line = String::new();
|
||||
let mut lines = s.lines().enumerate().peekable();
|
||||
let mut cur_line = String::new();
|
||||
|
||||
while let Some((i,line)) = lines.next() {
|
||||
if !line.starts_with(": ") {
|
||||
return Err(
|
||||
ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on line {i}"), notes: vec![] }
|
||||
)
|
||||
}
|
||||
let mut chars = line.chars().peekable();
|
||||
let mut feeding_lines = true;
|
||||
while feeding_lines {
|
||||
feeding_lines = false;
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'\\' => {
|
||||
if let Some(esc_ch) = chars.next() {
|
||||
cur_line.push(esc_ch);
|
||||
} else {
|
||||
cur_line.push('\n');
|
||||
feeding_lines = true;
|
||||
}
|
||||
}
|
||||
'\n' => {
|
||||
break
|
||||
}
|
||||
_ => {
|
||||
cur_line.push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
if feeding_lines {
|
||||
let Some((_,line)) = lines.next() else {
|
||||
return Err(
|
||||
ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on line {i}"), notes: vec![] }
|
||||
)
|
||||
};
|
||||
chars = line.chars().peekable();
|
||||
}
|
||||
}
|
||||
let entry = cur_line.parse::<HistEntry>()?;
|
||||
entries.push(entry);
|
||||
cur_line.clear();
|
||||
}
|
||||
while let Some((i, line)) = lines.next() {
|
||||
if !line.starts_with(": ") {
|
||||
return Err(ShErr::Simple {
|
||||
kind: ShErrKind::HistoryReadErr,
|
||||
msg: format!("Bad formatting on line {i}"),
|
||||
notes: vec![],
|
||||
});
|
||||
}
|
||||
let mut chars = line.chars().peekable();
|
||||
let mut feeding_lines = true;
|
||||
while feeding_lines {
|
||||
feeding_lines = false;
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'\\' => {
|
||||
if let Some(esc_ch) = chars.next() {
|
||||
cur_line.push(esc_ch);
|
||||
} else {
|
||||
cur_line.push('\n');
|
||||
feeding_lines = true;
|
||||
}
|
||||
}
|
||||
'\n' => break,
|
||||
_ => {
|
||||
cur_line.push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
if feeding_lines {
|
||||
let Some((_, line)) = lines.next() else {
|
||||
return Err(ShErr::Simple {
|
||||
kind: ShErrKind::HistoryReadErr,
|
||||
msg: format!("Bad formatting on line {i}"),
|
||||
notes: vec![],
|
||||
});
|
||||
};
|
||||
chars = line.chars().peekable();
|
||||
}
|
||||
}
|
||||
let entry = cur_line.parse::<HistEntry>()?;
|
||||
entries.push(entry);
|
||||
cur_line.clear();
|
||||
}
|
||||
|
||||
|
||||
Ok(Self(entries))
|
||||
}
|
||||
Ok(Self(entries))
|
||||
}
|
||||
}
|
||||
|
||||
fn read_hist_file(path: &Path) -> ShResult<Vec<HistEntry>> {
|
||||
if !path.exists() {
|
||||
fs::File::create(path)?;
|
||||
}
|
||||
let raw = fs::read_to_string(path)?;
|
||||
Ok(raw.parse::<HistEntries>()?.0)
|
||||
if !path.exists() {
|
||||
fs::File::create(path)?;
|
||||
}
|
||||
let raw = fs::read_to_string(path)?;
|
||||
Ok(raw.parse::<HistEntries>()?.0)
|
||||
}
|
||||
|
||||
pub struct History {
|
||||
path: PathBuf,
|
||||
entries: Vec<HistEntry>,
|
||||
search_mask: Vec<HistEntry>,
|
||||
cursor: usize,
|
||||
search_direction: Direction,
|
||||
ignore_dups: bool,
|
||||
max_size: Option<u32>,
|
||||
path: PathBuf,
|
||||
entries: Vec<HistEntry>,
|
||||
search_mask: Vec<HistEntry>,
|
||||
cursor: usize,
|
||||
search_direction: Direction,
|
||||
ignore_dups: bool,
|
||||
max_size: Option<u32>,
|
||||
}
|
||||
|
||||
impl History {
|
||||
pub fn new() -> ShResult<Self> {
|
||||
let path = PathBuf::from(env::var("FERNHIST").unwrap_or({
|
||||
let home = env::var("HOME").unwrap();
|
||||
format!("{home}/.fern_history")
|
||||
}));
|
||||
let mut entries = read_hist_file(&path)?;
|
||||
{
|
||||
let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0);
|
||||
let timestamp = SystemTime::now();
|
||||
let command = "".into();
|
||||
entries.push(HistEntry { id, timestamp, command, new: true })
|
||||
}
|
||||
let search_mask = entries.clone();
|
||||
let cursor = entries.len() - 1;
|
||||
let mut new = Self {
|
||||
path,
|
||||
entries,
|
||||
search_mask,
|
||||
cursor,
|
||||
search_direction: Direction::Backward,
|
||||
ignore_dups: true,
|
||||
max_size: None,
|
||||
};
|
||||
new.push_empty_entry(); // Current pending command
|
||||
Ok(new)
|
||||
}
|
||||
pub fn new() -> ShResult<Self> {
|
||||
let path = PathBuf::from(env::var("FERNHIST").unwrap_or({
|
||||
let home = env::var("HOME").unwrap();
|
||||
format!("{home}/.fern_history")
|
||||
}));
|
||||
let mut entries = read_hist_file(&path)?;
|
||||
{
|
||||
let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0);
|
||||
let timestamp = SystemTime::now();
|
||||
let command = "".into();
|
||||
entries.push(HistEntry {
|
||||
id,
|
||||
timestamp,
|
||||
command,
|
||||
new: true,
|
||||
})
|
||||
}
|
||||
let search_mask = entries.clone();
|
||||
let cursor = entries.len() - 1;
|
||||
let mut new = Self {
|
||||
path,
|
||||
entries,
|
||||
search_mask,
|
||||
cursor,
|
||||
search_direction: Direction::Backward,
|
||||
ignore_dups: true,
|
||||
max_size: None,
|
||||
};
|
||||
new.push_empty_entry(); // Current pending command
|
||||
Ok(new)
|
||||
}
|
||||
|
||||
pub fn entries(&self) -> &[HistEntry] {
|
||||
&self.entries
|
||||
}
|
||||
pub fn entries(&self) -> &[HistEntry] {
|
||||
&self.entries
|
||||
}
|
||||
|
||||
pub fn masked_entries(&self) -> &[HistEntry] {
|
||||
&self.search_mask
|
||||
}
|
||||
pub fn masked_entries(&self) -> &[HistEntry] {
|
||||
&self.search_mask
|
||||
}
|
||||
|
||||
pub fn push_empty_entry(&mut self) {
|
||||
}
|
||||
pub fn push_empty_entry(&mut self) {}
|
||||
|
||||
pub fn cursor_entry(&self) -> Option<&HistEntry> {
|
||||
self.search_mask.get(self.cursor)
|
||||
}
|
||||
pub fn cursor_entry(&self) -> Option<&HistEntry> {
|
||||
self.search_mask.get(self.cursor)
|
||||
}
|
||||
|
||||
pub fn update_pending_cmd(&mut self, command: &str) {
|
||||
let Some(ent) = self.last_mut() else {
|
||||
return
|
||||
};
|
||||
let cmd = command.to_string();
|
||||
let constraint = SearchConstraint { kind: SearchKind::Prefix, term: cmd.clone() };
|
||||
pub fn update_pending_cmd(&mut self, command: &str) {
|
||||
let Some(ent) = self.last_mut() else { return };
|
||||
let cmd = command.to_string();
|
||||
let constraint = SearchConstraint {
|
||||
kind: SearchKind::Prefix,
|
||||
term: cmd.clone(),
|
||||
};
|
||||
|
||||
ent.command = cmd;
|
||||
self.constrain_entries(constraint);
|
||||
}
|
||||
|
||||
ent.command = cmd;
|
||||
self.constrain_entries(constraint);
|
||||
}
|
||||
pub fn last_mut(&mut self) -> Option<&mut HistEntry> {
|
||||
self.entries.last_mut()
|
||||
}
|
||||
|
||||
pub fn last_mut(&mut self) -> Option<&mut HistEntry> {
|
||||
self.entries.last_mut()
|
||||
}
|
||||
pub fn get_new_id(&self) -> u32 {
|
||||
let Some(ent) = self.entries.last() else {
|
||||
return 0;
|
||||
};
|
||||
ent.id + 1
|
||||
}
|
||||
|
||||
pub fn get_new_id(&self) -> u32 {
|
||||
let Some(ent) = self.entries.last() else {
|
||||
return 0
|
||||
};
|
||||
ent.id + 1
|
||||
}
|
||||
pub fn ignore_dups(&mut self, yn: bool) {
|
||||
self.ignore_dups = yn
|
||||
}
|
||||
|
||||
pub fn ignore_dups(&mut self, yn: bool) {
|
||||
self.ignore_dups = yn
|
||||
}
|
||||
pub fn max_hist_size(&mut self, size: Option<u32>) {
|
||||
self.max_size = size
|
||||
}
|
||||
|
||||
pub fn max_hist_size(&mut self, size: Option<u32>) {
|
||||
self.max_size = size
|
||||
}
|
||||
pub fn constrain_entries(&mut self, constraint: SearchConstraint) {
|
||||
flog!(DEBUG, constraint);
|
||||
let SearchConstraint { kind, term } = constraint;
|
||||
match kind {
|
||||
SearchKind::Prefix => {
|
||||
if term.is_empty() {
|
||||
self.search_mask = self.entries.clone();
|
||||
} else {
|
||||
let filtered = self
|
||||
.entries
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|ent| ent.command().starts_with(&term));
|
||||
|
||||
pub fn constrain_entries(&mut self, constraint: SearchConstraint) {
|
||||
flog!(DEBUG,constraint);
|
||||
let SearchConstraint { kind, term } = constraint;
|
||||
match kind {
|
||||
SearchKind::Prefix => {
|
||||
if term.is_empty() {
|
||||
self.search_mask = self.entries.clone();
|
||||
} else {
|
||||
let filtered = self.entries
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|ent| ent.command().starts_with(&term));
|
||||
self.search_mask = filtered.collect();
|
||||
}
|
||||
self.cursor = self.search_mask.len().saturating_sub(1);
|
||||
}
|
||||
SearchKind::Fuzzy => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
self.search_mask = filtered.collect();
|
||||
}
|
||||
self.cursor = self.search_mask.len().saturating_sub(1);
|
||||
}
|
||||
SearchKind::Fuzzy => todo!(),
|
||||
}
|
||||
}
|
||||
pub fn hint_entry(&self) -> Option<&HistEntry> {
|
||||
let second_to_last = self.search_mask.len().checked_sub(2)?;
|
||||
self.search_mask.get(second_to_last)
|
||||
}
|
||||
|
||||
pub fn hint_entry(&self) -> Option<&HistEntry> {
|
||||
let second_to_last = self.search_mask.len().checked_sub(2)?;
|
||||
self.search_mask.get(second_to_last)
|
||||
}
|
||||
pub fn get_hint(&self) -> Option<String> {
|
||||
if self
|
||||
.cursor_entry()
|
||||
.is_some_and(|ent| ent.is_new() && !ent.command().is_empty())
|
||||
{
|
||||
let entry = self.hint_entry()?;
|
||||
let prefix = self.cursor_entry()?.command();
|
||||
Some(entry.command().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_hint(&self) -> Option<String> {
|
||||
if self.cursor_entry().is_some_and(|ent| ent.is_new() && !ent.command().is_empty()) {
|
||||
let entry = self.hint_entry()?;
|
||||
let prefix = self.cursor_entry()?.command();
|
||||
Some(entry.command().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> {
|
||||
let new_idx = self
|
||||
.cursor
|
||||
.saturating_add_signed(offset)
|
||||
.clamp(0, self.search_mask.len().saturating_sub(1));
|
||||
let ent = self.search_mask.get(new_idx)?;
|
||||
|
||||
pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> {
|
||||
let new_idx = self.cursor
|
||||
.saturating_add_signed(offset)
|
||||
.clamp(0, self.search_mask.len().saturating_sub(1));
|
||||
let ent = self.search_mask.get(new_idx)?;
|
||||
self.cursor = new_idx;
|
||||
|
||||
self.cursor = new_idx;
|
||||
Some(ent)
|
||||
}
|
||||
|
||||
Some(ent)
|
||||
}
|
||||
pub fn push(&mut self, command: String) {
|
||||
let timestamp = SystemTime::now();
|
||||
let id = self.get_new_id();
|
||||
if self.ignore_dups && self.is_dup(&command) {
|
||||
return;
|
||||
}
|
||||
self.entries.push(HistEntry {
|
||||
id,
|
||||
timestamp,
|
||||
command,
|
||||
new: true,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn push(&mut self, command: String) {
|
||||
let timestamp = SystemTime::now();
|
||||
let id = self.get_new_id();
|
||||
if self.ignore_dups && self.is_dup(&command) {
|
||||
return
|
||||
}
|
||||
self.entries.push(HistEntry { id, timestamp, command, new: true });
|
||||
}
|
||||
pub fn is_dup(&self, other: &str) -> bool {
|
||||
let Some(ent) = self.entries.last() else {
|
||||
return false;
|
||||
};
|
||||
let ent_cmd = &ent.command;
|
||||
ent_cmd == other
|
||||
}
|
||||
|
||||
pub fn is_dup(&self, other: &str) -> bool {
|
||||
let Some(ent) = self.entries.last() else {
|
||||
return false
|
||||
};
|
||||
let ent_cmd = &ent.command;
|
||||
ent_cmd == other
|
||||
}
|
||||
pub fn save(&mut self) -> ShResult<()> {
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&self.path)?;
|
||||
|
||||
pub fn save(&mut self) -> ShResult<()> {
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&self.path)?;
|
||||
let last_file_entry = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|ent| !ent.new)
|
||||
.next_back()
|
||||
.map(|ent| ent.command.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let last_file_entry = self.entries
|
||||
.iter()
|
||||
.filter(|ent| !ent.new)
|
||||
.next_back()
|
||||
.map(|ent| ent.command.clone())
|
||||
.unwrap_or_default();
|
||||
let entries = self.entries.iter_mut().filter(|ent| {
|
||||
ent.new
|
||||
&& !ent.command.is_empty()
|
||||
&& if self.ignore_dups {
|
||||
ent.command() != last_file_entry
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
let entries = self.entries
|
||||
.iter_mut()
|
||||
.filter(|ent| {
|
||||
ent.new &&
|
||||
!ent.command.is_empty() &&
|
||||
if self.ignore_dups {
|
||||
ent.command() != last_file_entry
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
let mut data = String::new();
|
||||
for ent in entries {
|
||||
ent.new = false;
|
||||
write!(data, "{ent}").unwrap();
|
||||
}
|
||||
|
||||
let mut data = String::new();
|
||||
for ent in entries {
|
||||
ent.new = false;
|
||||
write!(data, "{ent}").unwrap();
|
||||
}
|
||||
file.write_all(data.as_bytes())?;
|
||||
|
||||
file.write_all(data.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,140 +3,137 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
// Credit to Rustyline for the design ideas in this module
|
||||
// https://github.com/kkawakam/rustyline
|
||||
#[derive(Clone,PartialEq,Eq,Debug)]
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct KeyEvent(pub KeyCode, pub ModKeys);
|
||||
|
||||
|
||||
impl KeyEvent {
|
||||
pub fn new(ch: &str, mut mods: ModKeys) -> Self {
|
||||
use {KeyCode as K, KeyEvent as E, ModKeys as M};
|
||||
pub fn new(ch: &str, mut mods: ModKeys) -> Self {
|
||||
use {KeyCode as K, KeyEvent as E, ModKeys as M};
|
||||
|
||||
let mut graphemes = ch.graphemes(true);
|
||||
let mut graphemes = ch.graphemes(true);
|
||||
|
||||
let first = match graphemes.next() {
|
||||
Some(g) => g,
|
||||
None => return E(K::Null, mods),
|
||||
};
|
||||
let first = match graphemes.next() {
|
||||
Some(g) => g,
|
||||
None => return E(K::Null, mods),
|
||||
};
|
||||
|
||||
// If more than one grapheme, it's not a single key event
|
||||
if graphemes.next().is_some() {
|
||||
return E(K::Null, mods); // Or panic, or wrap in Grapheme if desired
|
||||
}
|
||||
// If more than one grapheme, it's not a single key event
|
||||
if graphemes.next().is_some() {
|
||||
return E(K::Null, mods); // Or panic, or wrap in Grapheme if desired
|
||||
}
|
||||
|
||||
let mut chars = first.chars();
|
||||
let mut chars = first.chars();
|
||||
|
||||
let single_char = chars.next();
|
||||
let is_single_char = chars.next().is_none();
|
||||
let single_char = chars.next();
|
||||
let is_single_char = chars.next().is_none();
|
||||
|
||||
match single_char {
|
||||
Some(c) if is_single_char && c.is_control() => {
|
||||
match c {
|
||||
'\x00' => E(K::Char('@'), mods | M::CTRL),
|
||||
'\x01' => E(K::Char('A'), mods | M::CTRL),
|
||||
'\x02' => E(K::Char('B'), mods | M::CTRL),
|
||||
'\x03' => E(K::Char('C'), mods | M::CTRL),
|
||||
'\x04' => E(K::Char('D'), mods | M::CTRL),
|
||||
'\x05' => E(K::Char('E'), mods | M::CTRL),
|
||||
'\x06' => E(K::Char('F'), mods | M::CTRL),
|
||||
'\x07' => E(K::Char('G'), mods | M::CTRL),
|
||||
'\x08' => E(K::Backspace, mods),
|
||||
'\x09' => {
|
||||
if mods.contains(M::SHIFT) {
|
||||
mods.remove(M::SHIFT);
|
||||
E(K::BackTab, mods)
|
||||
} else {
|
||||
E(K::Tab, mods)
|
||||
}
|
||||
}
|
||||
'\x0a' => E(K::Char('J'), mods | M::CTRL),
|
||||
'\x0b' => E(K::Char('K'), mods | M::CTRL),
|
||||
'\x0c' => E(K::Char('L'), mods | M::CTRL),
|
||||
'\x0d' => E(K::Enter, mods),
|
||||
'\x0e' => E(K::Char('N'), mods | M::CTRL),
|
||||
'\x0f' => E(K::Char('O'), mods | M::CTRL),
|
||||
'\x10' => E(K::Char('P'), mods | M::CTRL),
|
||||
'\x11' => E(K::Char('Q'), mods | M::CTRL),
|
||||
'\x12' => E(K::Char('R'), mods | M::CTRL),
|
||||
'\x13' => E(K::Char('S'), mods | M::CTRL),
|
||||
'\x14' => E(K::Char('T'), mods | M::CTRL),
|
||||
'\x15' => E(K::Char('U'), mods | M::CTRL),
|
||||
'\x16' => E(K::Char('V'), mods | M::CTRL),
|
||||
'\x17' => E(K::Char('W'), mods | M::CTRL),
|
||||
'\x18' => E(K::Char('X'), mods | M::CTRL),
|
||||
'\x19' => E(K::Char('Y'), mods | M::CTRL),
|
||||
'\x1a' => E(K::Char('Z'), mods | M::CTRL),
|
||||
'\x1b' => E(K::Esc, mods),
|
||||
'\x1c' => E(K::Char('\\'), mods | M::CTRL),
|
||||
'\x1d' => E(K::Char(']'), mods | M::CTRL),
|
||||
'\x1e' => E(K::Char('^'), mods | M::CTRL),
|
||||
'\x1f' => E(K::Char('_'), mods | M::CTRL),
|
||||
'\x7f' => E(K::Backspace, mods),
|
||||
'\u{9b}' => E(K::Esc, mods | M::SHIFT),
|
||||
_ => E(K::Null, mods),
|
||||
}
|
||||
}
|
||||
Some(c) if is_single_char => {
|
||||
if !mods.is_empty() {
|
||||
mods.remove(M::SHIFT);
|
||||
}
|
||||
E(K::Char(c), mods)
|
||||
}
|
||||
_ => {
|
||||
// multi-char grapheme (emoji, accented, etc)
|
||||
if !mods.is_empty() {
|
||||
mods.remove(M::SHIFT);
|
||||
}
|
||||
E(K::Grapheme(Arc::from(first)), mods)
|
||||
}
|
||||
}
|
||||
}
|
||||
match single_char {
|
||||
Some(c) if is_single_char && c.is_control() => match c {
|
||||
'\x00' => E(K::Char('@'), mods | M::CTRL),
|
||||
'\x01' => E(K::Char('A'), mods | M::CTRL),
|
||||
'\x02' => E(K::Char('B'), mods | M::CTRL),
|
||||
'\x03' => E(K::Char('C'), mods | M::CTRL),
|
||||
'\x04' => E(K::Char('D'), mods | M::CTRL),
|
||||
'\x05' => E(K::Char('E'), mods | M::CTRL),
|
||||
'\x06' => E(K::Char('F'), mods | M::CTRL),
|
||||
'\x07' => E(K::Char('G'), mods | M::CTRL),
|
||||
'\x08' => E(K::Backspace, mods),
|
||||
'\x09' => {
|
||||
if mods.contains(M::SHIFT) {
|
||||
mods.remove(M::SHIFT);
|
||||
E(K::BackTab, mods)
|
||||
} else {
|
||||
E(K::Tab, mods)
|
||||
}
|
||||
}
|
||||
'\x0a' => E(K::Char('J'), mods | M::CTRL),
|
||||
'\x0b' => E(K::Char('K'), mods | M::CTRL),
|
||||
'\x0c' => E(K::Char('L'), mods | M::CTRL),
|
||||
'\x0d' => E(K::Enter, mods),
|
||||
'\x0e' => E(K::Char('N'), mods | M::CTRL),
|
||||
'\x0f' => E(K::Char('O'), mods | M::CTRL),
|
||||
'\x10' => E(K::Char('P'), mods | M::CTRL),
|
||||
'\x11' => E(K::Char('Q'), mods | M::CTRL),
|
||||
'\x12' => E(K::Char('R'), mods | M::CTRL),
|
||||
'\x13' => E(K::Char('S'), mods | M::CTRL),
|
||||
'\x14' => E(K::Char('T'), mods | M::CTRL),
|
||||
'\x15' => E(K::Char('U'), mods | M::CTRL),
|
||||
'\x16' => E(K::Char('V'), mods | M::CTRL),
|
||||
'\x17' => E(K::Char('W'), mods | M::CTRL),
|
||||
'\x18' => E(K::Char('X'), mods | M::CTRL),
|
||||
'\x19' => E(K::Char('Y'), mods | M::CTRL),
|
||||
'\x1a' => E(K::Char('Z'), mods | M::CTRL),
|
||||
'\x1b' => E(K::Esc, mods),
|
||||
'\x1c' => E(K::Char('\\'), mods | M::CTRL),
|
||||
'\x1d' => E(K::Char(']'), mods | M::CTRL),
|
||||
'\x1e' => E(K::Char('^'), mods | M::CTRL),
|
||||
'\x1f' => E(K::Char('_'), mods | M::CTRL),
|
||||
'\x7f' => E(K::Backspace, mods),
|
||||
'\u{9b}' => E(K::Esc, mods | M::SHIFT),
|
||||
_ => E(K::Null, mods),
|
||||
},
|
||||
Some(c) if is_single_char => {
|
||||
if !mods.is_empty() {
|
||||
mods.remove(M::SHIFT);
|
||||
}
|
||||
E(K::Char(c), mods)
|
||||
}
|
||||
_ => {
|
||||
// multi-char grapheme (emoji, accented, etc)
|
||||
if !mods.is_empty() {
|
||||
mods.remove(M::SHIFT);
|
||||
}
|
||||
E(K::Grapheme(Arc::from(first)), mods)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,PartialEq,Eq,Debug)]
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub enum KeyCode {
|
||||
UnknownEscSeq,
|
||||
Backspace,
|
||||
BackTab,
|
||||
BracketedPasteStart,
|
||||
BracketedPasteEnd,
|
||||
Char(char),
|
||||
Grapheme(Arc<str>),
|
||||
Delete,
|
||||
Down,
|
||||
End,
|
||||
Enter,
|
||||
Esc,
|
||||
F(u8),
|
||||
Home,
|
||||
Insert,
|
||||
Left,
|
||||
Null,
|
||||
PageDown,
|
||||
PageUp,
|
||||
Right,
|
||||
Tab,
|
||||
Up,
|
||||
UnknownEscSeq,
|
||||
Backspace,
|
||||
BackTab,
|
||||
BracketedPasteStart,
|
||||
BracketedPasteEnd,
|
||||
Char(char),
|
||||
Grapheme(Arc<str>),
|
||||
Delete,
|
||||
Down,
|
||||
End,
|
||||
Enter,
|
||||
Esc,
|
||||
F(u8),
|
||||
Home,
|
||||
Insert,
|
||||
Left,
|
||||
Null,
|
||||
PageDown,
|
||||
PageUp,
|
||||
Right,
|
||||
Tab,
|
||||
Up,
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct ModKeys: u8 {
|
||||
/// Control modifier
|
||||
const CTRL = 1<<3;
|
||||
/// Escape or Alt modifier
|
||||
const ALT = 1<<2;
|
||||
/// Shift modifier
|
||||
const SHIFT = 1<<1;
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct ModKeys: u8 {
|
||||
/// Control modifier
|
||||
const CTRL = 1<<3;
|
||||
/// Escape or Alt modifier
|
||||
const ALT = 1<<2;
|
||||
/// Shift modifier
|
||||
const SHIFT = 1<<1;
|
||||
|
||||
/// No modifier
|
||||
const NONE = 0;
|
||||
/// Ctrl + Shift
|
||||
const CTRL_SHIFT = Self::CTRL.bits() | Self::SHIFT.bits();
|
||||
/// Alt + Shift
|
||||
const ALT_SHIFT = Self::ALT.bits() | Self::SHIFT.bits();
|
||||
/// Ctrl + Alt
|
||||
const CTRL_ALT = Self::CTRL.bits() | Self::ALT.bits();
|
||||
/// Ctrl + Alt + Shift
|
||||
const CTRL_ALT_SHIFT = Self::CTRL.bits() | Self::ALT.bits() | Self::SHIFT.bits();
|
||||
}
|
||||
/// No modifier
|
||||
const NONE = 0;
|
||||
/// Ctrl + Shift
|
||||
const CTRL_SHIFT = Self::CTRL.bits() | Self::SHIFT.bits();
|
||||
/// Alt + Shift
|
||||
const ALT_SHIFT = Self::ALT.bits() | Self::SHIFT.bits();
|
||||
/// Ctrl + Alt
|
||||
const CTRL_ALT = Self::CTRL.bits() | Self::ALT.bits();
|
||||
/// Ctrl + Alt + Shift
|
||||
const CTRL_ALT_SHIFT = Self::CTRL.bits() | Self::ALT.bits() | Self::SHIFT.bits();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,396 +6,377 @@ use term::{get_win_size, raw_mode, KeyReader, Layout, LineWriter, TermReader, Te
|
||||
use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd};
|
||||
use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
|
||||
|
||||
use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, sys::sh_quit, term::{Style, Styled}};
|
||||
use crate::libsh::{
|
||||
error::{ShErr, ShErrKind, ShResult},
|
||||
sys::sh_quit,
|
||||
term::{Style, Styled},
|
||||
};
|
||||
use crate::prelude::*;
|
||||
|
||||
pub mod term;
|
||||
pub mod linebuf;
|
||||
pub mod layout;
|
||||
pub mod keys;
|
||||
pub mod vicmd;
|
||||
pub mod register;
|
||||
pub mod vimode;
|
||||
pub mod history;
|
||||
pub mod keys;
|
||||
pub mod layout;
|
||||
pub mod linebuf;
|
||||
pub mod register;
|
||||
pub mod term;
|
||||
pub mod vicmd;
|
||||
pub mod vimode;
|
||||
|
||||
// Very useful for testing
|
||||
const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.";
|
||||
|
||||
pub trait Readline {
|
||||
fn readline(&mut self) -> ShResult<String>;
|
||||
fn readline(&mut self) -> ShResult<String>;
|
||||
}
|
||||
|
||||
pub struct FernVi {
|
||||
pub reader: Box<dyn KeyReader>,
|
||||
pub writer: Box<dyn LineWriter>,
|
||||
pub prompt: String,
|
||||
pub mode: Box<dyn ViMode>,
|
||||
pub old_layout: Option<Layout>,
|
||||
pub repeat_action: Option<CmdReplay>,
|
||||
pub repeat_motion: Option<MotionCmd>,
|
||||
pub editor: LineBuf,
|
||||
pub history: History
|
||||
pub reader: Box<dyn KeyReader>,
|
||||
pub writer: Box<dyn LineWriter>,
|
||||
pub prompt: String,
|
||||
pub mode: Box<dyn ViMode>,
|
||||
pub old_layout: Option<Layout>,
|
||||
pub repeat_action: Option<CmdReplay>,
|
||||
pub repeat_motion: Option<MotionCmd>,
|
||||
pub editor: LineBuf,
|
||||
pub history: History,
|
||||
}
|
||||
|
||||
impl Readline for FernVi {
|
||||
fn readline(&mut self) -> ShResult<String> {
|
||||
let raw_mode_guard = raw_mode(); // Restores termios state on drop
|
||||
fn readline(&mut self) -> ShResult<String> {
|
||||
let raw_mode_guard = raw_mode(); // Restores termios state on drop
|
||||
|
||||
loop {
|
||||
raw_mode_guard.disable_for(|| self.print_line())?;
|
||||
loop {
|
||||
raw_mode_guard.disable_for(|| self.print_line())?;
|
||||
|
||||
let Some(key) = self.reader.read_key() else {
|
||||
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
|
||||
std::mem::drop(raw_mode_guard);
|
||||
return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF"))
|
||||
};
|
||||
flog!(DEBUG, key);
|
||||
let Some(key) = self.reader.read_key() else {
|
||||
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
|
||||
std::mem::drop(raw_mode_guard);
|
||||
return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF"));
|
||||
};
|
||||
flog!(DEBUG, key);
|
||||
|
||||
if self.should_accept_hint(&key) {
|
||||
self.editor.accept_hint();
|
||||
self.history.update_pending_cmd(self.editor.as_str());
|
||||
self.print_line()?;
|
||||
continue
|
||||
}
|
||||
if self.should_accept_hint(&key) {
|
||||
self.editor.accept_hint();
|
||||
self.history.update_pending_cmd(self.editor.as_str());
|
||||
self.print_line()?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(mut cmd) = self.mode.handle_key(key) else {
|
||||
flog!(DEBUG, "got none??");
|
||||
continue
|
||||
};
|
||||
flog!(DEBUG,cmd);
|
||||
cmd.alter_line_motion_if_no_verb();
|
||||
let Some(mut cmd) = self.mode.handle_key(key) else {
|
||||
flog!(DEBUG, "got none??");
|
||||
continue;
|
||||
};
|
||||
flog!(DEBUG, cmd);
|
||||
cmd.alter_line_motion_if_no_verb();
|
||||
|
||||
if self.should_grab_history(&cmd) {
|
||||
self.scroll_history(cmd);
|
||||
self.print_line()?;
|
||||
continue
|
||||
}
|
||||
if self.should_grab_history(&cmd) {
|
||||
self.scroll_history(cmd);
|
||||
self.print_line()?;
|
||||
continue;
|
||||
}
|
||||
|
||||
if cmd.should_submit() {
|
||||
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
|
||||
std::mem::drop(raw_mode_guard);
|
||||
return Ok(self.editor.take_buf())
|
||||
}
|
||||
if cmd.should_submit() {
|
||||
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
|
||||
std::mem::drop(raw_mode_guard);
|
||||
return Ok(self.editor.take_buf());
|
||||
}
|
||||
|
||||
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
|
||||
if self.editor.buffer.is_empty() {
|
||||
std::mem::drop(raw_mode_guard);
|
||||
sh_quit(0);
|
||||
} else {
|
||||
self.editor.buffer.clear();
|
||||
continue
|
||||
}
|
||||
}
|
||||
flog!(DEBUG,cmd);
|
||||
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
|
||||
if self.editor.buffer.is_empty() {
|
||||
std::mem::drop(raw_mode_guard);
|
||||
sh_quit(0);
|
||||
} else {
|
||||
self.editor.buffer.clear();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
flog!(DEBUG, cmd);
|
||||
|
||||
let before = self.editor.buffer.clone();
|
||||
self.exec_cmd(cmd)?;
|
||||
let after = self.editor.as_str();
|
||||
let before = self.editor.buffer.clone();
|
||||
self.exec_cmd(cmd)?;
|
||||
let after = self.editor.as_str();
|
||||
|
||||
if before != after {
|
||||
self.history.update_pending_cmd(self.editor.as_str());
|
||||
}
|
||||
if before != after {
|
||||
self.history.update_pending_cmd(self.editor.as_str());
|
||||
}
|
||||
|
||||
let hint = self.history.get_hint();
|
||||
self.editor.set_hint(hint);
|
||||
}
|
||||
}
|
||||
let hint = self.history.get_hint();
|
||||
self.editor.set_hint(hint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FernVi {
|
||||
pub fn new(prompt: Option<String>) -> ShResult<Self> {
|
||||
Ok(Self {
|
||||
reader: Box::new(TermReader::new()),
|
||||
writer: Box::new(TermWriter::new(STDOUT_FILENO)),
|
||||
prompt: prompt.unwrap_or("$ ".styled(Style::Green)),
|
||||
mode: Box::new(ViInsert::new()),
|
||||
old_layout: None,
|
||||
repeat_action: None,
|
||||
repeat_motion: None,
|
||||
editor: LineBuf::new().with_initial(LOREM_IPSUM, 0),
|
||||
history: History::new()?
|
||||
})
|
||||
}
|
||||
pub fn new(prompt: Option<String>) -> ShResult<Self> {
|
||||
Ok(Self {
|
||||
reader: Box::new(TermReader::new()),
|
||||
writer: Box::new(TermWriter::new(STDOUT_FILENO)),
|
||||
prompt: prompt.unwrap_or("$ ".styled(Style::Green)),
|
||||
mode: Box::new(ViInsert::new()),
|
||||
old_layout: None,
|
||||
repeat_action: None,
|
||||
repeat_motion: None,
|
||||
editor: LineBuf::new().with_initial(LOREM_IPSUM, 0),
|
||||
history: History::new()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_layout(&mut self) -> Layout {
|
||||
let line = self.editor.to_string();
|
||||
flog!(DEBUG,line);
|
||||
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
|
||||
let (cols,_) = get_win_size(STDIN_FILENO);
|
||||
Layout::from_parts(
|
||||
/*tab_stop:*/ 8,
|
||||
cols,
|
||||
&self.prompt,
|
||||
to_cursor,
|
||||
&line
|
||||
)
|
||||
}
|
||||
pub fn scroll_history(&mut self, cmd: ViCmd) {
|
||||
flog!(DEBUG,"scrolling");
|
||||
/*
|
||||
if self.history.cursor_entry().is_some_and(|ent| ent.is_new()) {
|
||||
let constraint = SearchConstraint::new(SearchKind::Prefix, self.editor.to_string());
|
||||
self.history.constrain_entries(constraint);
|
||||
}
|
||||
*/
|
||||
let count = &cmd.motion().unwrap().0;
|
||||
let motion = &cmd.motion().unwrap().1;
|
||||
flog!(DEBUG,count,motion);
|
||||
flog!(DEBUG,self.history.masked_entries());
|
||||
let entry = match motion {
|
||||
Motion::LineUpCharwise => {
|
||||
let Some(hist_entry) = self.history.scroll(-(*count as isize)) else {
|
||||
return
|
||||
};
|
||||
flog!(DEBUG,"found entry");
|
||||
flog!(DEBUG,hist_entry.command());
|
||||
hist_entry
|
||||
}
|
||||
Motion::LineDownCharwise => {
|
||||
let Some(hist_entry) = self.history.scroll(*count as isize) else {
|
||||
return
|
||||
};
|
||||
flog!(DEBUG,"found entry");
|
||||
flog!(DEBUG,hist_entry.command());
|
||||
hist_entry
|
||||
}
|
||||
_ => unreachable!()
|
||||
};
|
||||
let col = self.editor.saved_col.unwrap_or(self.editor.cursor_col());
|
||||
let mut buf = LineBuf::new().with_initial(entry.command(),0);
|
||||
let line_end = buf.end_of_line();
|
||||
if let Some(dest) = self.mode.hist_scroll_start_pos() {
|
||||
match dest {
|
||||
To::Start => {
|
||||
/* Already at 0 */
|
||||
}
|
||||
To::End => {
|
||||
// History entries cannot be empty
|
||||
// So this subtraction is safe (maybe)
|
||||
buf.cursor.add(line_end);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let target = (col).min(line_end);
|
||||
buf.cursor.add(target);
|
||||
}
|
||||
pub fn get_layout(&mut self) -> Layout {
|
||||
let line = self.editor.to_string();
|
||||
flog!(DEBUG, line);
|
||||
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
|
||||
let (cols, _) = get_win_size(STDIN_FILENO);
|
||||
Layout::from_parts(/* tab_stop: */ 8, cols, &self.prompt, to_cursor, &line)
|
||||
}
|
||||
pub fn scroll_history(&mut self, cmd: ViCmd) {
|
||||
flog!(DEBUG, "scrolling");
|
||||
/*
|
||||
if self.history.cursor_entry().is_some_and(|ent| ent.is_new()) {
|
||||
let constraint = SearchConstraint::new(SearchKind::Prefix, self.editor.to_string());
|
||||
self.history.constrain_entries(constraint);
|
||||
}
|
||||
*/
|
||||
let count = &cmd.motion().unwrap().0;
|
||||
let motion = &cmd.motion().unwrap().1;
|
||||
flog!(DEBUG, count, motion);
|
||||
flog!(DEBUG, self.history.masked_entries());
|
||||
let entry = match motion {
|
||||
Motion::LineUpCharwise => {
|
||||
let Some(hist_entry) = self.history.scroll(-(*count as isize)) else {
|
||||
return;
|
||||
};
|
||||
flog!(DEBUG, "found entry");
|
||||
flog!(DEBUG, hist_entry.command());
|
||||
hist_entry
|
||||
}
|
||||
Motion::LineDownCharwise => {
|
||||
let Some(hist_entry) = self.history.scroll(*count as isize) else {
|
||||
return;
|
||||
};
|
||||
flog!(DEBUG, "found entry");
|
||||
flog!(DEBUG, hist_entry.command());
|
||||
hist_entry
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let col = self.editor.saved_col.unwrap_or(self.editor.cursor_col());
|
||||
let mut buf = LineBuf::new().with_initial(entry.command(), 0);
|
||||
let line_end = buf.end_of_line();
|
||||
if let Some(dest) = self.mode.hist_scroll_start_pos() {
|
||||
match dest {
|
||||
To::Start => { /* Already at 0 */ }
|
||||
To::End => {
|
||||
// History entries cannot be empty
|
||||
// So this subtraction is safe (maybe)
|
||||
buf.cursor.add(line_end);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let target = (col).min(line_end);
|
||||
buf.cursor.add(target);
|
||||
}
|
||||
|
||||
self.editor = buf
|
||||
}
|
||||
pub fn should_accept_hint(&self, event: &KeyEvent) -> bool {
|
||||
flog!(DEBUG,self.editor.cursor_at_max());
|
||||
flog!(DEBUG,self.editor.cursor);
|
||||
if self.editor.cursor_at_max() && self.editor.has_hint() {
|
||||
match self.mode.report_mode() {
|
||||
ModeReport::Replace |
|
||||
ModeReport::Insert => {
|
||||
matches!(
|
||||
event,
|
||||
KeyEvent(KeyCode::Right, ModKeys::NONE)
|
||||
)
|
||||
}
|
||||
ModeReport::Visual |
|
||||
ModeReport::Normal => {
|
||||
matches!(
|
||||
event,
|
||||
KeyEvent(KeyCode::Right, ModKeys::NONE)
|
||||
) ||
|
||||
(
|
||||
self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty() &&
|
||||
matches!(
|
||||
event,
|
||||
KeyEvent(KeyCode::Char('l'), ModKeys::NONE)
|
||||
)
|
||||
)
|
||||
}
|
||||
_ => unimplemented!()
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
self.editor = buf
|
||||
}
|
||||
pub fn should_accept_hint(&self, event: &KeyEvent) -> bool {
|
||||
flog!(DEBUG, self.editor.cursor_at_max());
|
||||
flog!(DEBUG, self.editor.cursor);
|
||||
if self.editor.cursor_at_max() && self.editor.has_hint() {
|
||||
match self.mode.report_mode() {
|
||||
ModeReport::Replace | ModeReport::Insert => {
|
||||
matches!(event, KeyEvent(KeyCode::Right, ModKeys::NONE))
|
||||
}
|
||||
ModeReport::Visual | ModeReport::Normal => {
|
||||
matches!(event, KeyEvent(KeyCode::Right, ModKeys::NONE))
|
||||
|| (self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty()
|
||||
&& matches!(event, KeyEvent(KeyCode::Char('l'), ModKeys::NONE)))
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_grab_history(&mut self, cmd: &ViCmd) -> bool {
|
||||
cmd.verb().is_none() &&
|
||||
(
|
||||
cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUpCharwise))) &&
|
||||
self.editor.start_of_line() == 0
|
||||
) ||
|
||||
(
|
||||
cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise))) &&
|
||||
self.editor.end_of_line() == self.editor.cursor_max() &&
|
||||
!self.history.cursor_entry().is_some_and(|ent| ent.is_new())
|
||||
)
|
||||
}
|
||||
pub fn should_grab_history(&mut self, cmd: &ViCmd) -> bool {
|
||||
cmd.verb().is_none()
|
||||
&& (cmd
|
||||
.motion()
|
||||
.is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUpCharwise)))
|
||||
&& self.editor.start_of_line() == 0)
|
||||
|| (cmd
|
||||
.motion()
|
||||
.is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise)))
|
||||
&& self.editor.end_of_line() == self.editor.cursor_max()
|
||||
&& !self.history.cursor_entry().is_some_and(|ent| ent.is_new()))
|
||||
}
|
||||
|
||||
pub fn print_line(&mut self) -> ShResult<()> {
|
||||
let new_layout = self.get_layout();
|
||||
if let Some(layout) = self.old_layout.as_ref() {
|
||||
self.writer.clear_rows(layout)?;
|
||||
}
|
||||
pub fn print_line(&mut self) -> ShResult<()> {
|
||||
let new_layout = self.get_layout();
|
||||
if let Some(layout) = self.old_layout.as_ref() {
|
||||
self.writer.clear_rows(layout)?;
|
||||
}
|
||||
|
||||
self.writer.redraw(
|
||||
&self.prompt,
|
||||
&self.editor,
|
||||
&new_layout
|
||||
)?;
|
||||
self
|
||||
.writer
|
||||
.redraw(&self.prompt, &self.editor, &new_layout)?;
|
||||
|
||||
self.writer.flush_write(&self.mode.cursor_style())?;
|
||||
self.writer.flush_write(&self.mode.cursor_style())?;
|
||||
|
||||
self.old_layout = Some(new_layout);
|
||||
Ok(())
|
||||
}
|
||||
self.old_layout = Some(new_layout);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> {
|
||||
let mut selecting = false;
|
||||
let mut is_insert_mode = false;
|
||||
if cmd.is_mode_transition() {
|
||||
let count = cmd.verb_count();
|
||||
let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap().1 {
|
||||
Verb::Change |
|
||||
Verb::InsertModeLineBreak(_) |
|
||||
Verb::InsertMode => {
|
||||
is_insert_mode = true;
|
||||
Box::new(ViInsert::new().with_count(count as u16))
|
||||
}
|
||||
pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> {
|
||||
let mut selecting = false;
|
||||
let mut is_insert_mode = false;
|
||||
if cmd.is_mode_transition() {
|
||||
let count = cmd.verb_count();
|
||||
let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap().1 {
|
||||
Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => {
|
||||
is_insert_mode = true;
|
||||
Box::new(ViInsert::new().with_count(count as u16))
|
||||
}
|
||||
|
||||
Verb::NormalMode => {
|
||||
Box::new(ViNormal::new())
|
||||
}
|
||||
Verb::NormalMode => Box::new(ViNormal::new()),
|
||||
|
||||
Verb::ReplaceMode => Box::new(ViReplace::new()),
|
||||
Verb::ReplaceMode => Box::new(ViReplace::new()),
|
||||
|
||||
Verb::VisualModeSelectLast => {
|
||||
if self.mode.report_mode() != ModeReport::Visual {
|
||||
self.editor.start_selecting(SelectMode::Char(SelectAnchor::End));
|
||||
}
|
||||
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new());
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
|
||||
Verb::VisualModeSelectLast => {
|
||||
if self.mode.report_mode() != ModeReport::Visual {
|
||||
self
|
||||
.editor
|
||||
.start_selecting(SelectMode::Char(SelectAnchor::End));
|
||||
}
|
||||
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new());
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
|
||||
|
||||
return self.editor.exec_cmd(cmd)
|
||||
}
|
||||
Verb::VisualMode => {
|
||||
selecting = true;
|
||||
Box::new(ViVisual::new())
|
||||
}
|
||||
return self.editor.exec_cmd(cmd);
|
||||
}
|
||||
Verb::VisualMode => {
|
||||
selecting = true;
|
||||
Box::new(ViVisual::new())
|
||||
}
|
||||
|
||||
_ => unreachable!()
|
||||
};
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
|
||||
if mode.is_repeatable() {
|
||||
self.repeat_action = mode.as_replay();
|
||||
}
|
||||
if mode.is_repeatable() {
|
||||
self.repeat_action = mode.as_replay();
|
||||
}
|
||||
|
||||
self.editor.exec_cmd(cmd)?;
|
||||
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
|
||||
self.editor.exec_cmd(cmd)?;
|
||||
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
|
||||
|
||||
if selecting {
|
||||
self.editor.start_selecting(SelectMode::Char(SelectAnchor::End));
|
||||
} else {
|
||||
self.editor.stop_selecting();
|
||||
}
|
||||
if is_insert_mode {
|
||||
self.editor.mark_insert_mode_start_pos();
|
||||
} else {
|
||||
self.editor.clear_insert_mode_start_pos();
|
||||
}
|
||||
return Ok(())
|
||||
} else if cmd.is_cmd_repeat() {
|
||||
let Some(replay) = self.repeat_action.clone() else {
|
||||
return Ok(())
|
||||
};
|
||||
let ViCmd { verb, .. } = cmd;
|
||||
let VerbCmd(count,_) = verb.unwrap();
|
||||
match replay {
|
||||
CmdReplay::ModeReplay { cmds, mut repeat } => {
|
||||
if count > 1 {
|
||||
repeat = count as u16;
|
||||
}
|
||||
for _ in 0..repeat {
|
||||
let cmds = cmds.clone();
|
||||
for cmd in cmds {
|
||||
self.editor.exec_cmd(cmd)?
|
||||
}
|
||||
}
|
||||
}
|
||||
CmdReplay::Single(mut cmd) => {
|
||||
if count > 1 {
|
||||
// Override the counts with the one passed to the '.' command
|
||||
if cmd.verb.is_some() {
|
||||
if let Some(v_mut) = cmd.verb.as_mut() {
|
||||
v_mut.0 = count
|
||||
}
|
||||
if let Some(m_mut) = cmd.motion.as_mut() {
|
||||
m_mut.0 = 1
|
||||
}
|
||||
} else {
|
||||
return Ok(()) // it has to have a verb to be repeatable, something weird happened
|
||||
}
|
||||
}
|
||||
self.editor.exec_cmd(cmd)?;
|
||||
}
|
||||
_ => unreachable!("motions should be handled in the other branch")
|
||||
}
|
||||
return Ok(())
|
||||
} else if cmd.is_motion_repeat() {
|
||||
match cmd.motion.as_ref().unwrap() {
|
||||
MotionCmd(count,Motion::RepeatMotion) => {
|
||||
let Some(motion) = self.repeat_motion.clone() else {
|
||||
return Ok(())
|
||||
};
|
||||
let repeat_cmd = ViCmd {
|
||||
register: RegisterName::default(),
|
||||
verb: None,
|
||||
motion: Some(motion),
|
||||
raw_seq: format!("{count};"),
|
||||
flags: CmdFlags::empty()
|
||||
};
|
||||
return self.editor.exec_cmd(repeat_cmd);
|
||||
}
|
||||
MotionCmd(count,Motion::RepeatMotionRev) => {
|
||||
let Some(motion) = self.repeat_motion.clone() else {
|
||||
return Ok(())
|
||||
};
|
||||
let mut new_motion = motion.invert_char_motion();
|
||||
new_motion.0 = *count;
|
||||
let repeat_cmd = ViCmd {
|
||||
register: RegisterName::default(),
|
||||
verb: None,
|
||||
motion: Some(new_motion),
|
||||
raw_seq: format!("{count},"),
|
||||
flags: CmdFlags::empty()
|
||||
};
|
||||
return self.editor.exec_cmd(repeat_cmd);
|
||||
}
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
if selecting {
|
||||
self
|
||||
.editor
|
||||
.start_selecting(SelectMode::Char(SelectAnchor::End));
|
||||
} else {
|
||||
self.editor.stop_selecting();
|
||||
}
|
||||
if is_insert_mode {
|
||||
self.editor.mark_insert_mode_start_pos();
|
||||
} else {
|
||||
self.editor.clear_insert_mode_start_pos();
|
||||
}
|
||||
return Ok(());
|
||||
} else if cmd.is_cmd_repeat() {
|
||||
let Some(replay) = self.repeat_action.clone() else {
|
||||
return Ok(());
|
||||
};
|
||||
let ViCmd { verb, .. } = cmd;
|
||||
let VerbCmd(count, _) = verb.unwrap();
|
||||
match replay {
|
||||
CmdReplay::ModeReplay { cmds, mut repeat } => {
|
||||
if count > 1 {
|
||||
repeat = count as u16;
|
||||
}
|
||||
for _ in 0..repeat {
|
||||
let cmds = cmds.clone();
|
||||
for cmd in cmds {
|
||||
self.editor.exec_cmd(cmd)?
|
||||
}
|
||||
}
|
||||
}
|
||||
CmdReplay::Single(mut cmd) => {
|
||||
if count > 1 {
|
||||
// Override the counts with the one passed to the '.' command
|
||||
if cmd.verb.is_some() {
|
||||
if let Some(v_mut) = cmd.verb.as_mut() {
|
||||
v_mut.0 = count
|
||||
}
|
||||
if let Some(m_mut) = cmd.motion.as_mut() {
|
||||
m_mut.0 = 1
|
||||
}
|
||||
} else {
|
||||
return Ok(()); // it has to have a verb to be repeatable,
|
||||
// something weird happened
|
||||
}
|
||||
}
|
||||
self.editor.exec_cmd(cmd)?;
|
||||
}
|
||||
_ => unreachable!("motions should be handled in the other branch"),
|
||||
}
|
||||
return Ok(());
|
||||
} else if cmd.is_motion_repeat() {
|
||||
match cmd.motion.as_ref().unwrap() {
|
||||
MotionCmd(count, Motion::RepeatMotion) => {
|
||||
let Some(motion) = self.repeat_motion.clone() else {
|
||||
return Ok(());
|
||||
};
|
||||
let repeat_cmd = ViCmd {
|
||||
register: RegisterName::default(),
|
||||
verb: None,
|
||||
motion: Some(motion),
|
||||
raw_seq: format!("{count};"),
|
||||
flags: CmdFlags::empty(),
|
||||
};
|
||||
return self.editor.exec_cmd(repeat_cmd);
|
||||
}
|
||||
MotionCmd(count, Motion::RepeatMotionRev) => {
|
||||
let Some(motion) = self.repeat_motion.clone() else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut new_motion = motion.invert_char_motion();
|
||||
new_motion.0 = *count;
|
||||
let repeat_cmd = ViCmd {
|
||||
register: RegisterName::default(),
|
||||
verb: None,
|
||||
motion: Some(new_motion),
|
||||
raw_seq: format!("{count},"),
|
||||
flags: CmdFlags::empty(),
|
||||
};
|
||||
return self.editor.exec_cmd(repeat_cmd);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.is_repeatable() {
|
||||
if self.mode.report_mode() == ModeReport::Visual {
|
||||
// The motion is assigned in the line buffer execution, so we also have to assign it here
|
||||
// in order to be able to repeat it
|
||||
let range = self.editor.select_range().unwrap();
|
||||
cmd.motion = Some(MotionCmd(1,Motion::Range(range.0, range.1)))
|
||||
}
|
||||
self.repeat_action = Some(CmdReplay::Single(cmd.clone()));
|
||||
}
|
||||
if cmd.is_repeatable() {
|
||||
if self.mode.report_mode() == ModeReport::Visual {
|
||||
// The motion is assigned in the line buffer execution, so we also have to
|
||||
// assign it here in order to be able to repeat it
|
||||
let range = self.editor.select_range().unwrap();
|
||||
cmd.motion = Some(MotionCmd(1, Motion::Range(range.0, range.1)))
|
||||
}
|
||||
self.repeat_action = Some(CmdReplay::Single(cmd.clone()));
|
||||
}
|
||||
|
||||
if cmd.is_char_search() {
|
||||
self.repeat_motion = cmd.motion.clone()
|
||||
}
|
||||
if cmd.is_char_search() {
|
||||
self.repeat_motion = cmd.motion.clone()
|
||||
}
|
||||
|
||||
self.editor.exec_cmd(cmd.clone())?;
|
||||
self.editor.exec_cmd(cmd.clone())?;
|
||||
|
||||
if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) {
|
||||
self.editor.stop_selecting();
|
||||
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) {
|
||||
self.editor.stop_selecting();
|
||||
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,166 +3,170 @@ use std::sync::Mutex;
|
||||
pub static REGISTERS: Mutex<Registers> = Mutex::new(Registers::new());
|
||||
|
||||
pub fn read_register(ch: Option<char>) -> Option<String> {
|
||||
let lock = REGISTERS.lock().unwrap();
|
||||
lock.get_reg(ch).map(|r| r.buf().clone())
|
||||
let lock = REGISTERS.lock().unwrap();
|
||||
lock.get_reg(ch).map(|r| r.buf().clone())
|
||||
}
|
||||
|
||||
pub fn write_register(ch: Option<char>, buf: String) {
|
||||
let mut lock = REGISTERS.lock().unwrap();
|
||||
if let Some(r) = lock.get_reg_mut(ch) { r.write(buf) }
|
||||
let mut lock = REGISTERS.lock().unwrap();
|
||||
if let Some(r) = lock.get_reg_mut(ch) {
|
||||
r.write(buf)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_register(ch: Option<char>, buf: String) {
|
||||
let mut lock = REGISTERS.lock().unwrap();
|
||||
if let Some(r) = lock.get_reg_mut(ch) { r.append(buf) }
|
||||
let mut lock = REGISTERS.lock().unwrap();
|
||||
if let Some(r) = lock.get_reg_mut(ch) {
|
||||
r.append(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default,Debug)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Registers {
|
||||
default: Register,
|
||||
a: Register,
|
||||
b: Register,
|
||||
c: Register,
|
||||
d: Register,
|
||||
e: Register,
|
||||
f: Register,
|
||||
g: Register,
|
||||
h: Register,
|
||||
i: Register,
|
||||
j: Register,
|
||||
k: Register,
|
||||
l: Register,
|
||||
m: Register,
|
||||
n: Register,
|
||||
o: Register,
|
||||
p: Register,
|
||||
q: Register,
|
||||
r: Register,
|
||||
s: Register,
|
||||
t: Register,
|
||||
u: Register,
|
||||
v: Register,
|
||||
w: Register,
|
||||
x: Register,
|
||||
y: Register,
|
||||
z: Register,
|
||||
default: Register,
|
||||
a: Register,
|
||||
b: Register,
|
||||
c: Register,
|
||||
d: Register,
|
||||
e: Register,
|
||||
f: Register,
|
||||
g: Register,
|
||||
h: Register,
|
||||
i: Register,
|
||||
j: Register,
|
||||
k: Register,
|
||||
l: Register,
|
||||
m: Register,
|
||||
n: Register,
|
||||
o: Register,
|
||||
p: Register,
|
||||
q: Register,
|
||||
r: Register,
|
||||
s: Register,
|
||||
t: Register,
|
||||
u: Register,
|
||||
v: Register,
|
||||
w: Register,
|
||||
x: Register,
|
||||
y: Register,
|
||||
z: Register,
|
||||
}
|
||||
|
||||
impl Registers {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
default: Register(String::new()),
|
||||
a: Register(String::new()),
|
||||
b: Register(String::new()),
|
||||
c: Register(String::new()),
|
||||
d: Register(String::new()),
|
||||
e: Register(String::new()),
|
||||
f: Register(String::new()),
|
||||
g: Register(String::new()),
|
||||
h: Register(String::new()),
|
||||
i: Register(String::new()),
|
||||
j: Register(String::new()),
|
||||
k: Register(String::new()),
|
||||
l: Register(String::new()),
|
||||
m: Register(String::new()),
|
||||
n: Register(String::new()),
|
||||
o: Register(String::new()),
|
||||
p: Register(String::new()),
|
||||
q: Register(String::new()),
|
||||
r: Register(String::new()),
|
||||
s: Register(String::new()),
|
||||
t: Register(String::new()),
|
||||
u: Register(String::new()),
|
||||
v: Register(String::new()),
|
||||
w: Register(String::new()),
|
||||
x: Register(String::new()),
|
||||
y: Register(String::new()),
|
||||
z: Register(String::new()),
|
||||
}
|
||||
}
|
||||
pub fn get_reg(&self, ch: Option<char>) -> Option<&Register> {
|
||||
let Some(ch) = ch else {
|
||||
return Some(&self.default)
|
||||
};
|
||||
match ch {
|
||||
'a' => Some(&self.a),
|
||||
'b' => Some(&self.b),
|
||||
'c' => Some(&self.c),
|
||||
'd' => Some(&self.d),
|
||||
'e' => Some(&self.e),
|
||||
'f' => Some(&self.f),
|
||||
'g' => Some(&self.g),
|
||||
'h' => Some(&self.h),
|
||||
'i' => Some(&self.i),
|
||||
'j' => Some(&self.j),
|
||||
'k' => Some(&self.k),
|
||||
'l' => Some(&self.l),
|
||||
'm' => Some(&self.m),
|
||||
'n' => Some(&self.n),
|
||||
'o' => Some(&self.o),
|
||||
'p' => Some(&self.p),
|
||||
'q' => Some(&self.q),
|
||||
'r' => Some(&self.r),
|
||||
's' => Some(&self.s),
|
||||
't' => Some(&self.t),
|
||||
'u' => Some(&self.u),
|
||||
'v' => Some(&self.v),
|
||||
'w' => Some(&self.w),
|
||||
'x' => Some(&self.x),
|
||||
'y' => Some(&self.y),
|
||||
'z' => Some(&self.z),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
pub fn get_reg_mut(&mut self, ch: Option<char>) -> Option<&mut Register> {
|
||||
let Some(ch) = ch else {
|
||||
return Some(&mut self.default)
|
||||
};
|
||||
match ch {
|
||||
'a' => Some(&mut self.a),
|
||||
'b' => Some(&mut self.b),
|
||||
'c' => Some(&mut self.c),
|
||||
'd' => Some(&mut self.d),
|
||||
'e' => Some(&mut self.e),
|
||||
'f' => Some(&mut self.f),
|
||||
'g' => Some(&mut self.g),
|
||||
'h' => Some(&mut self.h),
|
||||
'i' => Some(&mut self.i),
|
||||
'j' => Some(&mut self.j),
|
||||
'k' => Some(&mut self.k),
|
||||
'l' => Some(&mut self.l),
|
||||
'm' => Some(&mut self.m),
|
||||
'n' => Some(&mut self.n),
|
||||
'o' => Some(&mut self.o),
|
||||
'p' => Some(&mut self.p),
|
||||
'q' => Some(&mut self.q),
|
||||
'r' => Some(&mut self.r),
|
||||
's' => Some(&mut self.s),
|
||||
't' => Some(&mut self.t),
|
||||
'u' => Some(&mut self.u),
|
||||
'v' => Some(&mut self.v),
|
||||
'w' => Some(&mut self.w),
|
||||
'x' => Some(&mut self.x),
|
||||
'y' => Some(&mut self.y),
|
||||
'z' => Some(&mut self.z),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
default: Register(String::new()),
|
||||
a: Register(String::new()),
|
||||
b: Register(String::new()),
|
||||
c: Register(String::new()),
|
||||
d: Register(String::new()),
|
||||
e: Register(String::new()),
|
||||
f: Register(String::new()),
|
||||
g: Register(String::new()),
|
||||
h: Register(String::new()),
|
||||
i: Register(String::new()),
|
||||
j: Register(String::new()),
|
||||
k: Register(String::new()),
|
||||
l: Register(String::new()),
|
||||
m: Register(String::new()),
|
||||
n: Register(String::new()),
|
||||
o: Register(String::new()),
|
||||
p: Register(String::new()),
|
||||
q: Register(String::new()),
|
||||
r: Register(String::new()),
|
||||
s: Register(String::new()),
|
||||
t: Register(String::new()),
|
||||
u: Register(String::new()),
|
||||
v: Register(String::new()),
|
||||
w: Register(String::new()),
|
||||
x: Register(String::new()),
|
||||
y: Register(String::new()),
|
||||
z: Register(String::new()),
|
||||
}
|
||||
}
|
||||
pub fn get_reg(&self, ch: Option<char>) -> Option<&Register> {
|
||||
let Some(ch) = ch else {
|
||||
return Some(&self.default);
|
||||
};
|
||||
match ch {
|
||||
'a' => Some(&self.a),
|
||||
'b' => Some(&self.b),
|
||||
'c' => Some(&self.c),
|
||||
'd' => Some(&self.d),
|
||||
'e' => Some(&self.e),
|
||||
'f' => Some(&self.f),
|
||||
'g' => Some(&self.g),
|
||||
'h' => Some(&self.h),
|
||||
'i' => Some(&self.i),
|
||||
'j' => Some(&self.j),
|
||||
'k' => Some(&self.k),
|
||||
'l' => Some(&self.l),
|
||||
'm' => Some(&self.m),
|
||||
'n' => Some(&self.n),
|
||||
'o' => Some(&self.o),
|
||||
'p' => Some(&self.p),
|
||||
'q' => Some(&self.q),
|
||||
'r' => Some(&self.r),
|
||||
's' => Some(&self.s),
|
||||
't' => Some(&self.t),
|
||||
'u' => Some(&self.u),
|
||||
'v' => Some(&self.v),
|
||||
'w' => Some(&self.w),
|
||||
'x' => Some(&self.x),
|
||||
'y' => Some(&self.y),
|
||||
'z' => Some(&self.z),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn get_reg_mut(&mut self, ch: Option<char>) -> Option<&mut Register> {
|
||||
let Some(ch) = ch else {
|
||||
return Some(&mut self.default);
|
||||
};
|
||||
match ch {
|
||||
'a' => Some(&mut self.a),
|
||||
'b' => Some(&mut self.b),
|
||||
'c' => Some(&mut self.c),
|
||||
'd' => Some(&mut self.d),
|
||||
'e' => Some(&mut self.e),
|
||||
'f' => Some(&mut self.f),
|
||||
'g' => Some(&mut self.g),
|
||||
'h' => Some(&mut self.h),
|
||||
'i' => Some(&mut self.i),
|
||||
'j' => Some(&mut self.j),
|
||||
'k' => Some(&mut self.k),
|
||||
'l' => Some(&mut self.l),
|
||||
'm' => Some(&mut self.m),
|
||||
'n' => Some(&mut self.n),
|
||||
'o' => Some(&mut self.o),
|
||||
'p' => Some(&mut self.p),
|
||||
'q' => Some(&mut self.q),
|
||||
'r' => Some(&mut self.r),
|
||||
's' => Some(&mut self.s),
|
||||
't' => Some(&mut self.t),
|
||||
'u' => Some(&mut self.u),
|
||||
'v' => Some(&mut self.v),
|
||||
'w' => Some(&mut self.w),
|
||||
'x' => Some(&mut self.x),
|
||||
'y' => Some(&mut self.y),
|
||||
'z' => Some(&mut self.z),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Default,Debug)]
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct Register(String);
|
||||
impl Register {
|
||||
pub fn buf(&self) -> &String {
|
||||
&self.0
|
||||
}
|
||||
pub fn write(&mut self, buf: String) {
|
||||
self.0 = buf
|
||||
}
|
||||
pub fn append(&mut self, buf: String) {
|
||||
self.0.push_str(&buf)
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.0.clear()
|
||||
}
|
||||
pub fn buf(&self) -> &String {
|
||||
&self.0
|
||||
}
|
||||
pub fn write(&mut self, buf: String) {
|
||||
self.0 = buf
|
||||
}
|
||||
pub fn append(&mut self, buf: String) {
|
||||
self.0.push_str(&buf)
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.0.clear()
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,438 +2,458 @@ use bitflags::bitflags;
|
||||
|
||||
use super::register::{append_register, read_register, write_register};
|
||||
|
||||
//TODO: write tests that take edit results and cursor positions from actual neovim edits and test them against the behavior of this editor
|
||||
//TODO: write tests that take edit results and cursor positions from actual
|
||||
// neovim edits and test them against the behavior of this editor
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct RegisterName {
|
||||
name: Option<char>,
|
||||
count: usize,
|
||||
append: bool
|
||||
name: Option<char>,
|
||||
count: usize,
|
||||
append: bool,
|
||||
}
|
||||
|
||||
impl RegisterName {
|
||||
pub fn new(name: Option<char>, count: Option<usize>) -> Self {
|
||||
let Some(ch) = name else {
|
||||
return Self::default()
|
||||
};
|
||||
pub fn new(name: Option<char>, count: Option<usize>) -> Self {
|
||||
let Some(ch) = name else {
|
||||
return Self::default();
|
||||
};
|
||||
|
||||
let append = ch.is_uppercase();
|
||||
let name = ch.to_ascii_lowercase();
|
||||
Self {
|
||||
name: Some(name),
|
||||
count: count.unwrap_or(1),
|
||||
append
|
||||
}
|
||||
}
|
||||
pub fn name(&self) -> Option<char> {
|
||||
self.name
|
||||
}
|
||||
pub fn is_append(&self) -> bool {
|
||||
self.append
|
||||
}
|
||||
pub fn count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
pub fn write_to_register(&self, buf: String) {
|
||||
if self.append {
|
||||
append_register(self.name, buf);
|
||||
} else {
|
||||
write_register(self.name, buf);
|
||||
}
|
||||
}
|
||||
pub fn read_from_register(&self) -> Option<String> {
|
||||
read_register(self.name)
|
||||
}
|
||||
let append = ch.is_uppercase();
|
||||
let name = ch.to_ascii_lowercase();
|
||||
Self {
|
||||
name: Some(name),
|
||||
count: count.unwrap_or(1),
|
||||
append,
|
||||
}
|
||||
}
|
||||
pub fn name(&self) -> Option<char> {
|
||||
self.name
|
||||
}
|
||||
pub fn is_append(&self) -> bool {
|
||||
self.append
|
||||
}
|
||||
pub fn count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
pub fn write_to_register(&self, buf: String) {
|
||||
if self.append {
|
||||
append_register(self.name, buf);
|
||||
} else {
|
||||
write_register(self.name, buf);
|
||||
}
|
||||
}
|
||||
pub fn read_from_register(&self) -> Option<String> {
|
||||
read_register(self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RegisterName {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: None,
|
||||
count: 1,
|
||||
append: false
|
||||
}
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: None,
|
||||
count: 1,
|
||||
append: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct CmdFlags: u32 {
|
||||
const VISUAL = 1<<0;
|
||||
const VISUAL_LINE = 1<<1;
|
||||
const VISUAL_BLOCK = 1<<2;
|
||||
}
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct CmdFlags: u32 {
|
||||
const VISUAL = 1<<0;
|
||||
const VISUAL_LINE = 1<<1;
|
||||
const VISUAL_BLOCK = 1<<2;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Default,Debug)]
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct ViCmd {
|
||||
pub register: RegisterName,
|
||||
pub verb: Option<VerbCmd>,
|
||||
pub motion: Option<MotionCmd>,
|
||||
pub raw_seq: String,
|
||||
pub flags: CmdFlags,
|
||||
pub register: RegisterName,
|
||||
pub verb: Option<VerbCmd>,
|
||||
pub motion: Option<MotionCmd>,
|
||||
pub raw_seq: String,
|
||||
pub flags: CmdFlags,
|
||||
}
|
||||
|
||||
impl ViCmd {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn set_motion(&mut self, motion: MotionCmd) {
|
||||
self.motion = Some(motion)
|
||||
}
|
||||
pub fn set_verb(&mut self, verb: VerbCmd) {
|
||||
self.verb = Some(verb)
|
||||
}
|
||||
pub fn verb(&self) -> Option<&VerbCmd> {
|
||||
self.verb.as_ref()
|
||||
}
|
||||
pub fn motion(&self) -> Option<&MotionCmd> {
|
||||
self.motion.as_ref()
|
||||
}
|
||||
pub fn verb_count(&self) -> usize {
|
||||
self.verb.as_ref().map(|v| v.0).unwrap_or(1)
|
||||
}
|
||||
pub fn motion_count(&self) -> usize {
|
||||
self.motion.as_ref().map(|m| m.0).unwrap_or(1)
|
||||
}
|
||||
pub fn normalize_counts(&mut self) {
|
||||
let Some(verb) = self.verb.as_mut() else { return };
|
||||
let Some(motion) = self.motion.as_mut() else { return };
|
||||
let VerbCmd(v_count, _) = verb;
|
||||
let MotionCmd(m_count, _) = motion;
|
||||
let product = *v_count * *m_count;
|
||||
verb.0 = 1;
|
||||
motion.0 = product;
|
||||
}
|
||||
pub fn is_repeatable(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| v.1.is_repeatable())
|
||||
}
|
||||
pub fn is_cmd_repeat(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| matches!(v.1,Verb::RepeatLast))
|
||||
}
|
||||
pub fn is_motion_repeat(&self) -> bool {
|
||||
self.motion.as_ref().is_some_and(|m| matches!(m.1,Motion::RepeatMotion | Motion::RepeatMotionRev))
|
||||
}
|
||||
pub fn is_char_search(&self) -> bool {
|
||||
self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::CharSearch(..)))
|
||||
}
|
||||
pub fn should_submit(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::AcceptLineOrNewline))
|
||||
}
|
||||
pub fn is_undo_op(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo))
|
||||
}
|
||||
pub fn is_inplace_edit(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::ReplaceCharInplace(_,_) | Verb::ToggleCaseInplace(_))) &&
|
||||
self.motion.is_none()
|
||||
}
|
||||
pub fn is_line_motion(&self) -> bool {
|
||||
self.motion.as_ref().is_some_and(|m| {
|
||||
matches!(m.1,
|
||||
Motion::LineUp |
|
||||
Motion::LineDown |
|
||||
Motion::LineUpCharwise |
|
||||
Motion::LineDownCharwise
|
||||
)
|
||||
})
|
||||
}
|
||||
/// If a ViCmd has a linewise motion, but no verb, we change it to charwise
|
||||
pub fn alter_line_motion_if_no_verb(&mut self) {
|
||||
if self.is_line_motion() && self.verb.is_none() {
|
||||
if let Some(motion) = self.motion.as_mut() {
|
||||
match motion.1 {
|
||||
Motion::LineUp => motion.1 = Motion::LineUpCharwise,
|
||||
Motion::LineDown => motion.1 = Motion::LineDownCharwise,
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn is_mode_transition(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| {
|
||||
matches!(v.1,
|
||||
Verb::Change |
|
||||
Verb::InsertMode |
|
||||
Verb::InsertModeLineBreak(_) |
|
||||
Verb::NormalMode |
|
||||
Verb::VisualModeSelectLast |
|
||||
Verb::VisualMode |
|
||||
Verb::ReplaceMode
|
||||
)
|
||||
})
|
||||
}
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn set_motion(&mut self, motion: MotionCmd) {
|
||||
self.motion = Some(motion)
|
||||
}
|
||||
pub fn set_verb(&mut self, verb: VerbCmd) {
|
||||
self.verb = Some(verb)
|
||||
}
|
||||
pub fn verb(&self) -> Option<&VerbCmd> {
|
||||
self.verb.as_ref()
|
||||
}
|
||||
pub fn motion(&self) -> Option<&MotionCmd> {
|
||||
self.motion.as_ref()
|
||||
}
|
||||
pub fn verb_count(&self) -> usize {
|
||||
self.verb.as_ref().map(|v| v.0).unwrap_or(1)
|
||||
}
|
||||
pub fn motion_count(&self) -> usize {
|
||||
self.motion.as_ref().map(|m| m.0).unwrap_or(1)
|
||||
}
|
||||
pub fn normalize_counts(&mut self) {
|
||||
let Some(verb) = self.verb.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let Some(motion) = self.motion.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let VerbCmd(v_count, _) = verb;
|
||||
let MotionCmd(m_count, _) = motion;
|
||||
let product = *v_count * *m_count;
|
||||
verb.0 = 1;
|
||||
motion.0 = product;
|
||||
}
|
||||
pub fn is_repeatable(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| v.1.is_repeatable())
|
||||
}
|
||||
pub fn is_cmd_repeat(&self) -> bool {
|
||||
self
|
||||
.verb
|
||||
.as_ref()
|
||||
.is_some_and(|v| matches!(v.1, Verb::RepeatLast))
|
||||
}
|
||||
pub fn is_motion_repeat(&self) -> bool {
|
||||
self
|
||||
.motion
|
||||
.as_ref()
|
||||
.is_some_and(|m| matches!(m.1, Motion::RepeatMotion | Motion::RepeatMotionRev))
|
||||
}
|
||||
pub fn is_char_search(&self) -> bool {
|
||||
self
|
||||
.motion
|
||||
.as_ref()
|
||||
.is_some_and(|m| matches!(m.1, Motion::CharSearch(..)))
|
||||
}
|
||||
pub fn should_submit(&self) -> bool {
|
||||
self
|
||||
.verb
|
||||
.as_ref()
|
||||
.is_some_and(|v| matches!(v.1, Verb::AcceptLineOrNewline))
|
||||
}
|
||||
pub fn is_undo_op(&self) -> bool {
|
||||
self
|
||||
.verb
|
||||
.as_ref()
|
||||
.is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo))
|
||||
}
|
||||
pub fn is_inplace_edit(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| {
|
||||
matches!(
|
||||
v.1,
|
||||
Verb::ReplaceCharInplace(_, _) | Verb::ToggleCaseInplace(_)
|
||||
)
|
||||
}) && self.motion.is_none()
|
||||
}
|
||||
pub fn is_line_motion(&self) -> bool {
|
||||
self.motion.as_ref().is_some_and(|m| {
|
||||
matches!(
|
||||
m.1,
|
||||
Motion::LineUp | Motion::LineDown | Motion::LineUpCharwise | Motion::LineDownCharwise
|
||||
)
|
||||
})
|
||||
}
|
||||
/// If a ViCmd has a linewise motion, but no verb, we change it to charwise
|
||||
pub fn alter_line_motion_if_no_verb(&mut self) {
|
||||
if self.is_line_motion() && self.verb.is_none() {
|
||||
if let Some(motion) = self.motion.as_mut() {
|
||||
match motion.1 {
|
||||
Motion::LineUp => motion.1 = Motion::LineUpCharwise,
|
||||
Motion::LineDown => motion.1 = Motion::LineDownCharwise,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn is_mode_transition(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| {
|
||||
matches!(
|
||||
v.1,
|
||||
Verb::Change
|
||||
| Verb::InsertMode
|
||||
| Verb::InsertModeLineBreak(_)
|
||||
| Verb::NormalMode
|
||||
| Verb::VisualModeSelectLast
|
||||
| Verb::VisualMode
|
||||
| Verb::ReplaceMode
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct VerbCmd(pub usize,pub Verb);
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct MotionCmd(pub usize,pub Motion);
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VerbCmd(pub usize, pub Verb);
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MotionCmd(pub usize, pub Motion);
|
||||
|
||||
impl MotionCmd {
|
||||
pub fn invert_char_motion(self) -> Self {
|
||||
let MotionCmd(count,Motion::CharSearch(dir, dest, ch)) = self else {
|
||||
unreachable!()
|
||||
};
|
||||
let new_dir = match dir {
|
||||
Direction::Forward => Direction::Backward,
|
||||
Direction::Backward => Direction::Forward,
|
||||
};
|
||||
MotionCmd(count,Motion::CharSearch(new_dir, dest, ch))
|
||||
}
|
||||
pub fn invert_char_motion(self) -> Self {
|
||||
let MotionCmd(count, Motion::CharSearch(dir, dest, ch)) = self else {
|
||||
unreachable!()
|
||||
};
|
||||
let new_dir = match dir {
|
||||
Direction::Forward => Direction::Backward,
|
||||
Direction::Backward => Direction::Forward,
|
||||
};
|
||||
MotionCmd(count, Motion::CharSearch(new_dir, dest, ch))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Verb {
|
||||
Delete,
|
||||
Change,
|
||||
Yank,
|
||||
Rot13, // lol
|
||||
ReplaceChar(char), // char to replace with, number of chars to replace
|
||||
ReplaceCharInplace(char,u16), // char to replace with, number of chars to replace
|
||||
ToggleCaseInplace(u16), // Number of chars to toggle
|
||||
ToggleCaseRange,
|
||||
ToLower,
|
||||
ToUpper,
|
||||
Complete,
|
||||
CompleteBackward,
|
||||
Undo,
|
||||
Redo,
|
||||
RepeatLast,
|
||||
Put(Anchor),
|
||||
ReplaceMode,
|
||||
InsertMode,
|
||||
InsertModeLineBreak(Anchor),
|
||||
NormalMode,
|
||||
VisualMode,
|
||||
VisualModeLine,
|
||||
VisualModeBlock, // dont even know if im going to implement this
|
||||
VisualModeSelectLast,
|
||||
SwapVisualAnchor,
|
||||
JoinLines,
|
||||
InsertChar(char),
|
||||
Insert(String),
|
||||
Indent,
|
||||
Dedent,
|
||||
Equalize,
|
||||
AcceptLineOrNewline,
|
||||
EndOfFile
|
||||
Delete,
|
||||
Change,
|
||||
Yank,
|
||||
Rot13, // lol
|
||||
ReplaceChar(char), // char to replace with, number of chars to replace
|
||||
ReplaceCharInplace(char, u16), // char to replace with, number of chars to replace
|
||||
ToggleCaseInplace(u16), // Number of chars to toggle
|
||||
ToggleCaseRange,
|
||||
ToLower,
|
||||
ToUpper,
|
||||
Complete,
|
||||
CompleteBackward,
|
||||
Undo,
|
||||
Redo,
|
||||
RepeatLast,
|
||||
Put(Anchor),
|
||||
ReplaceMode,
|
||||
InsertMode,
|
||||
InsertModeLineBreak(Anchor),
|
||||
NormalMode,
|
||||
VisualMode,
|
||||
VisualModeLine,
|
||||
VisualModeBlock, // dont even know if im going to implement this
|
||||
VisualModeSelectLast,
|
||||
SwapVisualAnchor,
|
||||
JoinLines,
|
||||
InsertChar(char),
|
||||
Insert(String),
|
||||
Indent,
|
||||
Dedent,
|
||||
Equalize,
|
||||
AcceptLineOrNewline,
|
||||
EndOfFile,
|
||||
}
|
||||
|
||||
|
||||
impl Verb {
|
||||
pub fn is_repeatable(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::Delete |
|
||||
Self::Change |
|
||||
Self::ReplaceChar(_) |
|
||||
Self::ReplaceCharInplace(_,_) |
|
||||
Self::ToLower |
|
||||
Self::ToUpper |
|
||||
Self::ToggleCaseRange |
|
||||
Self::ToggleCaseInplace(_) |
|
||||
Self::Put(_) |
|
||||
Self::ReplaceMode |
|
||||
Self::InsertModeLineBreak(_) |
|
||||
Self::JoinLines |
|
||||
Self::InsertChar(_) |
|
||||
Self::Insert(_) |
|
||||
Self::Indent |
|
||||
Self::Dedent |
|
||||
Self::Equalize
|
||||
)
|
||||
}
|
||||
pub fn is_edit(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::Delete |
|
||||
Self::Change |
|
||||
Self::ReplaceChar(_) |
|
||||
Self::ReplaceCharInplace(_,_) |
|
||||
Self::ToggleCaseRange |
|
||||
Self::ToggleCaseInplace(_) |
|
||||
Self::ToLower |
|
||||
Self::ToUpper |
|
||||
Self::RepeatLast |
|
||||
Self::Put(_) |
|
||||
Self::ReplaceMode |
|
||||
Self::InsertModeLineBreak(_) |
|
||||
Self::JoinLines |
|
||||
Self::InsertChar(_) |
|
||||
Self::Insert(_) |
|
||||
Self::Rot13 |
|
||||
Self::EndOfFile
|
||||
)
|
||||
}
|
||||
pub fn is_char_insert(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::Change |
|
||||
Self::InsertChar(_) |
|
||||
Self::ReplaceChar(_) |
|
||||
Self::ReplaceCharInplace(_,_)
|
||||
)
|
||||
}
|
||||
pub fn is_repeatable(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Delete
|
||||
| Self::Change
|
||||
| Self::ReplaceChar(_)
|
||||
| Self::ReplaceCharInplace(_, _)
|
||||
| Self::ToLower
|
||||
| Self::ToUpper
|
||||
| Self::ToggleCaseRange
|
||||
| Self::ToggleCaseInplace(_)
|
||||
| Self::Put(_)
|
||||
| Self::ReplaceMode
|
||||
| Self::InsertModeLineBreak(_)
|
||||
| Self::JoinLines
|
||||
| Self::InsertChar(_)
|
||||
| Self::Insert(_)
|
||||
| Self::Indent
|
||||
| Self::Dedent
|
||||
| Self::Equalize
|
||||
)
|
||||
}
|
||||
pub fn is_edit(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Delete
|
||||
| Self::Change
|
||||
| Self::ReplaceChar(_)
|
||||
| Self::ReplaceCharInplace(_, _)
|
||||
| Self::ToggleCaseRange
|
||||
| Self::ToggleCaseInplace(_)
|
||||
| Self::ToLower
|
||||
| Self::ToUpper
|
||||
| Self::RepeatLast
|
||||
| Self::Put(_)
|
||||
| Self::ReplaceMode
|
||||
| Self::InsertModeLineBreak(_)
|
||||
| Self::JoinLines
|
||||
| Self::InsertChar(_)
|
||||
| Self::Insert(_)
|
||||
| Self::Rot13
|
||||
| Self::EndOfFile
|
||||
)
|
||||
}
|
||||
pub fn is_char_insert(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Change | Self::InsertChar(_) | Self::ReplaceChar(_) | Self::ReplaceCharInplace(_, _)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum Motion {
|
||||
WholeLine,
|
||||
TextObj(TextObj),
|
||||
EndOfLastWord,
|
||||
BeginningOfFirstWord,
|
||||
BeginningOfLine,
|
||||
EndOfLine,
|
||||
WordMotion(To,Word,Direction),
|
||||
CharSearch(Direction,Dest,char),
|
||||
BackwardChar,
|
||||
ForwardChar,
|
||||
BackwardCharForced, // These two variants can cross line boundaries
|
||||
ForwardCharForced,
|
||||
LineUp,
|
||||
LineUpCharwise,
|
||||
ScreenLineUp,
|
||||
ScreenLineUpCharwise,
|
||||
LineDown,
|
||||
LineDownCharwise,
|
||||
ScreenLineDown,
|
||||
ScreenLineDownCharwise,
|
||||
BeginningOfScreenLine,
|
||||
FirstGraphicalOnScreenLine,
|
||||
HalfOfScreen,
|
||||
HalfOfScreenLineText,
|
||||
WholeBuffer,
|
||||
BeginningOfBuffer,
|
||||
EndOfBuffer,
|
||||
ToColumn,
|
||||
ToDelimMatch,
|
||||
ToBrace(Direction),
|
||||
ToBracket(Direction),
|
||||
ToParen(Direction),
|
||||
Range(usize,usize),
|
||||
RepeatMotion,
|
||||
RepeatMotionRev,
|
||||
Null
|
||||
WholeLine,
|
||||
TextObj(TextObj),
|
||||
EndOfLastWord,
|
||||
BeginningOfFirstWord,
|
||||
BeginningOfLine,
|
||||
EndOfLine,
|
||||
WordMotion(To, Word, Direction),
|
||||
CharSearch(Direction, Dest, char),
|
||||
BackwardChar,
|
||||
ForwardChar,
|
||||
BackwardCharForced, // These two variants can cross line boundaries
|
||||
ForwardCharForced,
|
||||
LineUp,
|
||||
LineUpCharwise,
|
||||
ScreenLineUp,
|
||||
ScreenLineUpCharwise,
|
||||
LineDown,
|
||||
LineDownCharwise,
|
||||
ScreenLineDown,
|
||||
ScreenLineDownCharwise,
|
||||
BeginningOfScreenLine,
|
||||
FirstGraphicalOnScreenLine,
|
||||
HalfOfScreen,
|
||||
HalfOfScreenLineText,
|
||||
WholeBuffer,
|
||||
BeginningOfBuffer,
|
||||
EndOfBuffer,
|
||||
ToColumn,
|
||||
ToDelimMatch,
|
||||
ToBrace(Direction),
|
||||
ToBracket(Direction),
|
||||
ToParen(Direction),
|
||||
Range(usize, usize),
|
||||
RepeatMotion,
|
||||
RepeatMotionRev,
|
||||
Null,
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,PartialEq,Eq,Debug)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum MotionBehavior {
|
||||
Exclusive,
|
||||
Inclusive,
|
||||
Linewise
|
||||
Exclusive,
|
||||
Inclusive,
|
||||
Linewise,
|
||||
}
|
||||
|
||||
impl Motion {
|
||||
pub fn behavior(&self) -> MotionBehavior {
|
||||
if self.is_linewise() {
|
||||
MotionBehavior::Linewise
|
||||
} else if self.is_exclusive() {
|
||||
MotionBehavior::Exclusive
|
||||
} else {
|
||||
MotionBehavior::Inclusive
|
||||
}
|
||||
}
|
||||
pub fn is_exclusive(&self) -> bool {
|
||||
matches!(&self,
|
||||
Self::BeginningOfLine |
|
||||
Self::BeginningOfFirstWord |
|
||||
Self::BeginningOfScreenLine |
|
||||
Self::FirstGraphicalOnScreenLine |
|
||||
Self::LineDownCharwise |
|
||||
Self::LineUpCharwise |
|
||||
Self::ScreenLineUpCharwise |
|
||||
Self::ScreenLineDownCharwise |
|
||||
Self::ToColumn |
|
||||
Self::TextObj(TextObj::Sentence(_)) |
|
||||
Self::TextObj(TextObj::Paragraph(_)) |
|
||||
Self::CharSearch(Direction::Backward, _, _) |
|
||||
Self::WordMotion(To::Start,_,_) |
|
||||
Self::ToBrace(_) |
|
||||
Self::ToBracket(_) |
|
||||
Self::ToParen(_) |
|
||||
Self::ScreenLineDown |
|
||||
Self::ScreenLineUp |
|
||||
Self::Range(_, _)
|
||||
)
|
||||
}
|
||||
pub fn is_linewise(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::WholeLine |
|
||||
Self::LineUp |
|
||||
Self::LineDown |
|
||||
Self::ScreenLineDown |
|
||||
Self::ScreenLineUp
|
||||
)
|
||||
}
|
||||
pub fn behavior(&self) -> MotionBehavior {
|
||||
if self.is_linewise() {
|
||||
MotionBehavior::Linewise
|
||||
} else if self.is_exclusive() {
|
||||
MotionBehavior::Exclusive
|
||||
} else {
|
||||
MotionBehavior::Inclusive
|
||||
}
|
||||
}
|
||||
pub fn is_exclusive(&self) -> bool {
|
||||
matches!(
|
||||
&self,
|
||||
Self::BeginningOfLine
|
||||
| Self::BeginningOfFirstWord
|
||||
| Self::BeginningOfScreenLine
|
||||
| Self::FirstGraphicalOnScreenLine
|
||||
| Self::LineDownCharwise
|
||||
| Self::LineUpCharwise
|
||||
| Self::ScreenLineUpCharwise
|
||||
| Self::ScreenLineDownCharwise
|
||||
| Self::ToColumn
|
||||
| Self::TextObj(TextObj::Sentence(_))
|
||||
| Self::TextObj(TextObj::Paragraph(_))
|
||||
| Self::CharSearch(Direction::Backward, _, _)
|
||||
| Self::WordMotion(To::Start, _, _)
|
||||
| Self::ToBrace(_)
|
||||
| Self::ToBracket(_)
|
||||
| Self::ToParen(_)
|
||||
| Self::ScreenLineDown
|
||||
| Self::ScreenLineUp
|
||||
| Self::Range(_, _)
|
||||
)
|
||||
}
|
||||
pub fn is_linewise(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::WholeLine | Self::LineUp | Self::LineDown | Self::ScreenLineDown | Self::ScreenLineUp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum Anchor {
|
||||
After,
|
||||
Before
|
||||
After,
|
||||
Before,
|
||||
}
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum TextObj {
|
||||
/// `iw`, `aw` — inner word, around word
|
||||
Word(Word, Bound),
|
||||
/// `iw`, `aw` — inner word, around word
|
||||
Word(Word, Bound),
|
||||
|
||||
/// `)`, `(` — forward, backward
|
||||
Sentence(Direction),
|
||||
/// `)`, `(` — forward, backward
|
||||
Sentence(Direction),
|
||||
|
||||
/// `}`, `{` — forward, backward
|
||||
Paragraph(Direction),
|
||||
/// `}`, `{` — forward, backward
|
||||
Paragraph(Direction),
|
||||
|
||||
WholeSentence(Bound),
|
||||
WholeParagraph(Bound),
|
||||
WholeSentence(Bound),
|
||||
WholeParagraph(Bound),
|
||||
|
||||
/// `i"`, `a"` — inner/around double quotes
|
||||
DoubleQuote(Bound),
|
||||
/// `i'`, `a'`
|
||||
SingleQuote(Bound),
|
||||
/// `i\``, `a\``
|
||||
BacktickQuote(Bound),
|
||||
/// `i"`, `a"` — inner/around double quotes
|
||||
DoubleQuote(Bound),
|
||||
/// `i'`, `a'`
|
||||
SingleQuote(Bound),
|
||||
/// `i\``, `a\``
|
||||
BacktickQuote(Bound),
|
||||
|
||||
/// `i)`, `a)` — round parens
|
||||
Paren(Bound),
|
||||
/// `i]`, `a]`
|
||||
Bracket(Bound),
|
||||
/// `i}`, `a}`
|
||||
Brace(Bound),
|
||||
/// `i<`, `a<`
|
||||
Angle(Bound),
|
||||
/// `i)`, `a)` — round parens
|
||||
Paren(Bound),
|
||||
/// `i]`, `a]`
|
||||
Bracket(Bound),
|
||||
/// `i}`, `a}`
|
||||
Brace(Bound),
|
||||
/// `i<`, `a<`
|
||||
Angle(Bound),
|
||||
|
||||
/// `it`, `at` — HTML/XML tags
|
||||
Tag(Bound),
|
||||
/// `it`, `at` — HTML/XML tags
|
||||
Tag(Bound),
|
||||
|
||||
/// Custom user-defined objects maybe?
|
||||
Custom(char),
|
||||
/// Custom user-defined objects maybe?
|
||||
Custom(char),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum Word {
|
||||
Big,
|
||||
Normal
|
||||
Big,
|
||||
Normal,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum Bound {
|
||||
Inside,
|
||||
Around
|
||||
Inside,
|
||||
Around,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum Direction {
|
||||
#[default]
|
||||
Forward,
|
||||
Backward
|
||||
#[default]
|
||||
Forward,
|
||||
Backward,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum Dest {
|
||||
On,
|
||||
Before,
|
||||
After
|
||||
On,
|
||||
Before,
|
||||
After,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum To {
|
||||
Start,
|
||||
End
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1027
src/shopt.rs
1027
src/shopt.rs
File diff suppressed because it is too large
Load Diff
255
src/signal.rs
255
src/signal.rs
@@ -1,161 +1,168 @@
|
||||
use crate::{jobs::{take_term, JobCmdFlags, JobID}, libsh::{error::ShResult, sys::sh_quit}, prelude::*, state::{read_jobs, write_jobs}};
|
||||
use crate::{
|
||||
jobs::{take_term, JobCmdFlags, JobID},
|
||||
libsh::{error::ShResult, sys::sh_quit},
|
||||
prelude::*,
|
||||
state::{read_jobs, write_jobs},
|
||||
};
|
||||
|
||||
pub fn sig_setup() {
|
||||
unsafe {
|
||||
signal(Signal::SIGCHLD, SigHandler::Handler(handle_sigchld)).unwrap();
|
||||
signal(Signal::SIGQUIT, SigHandler::Handler(handle_sigquit)).unwrap();
|
||||
signal(Signal::SIGTSTP, SigHandler::Handler(handle_sigtstp)).unwrap();
|
||||
signal(Signal::SIGHUP, SigHandler::Handler(handle_sighup)).unwrap();
|
||||
signal(Signal::SIGINT, SigHandler::Handler(handle_sigint)).unwrap();
|
||||
signal(Signal::SIGTTIN, SigHandler::SigIgn).unwrap();
|
||||
signal(Signal::SIGTTOU, SigHandler::SigIgn).unwrap();
|
||||
}
|
||||
unsafe {
|
||||
signal(Signal::SIGCHLD, SigHandler::Handler(handle_sigchld)).unwrap();
|
||||
signal(Signal::SIGQUIT, SigHandler::Handler(handle_sigquit)).unwrap();
|
||||
signal(Signal::SIGTSTP, SigHandler::Handler(handle_sigtstp)).unwrap();
|
||||
signal(Signal::SIGHUP, SigHandler::Handler(handle_sighup)).unwrap();
|
||||
signal(Signal::SIGINT, SigHandler::Handler(handle_sigint)).unwrap();
|
||||
signal(Signal::SIGTTIN, SigHandler::SigIgn).unwrap();
|
||||
signal(Signal::SIGTTOU, SigHandler::SigIgn).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extern "C" fn handle_sighup(_: libc::c_int) {
|
||||
write_jobs(|j| {
|
||||
for job in j.jobs_mut().iter_mut().flatten() {
|
||||
job.killpg(Signal::SIGTERM).ok();
|
||||
}
|
||||
});
|
||||
std::process::exit(0);
|
||||
write_jobs(|j| {
|
||||
for job in j.jobs_mut().iter_mut().flatten() {
|
||||
job.killpg(Signal::SIGTERM).ok();
|
||||
}
|
||||
});
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
extern "C" fn handle_sigtstp(_: libc::c_int) {
|
||||
write_jobs(|j| {
|
||||
if let Some(job) = j.get_fg_mut() {
|
||||
job.killpg(Signal::SIGTSTP).ok();
|
||||
}
|
||||
});
|
||||
write_jobs(|j| {
|
||||
if let Some(job) = j.get_fg_mut() {
|
||||
job.killpg(Signal::SIGTSTP).ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
extern "C" fn handle_sigint(_: libc::c_int) {
|
||||
write_jobs(|j| {
|
||||
if let Some(job) = j.get_fg_mut() {
|
||||
job.killpg(Signal::SIGINT).ok();
|
||||
}
|
||||
});
|
||||
write_jobs(|j| {
|
||||
if let Some(job) = j.get_fg_mut() {
|
||||
job.killpg(Signal::SIGINT).ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub extern "C" fn ignore_sigchld(_: libc::c_int) {
|
||||
/*
|
||||
Do nothing
|
||||
/*
|
||||
Do nothing
|
||||
|
||||
This function exists because using SIGIGN to ignore SIGCHLD
|
||||
will cause the kernel to automatically reap the child process, which is not what we want.
|
||||
This handler will leave the signaling process as a zombie, allowing us
|
||||
to handle it somewhere else.
|
||||
This function exists because using SIGIGN to ignore SIGCHLD
|
||||
will cause the kernel to automatically reap the child process, which is not what we want.
|
||||
This handler will leave the signaling process as a zombie, allowing us
|
||||
to handle it somewhere else.
|
||||
|
||||
This handler is used when we want to handle SIGCHLD explicitly,
|
||||
like in the case of handling foreground jobs
|
||||
*/
|
||||
This handler is used when we want to handle SIGCHLD explicitly,
|
||||
like in the case of handling foreground jobs
|
||||
*/
|
||||
}
|
||||
|
||||
extern "C" fn handle_sigquit(_: libc::c_int) {
|
||||
sh_quit(0)
|
||||
sh_quit(0)
|
||||
}
|
||||
|
||||
pub extern "C" fn handle_sigchld(_: libc::c_int) {
|
||||
let flags = WtFlag::WNOHANG | WtFlag::WSTOPPED;
|
||||
while let Ok(status) = waitpid(None, Some(flags)) {
|
||||
if let Err(e) = match status {
|
||||
WtStat::Exited(pid, _) => child_exited(pid, status),
|
||||
WtStat::Signaled(pid, signal, _) => child_signaled(pid, signal),
|
||||
WtStat::Stopped(pid, signal) => child_stopped(pid, signal),
|
||||
WtStat::Continued(pid) => child_continued(pid),
|
||||
WtStat::StillAlive => break,
|
||||
_ => unimplemented!()
|
||||
} {
|
||||
eprintln!("{}",e)
|
||||
}
|
||||
}
|
||||
let flags = WtFlag::WNOHANG | WtFlag::WSTOPPED;
|
||||
while let Ok(status) = waitpid(None, Some(flags)) {
|
||||
if let Err(e) = match status {
|
||||
WtStat::Exited(pid, _) => child_exited(pid, status),
|
||||
WtStat::Signaled(pid, signal, _) => child_signaled(pid, signal),
|
||||
WtStat::Stopped(pid, signal) => child_stopped(pid, signal),
|
||||
WtStat::Continued(pid) => child_continued(pid),
|
||||
WtStat::StillAlive => break,
|
||||
_ => unimplemented!(),
|
||||
} {
|
||||
eprintln!("{}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn child_signaled(pid: Pid, sig: Signal) -> ShResult<()> {
|
||||
let pgid = getpgid(Some(pid)).unwrap_or(pid);
|
||||
write_jobs(|j| {
|
||||
if let Some(job) = j.query_mut(JobID::Pgid(pgid)) {
|
||||
let child = job.children_mut().iter_mut().find(|chld| pid == chld.pid()).unwrap();
|
||||
let stat = WtStat::Signaled(pid, sig, false);
|
||||
child.set_stat(stat);
|
||||
}
|
||||
});
|
||||
if sig == Signal::SIGINT {
|
||||
take_term().unwrap()
|
||||
}
|
||||
Ok(())
|
||||
let pgid = getpgid(Some(pid)).unwrap_or(pid);
|
||||
write_jobs(|j| {
|
||||
if let Some(job) = j.query_mut(JobID::Pgid(pgid)) {
|
||||
let child = job
|
||||
.children_mut()
|
||||
.iter_mut()
|
||||
.find(|chld| pid == chld.pid())
|
||||
.unwrap();
|
||||
let stat = WtStat::Signaled(pid, sig, false);
|
||||
child.set_stat(stat);
|
||||
}
|
||||
});
|
||||
if sig == Signal::SIGINT {
|
||||
take_term().unwrap()
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn child_stopped(pid: Pid, sig: Signal) -> ShResult<()> {
|
||||
let pgid = getpgid(Some(pid)).unwrap_or(pid);
|
||||
write_jobs(|j| {
|
||||
if let Some(job) = j.query_mut(JobID::Pgid(pgid)) {
|
||||
let child = job.children_mut().iter_mut().find(|chld| pid == chld.pid()).unwrap();
|
||||
let status = WtStat::Stopped(pid, sig);
|
||||
child.set_stat(status);
|
||||
} else if j.get_fg_mut().is_some_and(|fg| fg.pgid() == pgid) {
|
||||
j.fg_to_bg(WtStat::Stopped(pid, sig)).unwrap();
|
||||
}
|
||||
});
|
||||
take_term()?;
|
||||
Ok(())
|
||||
let pgid = getpgid(Some(pid)).unwrap_or(pid);
|
||||
write_jobs(|j| {
|
||||
if let Some(job) = j.query_mut(JobID::Pgid(pgid)) {
|
||||
let child = job
|
||||
.children_mut()
|
||||
.iter_mut()
|
||||
.find(|chld| pid == chld.pid())
|
||||
.unwrap();
|
||||
let status = WtStat::Stopped(pid, sig);
|
||||
child.set_stat(status);
|
||||
} else if j.get_fg_mut().is_some_and(|fg| fg.pgid() == pgid) {
|
||||
j.fg_to_bg(WtStat::Stopped(pid, sig)).unwrap();
|
||||
}
|
||||
});
|
||||
take_term()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn child_continued(pid: Pid) -> ShResult<()> {
|
||||
let pgid = getpgid(Some(pid)).unwrap_or(pid);
|
||||
write_jobs(|j| {
|
||||
if let Some(job) = j.query_mut(JobID::Pgid(pgid)) {
|
||||
job.killpg(Signal::SIGCONT).ok();
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
let pgid = getpgid(Some(pid)).unwrap_or(pid);
|
||||
write_jobs(|j| {
|
||||
if let Some(job) = j.query_mut(JobID::Pgid(pgid)) {
|
||||
job.killpg(Signal::SIGCONT).ok();
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> {
|
||||
/*
|
||||
* Here we are going to get metadata on the exited process by querying the job table with the pid.
|
||||
* Then if the discovered job is the fg task, return terminal control to rsh
|
||||
* If it is not the fg task, print the display info for the job in the job table
|
||||
* We can reasonably assume that if it is not a foreground job, then it exists in the job table
|
||||
* If this assumption is incorrect, the code has gone wrong somewhere.
|
||||
*/
|
||||
write_jobs(|j| j.close_job_fds(pid));
|
||||
if let Some((
|
||||
pgid,
|
||||
is_fg,
|
||||
is_finished
|
||||
)) = write_jobs(|j| {
|
||||
let fg_pgid = j.get_fg().map(|job| job.pgid());
|
||||
if let Some(job) = j.query_mut(JobID::Pid(pid)) {
|
||||
let pgid = job.pgid();
|
||||
let is_fg = fg_pgid.is_some_and(|fg| fg == pgid);
|
||||
job.update_by_id(JobID::Pid(pid), status).unwrap();
|
||||
let is_finished = !job.running();
|
||||
/*
|
||||
* Here we are going to get metadata on the exited process by querying the
|
||||
* job table with the pid. Then if the discovered job is the fg task,
|
||||
* return terminal control to rsh If it is not the fg task, print the
|
||||
* display info for the job in the job table We can reasonably assume that
|
||||
* if it is not a foreground job, then it exists in the job table
|
||||
* If this assumption is incorrect, the code has gone wrong somewhere.
|
||||
*/
|
||||
write_jobs(|j| j.close_job_fds(pid));
|
||||
if let Some((pgid, is_fg, is_finished)) = write_jobs(|j| {
|
||||
let fg_pgid = j.get_fg().map(|job| job.pgid());
|
||||
if let Some(job) = j.query_mut(JobID::Pid(pid)) {
|
||||
let pgid = job.pgid();
|
||||
let is_fg = fg_pgid.is_some_and(|fg| fg == pgid);
|
||||
job.update_by_id(JobID::Pid(pid), status).unwrap();
|
||||
let is_finished = !job.running();
|
||||
|
||||
if let Some(child) = job.children_mut().iter_mut().find(|chld| pid == chld.pid()) {
|
||||
child.set_stat(status);
|
||||
}
|
||||
|
||||
if let Some(child) = job.children_mut().iter_mut().find(|chld| pid == chld.pid()) {
|
||||
child.set_stat(status);
|
||||
}
|
||||
|
||||
Some((pgid, is_fg, is_finished))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
|
||||
if is_finished {
|
||||
if is_fg {
|
||||
take_term()?;
|
||||
} else {
|
||||
println!();
|
||||
let job_order = read_jobs(|j| j.order().to_vec());
|
||||
let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned());
|
||||
if let Some(job) = result {
|
||||
println!("{}",job.display(&job_order,JobCmdFlags::PIDS))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
Some((pgid, is_fg, is_finished))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
if is_finished {
|
||||
if is_fg {
|
||||
take_term()?;
|
||||
} else {
|
||||
println!();
|
||||
let job_order = read_jobs(|j| j.order().to_vec());
|
||||
let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned());
|
||||
if let Some(job) = result {
|
||||
println!("{}", job.display(&job_order, JobCmdFlags::PIDS))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
703
src/state.rs
703
src/state.rs
@@ -1,8 +1,23 @@
|
||||
use std::{collections::{HashMap, VecDeque}, ops::Deref, sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard}, time::Duration};
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
ops::Deref,
|
||||
sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use nix::unistd::{gethostname, getppid, User};
|
||||
|
||||
use crate::{exec_input, jobs::JobTab, libsh::{error::{ShErr, ShErrKind, ShResult}, utils::VecDequeExt}, parse::{ConjunctNode, NdRule, Node, ParsedSrc}, prelude::*, shopt::ShOpts};
|
||||
use crate::{
|
||||
exec_input,
|
||||
jobs::JobTab,
|
||||
libsh::{
|
||||
error::{ShErr, ShErrKind, ShResult},
|
||||
utils::VecDequeExt,
|
||||
},
|
||||
parse::{ConjunctNode, NdRule, Node, ParsedSrc},
|
||||
prelude::*,
|
||||
shopt::ShOpts,
|
||||
};
|
||||
|
||||
pub static JOB_TABLE: LazyLock<RwLock<JobTab>> = LazyLock::new(|| RwLock::new(JobTab::new()));
|
||||
|
||||
@@ -16,438 +31,458 @@ pub static SHOPTS: LazyLock<RwLock<ShOpts>> = LazyLock::new(|| RwLock::new(ShOpt
|
||||
|
||||
/// A shell function
|
||||
///
|
||||
/// Consists of the BraceGrp Node and the stored ParsedSrc that the node refers to
|
||||
/// The Node must be stored with the ParsedSrc because the tokens of the node contain an Arc<String>
|
||||
/// Which refers to the String held in ParsedSrc
|
||||
/// Consists of the BraceGrp Node and the stored ParsedSrc that the node refers
|
||||
/// to The Node must be stored with the ParsedSrc because the tokens of the node
|
||||
/// contain an Arc<String> Which refers to the String held in ParsedSrc
|
||||
///
|
||||
/// Can be dereferenced to pull out the wrapped Node
|
||||
#[derive(Clone,Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ShFunc(Node);
|
||||
|
||||
impl ShFunc {
|
||||
pub fn new(mut src: ParsedSrc) -> Self {
|
||||
let body = Self::extract_brc_grp_hack(src.extract_nodes());
|
||||
Self(body)
|
||||
}
|
||||
fn extract_brc_grp_hack(mut tree: Vec<Node>) -> Node {
|
||||
// FIXME: find a better way to do this
|
||||
let conjunction = tree.pop().unwrap();
|
||||
let NdRule::Conjunction { mut elements } = conjunction.class else {
|
||||
unreachable!()
|
||||
};
|
||||
let conjunct_node = elements.pop().unwrap();
|
||||
let ConjunctNode { cmd, operator: _ } = conjunct_node;
|
||||
*cmd
|
||||
}
|
||||
pub fn new(mut src: ParsedSrc) -> Self {
|
||||
let body = Self::extract_brc_grp_hack(src.extract_nodes());
|
||||
Self(body)
|
||||
}
|
||||
fn extract_brc_grp_hack(mut tree: Vec<Node>) -> Node {
|
||||
// FIXME: find a better way to do this
|
||||
let conjunction = tree.pop().unwrap();
|
||||
let NdRule::Conjunction { mut elements } = conjunction.class else {
|
||||
unreachable!()
|
||||
};
|
||||
let conjunct_node = elements.pop().unwrap();
|
||||
let ConjunctNode { cmd, operator: _ } = conjunct_node;
|
||||
*cmd
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ShFunc {
|
||||
type Target = Node;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
type Target = Node;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The logic table for the shell
|
||||
///
|
||||
/// Contains aliases and functions
|
||||
#[derive(Default,Clone,Debug)]
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct LogTab {
|
||||
functions: HashMap<String,ShFunc>,
|
||||
aliases: HashMap<String,String>
|
||||
functions: HashMap<String, ShFunc>,
|
||||
aliases: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl LogTab {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn insert_func(&mut self, name: &str, src: ShFunc) {
|
||||
self.functions.insert(name.into(), src);
|
||||
}
|
||||
pub fn get_func(&self, name: &str) -> Option<ShFunc> {
|
||||
self.functions.get(name).cloned()
|
||||
}
|
||||
pub fn funcs(&self) -> &HashMap<String,ShFunc> {
|
||||
&self.functions
|
||||
}
|
||||
pub fn aliases(&self) -> &HashMap<String,String> {
|
||||
&self.aliases
|
||||
}
|
||||
pub fn insert_alias(&mut self, name: &str, body: &str) {
|
||||
self.aliases.insert(name.into(), body.into());
|
||||
}
|
||||
pub fn get_alias(&self, name: &str) -> Option<String> {
|
||||
self.aliases.get(name).cloned()
|
||||
}
|
||||
pub fn remove_alias(&mut self, name: &str) {
|
||||
flog!(DEBUG, self.aliases);
|
||||
flog!(DEBUG, name);
|
||||
self.aliases.remove(name);
|
||||
flog!(DEBUG, self.aliases);
|
||||
}
|
||||
pub fn clear_aliases(&mut self) {
|
||||
self.aliases.clear()
|
||||
}
|
||||
pub fn clear_functions(&mut self) {
|
||||
self.functions.clear()
|
||||
}
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn insert_func(&mut self, name: &str, src: ShFunc) {
|
||||
self.functions.insert(name.into(), src);
|
||||
}
|
||||
pub fn get_func(&self, name: &str) -> Option<ShFunc> {
|
||||
self.functions.get(name).cloned()
|
||||
}
|
||||
pub fn funcs(&self) -> &HashMap<String, ShFunc> {
|
||||
&self.functions
|
||||
}
|
||||
pub fn aliases(&self) -> &HashMap<String, String> {
|
||||
&self.aliases
|
||||
}
|
||||
pub fn insert_alias(&mut self, name: &str, body: &str) {
|
||||
self.aliases.insert(name.into(), body.into());
|
||||
}
|
||||
pub fn get_alias(&self, name: &str) -> Option<String> {
|
||||
self.aliases.get(name).cloned()
|
||||
}
|
||||
pub fn remove_alias(&mut self, name: &str) {
|
||||
flog!(DEBUG, self.aliases);
|
||||
flog!(DEBUG, name);
|
||||
self.aliases.remove(name);
|
||||
flog!(DEBUG, self.aliases);
|
||||
}
|
||||
pub fn clear_aliases(&mut self) {
|
||||
self.aliases.clear()
|
||||
}
|
||||
pub fn clear_functions(&mut self) {
|
||||
self.functions.clear()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Var {
|
||||
export: bool,
|
||||
value: String
|
||||
export: bool,
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl Var {
|
||||
pub fn new(value: String) -> Self {
|
||||
Self { export: false, value }
|
||||
}
|
||||
pub fn mark_for_export(&mut self) {
|
||||
self.export = true;
|
||||
}
|
||||
pub fn new(value: String) -> Self {
|
||||
Self {
|
||||
export: false,
|
||||
value,
|
||||
}
|
||||
}
|
||||
pub fn mark_for_export(&mut self) {
|
||||
self.export = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Var {
|
||||
type Target = String;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.value
|
||||
}
|
||||
type Target = String;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default,Clone,Debug)]
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct VarTab {
|
||||
vars: HashMap<String,Var>,
|
||||
params: HashMap<String,String>,
|
||||
sh_argv: VecDeque<String>, // Using a VecDeque makes the implementation of `shift` straightforward
|
||||
vars: HashMap<String, Var>,
|
||||
params: HashMap<String, String>,
|
||||
sh_argv: VecDeque<String>, /* Using a VecDeque makes the implementation of `shift`
|
||||
* straightforward */
|
||||
}
|
||||
|
||||
impl VarTab {
|
||||
pub fn new() -> Self {
|
||||
let vars = HashMap::new();
|
||||
let params = Self::init_params();
|
||||
Self::init_env();
|
||||
let mut var_tab = Self { vars, params, sh_argv: VecDeque::new() };
|
||||
var_tab.init_sh_argv();
|
||||
var_tab
|
||||
}
|
||||
fn init_params() -> HashMap<String, String> {
|
||||
let mut params = HashMap::new();
|
||||
params.insert("?".into(), "0".into()); // Last command exit status
|
||||
params.insert("#".into(), "0".into()); // Number of positional parameters
|
||||
params.insert("0".into(), std::env::current_exe().unwrap().to_str().unwrap().to_string()); // Name of the shell
|
||||
params.insert("$".into(), Pid::this().to_string()); // PID of the shell
|
||||
params.insert("!".into(), "".into()); // PID of the last background job (if any)
|
||||
params
|
||||
}
|
||||
fn init_env() {
|
||||
let pathbuf_to_string = |pb: Result<PathBuf, std::io::Error>| pb.unwrap_or_default().to_string_lossy().to_string();
|
||||
// First, inherit any env vars from the parent process
|
||||
let term = {
|
||||
if isatty(1).unwrap() {
|
||||
if let Ok(term) = std::env::var("TERM") {
|
||||
term
|
||||
} else {
|
||||
"linux".to_string()
|
||||
}
|
||||
} else {
|
||||
"xterm-256color".to_string()
|
||||
}
|
||||
};
|
||||
let home;
|
||||
let username;
|
||||
let uid;
|
||||
if let Some(user) = User::from_uid(nix::unistd::Uid::current()).ok().flatten() {
|
||||
home = user.dir;
|
||||
username = user.name;
|
||||
uid = user.uid;
|
||||
} else {
|
||||
home = PathBuf::new();
|
||||
username = "unknown".into();
|
||||
uid = 0.into();
|
||||
}
|
||||
let home = pathbuf_to_string(Ok(home));
|
||||
let hostname = gethostname().map(|hname| hname.to_string_lossy().to_string()).unwrap_or_default();
|
||||
pub fn new() -> Self {
|
||||
let vars = HashMap::new();
|
||||
let params = Self::init_params();
|
||||
Self::init_env();
|
||||
let mut var_tab = Self {
|
||||
vars,
|
||||
params,
|
||||
sh_argv: VecDeque::new(),
|
||||
};
|
||||
var_tab.init_sh_argv();
|
||||
var_tab
|
||||
}
|
||||
fn init_params() -> HashMap<String, String> {
|
||||
let mut params = HashMap::new();
|
||||
params.insert("?".into(), "0".into()); // Last command exit status
|
||||
params.insert("#".into(), "0".into()); // Number of positional parameters
|
||||
params.insert(
|
||||
"0".into(),
|
||||
std::env::current_exe()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
); // Name of the shell
|
||||
params.insert("$".into(), Pid::this().to_string()); // PID of the shell
|
||||
params.insert("!".into(), "".into()); // PID of the last background job (if any)
|
||||
params
|
||||
}
|
||||
fn init_env() {
|
||||
let pathbuf_to_string =
|
||||
|pb: Result<PathBuf, std::io::Error>| pb.unwrap_or_default().to_string_lossy().to_string();
|
||||
// First, inherit any env vars from the parent process
|
||||
let term = {
|
||||
if isatty(1).unwrap() {
|
||||
if let Ok(term) = std::env::var("TERM") {
|
||||
term
|
||||
} else {
|
||||
"linux".to_string()
|
||||
}
|
||||
} else {
|
||||
"xterm-256color".to_string()
|
||||
}
|
||||
};
|
||||
let home;
|
||||
let username;
|
||||
let uid;
|
||||
if let Some(user) = User::from_uid(nix::unistd::Uid::current()).ok().flatten() {
|
||||
home = user.dir;
|
||||
username = user.name;
|
||||
uid = user.uid;
|
||||
} else {
|
||||
home = PathBuf::new();
|
||||
username = "unknown".into();
|
||||
uid = 0.into();
|
||||
}
|
||||
let home = pathbuf_to_string(Ok(home));
|
||||
let hostname = gethostname()
|
||||
.map(|hname| hname.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
env::set_var("IFS", " \t\n");
|
||||
env::set_var("HOST", hostname.clone());
|
||||
env::set_var("UID", uid.to_string());
|
||||
env::set_var("PPID", getppid().to_string());
|
||||
env::set_var("TMPDIR", "/tmp");
|
||||
env::set_var("TERM", term);
|
||||
env::set_var("LANG", "en_US.UTF-8");
|
||||
env::set_var("USER", username.clone());
|
||||
env::set_var("LOGNAME", username);
|
||||
env::set_var("PWD", pathbuf_to_string(std::env::current_dir()));
|
||||
env::set_var("OLDPWD", pathbuf_to_string(std::env::current_dir()));
|
||||
env::set_var("HOME", home.clone());
|
||||
env::set_var("SHELL", pathbuf_to_string(std::env::current_exe()));
|
||||
env::set_var("FERN_HIST",format!("{}/.fernhist",home));
|
||||
env::set_var("FERN_RC",format!("{}/.fernrc",home));
|
||||
}
|
||||
pub fn init_sh_argv(&mut self) {
|
||||
for arg in env::args() {
|
||||
self.bpush_arg(arg);
|
||||
}
|
||||
}
|
||||
pub fn update_exports(&mut self) {
|
||||
for var_name in self.vars.keys() {
|
||||
let var = self.vars.get(var_name).unwrap();
|
||||
if var.export {
|
||||
env::set_var(var_name, &var.value);
|
||||
} else {
|
||||
env::set_var(var_name, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn sh_argv(&self) -> &VecDeque<String> {
|
||||
&self.sh_argv
|
||||
}
|
||||
pub fn sh_argv_mut(&mut self) -> &mut VecDeque<String> {
|
||||
&mut self.sh_argv
|
||||
}
|
||||
pub fn clear_args(&mut self) {
|
||||
self.sh_argv.clear();
|
||||
// Push the current exe again
|
||||
// This makes sure that $0 is always the current shell, no matter what
|
||||
// It also updates the arg parameters '@' and '#' as well
|
||||
self.bpush_arg(env::current_exe().unwrap().to_str().unwrap().to_string());
|
||||
}
|
||||
fn update_arg_params(&mut self) {
|
||||
self.set_param("@", &self.sh_argv.clone().to_vec()[1..].join(" "));
|
||||
self.set_param("#", &(self.sh_argv.len() - 1).to_string());
|
||||
}
|
||||
/// Push an arg to the front of the arg deque
|
||||
pub fn fpush_arg(&mut self, arg: String) {
|
||||
self.sh_argv.push_front(arg);
|
||||
self.update_arg_params();
|
||||
}
|
||||
/// Push an arg to the back of the arg deque
|
||||
pub fn bpush_arg(&mut self, arg: String) {
|
||||
self.sh_argv.push_back(arg);
|
||||
self.update_arg_params();
|
||||
}
|
||||
/// Pop an arg from the front of the arg deque
|
||||
pub fn fpop_arg(&mut self) -> Option<String> {
|
||||
let arg = self.sh_argv.pop_front();
|
||||
self.update_arg_params();
|
||||
arg
|
||||
}
|
||||
/// Pop an arg from the back of the arg deque
|
||||
pub fn bpop_arg(&mut self) -> Option<String> {
|
||||
let arg = self.sh_argv.pop_back();
|
||||
self.update_arg_params();
|
||||
arg
|
||||
}
|
||||
pub fn vars(&self) -> &HashMap<String,Var> {
|
||||
&self.vars
|
||||
}
|
||||
pub fn vars_mut(&mut self) -> &mut HashMap<String,Var> {
|
||||
&mut self.vars
|
||||
}
|
||||
pub fn params(&self) -> &HashMap<String,String> {
|
||||
&self.params
|
||||
}
|
||||
pub fn params_mut(&mut self) -> &mut HashMap<String,String> {
|
||||
&mut self.params
|
||||
}
|
||||
pub fn export_var(&mut self, var_name: &str) {
|
||||
if let Some(var) = self.vars.get_mut(var_name) {
|
||||
var.mark_for_export();
|
||||
env::set_var(var_name, &var.value);
|
||||
}
|
||||
}
|
||||
pub fn get_var(&self, var: &str) -> String {
|
||||
if var.chars().count() == 1 ||
|
||||
var.parse::<usize>().is_ok() {
|
||||
let param = self.get_param(var);
|
||||
if !param.is_empty() {
|
||||
return param
|
||||
}
|
||||
}
|
||||
if let Some(var) = self.vars.get(var).map(|s| s.to_string()) {
|
||||
var
|
||||
} else {
|
||||
std::env::var(var).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
pub fn set_var(&mut self, var_name: &str, val: &str, export: bool) {
|
||||
if let Some(var) = self.vars.get_mut(var_name) {
|
||||
var.value = val.to_string();
|
||||
if var.export {
|
||||
env::set_var(var_name, val);
|
||||
}
|
||||
} else {
|
||||
let mut var = Var::new(val.to_string());
|
||||
if export {
|
||||
var.mark_for_export();
|
||||
env::set_var(var_name, &*var);
|
||||
}
|
||||
self.vars.insert(var_name.to_string(), var);
|
||||
}
|
||||
}
|
||||
pub fn var_exists(&self, var_name: &str) -> bool {
|
||||
if var_name.parse::<usize>().is_ok() {
|
||||
return self.params.contains_key(var_name);
|
||||
}
|
||||
self.vars.contains_key(var_name) ||
|
||||
(
|
||||
var_name.len() == 1 &&
|
||||
self.params.contains_key(var_name)
|
||||
)
|
||||
}
|
||||
pub fn set_param(&mut self, param: &str, val: &str) {
|
||||
self.params.insert(param.to_string(),val.to_string());
|
||||
}
|
||||
pub fn get_param(&self, param: &str) -> String {
|
||||
if param.parse::<usize>().is_ok() {
|
||||
let argv_idx = param
|
||||
.to_string()
|
||||
.parse::<usize>()
|
||||
.unwrap();
|
||||
let arg = self.sh_argv.get(argv_idx).map(|s| s.to_string()).unwrap_or_default();
|
||||
arg
|
||||
} else if param == "?" {
|
||||
self.params.get(param).map(|s| s.to_string()).unwrap_or("0".into())
|
||||
} else {
|
||||
self.params.get(param).map(|s| s.to_string()).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
env::set_var("IFS", " \t\n");
|
||||
env::set_var("HOST", hostname.clone());
|
||||
env::set_var("UID", uid.to_string());
|
||||
env::set_var("PPID", getppid().to_string());
|
||||
env::set_var("TMPDIR", "/tmp");
|
||||
env::set_var("TERM", term);
|
||||
env::set_var("LANG", "en_US.UTF-8");
|
||||
env::set_var("USER", username.clone());
|
||||
env::set_var("LOGNAME", username);
|
||||
env::set_var("PWD", pathbuf_to_string(std::env::current_dir()));
|
||||
env::set_var("OLDPWD", pathbuf_to_string(std::env::current_dir()));
|
||||
env::set_var("HOME", home.clone());
|
||||
env::set_var("SHELL", pathbuf_to_string(std::env::current_exe()));
|
||||
env::set_var("FERN_HIST", format!("{}/.fernhist", home));
|
||||
env::set_var("FERN_RC", format!("{}/.fernrc", home));
|
||||
}
|
||||
pub fn init_sh_argv(&mut self) {
|
||||
for arg in env::args() {
|
||||
self.bpush_arg(arg);
|
||||
}
|
||||
}
|
||||
pub fn update_exports(&mut self) {
|
||||
for var_name in self.vars.keys() {
|
||||
let var = self.vars.get(var_name).unwrap();
|
||||
if var.export {
|
||||
env::set_var(var_name, &var.value);
|
||||
} else {
|
||||
env::set_var(var_name, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn sh_argv(&self) -> &VecDeque<String> {
|
||||
&self.sh_argv
|
||||
}
|
||||
pub fn sh_argv_mut(&mut self) -> &mut VecDeque<String> {
|
||||
&mut self.sh_argv
|
||||
}
|
||||
pub fn clear_args(&mut self) {
|
||||
self.sh_argv.clear();
|
||||
// Push the current exe again
|
||||
// This makes sure that $0 is always the current shell, no matter what
|
||||
// It also updates the arg parameters '@' and '#' as well
|
||||
self.bpush_arg(env::current_exe().unwrap().to_str().unwrap().to_string());
|
||||
}
|
||||
fn update_arg_params(&mut self) {
|
||||
self.set_param("@", &self.sh_argv.clone().to_vec()[1..].join(" "));
|
||||
self.set_param("#", &(self.sh_argv.len() - 1).to_string());
|
||||
}
|
||||
/// Push an arg to the front of the arg deque
|
||||
pub fn fpush_arg(&mut self, arg: String) {
|
||||
self.sh_argv.push_front(arg);
|
||||
self.update_arg_params();
|
||||
}
|
||||
/// Push an arg to the back of the arg deque
|
||||
pub fn bpush_arg(&mut self, arg: String) {
|
||||
self.sh_argv.push_back(arg);
|
||||
self.update_arg_params();
|
||||
}
|
||||
/// Pop an arg from the front of the arg deque
|
||||
pub fn fpop_arg(&mut self) -> Option<String> {
|
||||
let arg = self.sh_argv.pop_front();
|
||||
self.update_arg_params();
|
||||
arg
|
||||
}
|
||||
/// Pop an arg from the back of the arg deque
|
||||
pub fn bpop_arg(&mut self) -> Option<String> {
|
||||
let arg = self.sh_argv.pop_back();
|
||||
self.update_arg_params();
|
||||
arg
|
||||
}
|
||||
pub fn vars(&self) -> &HashMap<String, Var> {
|
||||
&self.vars
|
||||
}
|
||||
pub fn vars_mut(&mut self) -> &mut HashMap<String, Var> {
|
||||
&mut self.vars
|
||||
}
|
||||
pub fn params(&self) -> &HashMap<String, String> {
|
||||
&self.params
|
||||
}
|
||||
pub fn params_mut(&mut self) -> &mut HashMap<String, String> {
|
||||
&mut self.params
|
||||
}
|
||||
pub fn export_var(&mut self, var_name: &str) {
|
||||
if let Some(var) = self.vars.get_mut(var_name) {
|
||||
var.mark_for_export();
|
||||
env::set_var(var_name, &var.value);
|
||||
}
|
||||
}
|
||||
pub fn get_var(&self, var: &str) -> String {
|
||||
if var.chars().count() == 1 || var.parse::<usize>().is_ok() {
|
||||
let param = self.get_param(var);
|
||||
if !param.is_empty() {
|
||||
return param;
|
||||
}
|
||||
}
|
||||
if let Some(var) = self.vars.get(var).map(|s| s.to_string()) {
|
||||
var
|
||||
} else {
|
||||
std::env::var(var).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
pub fn set_var(&mut self, var_name: &str, val: &str, export: bool) {
|
||||
if let Some(var) = self.vars.get_mut(var_name) {
|
||||
var.value = val.to_string();
|
||||
if var.export {
|
||||
env::set_var(var_name, val);
|
||||
}
|
||||
} else {
|
||||
let mut var = Var::new(val.to_string());
|
||||
if export {
|
||||
var.mark_for_export();
|
||||
env::set_var(var_name, &*var);
|
||||
}
|
||||
self.vars.insert(var_name.to_string(), var);
|
||||
}
|
||||
}
|
||||
pub fn var_exists(&self, var_name: &str) -> bool {
|
||||
if var_name.parse::<usize>().is_ok() {
|
||||
return self.params.contains_key(var_name);
|
||||
}
|
||||
self.vars.contains_key(var_name) || (var_name.len() == 1 && self.params.contains_key(var_name))
|
||||
}
|
||||
pub fn set_param(&mut self, param: &str, val: &str) {
|
||||
self.params.insert(param.to_string(), val.to_string());
|
||||
}
|
||||
pub fn get_param(&self, param: &str) -> String {
|
||||
if param.parse::<usize>().is_ok() {
|
||||
let argv_idx = param.to_string().parse::<usize>().unwrap();
|
||||
let arg = self
|
||||
.sh_argv
|
||||
.get(argv_idx)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default();
|
||||
arg
|
||||
} else if param == "?" {
|
||||
self
|
||||
.params
|
||||
.get(param)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or("0".into())
|
||||
} else {
|
||||
self
|
||||
.params
|
||||
.get(param)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A table of metadata for the shell
|
||||
#[derive(Default,Debug)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct MetaTab {
|
||||
runtime_start: Option<Instant>
|
||||
runtime_start: Option<Instant>,
|
||||
}
|
||||
|
||||
impl MetaTab {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn start_timer(&mut self) {
|
||||
self.runtime_start = Some(Instant::now());
|
||||
}
|
||||
pub fn stop_timer(&mut self) -> Option<Duration> {
|
||||
self.runtime_start
|
||||
.take() // runtime_start returns to None
|
||||
.map(|start| start.elapsed()) // return the duration, if any
|
||||
}
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn start_timer(&mut self) {
|
||||
self.runtime_start = Some(Instant::now());
|
||||
}
|
||||
pub fn stop_timer(&mut self) -> Option<Duration> {
|
||||
self
|
||||
.runtime_start
|
||||
.take() // runtime_start returns to None
|
||||
.map(|start| start.elapsed()) // return the duration, if any
|
||||
}
|
||||
}
|
||||
|
||||
/// Read from the job table
|
||||
pub fn read_jobs<T, F: FnOnce(RwLockReadGuard<JobTab>) -> T>(f: F) -> T {
|
||||
let lock = JOB_TABLE.read().unwrap();
|
||||
f(lock)
|
||||
let lock = JOB_TABLE.read().unwrap();
|
||||
f(lock)
|
||||
}
|
||||
|
||||
/// Write to the job table
|
||||
pub fn write_jobs<T, F: FnOnce(&mut RwLockWriteGuard<JobTab>) -> T>(f: F) -> T {
|
||||
let lock = &mut JOB_TABLE.write().unwrap();
|
||||
f(lock)
|
||||
let lock = &mut JOB_TABLE.write().unwrap();
|
||||
f(lock)
|
||||
}
|
||||
|
||||
/// Read from the variable table
|
||||
pub fn read_vars<T, F: FnOnce(RwLockReadGuard<VarTab>) -> T>(f: F) -> T {
|
||||
let lock = VAR_TABLE.read().unwrap();
|
||||
f(lock)
|
||||
let lock = VAR_TABLE.read().unwrap();
|
||||
f(lock)
|
||||
}
|
||||
|
||||
/// Write to the variable table
|
||||
pub fn write_vars<T, F: FnOnce(&mut RwLockWriteGuard<VarTab>) -> T>(f: F) -> T {
|
||||
let lock = &mut VAR_TABLE.write().unwrap();
|
||||
f(lock)
|
||||
let lock = &mut VAR_TABLE.write().unwrap();
|
||||
f(lock)
|
||||
}
|
||||
|
||||
pub fn read_meta<T, F: FnOnce(RwLockReadGuard<MetaTab>) -> T>(f: F) -> T {
|
||||
let lock = META_TABLE.read().unwrap();
|
||||
f(lock)
|
||||
let lock = META_TABLE.read().unwrap();
|
||||
f(lock)
|
||||
}
|
||||
|
||||
/// Write to the variable table
|
||||
pub fn write_meta<T, F: FnOnce(&mut RwLockWriteGuard<MetaTab>) -> T>(f: F) -> T {
|
||||
let lock = &mut META_TABLE.write().unwrap();
|
||||
f(lock)
|
||||
let lock = &mut META_TABLE.write().unwrap();
|
||||
f(lock)
|
||||
}
|
||||
|
||||
/// Read from the logic table
|
||||
pub fn read_logic<T, F: FnOnce(RwLockReadGuard<LogTab>) -> T>(f: F) -> T {
|
||||
let lock = LOGIC_TABLE.read().unwrap();
|
||||
f(lock)
|
||||
let lock = LOGIC_TABLE.read().unwrap();
|
||||
f(lock)
|
||||
}
|
||||
|
||||
/// Write to the logic table
|
||||
pub fn write_logic<T, F: FnOnce(&mut RwLockWriteGuard<LogTab>) -> T>(f: F) -> T {
|
||||
let lock = &mut LOGIC_TABLE.write().unwrap();
|
||||
f(lock)
|
||||
let lock = &mut LOGIC_TABLE.write().unwrap();
|
||||
f(lock)
|
||||
}
|
||||
|
||||
pub fn read_shopts<T, F: FnOnce(RwLockReadGuard<ShOpts>) -> T>(f: F) -> T {
|
||||
let lock = SHOPTS.read().unwrap();
|
||||
f(lock)
|
||||
let lock = SHOPTS.read().unwrap();
|
||||
f(lock)
|
||||
}
|
||||
|
||||
pub fn write_shopts<T, F: FnOnce(&mut RwLockWriteGuard<ShOpts>) -> T>(f: F) -> T {
|
||||
let lock = &mut SHOPTS.write().unwrap();
|
||||
f(lock)
|
||||
let lock = &mut SHOPTS.write().unwrap();
|
||||
f(lock)
|
||||
}
|
||||
|
||||
/// This function is used internally and ideally never sees user input
|
||||
///
|
||||
/// It will panic if you give it an invalid path.
|
||||
pub fn get_shopt(path: &str) -> String {
|
||||
read_shopts(|s| s.get(path)).unwrap().unwrap()
|
||||
read_shopts(|s| s.get(path)).unwrap().unwrap()
|
||||
}
|
||||
|
||||
pub fn get_status() -> i32 {
|
||||
read_vars(|v| v.get_param("?")).parse::<i32>().unwrap()
|
||||
read_vars(|v| v.get_param("?")).parse::<i32>().unwrap()
|
||||
}
|
||||
#[track_caller]
|
||||
pub fn set_status(code: i32) {
|
||||
write_vars(|v| v.set_param("?", &code.to_string()))
|
||||
write_vars(|v| v.set_param("?", &code.to_string()))
|
||||
}
|
||||
|
||||
/// Save the current state of the logic and variable table, and the working directory path
|
||||
/// Save the current state of the logic and variable table, and the working
|
||||
/// directory path
|
||||
pub fn get_snapshots() -> (LogTab, VarTab, String) {
|
||||
(
|
||||
read_logic(|l| l.clone()),
|
||||
read_vars(|v| v.clone()),
|
||||
env::var("PWD").unwrap_or_default()
|
||||
)
|
||||
(
|
||||
read_logic(|l| l.clone()),
|
||||
read_vars(|v| v.clone()),
|
||||
env::var("PWD").unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn restore_snapshot(snapshot: (LogTab, VarTab, String)) {
|
||||
write_logic(|l| **l = snapshot.0);
|
||||
write_vars(|v| {
|
||||
**v = snapshot.1;
|
||||
v.update_exports();
|
||||
});
|
||||
env::set_current_dir(&snapshot.2).unwrap();
|
||||
env::set_var("PWD", &snapshot.2);
|
||||
write_logic(|l| **l = snapshot.0);
|
||||
write_vars(|v| {
|
||||
**v = snapshot.1;
|
||||
v.update_exports();
|
||||
});
|
||||
env::set_current_dir(&snapshot.2).unwrap();
|
||||
env::set_var("PWD", &snapshot.2);
|
||||
}
|
||||
|
||||
pub fn source_rc() -> ShResult<()> {
|
||||
let path = if let Ok(path) = env::var("FERN_RC") {
|
||||
PathBuf::from(&path)
|
||||
} else {
|
||||
let home = env::var("HOME").unwrap();
|
||||
PathBuf::from(format!("{home}/.fernrc"))
|
||||
};
|
||||
if !path.exists() {
|
||||
return Err(
|
||||
ShErr::simple(ShErrKind::InternalErr, ".fernrc not found")
|
||||
)
|
||||
}
|
||||
source_file(path)
|
||||
let path = if let Ok(path) = env::var("FERN_RC") {
|
||||
PathBuf::from(&path)
|
||||
} else {
|
||||
let home = env::var("HOME").unwrap();
|
||||
PathBuf::from(format!("{home}/.fernrc"))
|
||||
};
|
||||
if !path.exists() {
|
||||
return Err(ShErr::simple(ShErrKind::InternalErr, ".fernrc not found"));
|
||||
}
|
||||
source_file(path)
|
||||
}
|
||||
|
||||
pub fn source_file(path: PathBuf) -> ShResult<()> {
|
||||
let mut file = OpenOptions::new()
|
||||
.read(true)
|
||||
.open(path)?;
|
||||
let mut file = OpenOptions::new().read(true).open(path)?;
|
||||
|
||||
let mut buf = String::new();
|
||||
file.read_to_string(&mut buf)?;
|
||||
exec_input(buf,None)?;
|
||||
Ok(())
|
||||
let mut buf = String::new();
|
||||
file.read_to_string(&mut buf)?;
|
||||
exec_input(buf, None)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2,173 +2,175 @@ use super::*;
|
||||
|
||||
#[test]
|
||||
fn cmd_not_found() {
|
||||
let input = "foo";
|
||||
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty()).next().unwrap().unwrap();
|
||||
let err = ShErr::full(ShErrKind::CmdNotFound("foo".into()), "", token.span);
|
||||
let input = "foo";
|
||||
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.next()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let err = ShErr::full(ShErrKind::CmdNotFound("foo".into()), "", token.span);
|
||||
|
||||
let err_fmt = format!("{err}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
let err_fmt = format!("{err}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unclosed_subsh() {
|
||||
let input = "(foo";
|
||||
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty()).nth(1).unwrap();
|
||||
let Err(err) = token else {
|
||||
panic!("{:?}",token);
|
||||
};
|
||||
let input = "(foo";
|
||||
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.nth(1)
|
||||
.unwrap();
|
||||
let Err(err) = token else {
|
||||
panic!("{:?}", token);
|
||||
};
|
||||
|
||||
let err_fmt = format!("{err}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
let err_fmt = format!("{err}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unclosed_dquote() {
|
||||
let input = "\"foo bar";
|
||||
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty()).nth(1).unwrap();
|
||||
let Err(err) = token else {
|
||||
panic!();
|
||||
};
|
||||
let input = "\"foo bar";
|
||||
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.nth(1)
|
||||
.unwrap();
|
||||
let Err(err) = token else {
|
||||
panic!();
|
||||
};
|
||||
|
||||
let err_fmt = format!("{err}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
let err_fmt = format!("{err}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unclosed_squote() {
|
||||
let input = "'foo bar";
|
||||
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty()).nth(1).unwrap();
|
||||
let Err(err) = token else {
|
||||
panic!();
|
||||
};
|
||||
let input = "'foo bar";
|
||||
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.nth(1)
|
||||
.unwrap();
|
||||
let Err(err) = token else {
|
||||
panic!();
|
||||
};
|
||||
|
||||
let err_fmt = format!("{err}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
let err_fmt = format!("{err}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unclosed_brc_grp() {
|
||||
let input = "{ foo bar";
|
||||
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let input = "{ foo bar";
|
||||
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let node = ParseStream::new(tokens).next().unwrap();
|
||||
let Err(err) = node else {
|
||||
panic!();
|
||||
};
|
||||
let node = ParseStream::new(tokens).next().unwrap();
|
||||
let Err(err) = node else {
|
||||
panic!();
|
||||
};
|
||||
|
||||
let err_fmt = format!("{err}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
let err_fmt = format!("{err}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_no_fi() {
|
||||
let input = "if foo; then bar;";
|
||||
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let input = "if foo; then bar;";
|
||||
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let node = ParseStream::new(tokens).next().unwrap();
|
||||
let Err(e) = node else { panic!() };
|
||||
let node = ParseStream::new(tokens).next().unwrap();
|
||||
let Err(e) = node else { panic!() };
|
||||
|
||||
let err_fmt = format!("{e}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
let err_fmt = format!("{e}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_no_then() {
|
||||
let input = "if foo; bar; fi";
|
||||
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let input = "if foo; bar; fi";
|
||||
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let node = ParseStream::new(tokens).next().unwrap();
|
||||
let Err(e) = node else { panic!() };
|
||||
let node = ParseStream::new(tokens).next().unwrap();
|
||||
let Err(e) = node else { panic!() };
|
||||
|
||||
let err_fmt = format!("{e}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
let err_fmt = format!("{e}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loop_no_done() {
|
||||
let input = "while true; do echo foo;";
|
||||
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let input = "while true; do echo foo;";
|
||||
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let node = ParseStream::new(tokens).next().unwrap();
|
||||
let Err(e) = node else { panic!() };
|
||||
let node = ParseStream::new(tokens).next().unwrap();
|
||||
let Err(e) = node else { panic!() };
|
||||
|
||||
let err_fmt = format!("{e}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
let err_fmt = format!("{e}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loop_no_do() {
|
||||
let input = "while true; echo foo; done";
|
||||
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let input = "while true; echo foo; done";
|
||||
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let node = ParseStream::new(tokens).next().unwrap();
|
||||
let Err(e) = node else { panic!() };
|
||||
let node = ParseStream::new(tokens).next().unwrap();
|
||||
let Err(e) = node else { panic!() };
|
||||
|
||||
let err_fmt = format!("{e}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
let err_fmt = format!("{e}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn case_no_esac() {
|
||||
let input = "case foo in foo) bar;; bar) foo;;";
|
||||
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let input = "case foo in foo) bar;; bar) foo;;";
|
||||
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let node = ParseStream::new(tokens).next().unwrap();
|
||||
let Err(e) = node else { panic!() };
|
||||
let node = ParseStream::new(tokens).next().unwrap();
|
||||
let Err(e) = node else { panic!() };
|
||||
|
||||
let err_fmt = format!("{e}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
let err_fmt = format!("{e}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn case_no_in() {
|
||||
let input = "case foo foo) bar;; bar) foo;; esac";
|
||||
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let input = "case foo foo) bar;; bar) foo;; esac";
|
||||
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let node = ParseStream::new(tokens).next().unwrap();
|
||||
let Err(e) = node else { panic!() };
|
||||
let node = ParseStream::new(tokens).next().unwrap();
|
||||
let Err(e) = node else { panic!() };
|
||||
|
||||
let err_fmt = format!("{e}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
let err_fmt = format!("{e}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_notes() {
|
||||
let err = ShErr::simple(ShErrKind::ExecFail, "Execution failed")
|
||||
.with_note(Note::new("Execution failed for this reason"))
|
||||
.with_note(Note::new("Here is how to fix it: blah blah blah"));
|
||||
let err = ShErr::simple(ShErrKind::ExecFail, "Execution failed")
|
||||
.with_note(Note::new("Execution failed for this reason"))
|
||||
.with_note(Note::new("Here is how to fix it: blah blah blah"));
|
||||
|
||||
let err_fmt = format!("{err}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
let err_fmt = format!("{err}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_notes_and_sub_notes() {
|
||||
let err = ShErr::simple(ShErrKind::ExecFail, "Execution failed")
|
||||
.with_note(Note::new("Execution failed for this reason"))
|
||||
.with_note(
|
||||
Note::new("Here is how to fix it:")
|
||||
.with_sub_notes(vec![
|
||||
"blah",
|
||||
"blah",
|
||||
"blah"
|
||||
])
|
||||
);
|
||||
let err = ShErr::simple(ShErrKind::ExecFail, "Execution failed")
|
||||
.with_note(Note::new("Execution failed for this reason"))
|
||||
.with_note(Note::new("Here is how to fix it:").with_sub_notes(vec!["blah", "blah", "blah"]));
|
||||
|
||||
let err_fmt = format!("{err}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
let err_fmt = format!("{err}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
}
|
||||
|
||||
@@ -6,297 +6,300 @@ use super::*;
|
||||
|
||||
#[test]
|
||||
fn simple_expansion() {
|
||||
let varsub = "$foo";
|
||||
write_vars(|v| v.set_var("foo", "this is the value of the variable", false));
|
||||
let varsub = "$foo";
|
||||
write_vars(|v| v.set_var("foo", "this is the value of the variable", false));
|
||||
|
||||
let mut tokens: Vec<Tk> = LexStream::new(Arc::new(varsub.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.filter(|tk| !matches!(tk.class, TkRule::EOI | TkRule::SOI))
|
||||
.collect();
|
||||
let var_tk = tokens.pop().unwrap();
|
||||
let mut tokens: Vec<Tk> = LexStream::new(Arc::new(varsub.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.filter(|tk| !matches!(tk.class, TkRule::EOI | TkRule::SOI))
|
||||
.collect();
|
||||
let var_tk = tokens.pop().unwrap();
|
||||
|
||||
let exp_tk = var_tk.expand().unwrap();
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
insta::assert_debug_snapshot!(exp_tk.get_words())
|
||||
let exp_tk = var_tk.expand().unwrap();
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
insta::assert_debug_snapshot!(exp_tk.get_words())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unescape_string() {
|
||||
let string = "echo $foo \\$bar";
|
||||
let unescaped = unescape_str(string);
|
||||
let string = "echo $foo \\$bar";
|
||||
let unescaped = unescape_str(string);
|
||||
|
||||
insta::assert_snapshot!(unescaped)
|
||||
insta::assert_snapshot!(unescaped)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_alias_simple() {
|
||||
write_logic(|l| {
|
||||
l.insert_alias("foo", "echo foo");
|
||||
let input = String::from("foo");
|
||||
write_logic(|l| {
|
||||
l.insert_alias("foo", "echo foo");
|
||||
let input = String::from("foo");
|
||||
|
||||
let result = expand_aliases(input, HashSet::new(), l);
|
||||
assert_eq!(result.as_str(),"echo foo");
|
||||
l.clear_aliases();
|
||||
});
|
||||
let result = expand_aliases(input, HashSet::new(), l);
|
||||
assert_eq!(result.as_str(), "echo foo");
|
||||
l.clear_aliases();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_alias_in_if() {
|
||||
write_logic(|l| {
|
||||
l.insert_alias("foo", "echo foo");
|
||||
let input = String::from("if foo; then echo bar; fi");
|
||||
write_logic(|l| {
|
||||
l.insert_alias("foo", "echo foo");
|
||||
let input = String::from("if foo; then echo bar; fi");
|
||||
|
||||
let result = expand_aliases(input, HashSet::new(), l);
|
||||
assert_eq!(result.as_str(),"if echo foo; then echo bar; fi");
|
||||
l.clear_aliases();
|
||||
});
|
||||
let result = expand_aliases(input, HashSet::new(), l);
|
||||
assert_eq!(result.as_str(), "if echo foo; then echo bar; fi");
|
||||
l.clear_aliases();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_alias_multiline() {
|
||||
write_logic(|l| {
|
||||
l.insert_alias("foo", "echo foo");
|
||||
l.insert_alias("bar", "echo bar");
|
||||
let input = String::from("
|
||||
write_logic(|l| {
|
||||
l.insert_alias("foo", "echo foo");
|
||||
l.insert_alias("bar", "echo bar");
|
||||
let input = String::from(
|
||||
"
|
||||
foo
|
||||
if true; then
|
||||
bar
|
||||
fi
|
||||
");
|
||||
let expected = String::from("
|
||||
",
|
||||
);
|
||||
let expected = String::from(
|
||||
"
|
||||
echo foo
|
||||
if true; then
|
||||
echo bar
|
||||
fi
|
||||
");
|
||||
",
|
||||
);
|
||||
|
||||
let result = expand_aliases(input, HashSet::new(), l);
|
||||
assert_eq!(result,expected)
|
||||
});
|
||||
let result = expand_aliases(input, HashSet::new(), l);
|
||||
assert_eq!(result, expected)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_multiple_aliases() {
|
||||
write_logic(|l| {
|
||||
l.insert_alias("foo", "echo foo");
|
||||
l.insert_alias("bar", "echo bar");
|
||||
l.insert_alias("biz", "echo biz");
|
||||
let input = String::from("foo; bar; biz");
|
||||
write_logic(|l| {
|
||||
l.insert_alias("foo", "echo foo");
|
||||
l.insert_alias("bar", "echo bar");
|
||||
l.insert_alias("biz", "echo biz");
|
||||
let input = String::from("foo; bar; biz");
|
||||
|
||||
let result = expand_aliases(input, HashSet::new(), l);
|
||||
assert_eq!(result.as_str(),"echo foo; echo bar; echo biz");
|
||||
});
|
||||
let result = expand_aliases(input, HashSet::new(), l);
|
||||
assert_eq!(result.as_str(), "echo foo; echo bar; echo biz");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_in_arg_position() {
|
||||
write_logic(|l| {
|
||||
l.insert_alias("foo", "echo foo");
|
||||
let input = String::from("echo foo");
|
||||
write_logic(|l| {
|
||||
l.insert_alias("foo", "echo foo");
|
||||
let input = String::from("echo foo");
|
||||
|
||||
let result = expand_aliases(input.clone(), HashSet::new(), l);
|
||||
assert_eq!(input,result);
|
||||
l.clear_aliases();
|
||||
});
|
||||
let result = expand_aliases(input.clone(), HashSet::new(), l);
|
||||
assert_eq!(input, result);
|
||||
l.clear_aliases();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_recursive_alias() {
|
||||
write_logic(|l| {
|
||||
l.insert_alias("foo", "echo foo");
|
||||
l.insert_alias("bar", "foo bar");
|
||||
write_logic(|l| {
|
||||
l.insert_alias("foo", "echo foo");
|
||||
l.insert_alias("bar", "foo bar");
|
||||
|
||||
let input = String::from("bar");
|
||||
let result = expand_aliases(input, HashSet::new(), l);
|
||||
assert_eq!(result.as_str(),"echo foo bar");
|
||||
});
|
||||
let input = String::from("bar");
|
||||
let result = expand_aliases(input, HashSet::new(), l);
|
||||
assert_eq!(result.as_str(), "echo foo bar");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_infinite_recursive_alias() {
|
||||
write_logic(|l| {
|
||||
l.insert_alias("foo", "foo bar");
|
||||
|
||||
let input = String::from("foo");
|
||||
let result = expand_aliases(input, HashSet::new(), l);
|
||||
assert_eq!(result.as_str(),"foo bar");
|
||||
l.clear_aliases();
|
||||
});
|
||||
write_logic(|l| {
|
||||
l.insert_alias("foo", "foo bar");
|
||||
|
||||
let input = String::from("foo");
|
||||
let result = expand_aliases(input, HashSet::new(), l);
|
||||
assert_eq!(result.as_str(), "foo bar");
|
||||
l.clear_aliases();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_expansion_defaultunsetornull() {
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
v.set_var("set_var", "value", false);
|
||||
});
|
||||
let result = perform_param_expansion("unset:-default").unwrap();
|
||||
assert_eq!(result, "default");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
v.set_var("set_var", "value", false);
|
||||
});
|
||||
let result = perform_param_expansion("unset:-default").unwrap();
|
||||
assert_eq!(result, "default");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_expansion_defaultunset() {
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
v.set_var("set_var", "value", false);
|
||||
});
|
||||
let result = perform_param_expansion("unset-default").unwrap();
|
||||
assert_eq!(result, "default");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
v.set_var("set_var", "value", false);
|
||||
});
|
||||
let result = perform_param_expansion("unset-default").unwrap();
|
||||
assert_eq!(result, "default");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_expansion_setdefaultunsetornull() {
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
v.set_var("set_var", "value", false);
|
||||
});
|
||||
let result = perform_param_expansion("unset:=assigned").unwrap();
|
||||
assert_eq!(result, "assigned");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
v.set_var("set_var", "value", false);
|
||||
});
|
||||
let result = perform_param_expansion("unset:=assigned").unwrap();
|
||||
assert_eq!(result, "assigned");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_expansion_setdefaultunset() {
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
v.set_var("set_var", "value", false);
|
||||
});
|
||||
let result = perform_param_expansion("unset=assigned").unwrap();
|
||||
assert_eq!(result, "assigned");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
v.set_var("set_var", "value", false);
|
||||
});
|
||||
let result = perform_param_expansion("unset=assigned").unwrap();
|
||||
assert_eq!(result, "assigned");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_expansion_altsetnotnull() {
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
v.set_var("set_var", "value", false);
|
||||
});
|
||||
let result = perform_param_expansion("set_var:+alt").unwrap();
|
||||
assert_eq!(result, "alt");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
v.set_var("set_var", "value", false);
|
||||
});
|
||||
let result = perform_param_expansion("set_var:+alt").unwrap();
|
||||
assert_eq!(result, "alt");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_expansion_altnotnull() {
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
v.set_var("set_var", "value", false);
|
||||
});
|
||||
let result = perform_param_expansion("set_var+alt").unwrap();
|
||||
assert_eq!(result, "alt");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
v.set_var("set_var", "value", false);
|
||||
});
|
||||
let result = perform_param_expansion("set_var+alt").unwrap();
|
||||
assert_eq!(result, "alt");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_expansion_len() {
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("#foo").unwrap();
|
||||
assert_eq!(result, "3");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("#foo").unwrap();
|
||||
assert_eq!(result, "3");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_expansion_substr() {
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo:1").unwrap();
|
||||
assert_eq!(result, "oo");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo:1").unwrap();
|
||||
assert_eq!(result, "oo");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_expansion_substrlen() {
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo:0:2").unwrap();
|
||||
assert_eq!(result, "fo");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo:0:2").unwrap();
|
||||
assert_eq!(result, "fo");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_expansion_remshortestprefix() {
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo#f*").unwrap();
|
||||
assert_eq!(result, "oo");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo#f*").unwrap();
|
||||
assert_eq!(result, "oo");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_expansion_remlongestprefix() {
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo##f*").unwrap();
|
||||
assert_eq!(result, "");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo##f*").unwrap();
|
||||
assert_eq!(result, "");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_expansion_remshortestsuffix() {
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo%*o").unwrap();
|
||||
assert_eq!(result, "fo");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo%*o").unwrap();
|
||||
assert_eq!(result, "fo");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_expansion_remlongestsuffix() {
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo%%*o").unwrap();
|
||||
assert_eq!(result, "");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo%%*o").unwrap();
|
||||
assert_eq!(result, "");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_expansion_replacefirstmatch() {
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo/foo/X").unwrap();
|
||||
assert_eq!(result, "X");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo/foo/X").unwrap();
|
||||
assert_eq!(result, "X");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_expansion_replaceallmatches() {
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo//o/X").unwrap();
|
||||
assert_eq!(result, "fXX");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo//o/X").unwrap();
|
||||
assert_eq!(result, "fXX");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_expansion_replaceprefix() {
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo/#f/X").unwrap();
|
||||
assert_eq!(result, "Xoo");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo/#f/X").unwrap();
|
||||
assert_eq!(result, "Xoo");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_expansion_replacesuffix() {
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo/%o/X").unwrap();
|
||||
assert_eq!(result, "foX");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
write_vars(|v| {
|
||||
v.set_var("foo", "foo", false);
|
||||
});
|
||||
let result = perform_param_expansion("foo/%o/X").unwrap();
|
||||
assert_eq!(result, "foX");
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
}
|
||||
|
||||
@@ -6,32 +6,44 @@ use super::super::*;
|
||||
|
||||
#[test]
|
||||
fn getopt_from_argv() {
|
||||
let node = get_nodes("echo -n -e foo", |node| matches!(node.class, NdRule::Command {..}))
|
||||
.pop()
|
||||
.unwrap();
|
||||
let NdRule::Command { assignments: _, argv } = node.class else {
|
||||
panic!()
|
||||
};
|
||||
let node = get_nodes("echo -n -e foo", |node| {
|
||||
matches!(node.class, NdRule::Command { .. })
|
||||
})
|
||||
.pop()
|
||||
.unwrap();
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
argv,
|
||||
} = node.class
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
|
||||
let (words,opts) = get_opts_from_tokens(argv);
|
||||
insta::assert_debug_snapshot!(words);
|
||||
insta::assert_debug_snapshot!(opts)
|
||||
let (words, opts) = get_opts_from_tokens(argv);
|
||||
insta::assert_debug_snapshot!(words);
|
||||
insta::assert_debug_snapshot!(opts)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn getopt_simple() {
|
||||
let raw = "echo -n foo".split_whitespace().map(|s| s.to_string()).collect::<Vec<_>>();
|
||||
let raw = "echo -n foo"
|
||||
.split_whitespace()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (words,opts) = get_opts(raw);
|
||||
insta::assert_debug_snapshot!(words);
|
||||
insta::assert_debug_snapshot!(opts);
|
||||
let (words, opts) = get_opts(raw);
|
||||
insta::assert_debug_snapshot!(words);
|
||||
insta::assert_debug_snapshot!(opts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn getopt_multiple_short() {
|
||||
let raw = "echo -nre foo".split_whitespace().map(|s| s.to_string()).collect::<Vec<_>>();
|
||||
let raw = "echo -nre foo"
|
||||
.split_whitespace()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (words,opts) = get_opts(raw);
|
||||
insta::assert_debug_snapshot!(words);
|
||||
insta::assert_debug_snapshot!(opts);
|
||||
let (words, opts) = get_opts(raw);
|
||||
insta::assert_debug_snapshot!(words);
|
||||
insta::assert_debug_snapshot!(opts);
|
||||
}
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
use super::*;
|
||||
#[test]
|
||||
fn lex_simple() {
|
||||
let input = "echo hello world";
|
||||
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
|
||||
let input = "echo hello world";
|
||||
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(tokens)
|
||||
insta::assert_debug_snapshot!(tokens)
|
||||
}
|
||||
#[test]
|
||||
fn lex_redir() {
|
||||
let input = "echo foo > bar.txt";
|
||||
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
|
||||
let input = "echo foo > bar.txt";
|
||||
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(tokens)
|
||||
insta::assert_debug_snapshot!(tokens)
|
||||
}
|
||||
#[test]
|
||||
fn lex_redir_fds() {
|
||||
let input = "echo foo 1>&2";
|
||||
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
|
||||
let input = "echo foo 1>&2";
|
||||
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(tokens)
|
||||
insta::assert_debug_snapshot!(tokens)
|
||||
}
|
||||
#[test]
|
||||
fn lex_quote_str() {
|
||||
let input = "echo \"foo bar\" biz baz";
|
||||
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
|
||||
let input = "echo \"foo bar\" biz baz";
|
||||
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(tokens)
|
||||
insta::assert_debug_snapshot!(tokens)
|
||||
}
|
||||
#[test]
|
||||
fn lex_with_keywords() {
|
||||
let input = "if true; then echo foo; fi";
|
||||
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
|
||||
let input = "if true; then echo foo; fi";
|
||||
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(tokens)
|
||||
insta::assert_debug_snapshot!(tokens)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lex_multiline() {
|
||||
let input = "echo hello world\necho foo bar\necho boo biz";
|
||||
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
|
||||
let input = "echo hello world\necho foo bar\necho boo biz";
|
||||
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(tokens)
|
||||
insta::assert_debug_snapshot!(tokens)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lex_case() {
|
||||
let input = "case $foo in foo) bar;; bar) foo;; biz) baz;; esac";
|
||||
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
|
||||
let input = "case $foo in foo) bar;; bar) foo;; biz) baz;; esac";
|
||||
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(tokens)
|
||||
insta::assert_debug_snapshot!(tokens)
|
||||
}
|
||||
|
||||
@@ -1,51 +1,41 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use crate::libsh::error::{
|
||||
Note, ShErr, ShErrKind
|
||||
};
|
||||
use crate::expand::{expand_aliases, unescape_str};
|
||||
use crate::libsh::error::{Note, ShErr, ShErrKind};
|
||||
use crate::parse::{
|
||||
node_operation, Node, NdRule, ParseStream,
|
||||
lex::{
|
||||
Tk, TkRule, LexFlags, LexStream
|
||||
}
|
||||
};
|
||||
use crate::expand::{
|
||||
expand_aliases, unescape_str
|
||||
};
|
||||
use crate::state::{
|
||||
write_logic, write_vars
|
||||
lex::{LexFlags, LexStream, Tk, TkRule},
|
||||
node_operation, NdRule, Node, ParseStream,
|
||||
};
|
||||
use crate::state::{write_logic, write_vars};
|
||||
|
||||
|
||||
pub mod error;
|
||||
pub mod expand;
|
||||
pub mod getopt;
|
||||
pub mod highlight;
|
||||
pub mod lexer;
|
||||
pub mod parser;
|
||||
pub mod expand;
|
||||
pub mod term;
|
||||
pub mod error;
|
||||
pub mod getopt;
|
||||
pub mod script;
|
||||
pub mod highlight;
|
||||
pub mod readline;
|
||||
pub mod script;
|
||||
pub mod term;
|
||||
|
||||
/// Unsafe to use outside of tests
|
||||
pub fn get_nodes<F1>(input: &str, filter: F1) -> Vec<Node>
|
||||
where
|
||||
F1: Fn(&Node) -> bool
|
||||
where
|
||||
F1: Fn(&Node) -> bool,
|
||||
{
|
||||
let mut nodes = vec![];
|
||||
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let mut parsed_nodes = ParseStream::new(tokens)
|
||||
.map(|nd| nd.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let mut nodes = vec![];
|
||||
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let mut parsed_nodes = ParseStream::new(tokens)
|
||||
.map(|nd| nd.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for node in parsed_nodes.iter_mut() {
|
||||
node_operation(node,
|
||||
&filter,
|
||||
&mut |node: &mut Node| nodes.push(node.clone())
|
||||
);
|
||||
}
|
||||
nodes
|
||||
for node in parsed_nodes.iter_mut() {
|
||||
node_operation(node, &filter, &mut |node: &mut Node| {
|
||||
nodes.push(node.clone())
|
||||
});
|
||||
}
|
||||
nodes
|
||||
}
|
||||
|
||||
@@ -2,95 +2,95 @@ use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_simple() {
|
||||
let input = "echo hello world";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
let input = "echo hello world";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_pipeline() {
|
||||
let input = "echo foo | sed s/foo/bar";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
let input = "echo foo | sed s/foo/bar";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_conjunction() {
|
||||
let input = "echo foo && echo bar";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
let input = "echo foo && echo bar";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_conjunction_and_pipeline() {
|
||||
let input = "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
let input = "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_multiline() {
|
||||
let input = "
|
||||
let input = "
|
||||
echo hello world
|
||||
echo foo bar
|
||||
echo boo biz";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_if_simple() {
|
||||
let input = "if foo; then echo bar; fi";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
let input = "if foo; then echo bar; fi";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
}
|
||||
#[test]
|
||||
fn parse_if_with_elif() {
|
||||
let input = "if foo; then echo bar; elif bar; then echo foo; fi";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
let input = "if foo; then echo bar; elif bar; then echo foo; fi";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
}
|
||||
#[test]
|
||||
fn parse_if_multiple_elif() {
|
||||
let input = "if foo; then echo bar; elif bar; then echo foo; elif biz; then echo baz; fi";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
let input = "if foo; then echo bar; elif bar; then echo foo; elif biz; then echo baz; fi";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
}
|
||||
#[test]
|
||||
fn parse_if_multiline() {
|
||||
let input = "
|
||||
let input = "
|
||||
if foo; then
|
||||
echo bar
|
||||
elif bar; then
|
||||
@@ -98,59 +98,59 @@ elif bar; then
|
||||
elif biz; then
|
||||
echo baz
|
||||
fi";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
}
|
||||
#[test]
|
||||
fn parse_loop_simple() {
|
||||
let input = "while foo; do bar; done";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
let input = "while foo; do bar; done";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
}
|
||||
#[test]
|
||||
fn parse_loop_until() {
|
||||
let input = "until foo; do bar; done";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
let input = "until foo; do bar; done";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
}
|
||||
#[test]
|
||||
fn parse_loop_multiline() {
|
||||
let input = "
|
||||
let input = "
|
||||
until foo; do
|
||||
bar
|
||||
done";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
}
|
||||
#[test]
|
||||
fn parse_case_simple() {
|
||||
let input = "case foo in foo) bar;; bar) foo;; biz) baz;; esac";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
let input = "case foo in foo) bar;; bar) foo;; biz) baz;; esac";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
}
|
||||
#[test]
|
||||
fn parse_case_multiline() {
|
||||
let input = "case foo in
|
||||
let input = "case foo in
|
||||
foo) bar
|
||||
;;
|
||||
bar) foo
|
||||
@@ -158,16 +158,16 @@ fn parse_case_multiline() {
|
||||
biz) baz
|
||||
;;
|
||||
esac";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
}
|
||||
#[test]
|
||||
fn parse_case_nested() {
|
||||
let input = "case foo in
|
||||
let input = "case foo in
|
||||
foo)
|
||||
if true; then
|
||||
while true; do
|
||||
@@ -194,41 +194,41 @@ fn parse_case_nested() {
|
||||
fi
|
||||
;;
|
||||
esac";
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
}
|
||||
#[test]
|
||||
fn parse_cursed() {
|
||||
let input = "if if if if case foo in foo) if true; then true; fi;; esac; then case foo in foo) until true; do true; done;; esac; fi; then until if case foo in foo) true;; esac; then if true; then true; fi; fi; do until until true; do true; done; do case foo in foo) true;; esac; done; done; fi; then until until case foo in foo) true;; esac; do if true; then true; fi; done; do until true; do true; done; done; fi; then until case foo in foo) case foo in foo) true;; esac;; esac; do if if true; then true; fi; then until true; do true; done; fi; done; elif until until case foo in foo) true;; esac; do if true; then true; fi; done; do case foo in foo) until true; do true; done;; esac; done; then case foo in foo) if case foo in foo) true;; esac; then if true; then true; fi; fi;; esac; else case foo in foo) until until true; do true; done; do case foo in foo) true;; esac; done;; esac; fi";
|
||||
let input = "if if if if case foo in foo) if true; then true; fi;; esac; then case foo in foo) until true; do true; done;; esac; fi; then until if case foo in foo) true;; esac; then if true; then true; fi; fi; do until until true; do true; done; do case foo in foo) true;; esac; done; done; fi; then until until case foo in foo) true;; esac; do if true; then true; fi; done; do until true; do true; done; done; fi; then until case foo in foo) case foo in foo) true;; esac;; esac; do if if true; then true; fi; then until true; do true; done; fi; done; elif until until case foo in foo) true;; esac; do if true; then true; fi; done; do case foo in foo) until true; do true; done;; esac; done; then case foo in foo) if case foo in foo) true;; esac; then if true; then true; fi; fi;; esac; else case foo in foo) until until true; do true; done; do case foo in foo) true;; esac; done;; esac; fi";
|
||||
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
|
||||
|
||||
// 15,000 line snapshot file btw
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
// 15,000 line snapshot file btw
|
||||
insta::assert_debug_snapshot!(nodes)
|
||||
}
|
||||
#[test]
|
||||
fn test_node_operation() {
|
||||
let input = String::from("echo hello world; echo foo bar");
|
||||
let mut check_nodes = vec![];
|
||||
let tokens: Vec<Tk> = LexStream::new(input.into(), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
let input = String::from("echo hello world; echo foo bar");
|
||||
let mut check_nodes = vec![];
|
||||
let tokens: Vec<Tk> = LexStream::new(input.into(), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect();
|
||||
|
||||
let nodes = ParseStream::new(tokens)
|
||||
.map(|nd| nd.unwrap());
|
||||
let nodes = ParseStream::new(tokens).map(|nd| nd.unwrap());
|
||||
|
||||
for mut node in nodes {
|
||||
node_operation(&mut node,
|
||||
&|node: &Node| matches!(node.class, NdRule::Command {..}),
|
||||
&mut |node: &mut Node| check_nodes.push(node.clone()),
|
||||
);
|
||||
}
|
||||
insta::assert_debug_snapshot!(check_nodes)
|
||||
for mut node in nodes {
|
||||
node_operation(
|
||||
&mut node,
|
||||
&|node: &Node| matches!(node.class, NdRule::Command { .. }),
|
||||
&mut |node: &mut Node| check_nodes.push(node.clone()),
|
||||
);
|
||||
}
|
||||
insta::assert_debug_snapshot!(check_nodes)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,49 +4,48 @@ use pretty_assertions::assert_eq;
|
||||
|
||||
use super::super::*;
|
||||
fn get_script_output(name: &str, args: &[&str]) -> Output {
|
||||
// Resolve the path to the fern binary.
|
||||
// Do not question me.
|
||||
let mut fern_path = env::current_exe()
|
||||
.expect("Failed to get test executable"); // The path to the test executable
|
||||
fern_path.pop(); // Hocus pocus
|
||||
fern_path.pop();
|
||||
fern_path.push("fern"); // Abra Kadabra
|
||||
// Resolve the path to the fern binary.
|
||||
// Do not question me.
|
||||
let mut fern_path = env::current_exe().expect("Failed to get test executable"); // The path to the test executable
|
||||
fern_path.pop(); // Hocus pocus
|
||||
fern_path.pop();
|
||||
fern_path.push("fern"); // Abra Kadabra
|
||||
|
||||
if !fern_path.is_file() {
|
||||
fern_path.pop();
|
||||
fern_path.pop();
|
||||
fern_path.push("release");
|
||||
fern_path.push("fern");
|
||||
}
|
||||
if !fern_path.is_file() {
|
||||
fern_path.pop();
|
||||
fern_path.pop();
|
||||
fern_path.push("release");
|
||||
fern_path.push("fern");
|
||||
}
|
||||
|
||||
if !fern_path.is_file() {
|
||||
panic!("where the hell is the binary")
|
||||
}
|
||||
if !fern_path.is_file() {
|
||||
panic!("where the hell is the binary")
|
||||
}
|
||||
|
||||
process::Command::new(fern_path) // Alakazam
|
||||
.arg(name)
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("Failed to run script")
|
||||
process::Command::new(fern_path) // Alakazam
|
||||
.arg(name)
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("Failed to run script")
|
||||
}
|
||||
#[test]
|
||||
fn script_hello_world() {
|
||||
let output = get_script_output("./test_scripts/hello.sh", &[]);
|
||||
assert!(output.status.success());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert_eq!(stdout.trim(), "Hello, World!")
|
||||
let output = get_script_output("./test_scripts/hello.sh", &[]);
|
||||
assert!(output.status.success());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert_eq!(stdout.trim(), "Hello, World!")
|
||||
}
|
||||
#[test]
|
||||
fn script_cmdsub() {
|
||||
let output = get_script_output("./test_scripts/cmdsub.sh", &[]);
|
||||
assert!(output.status.success());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert_eq!(stdout.trim(), "foo Hello bar")
|
||||
let output = get_script_output("./test_scripts/cmdsub.sh", &[]);
|
||||
assert!(output.status.success());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert_eq!(stdout.trim(), "foo Hello bar")
|
||||
}
|
||||
#[test]
|
||||
fn script_multiline() {
|
||||
let output = get_script_output("./test_scripts/multiline.sh", &[]);
|
||||
assert!(output.status.success());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert_eq!(stdout.trim(), "foo\nbar\nbiz\nbuzz")
|
||||
let output = get_script_output("./test_scripts/multiline.sh", &[]);
|
||||
assert!(output.status.success());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert_eq!(stdout.trim(), "foo\nbar\nbiz\nbuzz")
|
||||
}
|
||||
|
||||
@@ -3,39 +3,41 @@ use libsh::term::{Style, StyleSet, Styled};
|
||||
use super::super::*;
|
||||
#[test]
|
||||
fn styled_simple() {
|
||||
let input = "hello world";
|
||||
let styled = input.styled(Style::Green);
|
||||
let input = "hello world";
|
||||
let styled = input.styled(Style::Green);
|
||||
|
||||
insta::assert_snapshot!(styled)
|
||||
insta::assert_snapshot!(styled)
|
||||
}
|
||||
#[test]
|
||||
fn styled_multiple() {
|
||||
let input = "styled text";
|
||||
let styled = input.styled(Style::Red | Style::Bold | Style::Underline);
|
||||
insta::assert_snapshot!(styled);
|
||||
let input = "styled text";
|
||||
let styled = input.styled(Style::Red | Style::Bold | Style::Underline);
|
||||
insta::assert_snapshot!(styled);
|
||||
}
|
||||
#[test]
|
||||
fn styled_rgb() {
|
||||
let input = "RGB styled text";
|
||||
let styled = input.styled(Style::RGB(255, 99, 71)); // Tomato color
|
||||
insta::assert_snapshot!(styled);
|
||||
let input = "RGB styled text";
|
||||
let styled = input.styled(Style::RGB(255, 99, 71)); // Tomato color
|
||||
insta::assert_snapshot!(styled);
|
||||
}
|
||||
#[test]
|
||||
fn styled_background() {
|
||||
let input = "text with background";
|
||||
let styled = input.styled(Style::BgBlue | Style::Bold);
|
||||
insta::assert_snapshot!(styled);
|
||||
let input = "text with background";
|
||||
let styled = input.styled(Style::BgBlue | Style::Bold);
|
||||
insta::assert_snapshot!(styled);
|
||||
}
|
||||
#[test]
|
||||
fn styled_set() {
|
||||
let input = "multi-style text";
|
||||
let style_set = StyleSet::new().add_style(Style::Magenta).add_style(Style::Italic);
|
||||
let styled = input.styled(style_set);
|
||||
insta::assert_snapshot!(styled);
|
||||
let input = "multi-style text";
|
||||
let style_set = StyleSet::new()
|
||||
.add_style(Style::Magenta)
|
||||
.add_style(Style::Italic);
|
||||
let styled = input.styled(style_set);
|
||||
insta::assert_snapshot!(styled);
|
||||
}
|
||||
#[test]
|
||||
fn styled_reset() {
|
||||
let input = "reset test";
|
||||
let styled = input.styled(Style::Bold | Style::Reset);
|
||||
insta::assert_snapshot!(styled);
|
||||
let input = "reset test";
|
||||
let styled = input.styled(Style::Bold | Style::Reset);
|
||||
insta::assert_snapshot!(styled);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user