From a9a9642a2a9ce17a6327338b2cee3da7e63698c2 Mon Sep 17 00:00:00 2001 From: pagedmov Date: Sun, 2 Mar 2025 16:26:28 -0500 Subject: [PATCH] Initial commit for fern --- .gitignore | 31 + Cargo.lock | 324 +++++++++ Cargo.toml | 15 + flake.lock | 27 + flake.nix | 60 ++ nix/devshell/flake-module.nix | 25 + nix/rust-overlay/flake-module.nix | 18 + src/builtin/cd.rs | 15 + src/builtin/echo.rs | 53 ++ src/builtin/export.rs | 18 + src/builtin/jobctl.rs | 127 ++++ src/builtin/mod.rs | 17 + src/builtin/pwd.rs | 50 ++ src/builtin/read.rs | 39 ++ src/execute.rs | 344 ++++++++++ src/expand/expand_vars.rs | 63 ++ src/expand/mod.rs | 2 + src/expand/tilde.rs | 11 + src/libsh/collections.rs | 36 + src/libsh/error.rs | 195 ++++++ src/libsh/mod.rs | 6 + src/libsh/sys.rs | 51 ++ src/libsh/term.rs | 108 +++ src/libsh/utils.rs | 340 +++++++++ src/main.rs | 48 ++ src/parse/lex.rs | 1064 +++++++++++++++++++++++++++++ src/parse/mod.rs | 2 + src/parse/parse.rs | 537 +++++++++++++++ src/prelude.rs | 156 +++++ src/prompt/highlight.rs | 113 +++ src/prompt/mod.rs | 46 ++ src/prompt/readline.rs | 90 +++ src/shellenv/exec_ctx.rs | 116 ++++ src/shellenv/jobs.rs | 573 ++++++++++++++++ src/shellenv/logic.rs | 22 + src/shellenv/meta.rs | 18 + src/shellenv/mod.rs | 111 +++ src/shellenv/shenv.rs | 85 +++ src/shellenv/vars.rs | 162 +++++ src/signal.rs | 163 +++++ 40 files changed, 5281 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/devshell/flake-module.nix create mode 100644 nix/rust-overlay/flake-module.nix create mode 100644 src/builtin/cd.rs create mode 100644 src/builtin/echo.rs create mode 100644 src/builtin/export.rs create mode 100644 src/builtin/jobctl.rs create mode 100644 src/builtin/mod.rs create mode 100644 src/builtin/pwd.rs create mode 100644 src/builtin/read.rs create mode 100644 src/execute.rs create mode 100644 src/expand/expand_vars.rs create mode 100644 src/expand/mod.rs create mode 100644 src/expand/tilde.rs create mode 100644 src/libsh/collections.rs create mode 100644 src/libsh/error.rs create mode 100644 src/libsh/mod.rs create mode 100644 src/libsh/sys.rs create mode 100644 src/libsh/term.rs create mode 100644 src/libsh/utils.rs create mode 100644 src/main.rs create mode 100644 src/parse/lex.rs create mode 100644 src/parse/mod.rs create mode 100644 src/parse/parse.rs create mode 100644 src/prelude.rs create mode 100644 src/prompt/highlight.rs create mode 100644 src/prompt/mod.rs create mode 100644 src/prompt/readline.rs create mode 100644 src/shellenv/exec_ctx.rs create mode 100644 src/shellenv/jobs.rs create mode 100644 src/shellenv/logic.rs create mode 100644 src/shellenv/meta.rs create mode 100644 src/shellenv/mod.rs create mode 100644 src/shellenv/shenv.rs create mode 100644 src/shellenv/vars.rs create mode 100644 src/signal.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cff2d32 --- /dev/null +++ b/.gitignore @@ -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.* diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a383f14 --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d1ba8e7 --- /dev/null +++ b/Cargo.toml @@ -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" ] } diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..fbb56cc --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2ec27da --- /dev/null +++ b/flake.nix @@ -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 + ''; + }; + }; +} diff --git a/nix/devshell/flake-module.nix b/nix/devshell/flake-module.nix new file mode 100644 index 0000000..146490e --- /dev/null +++ b/nix/devshell/flake-module.nix @@ -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; + }; + }; + }; +} diff --git a/nix/rust-overlay/flake-module.nix b/nix/rust-overlay/flake-module.nix new file mode 100644 index 0000000..52fa53d --- /dev/null +++ b/nix/rust-overlay/flake-module.nix @@ -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 = { }; }; + }; +} diff --git a/src/builtin/cd.rs b/src/builtin/cd.rs new file mode 100644 index 0000000..714c982 --- /dev/null +++ b/src/builtin/cd.rs @@ -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(()) +} diff --git a/src/builtin/echo.rs b/src/builtin/echo.rs new file mode 100644 index 0000000..7c91454 --- /dev/null +++ b/src/builtin/echo.rs @@ -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(()) +} diff --git a/src/builtin/export.rs b/src/builtin/export.rs new file mode 100644 index 0000000..0ffc594 --- /dev/null +++ b/src/builtin/export.rs @@ -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(()) +} diff --git a/src/builtin/jobctl.rs b/src/builtin/jobctl.rs new file mode 100644 index 0000000..b80bbc4 --- /dev/null +++ b/src/builtin/jobctl.rs @@ -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 { + if arg.starts_with('%') { + let arg = arg.strip_prefix('%').unwrap(); + if arg.chars().all(|ch| ch.is_ascii_digit()) { + Ok(arg.parse::().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::().unwrap()))); + if let Some(job) = pgid_query_result { + return Some(job.tabid().unwrap()) + } + + if arg.parse::().unwrap() > 0 { + let table_id_query_result = j.query(JobID::TableID(arg.parse::().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(()) +} diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs new file mode 100644 index 0000000..15faee3 --- /dev/null +++ b/src/builtin/mod.rs @@ -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" +]; diff --git a/src/builtin/pwd.rs b/src/builtin/pwd.rs new file mode 100644 index 0000000..e27aaf0 --- /dev/null +++ b/src/builtin/pwd.rs @@ -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(()) +} diff --git a/src/builtin/read.rs b/src/builtin/read.rs new file mode 100644 index 0000000..66feb88 --- /dev/null +++ b/src/builtin/read.rs @@ -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(()) +} diff --git a/src/execute.rs b/src/execute.rs new file mode 100644 index 0000000..a29b761 --- /dev/null +++ b/src/execute.rs @@ -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, 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 = 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, shenv: &mut ShEnv) -> (Vec, Vec) { + 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::>(); + 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) +} diff --git a/src/expand/expand_vars.rs b/src/expand/expand_vars.rs new file mode 100644 index 0000000..ba44e3f --- /dev/null +++ b/src/expand/expand_vars.rs @@ -0,0 +1,63 @@ +use crate::{parse::lex::Token, prelude::*}; + +pub fn expand_var(var_sub: Token, shenv: &mut ShEnv) -> Vec { + 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 +} diff --git a/src/expand/mod.rs b/src/expand/mod.rs new file mode 100644 index 0000000..0c572e2 --- /dev/null +++ b/src/expand/mod.rs @@ -0,0 +1,2 @@ +pub mod expand_vars; +pub mod tilde; diff --git a/src/expand/tilde.rs b/src/expand/tilde.rs new file mode 100644 index 0000000..0d38b45 --- /dev/null +++ b/src/expand/tilde.rs @@ -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 + } +} diff --git a/src/libsh/collections.rs b/src/libsh/collections.rs new file mode 100644 index 0000000..ab6af83 --- /dev/null +++ b/src/libsh/collections.rs @@ -0,0 +1,36 @@ +use std::collections::VecDeque; + +pub trait VecDequeAliases { + fn fpop(&mut self) -> Option; + fn fpush(&mut self, value: T); + fn bpop(&mut self) -> Option; + fn bpush(&mut self, value: T); + fn to_vec(self) -> Vec; +} + +impl VecDequeAliases for VecDeque { + /// Alias for pop_front() + fn fpop(&mut self) -> Option { + 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 { + 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 { + let mut vec = vec![]; + while let Some(item) = self.fpop() { + vec.push(item) + } + vec + } +} diff --git a/src/libsh/error.rs b/src/libsh/error.rs new file mode 100644 index 0000000..7405c01 --- /dev/null +++ b/src/libsh/error.rs @@ -0,0 +1,195 @@ +use std::fmt::Display; + +use crate::parse::lex::Span; +use crate::prelude::*; + +pub type ShResult = Result; + + +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 for ShErr { + fn from(_: std::io::Error) -> Self { + ShErr::io() + } +} + +impl From for ShErr { + fn from(value: std::env::VarError) -> Self { + ShErr::simple(ShErrKind::InternalErr, &value.to_string()) + } +} + +impl From for ShErr { + fn from(value: rustyline::error::ReadlineError) -> Self { + ShErr::simple(ShErrKind::ParseErr, &value.to_string()) + } +} + +impl From for ShErr { + fn from(value: Errno) -> Self { + ShErr::simple(ShErrKind::Errno, &value.to_string()) + } +} + +impl Blame for Result { + 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) + } +} diff --git a/src/libsh/mod.rs b/src/libsh/mod.rs new file mode 100644 index 0000000..dc8a192 --- /dev/null +++ b/src/libsh/mod.rs @@ -0,0 +1,6 @@ +pub mod sys; +#[macro_use] +pub mod utils; +pub mod collections; +pub mod error; +pub mod term; diff --git a/src/libsh/sys.rs b/src/libsh/sys.rs new file mode 100644 index 0000000..52465d8 --- /dev/null +++ b/src/libsh/sys.rs @@ -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 { + 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, envp: Vec) -> Result<(),Errno> { + let cmd_raw = CString::new(cmd).unwrap(); + + let argv = argv.into_iter().map(|arg| CString::new(arg).unwrap()).collect::>(); + let envp = envp.into_iter().map(|var| CString::new(var).unwrap()).collect::>(); + + nix::unistd::execvpe(&cmd_raw, &argv, &envp).unwrap(); + Ok(()) +} diff --git a/src/libsh/term.rs b/src/libsh/term.rs new file mode 100644 index 0000000..edae0f0 --- /dev/null +++ b/src/libsh/term.rs @@ -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