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