Initial commit for fern

This commit is contained in:
2025-03-02 16:26:28 -05:00
parent 56917524c3
commit a9a9642a2a
40 changed files with 5281 additions and 0 deletions

31
.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
target
**/*.rs.bk
.idea
*.iml
/result*
*.log
default.nix
shell.nix
*~
TODO.md
rust-toolchain.toml
*snapshot*
# cachix tmp file
store-path-pre-build
# Devenv
.devenv*
devenv.local.nix
# direnv
.direnv
# pre-commit
.pre-commit-config.yaml
template/flake.lock
ideas.md
roadmap.md
README.md
file.*

324
Cargo.lock generated Normal file
View File

@@ -0,0 +1,324 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bitflags"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "clipboard-win"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892"
dependencies = [
"error-code",
]
[[package]]
name = "endian-type"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
[[package]]
name = "errno"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "error-code"
version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f"
[[package]]
name = "fd-lock"
version = "4.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947"
dependencies = [
"cfg-if",
"rustix",
"windows-sys 0.52.0",
]
[[package]]
name = "fern"
version = "0.1.0"
dependencies = [
"bitflags",
"nix",
"rustyline",
]
[[package]]
name = "home"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "libc"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "log"
version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "nibble_vec"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
dependencies = [
"smallvec",
]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "proc-macro2"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [
"proc-macro2",
]
[[package]]
name = "radix_trie"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
dependencies = [
"endian-type",
"nibble_vec",
]
[[package]]
name = "rustix"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
]
[[package]]
name = "rustyline"
version = "15.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ee1e066dc922e513bda599c6ccb5f3bb2b0ea5870a579448f2622993f0a9a2f"
dependencies = [
"bitflags",
"cfg-if",
"clipboard-win",
"fd-lock",
"home",
"libc",
"log",
"memchr",
"nix",
"radix_trie",
"rustyline-derive",
"unicode-segmentation",
"unicode-width",
"utf8parse",
"windows-sys 0.59.0",
]
[[package]]
name = "rustyline-derive"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327e9d075f6df7e25fbf594f1be7ef55cf0d567a6cb5112eeccbbd51ceb48e0d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "smallvec"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
[[package]]
name = "syn"
version = "2.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

15
Cargo.toml Normal file
View File

@@ -0,0 +1,15 @@
[package]
name = "fern"
description = "A linux shell written in rust"
publish = false
version = "0.1.0"
edition = "2021"
[profile.release]
debug = true
[dependencies]
bitflags = "2.8.0"
nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl"] }
rustyline = { version = "15.0.0", features = [ "derive" ] }

27
flake.lock generated Normal file
View File

@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1737746512,
"narHash": "sha256-nU6AezEX4EuahTO1YopzueAXfjFfmCHylYEFCagduHU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "825479c345a7f806485b7f00dbe3abb50641b083",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

60
flake.nix Normal file
View File

@@ -0,0 +1,60 @@
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { self, nixpkgs }:
let
pkgs = import nixpkgs {
system = "x86_64-linux"; # Replace with your target system if necessary
};
slashBuild = pkgs.rustPlatform.buildRustPackage rec {
pname = "slash";
version = "v0.2.0";
src = pkgs.fetchFromGitHub {
owner = "pagedMov";
repo = "slash";
rev = "65a7a713a954c0f3fba668c6d7e0cdd023f705f7";
hash = "sha256-AVBDv0HQn7hAGo0tW1ZFCdfO4+3VJQ0mCDkow8skD7U=";
};
doCheck = false; # TODO: Find a way to make tests work
cargoHash = "sha256-lekH6AESWpKjp6mCW7KgN6ACNcG8bHAg4Pu4OXhGJ3Y=";
nativeBuildInputs = [
pkgs.openssl
pkgs.openssl.dev
pkgs.pkg-config
];
PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig";
passthru = {
shellPath = "/bin/slash";
};
};
in
{
packages.${pkgs.system}.default = slashBuild;
devShells.default = pkgs.mkShell {
nativeBuildInputs = [
pkgs.rust-bin.stable.latest.default
pkgs.gcc
pkgs.clang
pkgs.pkg-config
pkgs.libgit2
pkgs.libssh2
pkgs.libssh2.dev
pkgs.openssl
pkgs.openssl.dev
pkgs.llvm
pkgs.libclang
pkgs.pam
];
shellHook = ''
exec slash
'';
};
};
}

View File

@@ -0,0 +1,25 @@
{ inputs, lib, ... }: {
imports = [
inputs.devshell.flakeModule
];
config.perSystem =
{ pkgs
, ...
}: {
config.devshells.default = {
imports = [
"${inputs.devshell}/extra/language/c.nix"
# "${inputs.devshell}/extra/language/rust.nix"
];
commands = with pkgs; [
{ package = rust-toolchain; category = "rust"; }
];
language.c = {
libraries = lib.optional pkgs.stdenv.isDarwin pkgs.libiconv;
};
};
};
}

View File

@@ -0,0 +1,18 @@
{ inputs, ... }:
let
overlays = [
(import inputs.rust-overlay)
(self: super: assert !(super ? rust-toolchain); rec {
rust-toolchain = super.rust-bin.fromRustupToolchainFile ../../rust-toolchain.toml;
# buildRustCrate/crate2nix depend on this.
rustc = rust-toolchain;
cargo = rust-toolchain;
})
];
in
{
perSystem = { system, ... }: {
_module.args.pkgs = import inputs.nixpkgs { inherit system overlays; config = { }; };
};
}

15
src/builtin/cd.rs Normal file
View File

@@ -0,0 +1,15 @@
use crate::{parse::parse::{Node, NdRule}, prelude::*};
pub fn cd(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::Command { argv, redirs: _ } = rule {
let mut argv_iter = argv.into_iter();
argv_iter.next(); // Ignore 'cd'
let dir_raw = argv_iter.next().map(|arg| arg.to_string()).unwrap_or(std::env::var("HOME")?);
let dir = PathBuf::from(&dir_raw);
std::env::set_current_dir(dir)?;
shenv.vars_mut().export("PWD",&dir_raw);
shenv.set_code(0);
}
Ok(())
}

53
src/builtin/echo.rs Normal file
View File

@@ -0,0 +1,53 @@
use shellenv::jobs::{ChildProc, JobBldr};
use crate::{libsh::utils::ArgVec, parse::parse::{Node, NdRule}, prelude::*};
pub fn echo(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::Command { argv, redirs } = rule {
let argv = argv.drop_first().as_strings(shenv);
let mut formatted = argv.join(" ");
formatted.push('\n');
if shenv.ctx().flags().contains(ExecFlags::NO_FORK) {
shenv.collect_redirs(redirs);
if let Err(e) = shenv.ctx_mut().activate_rdrs() {
eprintln!("{:?}",e);
exit(1);
}
if let Err(e) = write_out(formatted) {
eprintln!("{:?}",e);
exit(1);
}
exit(0);
} else {
match unsafe { fork()? } {
Child => {
shenv.collect_redirs(redirs);
if let Err(e) = shenv.ctx_mut().activate_rdrs() {
eprintln!("{:?}",e);
exit(1);
}
if let Err(e) = write_out(formatted) {
eprintln!("{:?}",e);
exit(1);
}
exit(0);
}
Parent { child } => {
shenv.reset_io()?;
let children = vec![
ChildProc::new(child, Some("echo"), Some(child))?
];
let job = JobBldr::new()
.with_children(children)
.with_pgid(child)
.build();
wait_fg(job, shenv)?;
}
}
}
} else { unreachable!() }
Ok(())
}

18
src/builtin/export.rs Normal file
View File

@@ -0,0 +1,18 @@
use crate::prelude::*;
pub fn export(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::Command { argv, redirs: _ } = rule {
let mut argv_iter = argv.into_iter();
argv_iter.next(); // Ignore 'export'
while let Some(arg) = argv_iter.next() {
let arg_raw = arg.to_string();
if let Some((var,val)) = arg_raw.split_once('=') {
shenv.vars_mut().export(var, val);
} else {
eprintln!("Expected an assignment in export args, found this: {}", arg_raw)
}
}
} else { unreachable!() }
Ok(())
}

127
src/builtin/jobctl.rs Normal file
View File

@@ -0,0 +1,127 @@
use shellenv::jobs::JobCmdFlags;
use crate::prelude::*;
pub fn continue_job(node: Node, shenv: &mut ShEnv, fg: bool) -> ShResult<()> {
let blame = node.span();
let cmd = if fg { "fg" } else { "bg" };
let rule = node.into_rule();
if let NdRule::Command { argv, redirs } = rule {
let mut argv_s = argv.drop_first().as_strings(shenv).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
)
)
}
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".into(), blame))
};
let tabid = match argv_s.next() {
Some(arg) => parse_job_id(&arg, blame.clone())?,
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))
}
})?;
job.killpg(Signal::SIGCONT)?;
if fg {
write_jobs(|j| j.new_fg(job))?;
} else {
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))?;
}
shenv.set_code(0);
} else { unreachable!() }
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()".into(),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());
}
None
});
match result {
Some(id) => Ok(id),
None => Err(ShErr::full(ShErrKind::InternalErr,"Found a job but no table id in parse_job_id()".into(),blame))
}
} else {
Err(ShErr::full(ShErrKind::SyntaxErr,format!("Invalid fd arg: {}", arg),blame))
}
}
pub fn jobs(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::Command { argv, redirs } = rule {
let mut argv = argv.drop_first().into_iter();
let mut flags = JobCmdFlags::empty();
while let Some(arg) = argv.next() {
let arg_s = arg.to_string();
let mut chars = arg_s.chars().peekable();
if chars.peek().is_none_or(|ch| *ch != '-') {
return Err(ShErr::full(ShErrKind::SyntaxErr, "Invalid flag in jobs call".into(), arg.span().clone()))
}
chars.next();
while let Some(ch) = chars.next() {
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".into(), arg.span().clone()))
};
flags |= flag
}
}
read_jobs(|j| j.print_jobs(flags))?;
shenv.set_code(0);
} else { unreachable!() }
Ok(())
}

17
src/builtin/mod.rs Normal file
View File

@@ -0,0 +1,17 @@
pub mod echo;
pub mod cd;
pub mod pwd;
pub mod export;
pub mod jobctl;
pub mod read;
pub const BUILTINS: [&str;8] = [
"echo",
"cd",
"pwd",
"export",
"fg",
"bg",
"jobs",
"read"
];

50
src/builtin/pwd.rs Normal file
View File

@@ -0,0 +1,50 @@
use shellenv::jobs::{ChildProc, JobBldr};
use crate::{parse::parse::{NdFlag, Node, NdRule}, prelude::*};
pub fn pwd(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::Command { argv: _, redirs } = rule {
let mut pwd = shenv.vars().get_var("PWD").to_string();
pwd.push('\n');
if shenv.ctx().flags().contains(ExecFlags::NO_FORK) {
shenv.collect_redirs(redirs);
if let Err(e) = shenv.ctx_mut().activate_rdrs() {
eprintln!("{:?}",e);
exit(1);
}
if let Err(e) = write_out(pwd) {
eprintln!("{:?}",e);
exit(1);
}
exit(0);
} else {
match unsafe { fork()? } {
Child => {
if let Err(e) = shenv.ctx_mut().activate_rdrs() {
eprintln!("{:?}",e);
exit(1);
}
if let Err(e) = write_out(pwd) {
eprintln!("{:?}",e);
exit(1);
}
exit(0);
}
Parent { child } => {
shenv.reset_io()?;
let children = vec![
ChildProc::new(child, Some("echo"), Some(child))?
];
let job = JobBldr::new()
.with_children(children)
.with_pgid(child)
.build();
wait_fg(job, shenv)?;
}
}
}
} else { unreachable!() }
Ok(())
}

39
src/builtin/read.rs Normal file
View File

@@ -0,0 +1,39 @@
use crate::prelude::*;
pub fn read_builtin(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::Command { argv, redirs: _ } = rule {
let argv = argv.drop_first();
let mut argv_iter = argv.iter();
// TODO: properly implement redirections
// using activate_redirs() was causing issues, may require manual handling
let mut buf = vec![0u8; 1024];
let bytes_read = read(0, &mut buf)?;
buf.truncate(bytes_read);
let read_input = String::from_utf8_lossy(&buf).trim_end().to_string();
if let Some(var) = argv_iter.next() {
/*
let words: Vec<&str> = read_input.split_whitespace().collect();
for (var, value) in argv_iter.zip(words.iter().chain(std::iter::repeat(&""))) {
shenv.vars_mut().set_var(&var.to_string(), value);
}
// Assign the rest of the string to the first variable if there's only one
if argv.len() == 1 {
shenv.vars_mut().set_var(&first_var.to_string(), &read_input);
}
*/
shenv.vars_mut().set_var(&var.to_string(), &read_input);
}
} else {
unreachable!()
}
log!(TRACE, "leaving read");
shenv.set_code(0);
Ok(())
}

344
src/execute.rs Normal file
View File

@@ -0,0 +1,344 @@
use std::os::fd::AsRawFd;
use shellenv::jobs::{ChildProc, JobBldr};
use crate::{builtin::export::export, libsh::{error::Blame, sys::{execvpe, get_bin_path}, utils::{ArgVec, StrOps}}, parse::{lex::Token, parse::{CmdGuard, NdFlag, Node, NdRule, SynTree}}, prelude::*};
pub struct Executor<'a> {
ast: SynTree,
shenv: &'a mut ShEnv
}
impl<'a> Executor<'a> {
pub fn new(ast: SynTree, shenv: &'a mut ShEnv) -> Self {
Self { ast, shenv }
}
pub fn walk(&mut self) -> ShResult<()> {
log!(DEBUG, "Starting walk");
while let Some(node) = self.ast.next_node() {
let span = node.span();
if let NdRule::CmdList { cmds } = node.clone().into_rule() {
log!(TRACE, "{:?}", cmds);
exec_list(cmds, self.shenv).try_blame(span)?
} else { unreachable!() }
}
Ok(())
}
}
fn exec_list(list: Vec<(Option<CmdGuard>, Node)>, shenv: &mut ShEnv) -> ShResult<()> {
log!(DEBUG, "Executing list");
let mut list = VecDeque::from(list);
while let Some(cmd_info) = list.fpop() {
let guard = cmd_info.0;
let cmd = cmd_info.1;
let span = cmd.span();
if let Some(guard) = guard {
let code = shenv.get_code();
match guard {
CmdGuard::And => {
if code != 0 { break; }
}
CmdGuard::Or => {
if code == 0 { break; }
}
}
}
log!(TRACE, "{:?}", *cmd.rule());
match *cmd.rule() {
NdRule::Command {..} if cmd.flags().contains(NdFlag::BUILTIN) => exec_builtin(cmd,shenv).try_blame(span)?,
NdRule::Command {..} => exec_cmd(cmd,shenv).try_blame(span)?,
NdRule::Subshell {..} => exec_subshell(cmd,shenv).try_blame(span)?,
NdRule::Assignment {..} => exec_assignment(cmd,shenv).try_blame(span)?,
NdRule::Pipeline {..} => exec_pipeline(cmd, shenv).try_blame(span)?,
_ => unimplemented!()
}
}
Ok(())
}
fn exec_subshell(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let snapshot = shenv.clone();
shenv.vars_mut().reset_params();
let rule = node.into_rule();
if let NdRule::Subshell { body, argv, redirs } = rule {
if shenv.ctx().flags().contains(ExecFlags::NO_FORK) {
shenv.ctx_mut().unset_flag(ExecFlags::NO_FORK); // Allow sub-forks in this case
shenv.collect_redirs(redirs);
if let Err(e) = shenv.ctx_mut().activate_rdrs() {
write_err(e)?;
exit(1);
}
for arg in argv {
shenv.vars_mut().bpush_arg(&arg.to_string());
}
let body_raw = body.to_string();
let lexer_input = Rc::new(
body_raw[1..body_raw.len() - 1].to_string()
);
let token_stream = Lexer::new(lexer_input).lex();
match Parser::new(token_stream).parse() {
Ok(syn_tree) => {
if let Err(e) = Executor::new(syn_tree, shenv).walk() {
write_err(e)?;
exit(1);
}
}
Err(e) => {
write_err(e)?;
exit(1);
}
}
exit(0);
} else {
match unsafe { fork()? } {
Child => {
shenv.collect_redirs(redirs);
if let Err(e) = shenv.ctx_mut().activate_rdrs() {
write_err(e)?;
exit(1);
}
for arg in argv {
shenv.vars_mut().bpush_arg(&arg.to_string());
}
let body_raw = body.to_string();
let lexer_input = Rc::new(
body_raw[1..body_raw.len() - 1].to_string()
);
let token_stream = Lexer::new(lexer_input).lex();
match Parser::new(token_stream).parse() {
Ok(syn_tree) => {
if let Err(e) = Executor::new(syn_tree, shenv).walk() {
write_err(e)?;
exit(1);
}
}
Err(e) => {
write_err(e)?;
exit(1);
}
}
exit(0);
}
Parent { child } => {
*shenv = snapshot;
let children = vec![
ChildProc::new(child, Some("anonymous subshell"), Some(child))?
];
let job = JobBldr::new()
.with_children(children)
.with_pgid(child)
.build();
wait_fg(job, shenv)?;
}
}
}
} else { unreachable!() }
Ok(())
}
fn exec_builtin(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
log!(DEBUG, "Executing builtin");
let command = if let NdRule::Command { argv, redirs: _ } = node.rule() {
argv.first().unwrap().to_string()
} else { unreachable!() };
log!(TRACE, "{}", command.as_str());
match command.as_str() {
"echo" => echo(node, shenv)?,
"cd" => cd(node,shenv)?,
"pwd" => pwd(node, shenv)?,
"export" => export(node, shenv)?,
"jobs" => jobs(node, shenv)?,
"fg" => continue_job(node, shenv, true)?,
"bg" => continue_job(node, shenv, false)?,
"read" => read_builtin(node, shenv)?,
_ => unimplemented!("Have not yet implemented support for builtin `{}'",command)
}
log!(TRACE, "done");
Ok(())
}
fn exec_assignment(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
log!(DEBUG, "Executing assignment");
let rule = node.into_rule();
if let NdRule::Assignment { assignments, cmd } = rule {
log!(TRACE, "Assignments: {:?}", assignments);
log!(TRACE, "Command: {:?}", cmd);
let mut assigns = assignments.into_iter();
if let Some(cmd) = cmd {
while let Some(assign) = assigns.next() {
let assign_raw = assign.to_string();
if let Some((var,val)) = assign_raw.split_once('=') {
shenv.vars_mut().export(var, val);
}
}
if cmd.flags().contains(NdFlag::BUILTIN) {
exec_builtin(*cmd, shenv)?;
} else {
exec_cmd(*cmd, shenv)?;
}
} else {
while let Some(assign) = assigns.next() {
let assign_raw = assign.to_string();
if let Some((var,val)) = assign_raw.split_once('=') {
shenv.vars_mut().set_var(var, val);
}
}
}
} else { unreachable!() }
Ok(())
}
fn exec_pipeline(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
log!(DEBUG, "Executing pipeline");
let rule = node.into_rule();
if let NdRule::Pipeline { cmds } = rule {
let mut prev_rpipe: Option<i32> = None;
let mut cmds = VecDeque::from(cmds);
let mut pgid = None;
let mut cmd_names = vec![];
let mut pids = vec![];
while let Some(cmd) = cmds.pop_front() {
let (r_pipe, w_pipe) = if cmds.is_empty() {
// If we are on the last command, don't make new pipes
(None,None)
} else {
let (r_pipe, w_pipe) = c_pipe()?;
(Some(r_pipe),Some(w_pipe))
};
if let NdRule::Command { argv, redirs: _ } = cmd.rule() {
let cmd_name = argv.first().unwrap().span().get_slice().to_string();
cmd_names.push(cmd_name);
} else { unimplemented!() }
match unsafe { fork()? } {
Child => {
// Set NO_FORK since we are already in a fork, to prevent unnecessarily forking again
shenv.ctx_mut().set_flag(ExecFlags::NO_FORK);
// We close this r_pipe since it's the one the next command will use, so not useful here
if let Some(r_pipe) = r_pipe {
close(r_pipe.as_raw_fd())?;
}
// Create some redirections
if let Some(w_pipe) = w_pipe {
let wpipe_redir = Redir::new(1, RedirType::Output, RedirTarget::Fd(w_pipe.as_raw_fd()));
shenv.ctx_mut().push_rdr(wpipe_redir);
}
// Use the r_pipe created in the last iteration
if let Some(prev_rpipe) = prev_rpipe {
let rpipe_redir = Redir::new(0, RedirType::Input, RedirTarget::Fd(prev_rpipe.as_raw_fd()));
shenv.ctx_mut().push_rdr(rpipe_redir);
}
if cmd.flags().contains(NdFlag::BUILTIN) {
exec_builtin(cmd, shenv).unwrap();
} else {
exec_cmd(cmd, shenv).unwrap();
}
exit(0);
}
Parent { child } => {
// Close the write pipe out here to signal EOF
if let Some(w_pipe) = w_pipe {
close(w_pipe.as_raw_fd())?;
}
if pgid.is_none() {
pgid = Some(child);
}
pids.push(child);
prev_rpipe = r_pipe;
}
}
}
for (i,pid) in pids.iter().enumerate() {
let command = cmd_names.get(i).unwrap();
let children = vec![
ChildProc::new(*pid, Some(&command), pgid)?
];
let job = JobBldr::new()
.with_children(children)
.with_pgid(pgid.unwrap())
.build();
wait_fg(job, shenv)?;
}
} else { unreachable!() }
Ok(())
}
fn exec_cmd(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
log!(DEBUG, "Executing command");
let blame = node.span();
let rule = node.into_rule();
if let NdRule::Command { argv, redirs } = rule {
let (argv,envp) = prep_execve(argv, shenv);
let command = argv.first().unwrap().to_string();
if get_bin_path(&command, shenv).is_some() {
shenv.save_io()?;
if shenv.ctx().flags().contains(ExecFlags::NO_FORK) {
log!(TRACE, "Not forking");
shenv.collect_redirs(redirs);
if let Err(e) = shenv.ctx_mut().activate_rdrs() {
eprintln!("{:?}",e);
exit(1);
}
if let Err(errno) = execvpe(command, argv, envp) {
if errno != Errno::EFAULT {
exit(errno as i32);
}
}
} else {
log!(TRACE, "Forking");
match unsafe { fork()? } {
Child => {
log!(DEBUG, redirs);
shenv.collect_redirs(redirs);
if let Err(e) = shenv.ctx_mut().activate_rdrs() {
eprintln!("{:?}",e);
exit(1);
}
execvpe(command, argv, envp)?;
exit(1);
}
Parent { child } => {
let children = vec![
ChildProc::new(child, Some(&command), Some(child))?
];
let job = JobBldr::new()
.with_children(children)
.with_pgid(child)
.build();
log!(TRACE, "New job: {:?}", job);
wait_fg(job, shenv)?;
}
}
}
} else {
return Err(ShErr::full(ShErrKind::CmdNotFound, format!("{}", command), blame))
}
} else { unreachable!("Found this rule in exec_cmd: {:?}", rule) }
Ok(())
}
fn prep_execve(argv: Vec<Token>, shenv: &mut ShEnv) -> (Vec<String>, Vec<String>) {
log!(DEBUG, "Preparing execvpe args");
let argv_s = argv.as_strings(shenv);
log!(DEBUG, argv_s);
let mut envp = vec![];
let env_vars = shenv.vars().env().clone();
let mut entries = env_vars.iter().collect::<VecDeque<(&String,&String)>>();
while let Some(entry) = entries.fpop() {
let key = entry.0;
let val = entry.1;
let formatted = format!("{}={}",key,val);
envp.push(formatted);
}
log!(TRACE, argv_s);
(argv_s, envp)
}

63
src/expand/expand_vars.rs Normal file
View File

@@ -0,0 +1,63 @@
use crate::{parse::lex::Token, prelude::*};
pub fn expand_var(var_sub: Token, shenv: &mut ShEnv) -> Vec<Token> {
let var_name = var_sub.to_string();
let var_name = var_name.trim_start_matches('$').trim_matches(['{','}']);
let value = Rc::new(
shenv.vars()
.get_var(var_name)
.to_string()
);
Lexer::new(value).lex() // Automatically handles word splitting for us
}
pub fn expand_dquote(dquote: Token, shenv: &mut ShEnv) -> String {
let dquote_raw = dquote.to_string();
let mut result = String::new();
let mut var_name = String::new();
let mut chars = dquote_raw.chars();
let mut in_brace = false;
while let Some(ch) = chars.next() {
match ch {
'\\' => {
if let Some(next_ch) = chars.next() {
result.push(next_ch)
}
}
'"' => continue,
'$' => {
while let Some(ch) = chars.next() {
match ch {
'"' => continue,
'{' => {
in_brace = true;
}
'}' if in_brace => {
break
}
_ if ch.is_ascii_digit() && var_name.is_empty() && !in_brace => {
var_name.push(ch);
break
}
'@' | '#' | '*' | '-' | '?' | '!' | '$' if var_name.is_empty() => {
var_name.push(ch);
break
}
' ' | '\t' => {
break
}
_ => var_name.push(ch)
}
}
log!(TRACE, var_name);
let value = shenv.vars().get_var(&var_name);
log!(TRACE, value);
result.push_str(value);
}
_ => result.push(ch)
}
}
result
}

2
src/expand/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod expand_vars;
pub mod tilde;

11
src/expand/tilde.rs Normal file
View File

@@ -0,0 +1,11 @@
use crate::prelude::*;
pub fn expand_tilde(tilde_sub: Token) -> String {
let tilde_sub_raw = tilde_sub.to_string();
if tilde_sub_raw.starts_with('~') {
let home = std::env::var("HOME").unwrap_or_default();
tilde_sub_raw.replacen('~', &home, 1)
} else {
tilde_sub_raw
}
}

36
src/libsh/collections.rs Normal file
View File

@@ -0,0 +1,36 @@
use std::collections::VecDeque;
pub trait VecDequeAliases<T> {
fn fpop(&mut self) -> Option<T>;
fn fpush(&mut self, value: T);
fn bpop(&mut self) -> Option<T>;
fn bpush(&mut self, value: T);
fn to_vec(self) -> Vec<T>;
}
impl<T> VecDequeAliases<T> for VecDeque<T> {
/// Alias for pop_front()
fn fpop(&mut self) -> Option<T> {
self.pop_front()
}
/// Alias for push_front()
fn fpush(&mut self, value: T) {
self.push_front(value);
}
/// Alias for pop_back()
fn bpop(&mut self) -> Option<T> {
self.pop_back()
}
/// Alias for push_back()
fn bpush(&mut self, value: T) {
self.push_back(value);
}
/// Just turns the deque into a vector
fn to_vec(mut self) -> Vec<T> {
let mut vec = vec![];
while let Some(item) = self.fpop() {
vec.push(item)
}
vec
}
}

195
src/libsh/error.rs Normal file
View File

@@ -0,0 +1,195 @@
use std::fmt::Display;
use crate::parse::lex::Span;
use crate::prelude::*;
pub type ShResult<T> = Result<T,ShErr>;
pub trait Blame {
/// Blame a span for a propagated error. This will convert a ShErr::Simple into a ShErr::Full
/// This will also set the span on a ShErr::Builder
fn blame(self, span: Span) -> Self;
/// If an error is propagated to this point, then attempt to blame a span.
/// If the error in question has already blamed a span, don't overwrite it.
/// Used as a last resort in higher level contexts in case an error somehow goes unblamed
fn try_blame(self, span: Span) -> Self;
}
impl From<std::io::Error> for ShErr {
fn from(_: std::io::Error) -> Self {
ShErr::io()
}
}
impl From<std::env::VarError> for ShErr {
fn from(value: std::env::VarError) -> Self {
ShErr::simple(ShErrKind::InternalErr, &value.to_string())
}
}
impl From<rustyline::error::ReadlineError> for ShErr {
fn from(value: rustyline::error::ReadlineError) -> Self {
ShErr::simple(ShErrKind::ParseErr, &value.to_string())
}
}
impl From<Errno> for ShErr {
fn from(value: Errno) -> Self {
ShErr::simple(ShErrKind::Errno, &value.to_string())
}
}
impl<T> Blame for Result<T,ShErr> {
fn blame(self, span: Span) -> Self {
if let Err(mut e) = self {
e.blame(span);
Err(e)
} else {
self
}
}
fn try_blame(self, span: Span) -> Self {
if let Err(mut e) = self {
e.try_blame(span);
Err(e)
} else {
self
}
}
}
#[derive(Debug,Clone)]
pub enum ShErrKind {
IoErr,
SyntaxErr,
ParseErr,
InternalErr,
ExecFail,
Errno,
CmdNotFound,
CleanExit,
FuncReturn,
LoopContinue,
LoopBreak,
Null
}
impl Default for ShErrKind {
fn default() -> Self {
Self::Null
}
}
#[derive(Clone,Debug)]
pub enum ShErr {
Simple { kind: ShErrKind, message: String },
Full { kind: ShErrKind, message: String, span: Span },
}
impl ShErr {
pub fn simple(kind: ShErrKind, message: &str) -> Self {
Self::Simple { kind, message: message.to_string() }
}
pub fn io() -> Self {
io::Error::last_os_error().into()
}
pub fn full(kind: ShErrKind, message: String, span: Span) -> Self {
Self::Full { kind, message, span }
}
pub fn try_blame(&mut self, blame: Span) {
match self {
Self::Full {..} => {
/* Do not overwrite */
}
Self::Simple { kind, message } => {
*self = Self::Full { kind: core::mem::take(kind), message: core::mem::take(message), span: blame }
}
}
}
pub fn blame(&mut self, blame: Span) {
match self {
Self::Full { kind: _, message: _, span } => {
*span = blame;
}
Self::Simple { kind, message } => {
*self = Self::Full { kind: core::mem::take(kind), message: core::mem::take(message), span: blame }
}
}
}
pub fn with_msg(&mut self, new_message: String) {
match self {
Self::Full { kind: _, message, span: _ } => {
*message = new_message
}
Self::Simple { kind: _, message } => {
*message = new_message
}
}
}
pub fn with_kind(&mut self, new_kind: ShErrKind) {
match self {
Self::Full { kind, message: _, span: _ } => {
*kind = new_kind
}
Self::Simple { kind, message: _ } => {
*kind = new_kind
}
}
}
pub fn display_kind(&self) -> String {
match self {
ShErr::Simple { kind, message: _ } |
ShErr::Full { kind, message: _, span: _ } => {
match kind {
ShErrKind::IoErr => "I/O Error: ".into(),
ShErrKind::SyntaxErr => "Syntax Error: ".into(),
ShErrKind::ParseErr => "Parse Error: ".into(),
ShErrKind::InternalErr => "Internal Error: ".into(),
ShErrKind::ExecFail => "Execution Failed: ".into(),
ShErrKind::Errno => "ERRNO: ".into(),
ShErrKind::CmdNotFound => "Command not found: ".into(),
ShErrKind::CleanExit |
ShErrKind::FuncReturn |
ShErrKind::LoopContinue |
ShErrKind::LoopBreak |
ShErrKind::Null => "".into()
}
}
}
}
pub fn get_window(&self) -> String {
if let ShErr::Full { kind: _, message: _, span } = self {
let window = span.get_slice();
window.split_once('\n').unwrap_or((&window,"")).0.to_string()
} else {
String::new()
}
}
}
impl Display for ShErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let error_display = match self {
ShErr::Simple { kind: _, message } => format!("{}{}",self.display_kind(),message),
ShErr::Full { kind: _, message, span } => {
let (offset,line_no,line_text) = span.get_line();
let dist = span.end() - span.start();
let padding = " ".repeat(offset);
let line_inner = "~".repeat(dist.saturating_sub(2));
let err_kind = style_text(&self.display_kind(), Style::Red | Style::Bold);
let stat_line = format!("[{}:{}] - {}{}",line_no,offset,err_kind,message);
let indicator_line = if dist == 1 {
format!("{}^",padding)
} else {
format!("{}^{}^",padding,line_inner)
};
let error_full = format!("\n{}\n{}\n{}\n",stat_line,line_text,indicator_line);
error_full
}
};
write!(f,"{}",error_display)
}
}

6
src/libsh/mod.rs Normal file
View File

@@ -0,0 +1,6 @@
pub mod sys;
#[macro_use]
pub mod utils;
pub mod collections;
pub mod error;
pub mod term;

51
src/libsh/sys.rs Normal file
View File

@@ -0,0 +1,51 @@
use std::fmt::Display;
use crate::prelude::*;
pub const SIG_EXIT_OFFSET: i32 = 128;
pub fn get_bin_path(command: &str, shenv: &ShEnv) -> Option<PathBuf> {
let env = shenv.vars().env();
let path_var = env.get("PATH")?;
let mut paths = path_var.split(':');
while let Some(raw_path) = paths.next() {
let mut path = PathBuf::from(raw_path);
path.push(command);
//TODO: handle this unwrap
if path.exists() {
return Some(path)
}
}
None
}
pub fn write_out(text: impl Display) -> ShResult<()> {
write(borrow_fd(1), text.to_string().as_bytes())?;
Ok(())
}
pub fn write_err(text: impl Display) -> ShResult<()> {
write(borrow_fd(2), text.to_string().as_bytes())?;
Ok(())
}
/// Return is `readpipe`, `writepipe`
/// Contains all of the necessary boilerplate for grabbing two pipe fds using libc::pipe()
pub fn c_pipe() -> Result<(RawFd,RawFd),Errno> {
let mut pipes: [i32;2] = [0;2];
let ret = unsafe { libc::pipe(pipes.as_mut_ptr()) };
if ret < 0 {
return Err(Errno::from_raw(ret))
}
Ok((pipes[0],pipes[1]))
}
pub fn execvpe(cmd: String, argv: Vec<String>, envp: Vec<String>) -> Result<(),Errno> {
let cmd_raw = CString::new(cmd).unwrap();
let argv = argv.into_iter().map(|arg| CString::new(arg).unwrap()).collect::<Vec<CString>>();
let envp = envp.into_iter().map(|var| CString::new(var).unwrap()).collect::<Vec<CString>>();
nix::unistd::execvpe(&cmd_raw, &argv, &envp).unwrap();
Ok(())
}

108
src/libsh/term.rs Normal file
View File

@@ -0,0 +1,108 @@
use std::{fmt::Display, ops::BitOr};
/// Enum representing a single ANSI style
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Style {
Reset,
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
BrightBlack,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite,
Bold,
Italic,
Underline,
Reversed,
}
impl Style {
pub fn as_str(&self) -> &'static str {
match self {
Style::Reset => "\x1b[0m",
Style::Black => "\x1b[30m",
Style::Red => "\x1b[31m",
Style::Green => "\x1b[32m",
Style::Yellow => "\x1b[33m",
Style::Blue => "\x1b[34m",
Style::Magenta => "\x1b[35m",
Style::Cyan => "\x1b[36m",
Style::White => "\x1b[37m",
Style::BrightBlack => "\x1b[90m",
Style::BrightRed => "\x1b[91m",
Style::BrightGreen => "\x1b[92m",
Style::BrightYellow => "\x1b[93m",
Style::BrightBlue => "\x1b[94m",
Style::BrightMagenta => "\x1b[95m",
Style::BrightCyan => "\x1b[96m",
Style::BrightWhite => "\x1b[97m",
Style::Bold => "\x1b[1m",
Style::Italic => "\x1b[3m",
Style::Underline => "\x1b[4m",
Style::Reversed => "\x1b[7m",
}
}
}
/// Struct representing a **set** of styles
#[derive(Debug, Default, Clone)]
pub struct StyleSet {
styles: Vec<Style>,
}
impl StyleSet {
pub fn new() -> Self {
Self { styles: Vec::new() }
}
pub fn add(mut self, style: Style) -> Self {
if !self.styles.contains(&style) {
self.styles.push(style);
}
self
}
pub fn as_str(&self) -> String {
self.styles.iter().map(|s| s.as_str()).collect::<String>()
}
}
/// Allow OR (`|`) operator to combine multiple `Style` values into a `StyleSet`
impl BitOr for Style {
type Output = StyleSet;
fn bitor(self, rhs: Self) -> StyleSet {
StyleSet::new().add(self).add(rhs)
}
}
/// Allow OR (`|`) operator to combine `StyleSet` with `Style`
impl BitOr<Style> for StyleSet {
type Output = StyleSet;
fn bitor(self, rhs: Style) -> StyleSet {
self.add(rhs)
}
}
impl From<Style> for StyleSet {
fn from(style: Style) -> Self {
StyleSet::new().add(style)
}
}
/// Apply styles to a string
pub fn style_text<Str: Display, Sty: Into<StyleSet>>(text: Str, styles: Sty) -> String {
let styles = styles.into();
format!("{}{}{}", styles.as_str(), text, Style::Reset.as_str())
}

340
src/libsh/utils.rs Normal file
View File

@@ -0,0 +1,340 @@
use core::{arch::asm, fmt::{self, Debug, Display, Write}, ops::Deref};
use std::{os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd}, str::FromStr};
use nix::libc::getpgrp;
use crate::{expand::{expand_vars::{expand_dquote, expand_var}, tilde::expand_tilde}, prelude::*};
pub trait StrOps {
fn trim_quotes(&self) -> String;
}
pub trait ArgVec {
fn as_strings(self, shenv: &mut ShEnv) -> Vec<String>;
fn drop_first(self) -> Vec<Token>;
}
impl ArgVec for Vec<Token> {
/// Converts the contained tokens into strings.
/// This function also performs token expansion.
fn as_strings(self, shenv: &mut ShEnv) -> Vec<String> {
let mut argv_iter = self.into_iter();
let mut argv_processed = vec![];
while let Some(arg) = argv_iter.next() {
match arg.rule() {
TkRule::VarSub => {
let mut tokens = expand_var(arg, shenv).into_iter();
while let Some(token) = tokens.next() {
argv_processed.push(token.to_string())
}
}
TkRule::TildeSub => {
let expanded = expand_tilde(arg);
argv_processed.push(expanded);
}
TkRule::DQuote => {
let expanded = expand_dquote(arg, shenv);
argv_processed.push(expanded)
}
_ => {
argv_processed.push(arg.to_string())
}
}
}
argv_processed
}
/// This is used to ignore the first argument
/// Most commonly used in builtins where execvpe is not used
fn drop_first(self) -> Vec<Token> {
self[1..].to_vec()
}
}
#[macro_export]
macro_rules! test {
($test:block) => {
$test
exit(1)
};
}
#[derive(Clone, Copy, PartialEq, PartialOrd, Ord, Eq , Debug)]
#[repr(i32)]
pub enum LogLevel {
ERROR = 1,
WARN = 2,
INFO = 3,
DEBUG = 4,
TRACE = 5,
NULL = 0
}
impl Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ERROR => write!(f,"{}",style_text("ERROR", Style::Red | Style::Bold)),
WARN => write!(f,"{}",style_text("WARN", Style::Yellow | Style::Bold)),
INFO => write!(f,"{}",style_text("INFO", Style::Green | Style::Bold)),
DEBUG => write!(f,"{}",style_text("DEBUG", Style::Magenta | Style::Bold)),
TRACE => write!(f,"{}",style_text("TRACE", Style::Blue | Style::Bold)),
NULL => write!(f,"")
}
}
}
#[macro_export]
macro_rules! log {
($level:expr, $($var:ident),+) => {{
$(
let var_name = stringify!($var);
if $level <= log_level() {
let file = file!();
let file_styled = style_text(file,Style::Cyan);
let line = line!();
let line_styled = style_text(line,Style::Cyan);
let logged = format!("[{}][{}:{}] {} = {:#?}",$level, file_styled,line_styled,var_name, &$var);
write(borrow_fd(2),format!("{}\n",logged).as_bytes()).unwrap();
}
)+
}};
($level:expr, $lit:literal) => {{
if $level <= log_level() {
let file = file!();
let file_styled = style_text(file, Style::Cyan);
let line = line!();
let line_styled = style_text(line, Style::Cyan);
let logged = format!("[{}][{}:{}] {}", $level, file_styled, line_styled, $lit);
write(borrow_fd(2), format!("{}\n", logged).as_bytes()).unwrap();
}
}};
($level:expr, $($arg:tt)*) => {{
if $level <= log_level() {
let formatted = format!($($arg)*);
let file = file!();
let file_styled = style_text(file, Style::Cyan);
let line = line!();
let line_styled = style_text(line, Style::Cyan);
let logged = format!("[{}][{}:{}] {}", $level, file_styled, line_styled, formatted);
write(borrow_fd(2), format!("{}\n", logged).as_bytes()).unwrap();
}
}};
}
#[macro_export]
macro_rules! bp {
($var:expr) => {
log!($var);
let mut buf = String::new();
readln!("Press enter to continue", buf);
};
($($arg:tt)*) => {
log!($(arg)*);
let mut buf = String::new();
readln!("Press enter to continue", buf);
};
}
pub fn borrow_fd<'a>(fd: i32) -> BorrowedFd<'a> {
unsafe { BorrowedFd::borrow_raw(fd) }
}
// TODO: add more of these
#[derive(Debug,Clone,Copy)]
pub enum RedirType {
Input,
Output,
Append,
HereDoc,
HereString
}
#[derive(Debug,Clone)]
pub enum RedirTarget {
Fd(i32),
File(PathBuf),
}
pub struct RedirBldr {
src: Option<i32>,
op: Option<RedirType>,
tgt: Option<RedirTarget>,
}
impl RedirBldr {
pub fn new() -> Self {
Self { src: None, op: None, tgt: None }
}
pub fn with_src(self, src: i32) -> Self {
Self { src: Some(src), op: self.op, tgt: self.tgt }
}
pub fn with_op(self, op: RedirType) -> Self {
Self { src: self.src, op: Some(op), tgt: self.tgt }
}
pub fn with_tgt(self, tgt: RedirTarget) -> Self {
Self { src: self.src, op: self.op, tgt: Some(tgt) }
}
pub fn src(&self) -> Option<i32> {
self.src
}
pub fn op(&self) -> Option<RedirType> {
self.op
}
pub fn tgt(&self) -> Option<&RedirTarget> {
self.tgt.as_ref()
}
pub fn build(self) -> Redir {
Redir::new(self.src.unwrap(), self.op.unwrap(), self.tgt.unwrap())
}
}
impl FromStr for RedirBldr {
type Err = ShErr;
fn from_str(raw: &str) -> ShResult<Self> {
let mut redir_bldr = RedirBldr::new().with_src(1);
let mut chars = raw.chars().peekable();
let mut raw_src = String::new();
while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) {
raw_src.push(chars.next().unwrap())
}
if !raw_src.is_empty() {
let src = raw_src.parse::<i32>().unwrap();
redir_bldr = redir_bldr.with_src(src);
}
while let Some(ch) = chars.next() {
match ch {
'<' => {
redir_bldr = redir_bldr.with_src(0);
if chars.peek() == Some(&'<') {
chars.next();
if chars.peek() == Some(&'<') {
chars.next();
redir_bldr = redir_bldr.with_op(RedirType::HereString);
} else {
redir_bldr = redir_bldr.with_op(RedirType::HereDoc);
}
} else {
redir_bldr = redir_bldr.with_op(RedirType::Input);
}
}
'>' => {
if chars.peek() == Some(&'>') {
chars.next();
redir_bldr = redir_bldr.with_op(RedirType::Append);
} else {
redir_bldr = redir_bldr.with_op(RedirType::Output);
}
}
'&' => {
let mut raw_tgt = String::new();
while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) {
raw_tgt.push(chars.next().unwrap())
}
let redir_target = RedirTarget::Fd(raw_tgt.parse::<i32>().unwrap());
redir_bldr = redir_bldr.with_tgt(redir_target);
}
_ => unreachable!()
}
}
Ok(redir_bldr)
}
}
#[derive(Debug,Clone)]
pub struct Redir {
pub src: i32,
pub op: RedirType,
pub tgt: RedirTarget
}
impl Redir {
pub fn new(src: i32, op: RedirType, tgt: RedirTarget) -> Self {
Self { src, op, tgt }
}
}
#[derive(Debug,Clone)]
pub struct CmdRedirs {
open: Vec<RawFd>,
targets_fd: Vec<Redir>,
targets_file: Vec<Redir>
}
impl CmdRedirs {
pub fn new(mut redirs: Vec<Redir>) -> Self {
let mut targets_fd = vec![];
let mut targets_file = vec![];
while let Some(redir) = redirs.pop() {
let Redir { src: _, op: _, tgt } = &redir;
match tgt {
RedirTarget::Fd(_) => targets_fd.push(redir),
RedirTarget::File(_) => targets_file.push(redir)
}
}
Self { open: vec![], targets_fd, targets_file }
}
pub fn close_all(&mut self) -> ShResult<()> {
while let Some(fd) = self.open.pop() {
if let Err(e) = close(fd) {
self.open.push(fd);
return Err(e.into())
}
}
Ok(())
}
pub fn activate(&mut self) -> ShResult<()> {
self.open_file_tgts()?;
self.open_fd_tgts()?;
Ok(())
}
pub fn open_file_tgts(&mut self) -> ShResult<()> {
while let Some(redir) = self.targets_file.pop() {
let Redir { src, op, tgt } = redir;
let src = borrow_fd(src);
let mut file_fd = if let RedirTarget::File(path) = tgt {
let flags = match op {
RedirType::Input => OFlag::O_RDONLY,
RedirType::Output => OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_TRUNC,
RedirType::Append => OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_APPEND,
_ => unimplemented!()
};
let mode = Mode::from_bits(0o644).unwrap();
open(&path,flags,mode)?
} else { unreachable!() };
dup2(file_fd.as_raw_fd(),src.as_raw_fd())?;
close(file_fd.as_raw_fd())?;
self.open.push(src.as_raw_fd());
}
Ok(())
}
pub fn open_fd_tgts(&mut self) -> ShResult<()> {
while let Some(redir) = self.targets_fd.pop() {
let Redir { src, op: _, tgt } = redir;
let mut tgt = if let RedirTarget::Fd(fd) = tgt {
borrow_fd(fd)
} else { unreachable!() };
let src = borrow_fd(src);
dup2(tgt.as_raw_fd(), src.as_raw_fd())?;
close(tgt.as_raw_fd())?;
self.open.push(src.as_raw_fd());
}
Ok(())
}
}
pub fn trim_quotes(s: impl ToString) -> String {
let s = s.to_string();
if s.starts_with('"') && s.ends_with('"') {
s.trim_matches('"').to_string()
} else if s.starts_with('\'') && s.ends_with('\'') {
s.trim_matches('\'').to_string()
} else {
s
}
}

48
src/main.rs Normal file
View File

@@ -0,0 +1,48 @@
#![allow(unused_unsafe)]
pub mod libsh;
pub mod shellenv;
pub mod parse;
pub mod prelude;
pub mod execute;
pub mod signal;
pub mod prompt;
pub mod builtin;
pub mod expand;
use libc::PIPE_BUF;
use nix::unistd::setpgid;
use signal::sig_setup;
use crate::prelude::*;
pub fn main() {
sig_setup();
let mut shenv = ShEnv::new();
loop {
log!(TRACE, "Entered loop");
let line = match prompt::read_line(&mut shenv) {
Ok(line) => line,
Err(e) => {
eprintln!("{}",e);
continue;
}
};
let input = Rc::new(line);
log!(INFO, "New input: {:?}", input);
let token_stream = Lexer::new(input).lex();
log!(TRACE, "Token stream: {:?}", token_stream);
match Parser::new(token_stream).parse() {
Err(e) => {
eprintln!("{}",e);
}
Ok(syn_tree) => {
if let Err(e) = Executor::new(syn_tree, &mut shenv).walk() {
eprintln!("{}",e);
}
}
}
log!(TRACE, "Finished iteration");
}
}

1064
src/parse/lex.rs Normal file

File diff suppressed because it is too large Load Diff

2
src/parse/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod lex;
pub mod parse;

537
src/parse/parse.rs Normal file
View File

@@ -0,0 +1,537 @@
use core::fmt::Display;
use std::str::FromStr;
use crate::prelude::*;
use super::lex::{TkRule, Span, Token};
bitflags! {
#[derive(Debug,Clone,Copy,PartialEq,Eq)]
pub struct NdFlag: u32 {
const BACKGROUND = 0b00000000000000000000000000000001;
const FUNCTION = 0b00000000000000000000000000000010;
const BUILTIN = 0b00000000000000000000000000000100;
}
}
pub trait ParseRule {
/// Used for cases where a rule is optional
fn try_match(input: &[Token]) -> ShResult<Option<Node>>;
/// Used for cases where a rule is assumed based on context
/// For instance, if the "for" keyword is encountered, then it *must* be a for loop
/// And if it isn't, return a parse error
fn assert_match(input: &[Token]) -> ShResult<Node> {
Self::try_match(input)?.ok_or_else(||
ShErr::simple(ShErrKind::ParseErr, "Parse Error")
)
}
}
#[derive(Debug,Clone)]
pub enum CmdGuard {
And,
Or
}
#[derive(Debug,Clone)]
pub struct Node {
node_rule: NdRule,
tokens: Vec<Token>,
span: Span,
flags: NdFlag,
}
impl Node {
pub fn len(&self) -> usize {
self.tokens.len()
}
pub fn tokens(&self) -> &Vec<Token> {
&self.tokens
}
pub fn rule(&self) -> &NdRule {
&self.node_rule
}
pub fn rule_mut(&mut self) -> &mut NdRule {
&mut self.node_rule
}
pub fn into_rule(self) -> NdRule {
self.node_rule
}
pub fn span(&self) -> Span {
self.span.clone()
}
pub fn flags(&self) -> NdFlag {
self.flags
}
pub fn flags_mut(&mut self) -> &mut NdFlag {
&mut self.flags
}
}
impl Display for Node {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let raw = self.span().get_slice();
write!(f, "{}", raw)
}
}
#[derive(Clone,Debug)]
pub enum NdRule {
Main { cmd_lists: Vec<Node> },
Command { argv: Vec<Token>, redirs: Vec<Redir> },
Assignment { assignments: Vec<Token>, cmd: Option<Box<Node>> },
Subshell { body: Token, argv: Vec<Token>, redirs: Vec<Redir> },
CmdList { cmds: Vec<(Option<CmdGuard>,Node)> },
Pipeline { cmds: Vec<Node> }
}
/// Define a Node rule. The body of this macro becomes the implementation for the try_match() method for the rule.
macro_rules! ndrule_def {
($name:ident,$try:expr) => {
#[derive(Debug)]
pub struct $name;
impl ParseRule for $name {
fn try_match(input: &[Token]) -> ShResult<Option<Node>> {
$try(input)
}
}
};
}
/// This macro attempts to match all of the given Rules. It returns upon finding the first match, so the order matters
/// Place the most specialized/specific rules first, and the most general rules last
macro_rules! try_rules {
($tokens:expr, $($name:ident),+) => {
$(
let result = $name::try_match($tokens)?;
if let Some(node) = result {
return Ok(Some(node))
}
)+
return Ok(None)
};
}
#[derive(Debug)]
pub struct SynTree {
tree: VecDeque<Node>
}
impl SynTree {
pub fn new() -> Self {
Self { tree: VecDeque::new() }
}
pub fn push_node(&mut self, node: Node) {
self.tree.bpush(node)
}
pub fn next_node(&mut self) -> Option<Node> {
self.tree.fpop()
}
}
pub struct Parser {
token_stream: Vec<Token>,
ast: SynTree
}
impl Parser {
pub fn new(mut token_stream: Vec<Token>) -> Self {
log!(TRACE, "New parser");
token_stream.retain(|tk| !matches!(tk.rule(), TkRule::Whitespace | TkRule::Comment));
Self { token_stream, ast: SynTree::new() }
}
pub fn parse(mut self) -> ShResult<SynTree> {
log!(TRACE, "Starting parse");
let mut lists = VecDeque::new();
let token_slice = &*self.token_stream;
// Get the Main rule
if let Some(mut node) = Main::try_match(token_slice)? {
// Extract the inner lists
if let NdRule::Main { ref mut cmd_lists } = node.rule_mut() {
while let Some(node) = cmd_lists.pop() {
log!(DEBUG, node);
lists.bpush(node)
}
}
}
while let Some(node) = lists.bpop() {
// Push inner command lists to self.ast
self.ast.push_node(node);
}
Ok(self.ast)
}
}
fn get_span(toks: &Vec<Token>) -> ShResult<Span> {
if toks.is_empty() {
Err(ShErr::simple(ShErrKind::InternalErr, "Get_span was given an empty token list"))
} else {
let start = toks.first().unwrap().span().start();
let end = toks.iter().last().unwrap().span().end();
let input = toks.iter().last().unwrap().span().get_input();
Ok(Span::new(input,start,end))
}
}
// TODO: Redirs with FD sources appear to be looping endlessly for some reason
ndrule_def!(Main, |tokens: &[Token]| {
log!(TRACE, "Parsing main");
let mut cmd_lists = vec![];
let mut node_toks = vec![];
let mut token_slice = &*tokens;
while let Some(node) = CmdList::try_match(token_slice)? {
node_toks.extend(node.tokens().clone());
token_slice = &token_slice[node.len()..];
cmd_lists.push(node);
}
if cmd_lists.is_empty() {
return Ok(None)
}
let span = get_span(&node_toks)?;
let node = Node {
node_rule: NdRule::Main { cmd_lists },
tokens: node_toks,
span,
flags: NdFlag::empty()
};
Ok(Some(node))
});
ndrule_def!(CmdList, |tokens: &[Token]| {
log!(TRACE, "Parsing cmdlist");
let mut commands: Vec<(Option<CmdGuard>,Node)> = vec![];
let mut node_toks = vec![];
let mut token_slice = &*tokens;
let mut cmd_guard = None; // Operators like '&&' and '||'
while let Some(mut node) = Expr::try_match(token_slice)? {
// Add sub-node tokens to our tokens
node_toks.extend(node.tokens().clone());
// Reflect changes in the token slice
log!(DEBUG, token_slice);
token_slice = &token_slice[node.len()..];
log!(DEBUG, token_slice);
// Push sub-node
if let NdRule::Command { argv, redirs: _ } = node.rule() {
if argv.first().is_some_and(|arg| BUILTINS.contains(&arg.to_string().as_str())) {
*node.flags_mut() |= NdFlag::BUILTIN;
}
}
commands.push((cmd_guard.take(),node));
// If the next token is '&&' or '||' then we set cmd_guard and go again
if token_slice.first().is_some_and(|tk| matches!(tk.rule(),TkRule::AndOp | TkRule::OrOp)) {
let token = token_slice.first().unwrap();
node_toks.push(token.clone());
match token.rule() {
TkRule::AndOp => cmd_guard = Some(CmdGuard::And),
TkRule::OrOp => cmd_guard = Some(CmdGuard::Or),
_ => unreachable!()
}
token_slice = &token_slice[1..];
} else {
break
}
}
if node_toks.is_empty() {
return Ok(None)
}
let span = get_span(&node_toks)?;
let node = Node {
node_rule: NdRule::CmdList { cmds: commands },
tokens: node_toks,
span,
flags: NdFlag::empty()
};
Ok(Some(node))
});
ndrule_def!(Expr, |tokens: &[Token]| {
try_rules!(tokens,
Pipeline,
Subshell,
Assignment,
Command
);
});
// Used in pipelines to avoid recursion
ndrule_def!(ExprNoPipeline, |tokens: &[Token]| {
try_rules!(tokens,
Subshell,
Assignment,
Command
);
});
ndrule_def!(Subshell, |tokens: &[Token]| {
let mut tokens_iter = tokens.iter();
let mut node_toks = vec![];
let mut argv = vec![];
let mut redirs = vec![];
if let Some(token) = tokens_iter.next() {
if let TkRule::Subshell = token.rule() {
node_toks.push(token.clone());
let body = token.clone();
while let Some(token) = tokens_iter.next() {
match token.rule() {
TkRule::AndOp |
TkRule::OrOp |
TkRule::PipeOp |
TkRule::ErrPipeOp => {
break
}
TkRule::Sep => {
node_toks.push(token.clone());
break;
}
TkRule::Ident |
TkRule::SQuote |
TkRule::DQuote |
TkRule::Assign |
TkRule::TildeSub |
TkRule::VarSub => {
node_toks.push(token.clone());
argv.push(token.clone());
}
TkRule::RedirOp => {
node_toks.push(token.clone());
// Get the raw redirection text, e.g. "1>&2" or "2>" or ">>" or something
let redir_raw = token.span().get_slice();
let mut redir_bldr = RedirBldr::from_str(&redir_raw).unwrap();
// If there isn't an FD target, get the next token and use it as the filename
if redir_bldr.tgt().is_none() {
if let Some(filename) = tokens_iter.next() {
// Make sure it's a word and not an operator or something
if !matches!(filename.rule(), TkRule::SQuote | TkRule::DQuote | TkRule::Ident | TkRule::Keyword) {
let mut err = ShErr::simple(ShErrKind::ParseErr, "Did not find a target for this redirection");
err.blame(token.span().clone());
return Err(err)
}
node_toks.push(filename.clone());
// Construct the Path object
let filename_raw = filename.span().get_slice();
let filename_path = PathBuf::from(filename_raw);
let tgt = RedirTarget::File(filename_path);
// Update the builder
redir_bldr = redir_bldr.with_tgt(tgt);
} else {
let mut err = ShErr::simple(ShErrKind::ParseErr, "Did not find a target for this redirection");
err.blame(token.span().clone());
return Err(err)
}
}
redirs.push(redir_bldr.build());
}
_ => break
}
}
let span = get_span(&node_toks)?;
let node = Node {
node_rule: NdRule::Subshell { body, argv, redirs },
tokens: node_toks,
span,
flags: NdFlag::empty()
};
return Ok(Some(node))
} else {
return Ok(None)
}
}
Ok(None)
});
ndrule_def!(Pipeline, |mut tokens: &[Token]| {
log!(TRACE, "Parsing pipeline");
let mut tokens_iter = tokens.iter().peekable();
let mut node_toks = vec![];
let mut cmds = vec![];
while let Some(token) = tokens_iter.peek() {
match token.rule() {
TkRule::AndOp | TkRule::OrOp => {
// If there are no commands or only one, this isn't a pipeline
match cmds.len() {
0 | 1 => return Ok(None),
_ => break
}
}
_ => { /* Keep going */ }
}
if let Some(mut cmd) = ExprNoPipeline::try_match(tokens)? {
// Add sub-node's tokens to our tokens
node_toks.extend(cmd.tokens().clone());
// Reflect changes in tokens and tokens_iter
tokens = &tokens[cmd.len()..];
for _ in 0..cmd.len() {
tokens_iter.next();
}
if let NdRule::Command { argv, redirs: _ } = cmd.rule() {
if argv.first().is_some_and(|arg| BUILTINS.contains(&arg.to_string().as_str())) {
*cmd.flags_mut() |= NdFlag::BUILTIN;
}
}
// Push sub-node
cmds.push(cmd);
if tokens_iter.peek().is_none_or(|tk| !matches!(tk.rule(),TkRule::PipeOp | TkRule::ErrPipeOp)) {
match cmds.len() {
0 | 1 => {
return Ok(None)
}
_ => break
}
} else {
if tokens_iter.peek().is_some() {
node_toks.push(tokens_iter.next().unwrap().clone());
tokens = &tokens[1..];
continue
} else {
match cmds.len() {
0 | 1 => return Ok(None),
_ => break
}
}
}
} else {
match cmds.len() {
0 | 1 => return Ok(None),
_ => break
}
}
}
if node_toks.is_empty() {
return Ok(None)
}
let span = get_span(&node_toks)?;
let node = Node {
node_rule: NdRule::Pipeline { cmds },
tokens: node_toks,
span,
flags: NdFlag::empty()
};
Ok(Some(node))
});
ndrule_def!(Command, |tokens: &[Token]| {
log!(TRACE, "Parsing command");
let mut tokens = tokens.iter().peekable();
let mut node_toks = vec![];
let mut argv = vec![];
let mut redirs = vec![];
while let Some(token) = tokens.peek() {
match token.rule() {
TkRule::AndOp | TkRule::OrOp | TkRule::PipeOp | TkRule::ErrPipeOp => {
break
}
_ => { /* Keep going */ }
}
let token = tokens.next().unwrap();
node_toks.push(token.clone());
match token.rule() {
TkRule::Ident |
TkRule::SQuote |
TkRule::DQuote |
TkRule::Assign |
TkRule::TildeSub |
TkRule::VarSub => {
argv.push(token.clone());
}
TkRule::RedirOp => {
// Get the raw redirection text, e.g. "1>&2" or "2>" or ">>" or something
let redir_raw = token.span().get_slice();
let mut redir_bldr = RedirBldr::from_str(&redir_raw).unwrap();
// If there isn't an FD target, get the next token and use it as the filename
if redir_bldr.tgt().is_none() {
if let Some(filename) = tokens.next() {
// Make sure it's a word and not an operator or something
if !matches!(filename.rule(), TkRule::SQuote | TkRule::DQuote | TkRule::Ident | TkRule::Keyword) {
let mut err = ShErr::simple(ShErrKind::ParseErr, "Did not find a target for this redirection");
err.blame(token.span().clone());
return Err(err)
}
node_toks.push(filename.clone());
// Construct the Path object
let filename_raw = filename.span().get_slice();
let filename_path = PathBuf::from(filename_raw);
let tgt = RedirTarget::File(filename_path);
// Update the builder
redir_bldr = redir_bldr.with_tgt(tgt);
} else {
let mut err = ShErr::simple(ShErrKind::ParseErr, "Did not find a target for this redirection");
err.blame(token.span().clone());
return Err(err)
}
}
redirs.push(redir_bldr.build());
}
TkRule::Sep => break,
_ => unreachable!("Found this rule: {:?}", token.rule())
}
}
if node_toks.is_empty() {
return Ok(None)
}
let span = get_span(&node_toks)?;
if !argv.is_empty() {
let node = Node {
node_rule: NdRule::Command { argv, redirs },
tokens: node_toks,
span,
flags: NdFlag::empty()
};
Ok(Some(node))
} else {
Ok(None)
}
});
ndrule_def!(Assignment, |tokens: &[Token]| {
log!(TRACE, "Parsing assignment");
let mut tokens = tokens.into_iter().peekable();
let mut node_toks = vec![];
let mut assignments = vec![];
while tokens.peek().is_some_and(|tk| tk.rule() == TkRule::Assign) {
let token = tokens.next().unwrap();
node_toks.push(token.clone());
assignments.push(token.clone());
}
if assignments.is_empty() {
return Ok(None)
}
if tokens.peek().is_some() {
let tokens_vec: Vec<Token> = tokens.into_iter().map(|token| token.clone()).collect();
let tokens_slice = &tokens_vec;
let cmd = Command::try_match(tokens_slice)?.map(|cmd| Box::new(cmd));
if let Some(ref cmd) = cmd {
node_toks.extend(cmd.tokens().clone());
}
let span = get_span(&node_toks)?;
let node = Node {
node_rule: NdRule::Assignment { assignments, cmd },
tokens: node_toks,
span,
flags: NdFlag::empty()
};
return Ok(Some(node))
} else {
let span = get_span(&node_toks)?;
let node = Node {
node_rule: NdRule::Assignment { assignments, cmd: None },
tokens: node_toks,
span,
flags: NdFlag::empty()
};
Ok(Some(node))
}
});

156
src/prelude.rs Normal file
View File

@@ -0,0 +1,156 @@
pub use std::{
io::{
self,
Read,
Write
},
cell::RefCell,
rc::Rc,
os::fd::{
OwnedFd,
BorrowedFd,
RawFd,
FromRawFd
},
collections::{
VecDeque,
HashMap,
},
ffi::{
CStr,
CString
},
path::{
Path,
PathBuf,
},
process::{
exit
},
};
pub use bitflags::bitflags;
pub use nix::{
fcntl::{
open,
OFlag,
},
sys::{
signal::{
killpg,
kill,
signal,
pthread_sigmask,
SigmaskHow,
SigSet,
SigHandler,
Signal
},
wait::{
waitpid,
WaitStatus as WtStat,
WaitPidFlag as WtFlag
},
stat::Mode,
memfd::memfd_create,
},
errno::Errno,
unistd::{
Pid,
ForkResult::*,
fork,
getppid,
getpid,
getpgid,
getpgrp,
geteuid,
read,
write,
isatty,
tcgetpgrp,
tcsetpgrp,
dup,
dup2,
close,
},
libc,
};
pub use crate::{
libsh::{
term::{
Style,
style_text
},
utils::{
LogLevel::*,
ArgVec,
Redir,
RedirType,
RedirBldr,
RedirTarget,
CmdRedirs,
borrow_fd,
trim_quotes
},
collections::{
VecDequeAliases
},
sys::{
self,
write_err,
write_out,
c_pipe,
execvpe
},
error::{
ShErrKind,
ShErr,
ShResult
},
},
builtin::{
echo::echo,
cd::cd,
pwd::pwd,
read::read_builtin,
jobctl::{
continue_job,
jobs
},
BUILTINS
},
shellenv::{
self,
wait_fg,
log_level,
attach_tty,
term_ctlr,
take_term,
jobs::{
JobTab,
JobID,
write_jobs,
read_jobs
},
exec_ctx::ExecFlags,
shenv::ShEnv
},
execute::Executor,
parse::{
parse::{
Node,
NdRule,
Parser,
ParseRule
},
lex::{
Span,
Token,
TkRule,
Lexer,
LexRule
},
},
log,
test,
bp,
};

113
src/prompt/highlight.rs Normal file
View File

@@ -0,0 +1,113 @@
use rustyline::highlight::Highlighter;
use sys::get_bin_path;
use crate::prelude::*;
use super::readline::SynHelper;
impl<'a> Highlighter for SynHelper<'a> {
fn highlight<'l>(&self, line: &'l str, pos: usize) -> std::borrow::Cow<'l, str> {
let mut result = String::new();
let mut tokens = Lexer::new(Rc::new(line.to_string())).lex().into_iter();
let mut is_command = true;
let mut in_array = false;
while let Some(token) = tokens.next() {
let raw = token.to_string();
match token.rule() {
TkRule::Comment => {
let styled = style_text(&raw, Style::BrightBlack);
result.push_str(&styled);
}
TkRule::ErrPipeOp |
TkRule::OrOp |
TkRule::AndOp |
TkRule::PipeOp |
TkRule::RedirOp |
TkRule::BgOp => {
is_command = true;
let styled = style_text(&raw, Style::Cyan);
result.push_str(&styled);
}
TkRule::Keyword => {
if &raw == "for" {
in_array = true;
}
let styled = style_text(&raw, Style::Yellow);
result.push_str(&styled);
}
TkRule::Subshell => {
let body = &raw[1..raw.len() - 1];
let highlighted = self.highlight(body, 0).to_string();
let styled_o_paren = style_text("(", Style::BrightBlue);
let styled_c_paren = style_text(")", Style::BrightBlue);
let rebuilt = format!("{styled_o_paren}{highlighted}{styled_c_paren}");
is_command = false;
result.push_str(&rebuilt);
}
TkRule::Ident => {
if in_array {
if &raw == "in" {
let styled = style_text(&raw, Style::Yellow);
result.push_str(&styled);
} else {
let styled = style_text(&raw, Style::Magenta);
result.push_str(&styled);
}
} else if is_command {
if get_bin_path(&token.to_string(), self.shenv).is_some() ||
self.shenv.logic().get_alias(&raw).is_some() ||
self.shenv.logic().get_function(&raw).is_some() ||
BUILTINS.contains(&raw.as_str()) {
let styled = style_text(&raw, Style::Green);
result.push_str(&styled);
} else {
let styled = style_text(&raw, Style::Red | Style::Bold);
result.push_str(&styled);
}
is_command = false;
} else {
result.push_str(&raw);
}
}
TkRule::Sep => {
is_command = true;
in_array = false;
result.push_str(&raw);
}
_ => {
result.push_str(&raw);
}
}
}
std::borrow::Cow::Owned(result)
}
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
&'s self,
prompt: &'p str,
default: bool,
) -> std::borrow::Cow<'b, str> {
let _ = default;
std::borrow::Cow::Borrowed(prompt)
}
fn highlight_hint<'h>(&self, hint: &'h str) -> std::borrow::Cow<'h, str> {
std::borrow::Cow::Borrowed(hint)
}
fn highlight_candidate<'c>(
&self,
candidate: &'c str, // FIXME should be Completer::Candidate
completion: rustyline::CompletionType,
) -> std::borrow::Cow<'c, str> {
let _ = completion;
std::borrow::Cow::Borrowed(candidate)
}
fn highlight_char(&self, line: &str, pos: usize, kind: rustyline::highlight::CmdKind) -> bool {
let _ = (line, pos, kind);
true
}
}

46
src/prompt/mod.rs Normal file
View File

@@ -0,0 +1,46 @@
use crate::prelude::*;
use readline::SynHelper;
use rustyline::{config::Configurer, history::DefaultHistory, ColorMode, CompletionType, Config, DefaultEditor, EditMode, Editor};
pub mod readline;
pub mod highlight;
fn init_rl<'a>(shenv: &'a mut ShEnv) -> Editor<SynHelper<'a>, DefaultHistory> {
let hist_path = std::env::var("FERN_HIST").unwrap_or_default();
let mut config = Config::builder()
.max_history_size(1000).unwrap()
.history_ignore_dups(true).unwrap()
.completion_prompt_limit(100)
.edit_mode(EditMode::Vi)
.color_mode(ColorMode::Enabled)
.tab_stop(2)
.build();
let mut editor = Editor::with_config(config).unwrap();
editor.set_completion_type(CompletionType::List);
editor.set_helper(Some(SynHelper::new(shenv)));
if !hist_path.is_empty() {
editor.load_history(&PathBuf::from(hist_path)).unwrap();
}
editor
}
pub fn read_line<'a>(shenv: &'a mut ShEnv) -> ShResult<String> {
log!(TRACE, "Entering prompt");
let prompt = &style_text("$ ", Style::Green | Style::Bold);
let mut editor = init_rl(shenv);
match editor.readline(prompt) {
Ok(line) => Ok(line),
Err(rustyline::error::ReadlineError::Eof) => {
kill(Pid::this(), Signal::SIGQUIT)?;
Ok(String::new())
}
Err(rustyline::error::ReadlineError::Interrupted) => {
Ok(String::new())
}
Err(e) => {
log!(ERROR, e);
Err(e.into())
}
}
}

90
src/prompt/readline.rs Normal file
View File

@@ -0,0 +1,90 @@
use rustyline::{completion::{Completer, FilenameCompleter}, highlight::Highlighter, hint::{Hint, Hinter}, history::{History, SearchDirection}, validate::{ValidationResult, Validator}, Helper};
use crate::prelude::*;
pub struct SynHelper<'a> {
file_comp: FilenameCompleter,
pub shenv: &'a mut ShEnv,
pub commands: Vec<String>
}
impl<'a> Helper for SynHelper<'a> {}
impl<'a> SynHelper<'a> {
pub fn new(shenv: &'a mut ShEnv) -> Self {
Self {
file_comp: FilenameCompleter::new(),
shenv,
commands: vec![]
}
}
pub fn hist_search(&self, term: &str, hist: &dyn History) -> Option<String> {
let limit = hist.len();
let mut latest_match = None;
for i in 0..limit {
if let Some(hist_entry) = hist.get(i, SearchDirection::Forward).ok()? {
if hist_entry.entry.starts_with(term) {
latest_match = Some(hist_entry.entry.into_owned())
}
}
}
latest_match
}
}
impl<'a> Validator for SynHelper<'a> {
fn validate(&self, ctx: &mut rustyline::validate::ValidationContext) -> rustyline::Result<rustyline::validate::ValidationResult> {
Ok(ValidationResult::Valid(None))
}
}
impl<'a> Completer for SynHelper<'a> {
type Candidate = String;
fn complete( &self, line: &str, pos: usize, ctx: &rustyline::Context<'_>,) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
Ok((0,vec![]))
}
}
pub struct SynHint {
text: String,
styled: String
}
impl SynHint {
pub fn new(text: String) -> Self {
let styled = style_text(&text, Style::BrightBlack);
Self { text, styled }
}
pub fn empty() -> Self {
Self { text: String::new(), styled: String::new() }
}
}
impl Hint for SynHint {
fn display(&self) -> &str {
&self.styled
}
fn completion(&self) -> Option<&str> {
if !self.text.is_empty() {
Some(&self.text)
} else {
None
}
}
}
impl<'a> Hinter for SynHelper<'a> {
type Hint = SynHint;
fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option<Self::Hint> {
return Some(SynHint::empty());
if line.is_empty() {
return None
}
let history = ctx.history();
let result = self.hist_search(line, history)?;
let window = result[line.len()..].to_string();
Some(SynHint::new(window))
}
}

116
src/shellenv/exec_ctx.rs Normal file
View File

@@ -0,0 +1,116 @@
use crate::prelude::*;
bitflags! {
#[derive(Copy,Clone,Debug,PartialEq,PartialOrd)]
pub struct ExecFlags: u32 {
const NO_FORK = 0x00000001;
}
}
#[derive(Clone,Debug)]
pub struct ExecCtx {
redirs: Vec<Redir>,
flags: ExecFlags,
io_masks: IoMasks,
saved_io: Option<SavedIo>
}
impl ExecCtx {
pub fn new() -> Self {
Self {
redirs: vec![],
flags: ExecFlags::empty(),
io_masks: IoMasks::new(),
saved_io: None
}
}
pub fn masks(&self) -> &IoMasks {
&self.io_masks
}
pub fn push_rdr(&mut self, redir: Redir) {
self.redirs.push(redir)
}
pub fn saved_io(&mut self) -> &mut Option<SavedIo> {
&mut self.saved_io
}
pub fn activate_rdrs(&mut self) -> ShResult<()> {
let mut redirs = CmdRedirs::new(core::mem::take(&mut self.redirs));
self.redirs = vec![];
redirs.activate()?;
Ok(())
}
pub fn flags(&self) -> ExecFlags {
self.flags
}
pub fn set_flag(&mut self, flag: ExecFlags) {
self.flags |= flag
}
pub fn unset_flag(&mut self, flag: ExecFlags) {
self.flags &= !flag
}
}
#[derive(Debug,Clone)]
pub struct SavedIo {
pub stdin: RawFd,
pub stdout: RawFd,
pub stderr: RawFd
}
impl SavedIo {
pub fn save(stdin: RawFd, stdout: RawFd, stderr: RawFd) -> Self {
Self { stdin, stdout, stderr }
}
}
#[derive(Debug,Clone)]
pub struct IoMask {
default: RawFd,
mask: Option<RawFd>
}
impl IoMask {
pub fn new(default: RawFd) -> Self {
Self { default, mask: None }
}
pub fn new_mask(&mut self, mask: RawFd) {
self.mask = Some(mask)
}
pub fn unmask(&mut self) {
self.mask = None
}
pub fn get_fd(&self) -> RawFd {
if let Some(fd) = self.mask {
fd
} else {
self.default
}
}
}
#[derive(Clone,Debug)]
/// Necessary for when process file descriptors are permanently redirected using `exec`
pub struct IoMasks {
stdin: IoMask,
stdout: IoMask,
stderr: IoMask
}
impl IoMasks {
pub fn new() -> Self {
Self {
stdin: IoMask::new(0),
stdout: IoMask::new(1),
stderr: IoMask::new(2),
}
}
pub fn stdin(&self) -> &IoMask {
&self.stdin
}
pub fn stdout(&self) -> &IoMask {
&self.stdout
}
pub fn stderr(&self) -> &IoMask {
&self.stderr
}
}

573
src/shellenv/jobs.rs Normal file
View File

@@ -0,0 +1,573 @@
use std::{fmt, sync::{Arc, LazyLock, RwLock}};
use nix::unistd::setpgid;
use shellenv::{disable_reaping, enable_reaping};
use sys::SIG_EXIT_OFFSET;
use crate::prelude::*;
bitflags! {
#[derive(Debug, Copy, Clone)]
pub struct JobCmdFlags: u8 {
const LONG = 0b0000_0001; // 0x01
const PIDS = 0b0000_0010; // 0x02
const NEW_ONLY = 0b0000_0100; // 0x04
const RUNNING = 0b0000_1000; // 0x08
const STOPPED = 0b0001_0000; // 0x10
const INIT = 0b0010_0000; // 0x20
}
}
#[derive(Debug)]
pub struct DisplayWaitStatus(pub WtStat);
impl fmt::Display for DisplayWaitStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 {
WtStat::Exited(_, code) => {
match code {
0 => write!(f, "done"),
_ => write!(f, "failed: {}", code),
}
}
WtStat::Signaled(_, signal, _) => {
write!(f, "signaled: {:?}", signal)
}
WtStat::Stopped(_, signal) => {
write!(f, "stopped: {:?}", signal)
}
WtStat::PtraceEvent(_, signal, _) => {
write!(f, "ptrace event: {:?}", signal)
}
WtStat::PtraceSyscall(_) => {
write!(f, "ptrace syscall")
}
WtStat::Continued(_) => {
write!(f, "continued")
}
WtStat::StillAlive => {
write!(f, "running")
}
}
}
}
/// The job table
pub static JOBS: LazyLock<Arc<RwLock<JobTab>>> = LazyLock::new(|| {
Arc::new(
RwLock::new(
JobTab::new()
)
)
});
pub fn write_jobs<'a,T,F: FnOnce(&mut JobTab) -> T>(operation: F) -> T {
unsafe {
let mut jobs = JOBS.write().unwrap();
operation(&mut jobs)
}
}
pub fn read_jobs<'a,T,F: FnOnce(&JobTab) -> T>(operation: F) -> T {
unsafe {
let jobs = JOBS.read().unwrap();
operation(&jobs)
}
}
#[derive(Clone,Debug)]
pub enum JobID {
Pgid(Pid),
Pid(Pid),
TableID(usize),
Command(String)
}
#[derive(Debug,Clone)]
pub struct ChildProc {
pgid: Pid,
pid: Pid,
command: Option<String>,
stat: WtStat
}
impl<'a> ChildProc {
pub fn new(pid: Pid, command: Option<&str>, pgid: Option<Pid>) -> ShResult<Self> {
let command = command.map(|str| str.to_string());
let stat = if kill(pid,None).is_ok() {
WtStat::StillAlive
} else {
WtStat::Exited(pid, 0)
};
let mut child = Self { pgid: pid, pid, command, stat };
if let Some(pgid) = pgid {
child.set_pgid(pgid)?;
}
Ok(child)
}
pub fn pid(&self) -> Pid {
self.pid
}
pub fn pgid(&self) -> Pid {
self.pgid
}
pub fn cmd(&self) -> Option<&str> {
self.command.as_ref().map(|cmd| cmd.as_str())
}
pub fn stat(&self) -> WtStat {
self.stat
}
pub fn wait(&mut self, flags: Option<WtFlag>) -> Result<WtStat,Errno> {
let result = waitpid(self.pid, flags);
if let Ok(stat) = result {
self.stat = stat
}
result
}
pub fn kill<T: Into<Option<Signal>>>(&self, sig: T) -> ShResult<()> {
Ok(kill(self.pid, sig)?)
}
pub fn set_pgid(&mut self, pgid: Pid) -> ShResult<()> {
unsafe { setpgid(self.pid, pgid)? };
self.pgid = pgid;
Ok(())
}
pub fn set_stat(&mut self, stat: WtStat) {
self.stat = stat
}
pub fn is_alive(&self) -> bool {
self.stat == WtStat::StillAlive
}
pub fn is_stopped(&self) -> bool {
matches!(self.stat,WtStat::Stopped(..))
}
pub fn exited(&self) -> bool {
matches!(self.stat,WtStat::Exited(..))
}
}
pub struct JobBldr {
table_id: Option<usize>,
pgid: Option<Pid>,
children: Vec<ChildProc>
}
impl Default for JobBldr {
fn default() -> Self {
Self::new()
}
}
impl JobBldr {
pub fn new() -> Self {
Self { table_id: None, pgid: None, children: vec![] }
}
pub fn with_id(self, id: usize) -> Self {
Self {
table_id: Some(id),
pgid: self.pgid,
children: self.children
}
}
pub fn with_pgid(self, pgid: Pid) -> Self {
Self {
table_id: self.table_id,
pgid: Some(pgid),
children: self.children
}
}
pub fn with_children(self, children: Vec<ChildProc>) -> Self {
Self {
table_id: self.table_id,
pgid: self.pgid,
children
}
}
pub fn build(self) -> Job {
Job {
table_id: self.table_id,
pgid: self.pgid.unwrap_or(Pid::from_raw(0)),
children: self.children
}
}
}
#[derive(Debug,Clone)]
pub struct Job {
table_id: Option<usize>,
pgid: Pid,
children: Vec<ChildProc>
}
impl Job {
pub fn set_tabid(&mut self, id: usize) {
self.table_id = Some(id)
}
pub fn running(&self) -> bool {
!self.children.iter().all(|chld| chld.exited())
}
pub fn tabid(&self) -> Option<usize> {
self.table_id
}
pub fn pgid(&self) -> Pid {
self.pgid
}
pub fn get_cmds(&self) -> Vec<&str> {
let mut cmds = vec![];
for child in &self.children {
cmds.push(child.cmd().unwrap_or_default())
}
cmds
}
pub fn set_stats(&mut self, stat: WtStat) {
for child in self.children.iter_mut() {
child.set_stat(stat);
}
}
pub fn get_stats(&self) -> Vec<WtStat> {
self.children
.iter()
.map(|chld| chld.stat())
.collect::<Vec<WtStat>>()
}
pub fn get_pids(&self) -> Vec<Pid> {
self.children
.iter()
.map(|chld| chld.pid())
.collect::<Vec<Pid>>()
}
pub fn children(&self) -> &[ChildProc] {
&self.children
}
pub fn children_mut(&mut self) -> &mut Vec<ChildProc> {
&mut self.children
}
pub fn killpg(&mut self, sig: Signal) -> ShResult<()> {
let stat = match sig {
Signal::SIGTSTP => WtStat::Stopped(self.pgid, Signal::SIGTSTP),
Signal::SIGCONT => WtStat::Continued(self.pgid),
Signal::SIGTERM => WtStat::Signaled(self.pgid, Signal::SIGTERM, false),
_ => unimplemented!("{}",sig)
};
self.set_stats(stat);
Ok(killpg(self.pgid, sig)?)
}
pub fn wait_pgrp<'a>(&mut self) -> ShResult<Vec<WtStat>> {
let mut stats = vec![];
for child in self.children.iter_mut() {
let result = child.wait(Some(WtFlag::WUNTRACED));
match result {
Ok(stat) => {
stats.push(stat);
}
Err(Errno::ECHILD) => break,
Err(e) => return Err(e.into())
}
}
Ok(stats)
}
pub fn update_by_id(&mut self, id: JobID, stat: WtStat) -> ShResult<()> {
match id {
JobID::Pid(pid) => {
let query_result = self.children.iter_mut().find(|chld| chld.pid == pid);
if let Some(child) = query_result {
child.set_stat(stat);
}
}
JobID::Command(cmd) => {
let query_result = self.children
.iter_mut()
.find(|chld| chld
.cmd()
.is_some_and(|chld_cmd| chld_cmd.contains(&cmd))
);
if let Some(child) = query_result {
child.set_stat(stat);
}
}
JobID::TableID(tid) => {
if self.table_id.is_some_and(|tblid| tblid == tid) {
for child in self.children.iter_mut() {
child.set_stat(stat);
}
}
}
JobID::Pgid(pgid) => {
if pgid == self.pgid {
for child in self.children.iter_mut() {
child.set_stat(stat);
}
}
}
}
Ok(())
}
pub fn display(&self, job_order: &[usize], flags: JobCmdFlags) -> String {
let long = flags.contains(JobCmdFlags::LONG);
let init = flags.contains(JobCmdFlags::INIT);
let pids = flags.contains(JobCmdFlags::PIDS);
let current = job_order.last();
let prev = if job_order.len() > 2 {
job_order.get(job_order.len() - 2)
} else {
None
};
let id = self.table_id.unwrap();
let symbol = if current == self.table_id.as_ref() {
"+"
} else if prev == self.table_id.as_ref() {
"-"
} else {
" "
};
let padding_count = symbol.len() + id.to_string().len() + 3;
let padding = " ".repeat(padding_count);
let mut output = format!("[{}]{}\t", id + 1, symbol);
for (i, cmd) in self.get_cmds().iter().enumerate() {
let pid = if pids || init {
let mut pid = self.get_pids().get(i).unwrap().to_string();
pid.push(' ');
pid
} else {
"".to_string()
};
let job_stat = *self.get_stats().get(i).unwrap();
let fmt_stat = DisplayWaitStatus(job_stat).to_string();
let mut stat_line = if init {
"".to_string()
} else {
fmt_stat.clone()
};
stat_line = format!("{}{} ",pid,stat_line);
stat_line = format!("{} {}", stat_line, cmd);
stat_line = match job_stat {
WtStat::Stopped(..) | WtStat::Signaled(..) => style_text(stat_line, Style::Magenta),
WtStat::Exited(_, code) => {
match code {
0 => style_text(stat_line, Style::Green),
_ => style_text(stat_line, Style::Red),
}
}
_ => style_text(stat_line, Style::Cyan)
};
if i != self.get_cmds().len() - 1 {
stat_line = format!("{} |",stat_line);
}
let stat_final = if long {
format!(
"{}{} {}",
if i != 0 { &padding } else { "" },
self.get_pids().get(i).unwrap(),
stat_line
)
} else {
format!(
"{}{}",
if i != 0 { &padding } else { "" },
stat_line
)
};
output.push_str(&stat_final);
output.push('\n');
}
output
}
}
pub struct JobTab {
fg: Option<Job>,
order: Vec<usize>,
new_updates: Vec<usize>,
jobs: Vec<Option<Job>>
}
impl JobTab {
pub fn new() -> Self {
Self { fg: None, order: vec![], new_updates: vec![], jobs: vec![] }
}
pub fn take_fg(&mut self) -> Option<Job> {
self.fg.take()
}
fn next_open_pos(&self) -> usize {
if let Some(position) = self.jobs.iter().position(|slot| slot.is_none()) {
position
} else {
self.jobs.len()
}
}
pub fn jobs(&self) -> &Vec<Option<Job>> {
&self.jobs
}
pub fn jobs_mut(&mut self) -> &mut Vec<Option<Job>> {
&mut self.jobs
}
pub fn curr_job(&self) -> Option<usize> {
self.order.last().copied()
}
pub fn prev_job(&self) -> Option<usize> {
self.order.last().copied()
}
fn prune_jobs(&mut self) {
while let Some(job) = self.jobs.last() {
if job.is_none() {
self.jobs.pop();
} else {
break
}
}
}
pub fn insert_job(&mut self, mut job: Job, silent: bool) -> ShResult<usize> {
self.prune_jobs();
let tab_pos = if let Some(id) = job.tabid() { id } else { self.next_open_pos() };
job.set_tabid(tab_pos);
self.order.push(tab_pos);
if !silent {
write(borrow_fd(1),format!("{}", job.display(&self.order, JobCmdFlags::INIT)).as_bytes())?;
}
if tab_pos == self.jobs.len() {
self.jobs.push(Some(job))
} else {
self.jobs[tab_pos] = Some(job);
}
Ok(tab_pos)
}
pub fn order(&self) -> &[usize] {
&self.order
}
pub fn query(&self, identifier: JobID) -> Option<&Job> {
match identifier {
// Match by process group ID
JobID::Pgid(pgid) => {
self.jobs.iter().find_map(|job| {
job.as_ref().filter(|j| j.pgid == pgid)
})
}
// Match by process ID
JobID::Pid(pid) => {
self.jobs.iter().find_map(|job| {
job.as_ref().filter(|j| j.children.iter().any(|child| child.pid == pid))
})
}
// Match by table ID (index in the job table)
JobID::TableID(id) => {
self.jobs.get(id).and_then(|job| job.as_ref())
}
// Match by command name (partial match)
JobID::Command(cmd) => {
self.jobs.iter().find_map(|job| {
job.as_ref().filter(|j| {
j.children.iter().any(|child| {
child.command.as_ref().is_some_and(|c| c.contains(&cmd))
})
})
})
}
}
}
pub fn query_mut(&mut self, identifier: JobID) -> Option<&mut Job> {
match identifier {
// Match by process group ID
JobID::Pgid(pgid) => {
self.jobs.iter_mut().find_map(|job| {
job.as_mut().filter(|j| j.pgid == pgid)
})
}
// Match by process ID
JobID::Pid(pid) => {
self.jobs.iter_mut().find_map(|job| {
job.as_mut().filter(|j| j.children.iter().any(|child| child.pid == pid))
})
}
// Match by table ID (index in the job table)
JobID::TableID(id) => {
self.jobs.get_mut(id).and_then(|job| job.as_mut())
}
// Match by command name (partial match)
JobID::Command(cmd) => {
self.jobs.iter_mut().find_map(|job| {
job.as_mut().filter(|j| {
j.children.iter().any(|child| {
child.command.as_ref().is_some_and(|c| c.contains(&cmd))
})
})
})
}
}
}
pub fn get_fg(&self) -> Option<&Job> {
self.fg.as_ref()
}
pub fn get_fg_mut(&mut self) -> Option<&mut Job> {
self.fg.as_mut()
}
pub fn new_fg<'a>(&mut self, job: Job) -> ShResult<Vec<WtStat>> {
log!(DEBUG, "New fg job: {:?}", job);
let pgid = job.pgid();
self.fg = Some(job);
attach_tty(pgid)?;
let statuses = self.fg.as_mut().unwrap().wait_pgrp()?;
attach_tty(getpgrp())?;
Ok(statuses)
}
pub fn fg_to_bg(&mut self, stat: WtStat) -> ShResult<()> {
if self.fg.is_none() {
return Ok(())
}
take_term()?;
let fg = std::mem::take(&mut self.fg);
log!(DEBUG, "Moving foreground job to background");
if let Some(mut job) = fg {
job.set_stats(stat);
self.insert_job(job, false)?;
}
Ok(())
}
pub fn bg_to_fg(&mut self, shenv: &mut ShEnv, id: JobID) -> ShResult<()> {
let job = self.remove_job(id);
if let Some(job) = job {
super::wait_fg(job, shenv)?;
}
Ok(())
}
pub fn remove_job(&mut self, id: JobID) -> Option<Job> {
let tabid = self.query(id).map(|job| job.tabid().unwrap());
if let Some(tabid) = tabid {
self.jobs.get_mut(tabid).and_then(Option::take)
} else {
None
}
}
pub fn print_jobs(&self, flags: JobCmdFlags) -> ShResult<()> {
let jobs = if flags.contains(JobCmdFlags::NEW_ONLY) {
&self.jobs
.iter()
.filter(|job| job.as_ref().is_some_and(|job| self.new_updates.contains(&job.tabid().unwrap())))
.map(|job| job.as_ref())
.collect::<Vec<Option<&Job>>>()
} else {
&self.jobs
.iter()
.map(|job| job.as_ref())
.collect::<Vec<Option<&Job>>>()
};
for job in jobs.iter().flatten() {
// Skip foreground job
let id = job.tabid().unwrap();
// Filter jobs based on flags
if flags.contains(JobCmdFlags::RUNNING) && !matches!(job.get_stats().get(id).unwrap(), WtStat::StillAlive | WtStat::Continued(_)) {
continue;
}
if flags.contains(JobCmdFlags::STOPPED) && !matches!(job.get_stats().get(id).unwrap(), WtStat::Stopped(_,_)) {
continue;
}
// Print the job in the selected format
write(borrow_fd(1), format!("{}\n",job.display(&self.order,flags)).as_bytes())?;
}
Ok(())
}
}

22
src/shellenv/logic.rs Normal file
View File

@@ -0,0 +1,22 @@
use crate::prelude::*;
#[derive(Clone,Debug)]
pub struct LogTab {
aliases: HashMap<String,String>,
functions: HashMap<String,String>
}
impl LogTab {
pub fn new() -> Self {
Self {
aliases: HashMap::new(),
functions: HashMap::new()
}
}
pub fn get_alias(&self,name: &str) -> Option<&str> {
self.aliases.get(name).map(|a| a.as_str())
}
pub fn get_function(&self,name: &str) -> Option<&str> {
self.functions.get(name).map(|a| a.as_str())
}
}

18
src/shellenv/meta.rs Normal file
View File

@@ -0,0 +1,18 @@
#[derive(Clone,Debug)]
pub struct MetaTab {
last_status: i32
}
impl MetaTab {
pub fn new() -> Self {
Self {
last_status: 0
}
}
pub fn set_status(&mut self, code: i32) {
self.last_status = code
}
pub fn last_status(&self) -> i32 {
self.last_status
}
}

111
src/shellenv/mod.rs Normal file
View File

@@ -0,0 +1,111 @@
use std::env;
use jobs::Job;
use crate::prelude::*;
pub mod jobs;
pub mod logic;
pub mod exec_ctx;
pub mod meta;
pub mod shenv;
pub mod vars;
/// Calls attach_tty() on the shell's process group to retake control of the terminal
pub fn take_term() -> ShResult<()> {
attach_tty(getpgrp())?;
Ok(())
}
pub fn disable_reaping() -> ShResult<()> {
log!(TRACE, "Disabling reaping");
unsafe { signal(Signal::SIGCHLD, SigHandler::Handler(crate::signal::ignore_sigchld)) }?;
Ok(())
}
/// Waits on the current foreground job and updates the shell's last status code
pub fn wait_fg(job: Job, shenv: &mut ShEnv) -> ShResult<()> {
log!(TRACE, "Waiting on foreground job");
let mut code = 0;
attach_tty(job.pgid())?;
disable_reaping()?;
let statuses = write_jobs(|j| j.new_fg(job))?;
for status in statuses {
match status {
WtStat::Exited(_, exit_code) => {
code = exit_code;
}
WtStat::Stopped(pid, sig) => {
write_jobs(|j| j.fg_to_bg(status))?;
code = sys::SIG_EXIT_OFFSET + sig as i32;
},
WtStat::Signaled(pid, sig, _) => {
if sig == Signal::SIGTSTP {
write_jobs(|j| j.fg_to_bg(status))?;
}
code = sys::SIG_EXIT_OFFSET + sig as i32;
},
_ => { /* Do nothing */ }
}
}
take_term()?;
shenv.set_code(code);
log!(TRACE, "exit code: {}", code);
enable_reaping()?;
Ok(())
}
pub fn log_level() -> crate::libsh::utils::LogLevel {
let level = env::var("FERN_LOG_LEVEL").unwrap_or_default();
match level.to_lowercase().as_str() {
"error" => ERROR,
"warn" => WARN,
"info" => INFO,
"debug" => DEBUG,
"trace" => TRACE,
_ => NULL
}
}
pub fn enable_reaping() -> ShResult<()> {
log!(TRACE, "Enabling reaping");
unsafe { signal(Signal::SIGCHLD, SigHandler::Handler(crate::signal::handle_sigchld)) }.unwrap();
Ok(())
}
pub fn attach_tty(pgid: Pid) -> ShResult<()> {
if !isatty(0).unwrap_or(false) || pgid == term_ctlr() {
return Ok(())
}
log!(DEBUG, "Attaching tty to pgid: {}",pgid);
if pgid == getpgrp() && term_ctlr() != getpgrp() {
kill(term_ctlr(), Signal::SIGTTOU).ok();
}
let mut new_mask = SigSet::empty();
let mut mask_bkup = SigSet::empty();
new_mask.add(Signal::SIGTSTP);
new_mask.add(Signal::SIGTTIN);
new_mask.add(Signal::SIGTTOU);
pthread_sigmask(SigmaskHow::SIG_BLOCK, Some(&mut new_mask), Some(&mut mask_bkup))?;
let result = unsafe { tcsetpgrp(borrow_fd(0), pgid) };
pthread_sigmask(SigmaskHow::SIG_SETMASK, Some(&mut mask_bkup), Some(&mut new_mask))?;
match result {
Ok(_) => return Ok(()),
Err(e) => {
log!(ERROR, "error while switching term control: {}",e);
unsafe { tcsetpgrp(borrow_fd(0), getpgrp())? };
Ok(())
}
}
}
pub fn term_ctlr() -> Pid {
unsafe { tcgetpgrp(borrow_fd(0)).unwrap_or(getpgrp()) }
}

85
src/shellenv/shenv.rs Normal file
View File

@@ -0,0 +1,85 @@
use crate::prelude::*;
#[derive(Clone,Debug)]
pub struct ShEnv {
vars: shellenv::vars::VarTab,
logic: shellenv::logic::LogTab,
meta: shellenv::meta::MetaTab,
ctx: shellenv::exec_ctx::ExecCtx
}
impl ShEnv {
pub fn new() -> Self {
Self {
vars: shellenv::vars::VarTab::new(),
logic: shellenv::logic::LogTab::new(),
meta: shellenv::meta::MetaTab::new(),
ctx: shellenv::exec_ctx::ExecCtx::new(),
}
}
pub fn vars(&self) -> &shellenv::vars::VarTab {
&self.vars
}
pub fn vars_mut(&mut self) -> &mut shellenv::vars::VarTab {
&mut self.vars
}
pub fn meta(&self) -> &shellenv::meta::MetaTab {
&self.meta
}
pub fn meta_mut(&mut self) -> &mut shellenv::meta::MetaTab {
&mut self.meta
}
pub fn logic(&self) -> &shellenv::logic::LogTab {
&self.logic
}
pub fn logic_mut(&mut self) -> &mut shellenv::logic::LogTab {
&mut self.logic
}
pub fn save_io(&mut self) -> ShResult<()> {
let ctx = self.ctx_mut();
let stdin = ctx.masks().stdin().get_fd();
let stdout = ctx.masks().stdout().get_fd();
let stderr = ctx.masks().stderr().get_fd();
let saved_in = dup(stdin)?;
let saved_out = dup(stdout)?;
let saved_err = dup(stderr)?;
let saved_io = shellenv::exec_ctx::SavedIo::save(saved_in, saved_out, saved_err);
*ctx.saved_io() = Some(saved_io);
Ok(())
}
pub fn reset_io(&mut self) -> ShResult<()> {
let ctx = self.ctx_mut();
if let Some(saved) = ctx.saved_io().take() {
let saved_in = saved.stdin;
let saved_out = saved.stdout;
let saved_err = saved.stderr;
dup2(0,saved_in)?;
close(saved_in)?;
dup2(1,saved_out)?;
close(saved_out)?;
dup2(2,saved_err)?;
close(saved_err)?;
}
Ok(())
}
pub fn collect_redirs(&mut self, mut redirs: Vec<Redir>) {
let ctx = self.ctx_mut();
while let Some(redir) = redirs.pop() {
ctx.push_rdr(redir);
}
}
pub fn set_code(&mut self, code: i32) {
self.vars_mut().set_param("?", &code.to_string());
}
pub fn get_code(&self) -> i32 {
self.vars().get_param("?").parse::<i32>().unwrap_or(0)
}
pub fn ctx(&self) -> &shellenv::exec_ctx::ExecCtx {
&self.ctx
}
pub fn ctx_mut(&mut self) -> &mut shellenv::exec_ctx::ExecCtx {
&mut self.ctx
}
}

162
src/shellenv/vars.rs Normal file
View File

@@ -0,0 +1,162 @@
use std::env;
use nix::unistd::{gethostname, User};
use crate::prelude::*;
#[derive(Clone,Debug)]
pub struct VarTab {
env: HashMap<String,String>,
params: HashMap<String,String>,
pos_params: VecDeque<String>,
vars: HashMap<String,String>
}
impl VarTab {
pub fn new() -> Self {
let (params,pos_params) = Self::init_params();
Self {
env: Self::init_env(),
params,
pos_params,
vars: HashMap::new(),
}
}
pub fn init_params() -> (HashMap<String,String>, VecDeque<String>) {
let mut args = std::env::args().collect::<Vec<String>>();
let mut params = HashMap::new();
let mut pos_params = VecDeque::new();
params.insert("@".to_string(), args.join(" "));
params.insert("#".to_string(), args.len().to_string());
while let Some(arg) = args.pop() {
pos_params.fpush(arg);
}
(params,pos_params)
}
pub fn init_env() -> HashMap<String,String> {
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 mut env_vars = std::env::vars().collect::<HashMap<String,String>>();
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_vars.insert("IFS".into(), " \t\n".into());
env::set_var("IFS", " \t\n");
env_vars.insert("HOSTNAME".into(), hostname.clone());
env::set_var("HOSTNAME", hostname);
env_vars.insert("UID".into(), uid.to_string());
env::set_var("UID", uid.to_string());
env_vars.insert("PPID".into(), getppid().to_string());
env::set_var("PPID", getppid().to_string());
env_vars.insert("TMPDIR".into(), "/tmp".into());
env::set_var("TMPDIR", "/tmp");
env_vars.insert("TERM".into(), term.clone());
env::set_var("TERM", term);
env_vars.insert("LANG".into(), "en_US.UTF-8".into());
env::set_var("LANG", "en_US.UTF-8");
env_vars.insert("USER".into(), username.clone());
env::set_var("USER", username.clone());
env_vars.insert("LOGNAME".into(), username.clone());
env::set_var("LOGNAME", username);
env_vars.insert("PWD".into(), pathbuf_to_string(std::env::current_dir()));
env::set_var("PWD", pathbuf_to_string(std::env::current_dir()));
env_vars.insert("OLDPWD".into(), pathbuf_to_string(std::env::current_dir()));
env::set_var("OLDPWD", pathbuf_to_string(std::env::current_dir()));
env_vars.insert("HOME".into(), home.clone());
env::set_var("HOME", home.clone());
env_vars.insert("SHELL".into(), pathbuf_to_string(std::env::current_exe()));
env::set_var("SHELL", pathbuf_to_string(std::env::current_exe()));
env_vars.insert("HIST_FILE".into(),format!("{}/.fern_hist",home));
env::set_var("HIST_FILE",format!("{}/.fern_hist",home));
env_vars
}
pub fn env(&self) -> &HashMap<String,String> {
&self.env
}
pub fn env_mut(&mut self) -> &mut HashMap<String,String> {
&mut self.env
}
pub fn reset_params(&mut self) {
self.params.clear();
}
pub fn unset_param(&mut self, key: &str) {
self.params.remove(key);
}
pub fn set_param(&mut self, key: &str, val: &str) {
self.params.insert(key.to_string(), val.to_string());
}
pub fn get_param(&self, key: &str) -> &str {
self.params.get(key).map(|s| s.as_str()).unwrap_or_default()
}
/// Push an arg to the back of the positional parameter deque
pub fn bpush_arg(&mut self, arg: &str) {
self.pos_params.bpush(arg.to_string());
self.set_param("@", &self.pos_params.clone().to_vec().join(" "));
self.set_param("#", &self.pos_params.len().to_string());
}
/// Pop an arg from the back of the positional parameter deque
pub fn bpop_arg(&mut self) -> Option<String> {
let item = self.pos_params.bpop();
self.set_param("@", &self.pos_params.clone().to_vec().join(" "));
self.set_param("#", &self.pos_params.len().to_string());
item
}
/// Push an arg to the front of the positional parameter deque
pub fn fpush_arg(&mut self, arg: &str) {
self.pos_params.fpush(arg.to_string());
self.set_param("@", &self.pos_params.clone().to_vec().join(" "));
self.set_param("#", &self.pos_params.len().to_string());
}
/// Pop an arg from the front of the positional parameter deque
pub fn fpop_arg(&mut self) -> Option<String> {
let item = self.pos_params.fpop();
self.set_param("@", &self.pos_params.clone().to_vec().join(" "));
self.set_param("#", &self.pos_params.len().to_string());
item
}
pub fn get_var(&self, var: &str) -> &str {
if let Ok(idx) = var.parse::<usize>() {
self.pos_params.get(idx).map(|p| p.as_str()).unwrap_or_default()
} else if let Some(var) = self.env.get(var) {
var.as_str()
} else if let Some(param) = self.params.get(var) {
param.as_str()
} else {
self.vars.get(var).map(|v| v.as_str()).unwrap_or_default()
}
}
pub fn set_var(&mut self, var: &str, val: &str) {
self.vars.insert(var.to_string(), val.to_string());
}
pub fn export(&mut self, var: &str, val: &str) {
self.env.insert(var.to_string(),val.to_string());
std::env::set_var(var, val);
}
}

163
src/signal.rs Normal file
View File

@@ -0,0 +1,163 @@
use crate::prelude::*;
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();
}
}
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);
}
extern "C" fn handle_sigtstp(_: libc::c_int) {
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();
}
});
}
pub extern "C" fn ignore_sigchld(_: libc::c_int) {
/*
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 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) {
write_jobs(|j| {
for job in j.jobs_mut().iter_mut().flatten() {
job.killpg(Signal::SIGTERM).ok();
}
});
exit(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)
}
}
}
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(())
}
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(())
}
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(())
}
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.
*/
let (
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);
}
Ok((pgid, is_fg, is_finished))
} else {
Err(ShErr::simple(ShErrKind::InternalErr, "Job not found"))
}
})?;
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,shellenv::jobs::JobCmdFlags::PIDS))
}
}
}
Ok(())
}