feat: add incredibly basic features
This commit is contained in:
parent
298cda64f5
commit
ea6c94a584
6 changed files with 380 additions and 0 deletions
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"rust-analyzer.check.command": "clippy"
|
||||||
|
}
|
12
Cargo.toml
Normal file
12
Cargo.toml
Normal 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
32
build.rs
Normal 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
20
src/builtins.rs
Normal 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
98
src/escapes.rs
Normal 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
215
src/main.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue