feat: add incredibly basic features

This commit is contained in:
Arthur Beck 2025-05-01 21:31:23 -05:00
parent 298cda64f5
commit ea6c94a584
Signed by: ArthurB
GPG key ID: CA200B389F0F6BC9
6 changed files with 380 additions and 0 deletions

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"rust-analyzer.check.command": "clippy"
}

12
Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "sesh"
version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4.5.37", features = ["derive", "env"] }
hostname = "0.4.1"
users = "0.11.0"
[build-dependencies]
roff = "0.2.2"

32
build.rs Normal file
View file

@ -0,0 +1,32 @@
use roff::{Roff, bold, italic, roman};
fn main() {
let page = Roff::new()
.control("TH", ["SESH", "1"])
.control("SH", ["NAME"])
.text([roman("sesh - Semantic Shell")])
.control("SH", ["SYNOPSIS"])
.text([
bold("sesh"),
roman(" [options]"),
])
.control("SH", ["DESCRIPTION"])
.text([
bold("sesh"),
roman("is a shell designed to be as semantic to use as possible"),
])
.control("SH", ["OPTIONS"])
.control("TP", [])
.text([
bold("-n"),
roman(", "),
bold("--bits"),
roman("="),
italic("BITS"),
])
.text([roman(
"Set the number of bits to modify. Default is one bit.",
)])
.render();
print!("{}", page);
}

20
src/builtins.rs Normal file
View file

@ -0,0 +1,20 @@
//! builtins to sesh
#![allow(clippy::type_complexity)]
/// List of builtins
pub const BUILTINS: [(&str, fn (args: Vec<String>, state: &mut super::State) -> i32); 2] = [("cd", cd), ("exit", exit)];
/// Change the directory
pub fn cd(args: Vec<String>, state: &mut super::State) -> i32 {
if args[1] == ".." {
state.working_dir.pop();
return 0;
}
state.working_dir.push(args[1].clone());
0
}
/// Exit the shell
pub fn exit(_: Vec<String>, _: &mut super::State) -> i32 {
std::process::exit(0);
}

98
src/escapes.rs Normal file
View file

@ -0,0 +1,98 @@
//! Escape sequences
//!
//! Thanks, stack overflow!
//!
//! (modifications were made)
use std::fmt::Display;
/// Escape error
#[derive(Debug, PartialEq)]
pub enum EscapeError {
/// there's an escape at the end of the string
EscapeAtEndOfString,
/// unknown unicode character in a \u escape
InvalidUnicodeChar(char),
/// invalid unicode codepoint
InvalidUnicodeCodepoint(u32)
}
impl Display for EscapeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EscapeError::EscapeAtEndOfString => {
f.write_str("escape at end of statement")
},
EscapeError::InvalidUnicodeChar(c) => {
f.write_fmt(format_args!("invalid character in a unicode escape: {}", *c))
},
EscapeError::InvalidUnicodeCodepoint(c) => {
f.write_fmt(format_args!("invalid unicode codepoint in escape: {}", *c))
}
}
}
}
/// iterator
struct InterpretEscapedString<'a> {
/// chars
s: std::str::Chars<'a>,
}
impl<'a> Iterator for InterpretEscapedString<'a> {
type Item = Result<char, EscapeError>;
fn next(&mut self) -> Option<Self::Item> {
let mut ret_next = false;
let out = self.s.next().map(|c| match c {
'\\' => match self.s.next() {
None => Err(EscapeError::EscapeAtEndOfString),
Some('n') => Ok('\n'),
Some('t') => Ok('\t'),
Some('\\') => Ok('\\'),
Some('"') => Ok('"'),
Some('\'') => Ok('\''),
Some('e') => Ok('\x1b'),
Some('\n') => {
ret_next = true;
Err(EscapeError::EscapeAtEndOfString)
},
Some('u') | Some('U') | Some('x') => {
let code = [self.s.next(), self.s.next(), self.s.next(), self.s.next()];
if code.iter().any(|val| val.is_none()) {
return Err(EscapeError::EscapeAtEndOfString);
}
let code = TryInto::<[char; 4]>::try_into(
code.iter().map(|ch| ch.unwrap().to_ascii_lowercase()).collect::<Vec<char>>(),
)
.unwrap();
for c in code {
if !(c.is_numeric() || ['a', 'b', 'c', 'd', 'e', 'f'].contains(&c)) {
return Err(EscapeError::InvalidUnicodeChar(c))
}
}
let code = u32::from_str_radix(&String::from_iter(code), 16).unwrap();
let out = char::from_u32(code);
if out.is_none() {
return Err(EscapeError::InvalidUnicodeCodepoint(code));
}
Ok(out.unwrap())
},
Some(c) => Ok(c),
},
c => Ok(c),
});
if ret_next {
self.next()
} else {
out
}
}
}
/// interpret an escaped string
pub fn interpret_escaped_string(s: &str) -> Result<String, EscapeError> {
(InterpretEscapedString { s: s.chars() }).collect()
}

215
src/main.rs Normal file
View file

@ -0,0 +1,215 @@
//! Semantic Shell
#![warn(missing_docs, clippy::missing_docs_in_private_items)]
#![feature(cfg_match)]
use std::{
ffi::{OsStr, OsString},
io::Write,
path::PathBuf,
rc::Rc,
sync::Mutex,
};
use clap::Parser;
mod builtins;
mod escapes;
/// sesh is a shell designed to be as semantic to use as possible
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {}
/// A single shell variable
#[derive(Clone, Debug, PartialEq, Eq)]
struct ShellVar {
/// The name of it
name: String,
/// The value of it
value: String,
}
/// A lot of [ShellVar]s.
type ShellVars = Vec<ShellVar>;
/// Whether a variable is local or not.
#[derive(Clone, Debug, PartialEq, Eq)]
enum VariableLocality {
/// A local variable.
Local,
/// A nonlocal variable.
Nonlocal,
}
/// A reference to a variable.
#[derive(Clone, Debug, PartialEq, Eq)]
enum Variable {
/// A local variable.
Local(String),
/// A nonlocal variable.
Nonlocal(OsString),
}
/// The state of the shell
#[derive(Clone, Debug)]
struct State {
/// Environment variables
env: Rc<Mutex<std::env::VarsOs>>,
/// Shell-local variables only accessible via builtins.
shell_env: ShellVars,
/// The focused variable.
focus: Variable,
/// The previous history of the states.
history: Vec<State>,
/// Current working directory.
working_dir: PathBuf,
}
fn split_statement(statement: &str) -> Vec<String> {
let mut out = vec![String::new()];
let mut i: usize = 0;
let mut in_str = (false, ' ');
for ch in statement.chars() {
if ['"', '\'', '`'].contains(&ch) {
if in_str.0 && in_str.1 == ch {
in_str.0 = false
} else {
in_str = (true, ch);
}
continue;
}
if !in_str.0 && ch == ' ' {
i += 1;
if i >= out.len() {
out.push(String::new());
}
continue;
}
out[i].push(ch);
}
out.iter()
.map(|v| v.trim().to_string())
.collect::<Vec<String>>()
}
/// Evaluate a statement. May include multiple.
fn eval(statement: &str, state: &mut State) {
let statement = escapes::interpret_escaped_string(statement);
if statement.is_err() {
println!("sesh: invalid escape: {}", statement.unwrap_err());
return;
}
let statements = statement
.unwrap()
.split("\n")
.map(|val| val.split(";").collect::<Vec<&str>>())
.collect::<Vec<Vec<&str>>>()
.iter()
.map(|val| val.iter().map(|val| val.trim()).collect::<Vec<&str>>())
.collect::<Vec<Vec<&str>>>()
.concat()
.iter()
.map(|val| split_statement(val))
.collect::<Vec<Vec<String>>>();
for statement in statements {
if statement.is_empty() || statement[0].is_empty() {
continue;
}
if let Some(builtin) = builtins::BUILTINS.iter().find(|v| v.0 == statement[0]) {
let status = builtin.1(statement, state);
for (i, var) in state.shell_env.clone().into_iter().enumerate() {
if var.name == "STATUS" {
state.shell_env.swap_remove(i);
}
}
state.shell_env.push(ShellVar {
name: "STATUS".to_string(),
value: status.to_string(),
});
continue;
}
match std::process::Command::new(statement[0].clone())
.args(&statement[1..])
.current_dir(state.working_dir.clone())
.spawn()
{
Ok(mut child) => {
for (i, var) in state.shell_env.clone().into_iter().enumerate() {
if var.name == "STATUS" {
state.shell_env.swap_remove(i);
}
}
state.shell_env.push(ShellVar {
name: "STATUS".to_string(),
value: child.wait().unwrap().code().unwrap().to_string(),
});
continue;
}
Err(error) => {
println!("sesh: error spawning program: {}", error);
return;
}
}
}
state.env = Rc::new(Mutex::new(std::env::vars_os()));
state.history.push(state.clone());
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let interactive = true;
let mut state = State {
env: Rc::new(Mutex::new(std::env::vars_os())),
shell_env: Vec::new(),
focus: Variable::Local(String::new()),
history: Vec::new(),
working_dir: std::env::current_dir()
.unwrap_or(std::env::home_dir().unwrap_or(PathBuf::from("/"))),
};
state.shell_env.push(ShellVar {
name: "PROMPT".to_string(),
value: "$u@$h $P> ".to_string(),
});
loop {
let mut prompt = state
.shell_env
.iter()
.find(|var| var.name == "PROMPT")
.unwrap_or(&ShellVar {
name: "PROMPT".to_string(),
value: String::new(),
})
.value
.clone();
prompt = prompt.replace(
"$u",
&users::get_effective_username()
.unwrap_or(users::get_current_username().unwrap_or("?".into()))
.to_string_lossy(),
);
prompt = prompt.replace(
"$h",
&hostname::get().unwrap_or("?".into()).to_string_lossy(),
);
prompt = prompt.replace("$p", &state.working_dir.as_os_str().to_string_lossy());
prompt = prompt.replace(
"$P",
&state
.working_dir
.file_name()
.unwrap_or(OsStr::new("?"))
.to_string_lossy(),
);
print!("{}", prompt);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
eval(&input, &mut state);
}
}