shell: add a simple shell

This commit is contained in:
Mark Poliakov 2023-11-14 14:55:13 +02:00
parent 17cf067d10
commit 7e42999539
17 changed files with 672 additions and 678 deletions

41
Cargo.lock generated
View File

@ -53,6 +53,15 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "humansize"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
dependencies = [
"libm",
]
[[package]]
name = "init"
version = "0.1.0"
@ -60,6 +69,34 @@ dependencies = [
"yggdrasil-rt",
]
[[package]]
name = "libm"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
[[package]]
name = "memchr"
version = "2.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "once_cell"
version = "1.18.0"
@ -88,6 +125,9 @@ dependencies = [
name = "shell"
version = "0.1.0"
dependencies = [
"clap",
"nom",
"yggdrasil-abi",
"yggdrasil-rt",
]
@ -107,6 +147,7 @@ name = "sysutils"
version = "0.1.0"
dependencies = [
"clap",
"humansize",
"yggdrasil-rt",
]

View File

@ -52,6 +52,7 @@ pack_initrd() {
# sysutils
cp ${build_dir}/mount ${root_dir}/sbin/
cp ${build_dir}/login ${root_dir}/sbin/
cp ${build_dir}/ls ${root_dir}/bin/
cp -r ${workspace_dir}/etc ${root_dir}/

1
etc/profile Normal file
View File

@ -0,0 +1 @@
set PATH /bin:/sbin

View File

@ -1 +1 @@
!(~/sbin/mount nil "/dev" "devfs")
/sbin/mount -t devfs /dev

View File

@ -1 +1 @@
(echo "TODO: message of the day")
echo TODO: message of the day

View File

@ -7,3 +7,7 @@ edition = "2021"
[dependencies]
yggdrasil-rt = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-rt.git" }
yggdrasil-abi = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-abi.git" }
clap = { version = "4.3.19", features = ["std", "derive"], default-features = false }
nom = "7.1.3"

75
shell/src/builtins.rs Normal file
View File

@ -0,0 +1,75 @@
use std::{collections::HashMap, path::{Path, PathBuf}, env};
use crate::{Error, Outcome};
pub type BuiltinCommand = fn(&[String], &mut HashMap<String, String>) -> Result<Outcome, Error>;
static BUILTINS: &[(&str, BuiltinCommand)] = &[("echo", b_echo), ("set", b_set), ("which", b_which)];
pub fn get_builtin(name: &str) -> Option<BuiltinCommand> {
BUILTINS
.iter()
.find_map(|&(key, value)| if key == name { Some(value) } else { None })
}
fn b_which(args: &[String], _envs: &mut HashMap<String, String>) -> Result<Outcome, Error> {
fn find_in_path(path: &str, program: &str) -> Option<String> {
for entry in path.split(':') {
let full_path = PathBuf::from(entry).join(program);
if full_path.exists() {
return Some(full_path.to_str().unwrap().to_owned());
}
}
None
}
if args.len() != 1 {
eprintln!("which usage: which PROGRAM");
return Ok(Outcome::Exited(1));
}
let program = args[0].as_str();
let resolution = if program.starts_with('/') || program.starts_with('.') {
if Path::new(program).exists() {
Some(program.to_owned())
} else {
None
}
} else if let Ok(path) = env::var("PATH") {
find_in_path(&path, program)
} else {
None
};
match resolution {
Some(path) => {
println!("{}: {}", program, path);
Ok(Outcome::Exited(0))
}
_ => {
Ok(Outcome::Exited(1))
}
}
}
fn b_set(args: &[String], envs: &mut HashMap<String, String>) -> Result<Outcome, Error> {
if args.len() != 2 {
eprintln!("set usage: set VAR VALUE");
return Ok(Outcome::Exited(1));
}
envs.insert(args[0].clone(), args[1].clone());
Ok(Outcome::ok())
}
fn b_echo(args: &[String], _envs: &mut HashMap<String, String>) -> Result<Outcome, Error> {
for (i, arg) in args.iter().enumerate() {
if i != 0 {
print!(" ");
}
print!("{}", arg);
}
println!();
Ok(Outcome::ok())
}

View File

@ -1,204 +0,0 @@
use std::{collections::LinkedList, fmt, rc::Rc};
#[derive(Default, Debug, Clone, Copy)]
pub struct Position {
pub line: u64,
pub column: u64,
}
#[derive(Debug)]
pub enum ExprValue {
List(LinkedList<Rc<Expr>>),
Nil,
Ident(String),
StringLiteral(String),
IntLiteral(i64),
Quote(Rc<Expr>),
Meta(char, Rc<Expr>),
Error(String),
}
#[derive(Debug)]
pub struct Expr {
pub value: ExprValue,
pub position: Option<Position>,
}
pub struct ExprPrinter<'a> {
expr: &'a Expr,
ellipsisize_lists: bool,
pretty: bool,
}
impl Expr {
pub fn nil() -> Rc<Self> {
Rc::new(Self {
value: ExprValue::Nil,
position: None,
})
}
pub fn ident<S: Into<String>>(s: S) -> Rc<Self> {
Rc::new(Self {
value: ExprValue::Ident(s.into()),
position: None,
})
}
pub fn str<S: Into<String>>(s: S) -> Rc<Self> {
Rc::new(Self {
value: ExprValue::StringLiteral(s.into()),
position: None,
})
}
pub fn t() -> Rc<Self> {
Rc::new(Self {
value: ExprValue::IntLiteral(1),
position: None,
})
}
pub fn quote(expr: Rc<Self>) -> Rc<Self> {
Rc::new(Self {
value: ExprValue::Quote(expr),
position: None,
})
}
pub fn meta(m: char, expr: Rc<Self>) -> Rc<Self> {
Rc::new(Self {
value: ExprValue::Meta(m, expr),
position: None,
})
}
pub fn list(list: LinkedList<Rc<Self>>) -> Rc<Self> {
Rc::new(Self {
value: ExprValue::List(list),
position: None,
})
}
pub fn error<S: Into<String>>(s: S) -> Rc<Self> {
Rc::new(Self {
value: ExprValue::Error(s.into()),
position: None,
})
}
pub fn is_nil(&self) -> bool {
match &self.value {
ExprValue::Nil => true,
ExprValue::List(e) if e.is_empty() => true,
_ => false,
}
}
pub fn is_list(&self) -> bool {
match &self.value {
ExprValue::Nil => true,
ExprValue::List(_) => true,
_ => false,
}
}
pub fn as_string(&self) -> String {
match &self.value {
ExprValue::Nil => "".to_owned(),
ExprValue::List(items) => items.iter().fold(String::new(), |acc, i| {
if acc.is_empty() {
i.as_string()
} else {
acc + " " + &i.as_string()
}
}),
ExprValue::Meta(_, s) => s.as_string(),
ExprValue::IntLiteral(s) => format!("{}", s),
ExprValue::StringLiteral(s) | ExprValue::Ident(s) => s.clone(),
ExprValue::Error(e) => format!("(error {:?})", e),
ExprValue::Quote(_) => todo!(),
}
}
pub fn as_error(&self) -> Option<&str> {
match &self.value {
ExprValue::Error(e) => Some(e),
_ => None,
}
}
}
impl<'a> ExprPrinter<'a> {
pub fn new(expr: &'a Expr) -> Self {
Self {
expr,
ellipsisize_lists: false,
pretty: false,
}
}
pub fn ellipsisize_lists(&mut self) -> &mut Self {
self.ellipsisize_lists = true;
self
}
pub fn pretty(&mut self) -> &mut Self {
self.pretty = true;
self
}
fn print_node(&self, f: &mut fmt::Formatter<'_>, node: &Expr, depth: usize) -> fmt::Result {
if node.is_nil() {
f.write_str("nil")
} else if node.is_list() {
f.write_str("(")?;
if depth >= 2 && self.ellipsisize_lists {
f.write_str("...")?;
} else {
// Non-empty list
if let ExprValue::List(items) = &node.value {
for (i, item) in items.iter().enumerate() {
if i != 0 {
f.write_str(" ")?;
}
self.print_node(f, item, depth + 1)?;
}
}
}
f.write_str(")")
} else {
match &node.value {
ExprValue::Ident(name) => f.write_str(&name),
ExprValue::Meta(q, expr) => {
write!(f, "{}", q)?;
self.print_node(f, expr, depth)
}
ExprValue::IntLiteral(value) => write!(f, "{}", value),
ExprValue::StringLiteral(value) => {
if self.pretty {
write!(f, "{}", value)
} else {
write!(f, "{:?}", value)
}
},
ExprValue::Quote(expr) => {
f.write_str("'")?;
self.print_node(f, expr, depth)
}
ExprValue::Error(s) => {
write!(f, "(error {:?})", s)
}
_ => todo!(),
}
}
}
}
impl<'a> fmt::Display for ExprPrinter<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.print_node(f, self.expr, 0)
}
}

View File

@ -1,243 +1,181 @@
#![feature(linked_list_cursors)]
use std::{
collections::{linked_list::Cursor, HashMap, LinkedList},
collections::HashMap,
env,
fs::File,
io::BufReader,
process::{Command, ExitCode},
rc::Rc,
io::{self, stdin, stdout, BufRead, BufReader, Stdin, Write},
path::Path,
process::{Command, ExitCode, ExitStatus},
};
use expr::{Expr, ExprPrinter, ExprValue};
use parse::{Input, InputError, Parser};
use clap::Parser;
use yggdrasil_rt::{
io::{DeviceRequest, RawFd},
sys,
};
pub mod expr;
pub mod parse;
mod builtins;
mod parser;
#[derive(Debug)]
pub enum EvalError {
UndefinedIdent(String),
MetaError(Rc<Expr>, String),
pub enum Error {
IoError(io::Error),
}
#[derive(Debug)]
pub enum ScriptError {
Input(InputError),
Eval(EvalError),
#[derive(Parser)]
pub struct Args {
#[arg(short)]
login: bool,
script: Option<String>,
args: Vec<String>
}
impl From<InputError> for ScriptError {
fn from(e: InputError) -> Self {
Self::Input(e)
pub enum Outcome {
Exited(i32),
Killed(i32),
ExitShell(ExitCode),
}
pub enum Input {
Interactive(Stdin),
File(BufReader<File>),
}
impl Outcome {
pub const fn ok() -> Self {
Self::Exited(0)
}
}
impl From<EvalError> for ScriptError {
fn from(e: EvalError) -> Self {
Self::Eval(e)
}
}
impl Input {
pub fn getline(&mut self, buf: &mut String) -> io::Result<usize> {
match self {
Self::Interactive(input) => {
let mut stdout = stdout();
print!("$ ");
stdout.flush().ok();
#[derive(Clone)]
pub enum Function {
Script(LinkedList<String>, Rc<Expr>),
Native(Rc<dyn Fn(&mut Shell, Cursor<'_, Rc<Expr>>) -> Result<Rc<Expr>, EvalError>>),
}
pub struct Shell {
parser: Parser,
// TODO variable substitution
#[allow(unused)]
variables: HashMap<String, String>,
functions: HashMap<String, Function>,
}
impl Shell {
pub fn new<R: Into<Input>, I: IntoIterator<Item = String>>(input: R, args: I) -> Self {
let parser = Parser::new(input.into());
let variables = Self::init_variables(args);
let functions = Self::init_functions();
Self {
parser,
variables,
functions,
}
}
fn init_variables<I: IntoIterator<Item = String>>(args: I) -> HashMap<String, String> {
let mut vars = HashMap::new();
for (i, arg) in args.into_iter().enumerate() {
vars.insert(i.to_string(), arg);
}
vars
}
fn init_functions() -> HashMap<String, Function> {
// Add builtins
let mut funcs = HashMap::new();
funcs.insert("echo".to_string(), Function::Native(Rc::new(builtin_echo)));
funcs
}
fn eval_command(
&mut self,
program: &str,
mut args: Cursor<'_, Rc<Expr>>,
_capture_output: bool,
) -> Result<Rc<Expr>, EvalError> {
let mut str_args = vec![];
while let Some(arg) = args.current() {
let arg = self.eval(arg)?;
str_args.push(arg.as_string());
args.move_next();
}
let mut process = match Command::new(program).args(str_args).spawn() {
Ok(p) => p,
Err(e) => {
return Ok(Expr::error(format!("{}: {}", program, e.to_string())));
input.read_line(buf)
}
};
Self::File(input) => input.read_line(buf),
}
}
let status = process.wait().unwrap();
pub fn is_interactive(&self) -> bool {
matches!(self, Self::Interactive(_))
}
}
if status.success() {
Ok(Expr::t())
impl From<io::Error> for Error {
fn from(value: io::Error) -> Self {
Self::IoError(value)
}
}
#[cfg(any(target_os = "unix", target_os = "linux"))]
impl From<ExitStatus> for Outcome {
fn from(value: ExitStatus) -> Self {
use std::os::unix::process::ExitStatusExt;
if let Some(code) = value.code() {
Self::Exited(code)
} else if let Some(sig) = value.signal() {
Self::Killed(sig)
} else {
Ok(Expr::nil())
todo!()
}
}
}
fn eval_call(&mut self, list: &LinkedList<Rc<Expr>>) -> Result<Rc<Expr>, EvalError> {
let Some(head) = list.front() else {
todo!();
};
let ExprValue::Ident(head) = &head.as_ref().value else {
todo!();
};
let mut cursor = list.cursor_front();
cursor.move_next();
if head.starts_with('~') {
// Execute external command
let head = head.trim_start_matches('~');
self.eval_command(head, cursor, false)
#[cfg(target_os = "yggdrasil")]
impl From<ExitStatus> for Outcome {
fn from(value: ExitStatus) -> Self {
if let Some(code) = value.code() {
Self::Exited(code)
} else {
let Some(func) = self.functions.get(head).cloned() else {
return Err(EvalError::UndefinedIdent(head.clone()));
};
// func.eval(self, cursor)
match func {
Function::Native(f) => f(self, cursor),
_ => todo!(),
}
todo!()
}
}
}
fn eval(&mut self, expr: &Rc<Expr>) -> Result<Rc<Expr>, EvalError> {
match &expr.as_ref().value {
ExprValue::List(list) => self.eval_call(list),
ExprValue::Ident(name) => todo!("Ident: {:?}", name),
ExprValue::StringLiteral(_) | ExprValue::IntLiteral(_) | ExprValue::Nil => {
Ok(expr.clone())
}
ExprValue::Quote(q) => Ok(q.clone()),
ExprValue::Error(_) => todo!(),
ExprValue::Meta('!', q) => {
let expr = self.eval(q)?;
if expr.is_nil() {
Err(EvalError::MetaError(q.clone(), "unexpected nil".to_owned()))
} else if let Some(err) = expr.as_error() {
Err(EvalError::MetaError(q.clone(), err.to_owned()))
} else {
Ok(expr)
// TODO this has one flaw: it needs to somehow fork() (?) to set the created process' process group
pub fn exec(cmd: &[String], env: &mut HashMap<String, String>) -> Result<Outcome, Error> {
let Some((cmd, args)) = cmd.split_first() else {
return Ok(Outcome::ok());
};
if let Some(builtin) = builtins::get_builtin(cmd) {
builtin(args, env)
} else {
let status = Command::new(&cmd).args(args).envs(env.iter()).status()?;
Ok(Outcome::from(status))
}
}
fn run(mut input: Input, vars: &mut HashMap<String, String>) -> io::Result<ExitCode> {
let mut line = String::new();
loop {
line.clear();
let len = input.getline(&mut line)?;
if len == 0 {
break;
}
let line = line.trim();
let cmd = parser::parse_line(vars, line).unwrap();
match exec(&cmd, vars) {
Ok(status) => {
if input.is_interactive() {
match status {
Outcome::Killed(signal) => {
eprintln!("Killed: {}", signal);
}
_ => (),
}
}
}
ExprValue::Meta(_, _) => todo!(),
Err(e) => eprintln!("{:?}", e),
}
}
pub fn run(mut self) -> Result<ExitCode, ScriptError> {
loop {
let Some(expr) = self.parser.parse()? else {
break;
};
self.eval(&expr)?;
}
Ok(ExitCode::SUCCESS)
}
Ok(ExitCode::SUCCESS)
}
fn builtin_echo(shell: &mut Shell, mut args: Cursor<'_, Rc<Expr>>) -> Result<Rc<Expr>, EvalError> {
while let Some(arg) = args.current() {
let arg = shell.eval(arg)?;
print!("{}", ExprPrinter::new(&arg).pretty());
if args.peek_next().is_some() {
print!(" ");
}
args.move_next();
}
println!();
Ok(Expr::nil())
fn run_file<P: AsRef<Path>>(path: P, env: &mut HashMap<String, String>) -> io::Result<ExitCode> {
let input = BufReader::new(File::open(path)?);
run(Input::File(input), env)
}
fn print_eval_error(e: &EvalError) {
match e {
EvalError::MetaError(e, msg) => {
let pos_string = if let Some(pos) = e.position {
format!("{},{}", pos.line + 1, pos.column)
} else {
"[...]".to_owned()
};
eprintln!("{}: expression failed in:", pos_string);
let mut p = ExprPrinter::new(e);
p.ellipsisize_lists();
eprintln!(" {}", p);
eprintln!(": {}", msg);
}
_ => todo!(),
}
fn run_stdin(env: &mut HashMap<String, String>) -> io::Result<ExitCode> {
run(Input::Interactive(stdin()), env)
}
fn main() -> ExitCode {
let script_name = env::args().skip(1).next();
let args = Args::parse();
let mut vars = HashMap::new();
let input = if let Some(script_name) = script_name {
let file = File::open(&script_name).unwrap();
Input::from(BufReader::new(file))
for (key, value) in env::vars() {
vars.insert(key, value);
}
if args.login {
run_file("/etc/profile", &mut vars).ok();
}
// Insert PATH to current process env
if let Some(path) = vars.get("PATH") {
env::set_var("PATH", path);
}
let result = if let Some(script) = &args.script {
run_file(script, &mut vars)
} else {
// Interactive
// Gain control of the terminal signals
let pid = std::process::id();
unsafe {
sys::set_process_group_id(pid, pid).unwrap();
let mut req = DeviceRequest::SetTerminalGroup(pid);
sys::device_request(RawFd::STDIN, &mut req).unwrap();
}
Input::from(std::io::stdin())
run_stdin(&mut vars)
};
match Shell::new(input, env::args()).run() {
Ok(res) => res,
Err(ScriptError::Eval(e)) => {
eprintln!("Script error:");
print_eval_error(&e);
ExitCode::FAILURE
}
Err(ScriptError::Input(e)) => {
eprintln!("Could not load the script:");
match result {
Ok(_) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("{:?}", e);
ExitCode::FAILURE
}

View File

@ -1,241 +0,0 @@
use std::{fs::File, io::{Read, BufReader, Stdin}, rc::Rc, collections::LinkedList};
use crate::expr::{Position, Expr, ExprValue};
trait CharExt {
fn is_ident0(self) -> bool;
fn is_ident1(self) -> bool;
}
const IDENT_CHARS: &str = ":+-*/%$=!_-~";
impl CharExt for char {
fn is_ident0(self) -> bool {
self.is_ascii_alphabetic() || IDENT_CHARS.contains(self)
}
fn is_ident1(self) -> bool {
self.is_ascii_alphanumeric() || IDENT_CHARS.contains(self)
}
}
#[derive(Debug)]
pub enum InputError {
IoError(std::io::Error),
Eof,
ParseError(String)
}
enum InputInner {
File(BufReader<File>),
Stdin(Stdin)
}
pub struct Input {
position: Position,
inner: InputInner,
buffer: Option<char>,
}
pub struct Parser {
input: Input
}
impl Parser {
pub fn new(input: Input) -> Self {
Self { input }
}
fn skip_whitespace(&mut self) -> Result<(), InputError> {
while let Some(c) = self.input.peek()? {
if c.is_ascii_whitespace() {
self.input.next()?;
} else {
break;
}
}
Ok(())
}
fn parse_list(&mut self, p: Position) -> Result<Rc<Expr>, InputError> {
let mut buf = LinkedList::new();
loop {
self.skip_whitespace()?;
let Some(c) = self.input.peek()? else {
return Err(InputError::Eof);
};
if c == ')' {
self.input.next()?.unwrap();
break;
}
let Some(item) = self.parse()? else {
return Err(InputError::Eof);
};
buf.push_back(item);
}
Ok(Rc::new(Expr {
value: ExprValue::List(buf),
position: Some(p),
}))
}
fn parse_ident(&mut self, p: Position) -> Result<Rc<Expr>, InputError> {
let mut buf = String::new();
while let Some(c) = self.input.peek()? {
if !c.is_ident1() {
break;
}
self.input.next()?.unwrap();
buf.push(c);
}
Ok(Rc::new(Expr{
value: if buf == "nil" {
ExprValue::Nil
} else {
ExprValue::Ident(buf)
},
position: Some(p)
}))
}
fn parse_string(&mut self, p: Position) -> Result<Rc<Expr>, InputError> {
let mut buf = String::new();
loop {
let Some(c) = self.input.peek()? else {
return Err(InputError::Eof);
};
self.input.next()?.unwrap();
if c == '"' {
break;
}
buf.push(c);
}
Ok(Rc::new(Expr {
value: ExprValue::StringLiteral(buf),
position: Some(p)
}))
}
pub fn parse(&mut self) -> Result<Option<Rc<Expr>>, InputError> {
self.skip_whitespace()?;
let Some(c) = self.input.peek()? else {
return Ok(None);
};
let p = self.input.position;
// TODO omit '(' for toplevels
match c {
'(' => {
self.input.next()?.unwrap();
self.parse_list(p)
},
'"' => {
self.input.next()?.unwrap();
self.parse_string(p)
},
'\'' => {
self.input.next()?.unwrap();
let Some(expr) = self.parse()? else {
todo!();
};
Ok(Rc::new(Expr {
value: ExprValue::Quote(expr),
position: Some(p)
}))
}
'!' => {
self.input.next()?.unwrap();
let Some(expr) = self.parse()? else {
todo!();
};
Ok(Rc::new(Expr {
value: ExprValue::Meta('!', expr),
position: Some(p)
}))
}
_ if c.is_ident0() => self.parse_ident(p),
_ => todo!("Undefined char: {:?}", c),
}.map(Some)
}
}
impl Input {
pub fn peek(&mut self) -> Result<Option<char>, InputError> {
if self.buffer.is_none() {
self.next()?;
}
Ok(self.buffer)
}
pub fn next(&mut self) -> Result<Option<char>, InputError> {
if let Some(old) = self.buffer {
self.buffer = self.getc()?;
Ok(Some(old))
} else {
self.buffer = self.getc()?;
Ok(self.buffer)
}
}
fn getc(&mut self) -> Result<Option<char>, InputError> {
let mut buf = [0u8; 1];
let count = match &mut self.inner {
InputInner::File(f) => f.read(&mut buf),
InputInner::Stdin(f) => f.read(&mut buf)
}?;
Ok(if count == 1 {
self.position.column += 1;
if buf[0] == b'\n' {
self.position.line += 1;
self.position.column = 0;
}
Some(buf[0] as char)
} else {
None
})
}
}
impl From<Stdin> for Input {
fn from(value: Stdin) -> Self {
Self {
position: Position::default(),
inner: InputInner::Stdin(value),
buffer: None
}
}
}
impl From<BufReader<File>> for Input {
fn from(value: BufReader<File>) -> Self {
Self {
position: Position::default(),
inner: InputInner::File(value),
buffer: None,
}
}
}
impl From<std::io::Error> for InputError {
fn from(e: std::io::Error) -> Self {
Self::IoError(e)
}
}

168
shell/src/parser.rs Normal file
View File

@ -0,0 +1,168 @@
use std::collections::HashMap;
use nom::{
branch::alt,
bytes::complete::{is_not, tag},
character::complete::{space0, space1},
combinator::{map, value, verify},
multi::{many0, many1, separated_list0},
sequence::{delimited, pair, preceded, terminated},
IResult, Parser,
};
fn text(input: &str) -> IResult<&str, &str> {
let not_delimiter = is_not("\"\\ $\t");
verify(not_delimiter, |s: &str| !s.is_empty())(input)
}
fn token_text(input: &str) -> IResult<&str, &str> {
let not_delimiter = is_not(" \\$\t");
verify(not_delimiter, |s: &str| !s.is_empty())(input)
}
fn dquoted(input: &str) -> IResult<&str, &str> {
let not_delimiter = is_not("\\\"$");
alt((
delimited(tag("\""), not_delimiter, tag("\"")),
value("", tag("\"\"")),
))(input)
}
fn parse_word(input: &str) -> IResult<&str, String> {
map(many1(alt((text, dquoted))), |items| items.join(""))(input)
}
pub fn parse_line_raw(input: &str) -> IResult<&str, Vec<String>> {
delimited(space0, separated_list0(space1, parse_word), space0)(input)
}
#[derive(Debug, PartialEq)]
enum Token<'a> {
Text(&'a str),
Var(&'a str),
Space,
}
impl Token<'_> {
pub fn substitute(self, env: &HashMap<String, String>) -> String {
match self {
Self::Text(text) => text.to_owned(),
Self::Space => " ".to_owned(),
Self::Var(name) => env.get(name).cloned().unwrap_or_default(),
}
}
}
fn token(input: &str) -> IResult<&str, Token> {
alt((
map(preceded(tag("$"), text), Token::Var),
map(token_text, Token::Text),
map(space1, |_| Token::Space),
))(input)
}
fn tokens(input: &str) -> IResult<&str, Vec<Token>> {
many0(token)(input)
}
fn subst_vars<'e, 's>(
env: &'e HashMap<String, String>,
input: &'s str,
) -> IResult<&'s str, String> {
map(tokens, |items| {
items
.into_iter()
.map(|token| token.substitute(env))
.collect()
})(input)
}
pub fn parse_line(
env: &HashMap<String, String>,
input: &str,
) -> Result<Vec<String>, ()> {
let (_, substituted) = subst_vars(env, input).map_err(|_| ())?;
let (rem, words) = parse_line_raw(&substituted).map_err(|_| ())?;
if !rem.is_empty() {
return Err(());
}
Ok(words)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple_cases() {
let input = "a b c d";
let (rem, output) = parse_line_raw(input).unwrap();
assert_eq!(rem, "");
assert_eq!(output, vec!["a", "b", "c", "d"]);
let input = " a b c d ";
let (rem, output) = parse_line_raw(input).unwrap();
assert_eq!(rem, "");
assert_eq!(output, vec!["a", "b", "c", "d"]);
}
#[test]
fn strings() {
let input = r#" a b "c" d "#;
let (rem, output) = parse_line_raw(input).unwrap();
assert_eq!(rem, "");
assert_eq!(output, vec!["a", "b", "c", "d"]);
// dq adjacent to a word
let input = r#" a b"c" "e"d "#;
let (rem, output) = parse_line_raw(input).unwrap();
assert_eq!(rem, "");
assert_eq!(output, vec!["a", "bc", "ed"]);
// Empty dq
let input = r#" a "" c "#;
let (rem, output) = parse_line_raw(input).unwrap();
assert_eq!(rem, "");
assert_eq!(output, vec!["a", "", "c"]);
// Empty dq adjacent to a word
let input = r#" a ""c "#;
let (rem, output) = parse_line_raw(input).unwrap();
assert_eq!(rem, "");
assert_eq!(output, vec!["a", "c"]);
}
#[test]
fn var_tokens() {
let input = r#"a"b$c" d$e"f""#;
let (rem, output) = tokens(input).unwrap();
assert_eq!(rem, "");
assert_eq!(
output,
vec![
Token::Text("a\"b"),
Token::Var("c"),
Token::Text("\""),
Token::Space,
Token::Text("d"),
Token::Var("e"),
Token::Text("\"f\"")
]
);
}
#[test]
fn var_subst() {
let env = HashMap::from_iter([
("a".to_owned(), "VAR?".to_owned()),
("c".to_owned(), "VAR0".to_owned()),
("e".to_owned(), "VAR1".to_owned()),
]);
let input = r#"ab$c d$e"f"$g h"#;
let (rem, output) = subst_vars(&env, input).unwrap();
assert_eq!(rem, "");
assert_eq!(output, "abVAR0 dVAR1\"f\" h");
}
}

View File

@ -7,8 +7,13 @@ edition = "2021"
[dependencies]
clap = { version = "4.3.19", features = ["std", "derive"], default-features = false }
# TODO own impl
humansize = { version = "2.1.3", features = ["impl_style"] }
yggdrasil-rt = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-rt.git" }
[lib]
path = "src/lib.rs"
[[bin]]
name = "mount"
path = "src/mount.rs"
@ -16,3 +21,7 @@ path = "src/mount.rs"
[[bin]]
name = "login"
path = "src/login.rs"
[[bin]]
name = "ls"
path = "src/ls.rs"

0
sysutils/src/display.rs Normal file
View File

1
sysutils/src/lib.rs Normal file
View File

@ -0,0 +1 @@
pub mod display;

View File

@ -27,7 +27,7 @@ fn login_readline<R: BufRead + AsRawFd>(
}
fn login_as(username: &str, _password: &str) -> Result<(), std::io::Error> {
let mut shell = Command::new("/bin/sh").spawn()?;
let mut shell = Command::new("/bin/sh").arg("-l").spawn()?;
println!("Hello {:?}", username);
shell.wait()?;
Ok(())
@ -53,6 +53,20 @@ fn login_attempt(erase: bool) -> Result<(), std::io::Error> {
login_as(username.trim(), "")
}
fn setup_session(terminal: &str) -> Result<(), yggdrasil_rt::Error> {
// This will close the file descriptors associated with the old terminal
unsafe { sys::start_session()? };
unsafe {
// File descriptors 0, 1, 2 are now free, need to reopen them with a new terminal
sys::open(None, terminal, OpenOptions::READ, FileMode::empty())?;
sys::open(None, terminal, OpenOptions::WRITE, FileMode::empty())?;
sys::open(None, terminal, OpenOptions::WRITE, FileMode::empty())?;
}
Ok(())
}
fn main() -> ExitCode {
let args: Vec<_> = env::args().skip(1).collect();
if args.len() != 1 {
@ -63,26 +77,16 @@ fn main() -> ExitCode {
// TODO check that `terminal` is a terminal
// Start a new session
if let Err(err) = unsafe { sys::start_session() } {
eprintln!("setsid(): {:?}", err);
debug_trace!("Starting a session at {}", terminal);
if let Err(err) = setup_session(terminal) {
debug_trace!("Failed: {:?}", err);
eprintln!("Could not setup session at {}: {:?}", terminal, err);
return ExitCode::FAILURE;
}
debug_trace!("Opening terminal: {}", terminal);
// Open the target terminal
unsafe {
// File descriptors 0, 1, 2 are now free, need to reopen them with a new terminal
sys::open(None, terminal, OpenOptions::READ, FileMode::empty()).unwrap();
sys::open(None, terminal, OpenOptions::WRITE, FileMode::empty()).unwrap();
sys::open(None, terminal, OpenOptions::WRITE, FileMode::empty()).unwrap();
}
set_signal_handler(Signal::Interrupted, SignalHandler::Function(handler));
let mut attempt_number = 0;
loop {
debug_trace!("Login attempt {}", attempt_number);
// "Attach" the terminal
unsafe {
let mut req = DeviceRequest::SetTerminalGroup(sys::get_pid());

202
sysutils/src/ls.rs Normal file
View File

@ -0,0 +1,202 @@
use std::{
fmt,
fs::{read_dir, FileType},
io,
mem::MaybeUninit,
path::Path,
};
use clap::Parser;
use humansize::{FormatSize, BINARY};
use yggdrasil_rt::io::FileAttr;
#[derive(Parser)]
pub struct Args {
#[arg(short)]
long: bool,
#[arg(short, long)]
human_readable: bool,
paths: Vec<String>,
}
trait DisplayBit {
fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result;
fn display_with<'a, 'b>(&'a self, opts: &'b Args) -> DisplayWith<'a, 'b, Self>
where
Self: Sized,
{
DisplayWith { item: self, opts }
}
}
trait DisplaySizeBit: Sized {
fn display_size_bit(self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result;
fn display_size_with<'a>(self, opts: &'a Args) -> DisplaySizeWith<'a, Self> {
DisplaySizeWith { size: self, opts }
}
}
impl DisplaySizeBit for u64 {
fn display_size_bit(self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if opts.human_readable {
fmt::Display::fmt(&self.format_size(BINARY), f)
} else {
fmt::Display::fmt(&self, f)
}
}
}
struct DisplaySizeWith<'a, T> {
size: T,
opts: &'a Args
}
struct DisplayWith<'a, 'b, T: DisplayBit> {
item: &'a T,
opts: &'b Args,
}
impl<T: DisplayBit> fmt::Display for DisplayWith<'_, '_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.item.display_bit(self.opts, f)
}
}
impl<T: DisplaySizeBit + Copy> fmt::Display for DisplaySizeWith<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.size.display_size_bit(self.opts, f)
}
}
struct Entry {
name: String,
ty: Option<FileType>,
attrs: Option<FileAttr>,
}
impl DisplayBit for Option<FileType> {
fn display_bit(&self, _opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(ty) = self {
if ty.is_dir() {
f.write_str("d")
} else if ty.is_symlink() {
f.write_str("l")
} else {
f.write_str("-")
}
} else {
f.write_str("?")
}
}
}
impl DisplayBit for Option<FileAttr> {
fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Some(attrs) = self else {
return write!(f, "--------- {:<8}", "???");
};
write!(f, "{} {:>8}", attrs.mode, attrs.size.display_size_with(opts))
}
}
impl DisplayBit for Entry {
fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if opts.long {
write!(
f,
"{}{} {}",
self.ty.display_with(opts),
self.attrs.display_with(opts),
self.name
)
} else {
f.write_str(&self.name)
}
}
}
fn fetch_entry<P: AsRef<Path>>(path: P) -> Option<FileAttr> {
let mut attrs = MaybeUninit::uninit();
let attrs = unsafe {
yggdrasil_rt::sys::get_metadata(None, path.as_ref().to_str()?, &mut attrs, false).ok()?;
attrs.assume_init()
};
Some(attrs)
}
impl Entry {
fn invalid() -> Self {
Self {
name: "???".to_owned(),
ty: None,
attrs: None,
}
}
}
fn list_directory(path: &Path) -> io::Result<Vec<Entry>> {
let mut entries = vec![];
for entry in read_dir(path)? {
let Ok(entry) = entry else {
entries.push(Entry::invalid());
continue;
};
let os_filename = entry.file_name();
let ty = entry.file_type().ok();
let attrs = fetch_entry(entry.path());
entries.push(Entry {
name: os_filename.to_string_lossy().to_string(),
ty,
attrs,
});
}
Ok(entries)
}
fn list(opts: &Args, path: &Path) -> io::Result<()> {
if path.is_dir() {
let entries = list_directory(path)?;
for entry in entries {
println!("{}", entry.display_with(opts));
}
} else {
// TODO fetch info
println!("{}", path.display());
}
Ok(())
}
fn list_wrap<P: AsRef<Path>>(opts: &Args, path: P) {
let path = path.as_ref();
match list(opts, path) {
Ok(_) => (),
Err(e) => {
eprintln!("{}: {}", path.display(), e);
}
}
}
pub fn main() {
let args = Args::parse();
if args.paths.is_empty() {
list_wrap(&args, ".");
} else {
for path in args.paths.iter() {
if args.paths.len() > 1 {
println!("{}:", path);
}
list_wrap(&args, path);
}
}
}

View File

@ -5,31 +5,26 @@ use yggdrasil_rt::{io::MountOptions, sys::mount};
#[derive(Parser, Debug)]
struct Args {
#[arg(short)]
ty: Option<String>,
source: String,
target: String,
filesystem: Option<String>,
target: Option<String>,
}
fn main() -> ExitCode {
let args = Args::parse();
let source = if args.source.is_empty() {
None
} else {
Some(args.source.as_str())
};
let filesystem = args.filesystem.as_deref();
let target = args.target.as_str();
let source = if args.target.is_some() { Some(args.source.as_str()) } else { None };
let target = args.target.as_deref().unwrap_or(args.source.as_str());
let filesystem = args.ty.as_deref();
println!("Mount {:?}, {:?}, {:?}", source, target, filesystem);
// Permissions are not yet implemented, lol
let result = unsafe {
let options = MountOptions {
source,
filesystem,
target
};
// Permissions are not yet implemented, lol
let result = unsafe {
let options = MountOptions {
source,
filesystem,
target
};
mount(&options)
};