Add shell aliases (#357)

* Add shell aliases

* Fix env output error

* Fix sort

* Read aliases from config file

* Simplify arguments parsing

* Fix test

* Clone params to spawn syscall

* Run clippy

* Revert "Clone params to spawn syscall"

This reverts commit 4c91bea196.

* Disable binary stripping

* Remove exit alias

* Update doc
This commit is contained in:
Vincent Ollivier 2022-06-26 10:00:54 +02:00 committed by GitHub
parent cd6cdceb73
commit 82882ec355
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 248 additions and 145 deletions

View File

@ -28,7 +28,7 @@ user-rust:
-C relocation-model=static
basename -s .rs src/bin/*.rs | xargs -I {} \
cp target/x86_64-moros/release/{} dsk/bin/{}
strip dsk/bin/*
#strip dsk/bin/*
bin = target/x86_64-moros/release/bootimage-moros.bin
img = disk.img

View File

@ -46,7 +46,7 @@ MOROS Lisp v0.1.0
> (+ 1 2)
3
> (exit)
> (quit)
```
And it can execute a file. For example a file located in `/tmp/fibonacci.lsp`

View File

@ -1,15 +1,20 @@
# MOROS Shell
## Config
The shell will read `/ini/shell.sh` during initialization to setup its
configuration.
## Commands
The main commands have a long name, a one-letter alias, and may have
additional common aliases.
<!--
**Alias** command:
> alias d delete
<!--
**Append** to file:
> a a.txt
@ -130,20 +135,26 @@ Which is more efficient than doing:
Setting a variable in the shell environment is done with the following command:
> foo = "world"
> set foo 42
And accessing that variable is done with the `$` operator:
> set bar "Alice and Bob"
And accessing a variable is done with the `$` operator:
> print $foo
world
42
> print "hello $foo"
hello world
> print "Hello $bar"
Hello Alice and Bob
The process environment is copied to the shell environment when a session is
started. By convention a process env var should be in uppercase and a shell
env var should be lowercase.
Unsetting a variable is done like this:
> unset foo
## Globbing
MOROS Shell support filename expansion or globbing for `*` and `?` wildcard

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

28
dsk/ini/shell.sh Normal file
View File

@ -0,0 +1,28 @@
# Command shortcuts
alias p print
alias c copy
alias d delete
alias del delete
alias e edit
alias f find
alias g goto
alias go goto
alias h help
alias l list
alias m move
alias q quit
alias r read
alias w write
alias sh shell
alias dsk disk
alias mem memory
alias kbd keyboard
# Unix compatibility
# alias cd goto
# alias cp copy
# alias echo print
# alias exit quit
# alias ls list
# alias mv move
# alias rm delete

View File

@ -14,10 +14,9 @@ fn main(boot_info: &'static BootInfo) -> ! {
print!("\x1b[?25h"); // Enable cursor
loop {
if let Some(cmd) = option_env!("MOROS_CMD") {
let mut env = usr::shell::default_env();
let prompt = usr::shell::prompt_string(true);
println!("{}{}", prompt, cmd);
usr::shell::exec(cmd, &mut env);
usr::shell::exec(cmd);
sys::acpi::shutdown();
} else {
user_boot();

View File

@ -118,7 +118,7 @@ fn repl() -> usr::shell::ExitCode {
prompt.history.load(history_file);
while let Some(line) = prompt.input(&prompt_string) {
if line == "exit" || line == "quit" {
if line == "quit" {
break;
}
if line.is_empty() {

View File

@ -86,7 +86,7 @@ impl Chess {
while let Some(cmd) = prompt.input(&prompt_string) {
let args: Vec<&str> = cmd.trim().split(' ').collect();
match args[0] {
"q" | "quit" | "exit" => break,
"q" | "quit" => break,
"h" | "help" => self.cmd_help(args),
"i" | "init" => self.cmd_init(args),
"t" | "time" => self.cmd_time(args),

View File

@ -1,22 +1,30 @@
use crate::{sys, usr};
pub fn main(args: &[&str]) -> usr::shell::ExitCode {
if args.len() == 1 {
for (key, val) in sys::process::envs() {
println!("{}={}", key, val);
match args.len() {
1 => {
for (key, val) in sys::process::envs() {
println!("{:10} \"{}\"", key, val);
}
usr::shell::ExitCode::CommandSuccessful
}
} else {
for arg in args[1..].iter() {
if let Some(i) = arg.find('=') {
let (key, mut val) = arg.split_at(i);
val = &val[1..];
sys::process::set_env(key, val);
println!("{}={}", key, val);
2 => {
let key = args[1];
if let Some(val) = sys::process::env(key) {
println!("{}", val);
usr::shell::ExitCode::CommandSuccessful
} else {
error!("Error: could not parse '{}'", arg);
return usr::shell::ExitCode::CommandError;
error!("Could not get '{}'", key);
usr::shell::ExitCode::CommandError
}
}
3 => {
sys::process::set_env(args[1], args[2]);
usr::shell::ExitCode::CommandSuccessful
}
_ => {
error!("Invalid number of arguments");
usr::shell::ExitCode::CommandError
}
}
usr::shell::ExitCode::CommandSuccessful
}

View File

@ -32,10 +32,11 @@ pub fn copy_files(verbose: bool) {
create_dev("/dev/random", DeviceType::Random, verbose);
create_dev("/dev/console", DeviceType::Console, verbose);
copy_file("/ini/boot.sh", include_bytes!("../../dsk/ini/boot.sh"), verbose);
copy_file("/ini/banner.txt", include_bytes!("../../dsk/ini/banner.txt"), verbose);
copy_file("/ini/version.txt", include_bytes!("../../dsk/ini/version.txt"), verbose);
copy_file("/ini/boot.sh", include_bytes!("../../dsk/ini/boot.sh"), verbose);
copy_file("/ini/palette.csv", include_bytes!("../../dsk/ini/palette.csv"), verbose);
copy_file("/ini/shell.sh", include_bytes!("../../dsk/ini/shell.sh"), verbose);
copy_file("/ini/version.txt", include_bytes!("../../dsk/ini/version.txt"), verbose);
create_dir("/ini/lisp", verbose);
copy_file("/ini/lisp/core.lsp", include_bytes!("../../dsk/ini/lisp/core.lsp"), verbose);

View File

@ -294,8 +294,7 @@ fn default_env() -> Rc<RefCell<Env>> {
data.insert("system".to_string(), Exp::Func(|args: &[Exp]| -> Result<Exp, Err> {
ensure_length_eq!(args, 1);
let cmd = string(&args[0])?;
let mut env = usr::shell::default_env();
let res = usr::shell::exec(&cmd, &mut env);
let res = usr::shell::exec(&cmd);
Ok(Exp::Num(res as u8 as f64))
}));
data.insert("print".to_string(), Exp::Func(|args: &[Exp]| -> Result<Exp, Err> {
@ -673,7 +672,7 @@ fn repl(env: &mut Rc<RefCell<Env>>) -> usr::shell::ExitCode {
prompt.completion.set(&lisp_completer);
while let Some(line) = prompt.input(&prompt_string) {
if line == "(exit)" || line == "(quit)" {
if line == "(quit)" {
break;
}
if line.is_empty() {

View File

@ -14,13 +14,13 @@ use alloc::string::{String, ToString};
// TODO: Scan /bin
const AUTOCOMPLETE_COMMANDS: [&str; 35] = [
"2048", "base64", "calc", "colors", "copy", "date", "delete", "dhcp", "disk", "edit",
"env", "exit", "geotime", "goto", "help", "hex", "host", "http", "httpd", "install",
"keyboard", "lisp", "list", "memory", "move", "net", "pci", "read",
"env", "geotime", "goto", "help", "hex", "host", "http", "httpd", "install",
"keyboard", "lisp", "list", "memory", "move", "net", "pci", "quit", "read",
"shell", "socket", "tcp", "time", "user", "vga", "write"
];
#[repr(u8)]
#[derive(PartialEq)]
#[derive(PartialEq, Eq)]
pub enum ExitCode {
CommandSuccessful = 0,
CommandUnknown = 1,
@ -28,6 +28,23 @@ pub enum ExitCode {
ShellExit = 255,
}
struct Config {
env: BTreeMap<String, String>,
aliases: BTreeMap<String, String>,
}
impl Config {
fn new() -> Config {
let aliases = BTreeMap::new();
let mut env = BTreeMap::new();
for (key, val) in sys::process::envs() {
env.insert(key, val); // Copy the process environment to the shell environment
}
env.insert("DIR".to_string(), sys::process::dir());
Config { env, aliases }
}
}
fn autocomplete_commands() -> Vec<String> {
let mut res = Vec::new();
for cmd in AUTOCOMPLETE_COMMANDS {
@ -38,7 +55,6 @@ fn autocomplete_commands() -> Vec<String> {
res.push(file.name());
}
}
res.sort();
res
}
@ -69,6 +85,7 @@ fn shell_completer(line: &str) -> Vec<String> {
}
}
}
entries.sort();
entries
}
@ -79,19 +96,6 @@ pub fn prompt_string(success: bool) -> String {
format!("{}>{} ", if success { csi_color } else { csi_error }, csi_reset)
}
pub fn default_env() -> BTreeMap<String, String> {
let mut env = BTreeMap::new();
// Copy the process environment to the shell environment
for (key, val) in sys::process::envs() {
env.insert(key, val);
}
env.insert("DIR".to_string(), sys::process::dir());
env
}
fn is_globbing(arg: &str) -> bool {
let arg: Vec<char> = arg.chars().collect();
let n = arg.len();
@ -124,10 +128,10 @@ fn glob_to_regex(pattern: &str) -> String {
fn glob(arg: &str) -> Vec<String> {
let mut matches = Vec::new();
if is_globbing(arg) {
let (dir, pattern) = if arg.contains("/") {
(fs::dirname(&arg).to_string(), fs::filename(&arg).to_string())
let (dir, pattern) = if arg.contains('/') {
(fs::dirname(arg).to_string(), fs::filename(arg).to_string())
} else {
(sys::process::dir().clone(), arg.to_string())
(sys::process::dir(), arg.to_string())
};
let re = Regex::new(&glob_to_regex(&pattern));
@ -192,7 +196,7 @@ pub fn split_args(cmd: &str) -> Vec<String> {
args
}
fn proc(args: &[&str]) -> ExitCode {
fn cmd_proc(args: &[&str]) -> ExitCode {
match args.len() {
1 => {
ExitCode::CommandSuccessful
@ -222,7 +226,7 @@ fn proc(args: &[&str]) -> ExitCode {
}
}
fn change_dir(args: &[&str], env: &mut BTreeMap<String, String>) -> ExitCode {
fn cmd_change_dir(args: &[&str], config: &mut Config) -> ExitCode {
match args.len() {
1 => {
println!("{}", sys::process::dir());
@ -235,7 +239,7 @@ fn change_dir(args: &[&str], env: &mut BTreeMap<String, String>) -> ExitCode {
}
if api::fs::is_dir(&pathname) {
sys::process::set_dir(&pathname);
env.insert("DIR".to_string(), sys::process::dir());
config.env.insert("DIR".to_string(), sys::process::dir());
ExitCode::CommandSuccessful
} else {
error!("File not found '{}'", pathname);
@ -248,27 +252,86 @@ fn change_dir(args: &[&str], env: &mut BTreeMap<String, String>) -> ExitCode {
}
}
pub fn exec(cmd: &str, env: &mut BTreeMap<String, String>) -> ExitCode {
fn cmd_alias(args: &[&str], config: &mut Config) -> ExitCode {
if args.len() != 3 {
let csi_option = Style::color("LightCyan");
let csi_title = Style::color("Yellow");
let csi_reset = Style::reset();
println!("{}Usage:{} alias {}<key> <val>{1}", csi_title, csi_reset, csi_option);
return usr::shell::ExitCode::CommandError;
}
config.aliases.insert(args[1].to_string(), args[2].to_string());
ExitCode::CommandSuccessful
}
fn cmd_unalias(args: &[&str], config: &mut Config) -> ExitCode {
if args.len() != 2 {
let csi_option = Style::color("LightCyan");
let csi_title = Style::color("Yellow");
let csi_reset = Style::reset();
println!("{}Usage:{} unalias {}<key>{1}", csi_title, csi_reset, csi_option);
return usr::shell::ExitCode::CommandError;
}
if config.aliases.remove(&args[1].to_string()).is_none() {
error!("Error: could not unalias '{}'", args[1]);
return usr::shell::ExitCode::CommandError;
}
ExitCode::CommandSuccessful
}
fn cmd_set(args: &[&str], config: &mut Config) -> ExitCode {
if args.len() != 3 {
let csi_option = Style::color("LightCyan");
let csi_title = Style::color("Yellow");
let csi_reset = Style::reset();
println!("{}Usage:{} set {}<key> <val>{1}", csi_title, csi_reset, csi_option);
return usr::shell::ExitCode::CommandError;
}
config.env.insert(args[1].to_string(), args[2].to_string());
ExitCode::CommandSuccessful
}
fn cmd_unset(args: &[&str], config: &mut Config) -> ExitCode {
if args.len() != 2 {
let csi_option = Style::color("LightCyan");
let csi_title = Style::color("Yellow");
let csi_reset = Style::reset();
println!("{}Usage:{} unset {}<key>{1}", csi_title, csi_reset, csi_option);
return usr::shell::ExitCode::CommandError;
}
if config.env.remove(&args[1].to_string()).is_none() {
error!("Error: could not unset '{}'", args[1]);
return usr::shell::ExitCode::CommandError;
}
ExitCode::CommandSuccessful
}
fn exec_with_config(cmd: &str, config: &mut Config) -> ExitCode {
let mut cmd = cmd.to_string();
// Replace `$key` with its value in the environment or an empty string
let re = Regex::new("\\$\\w+");
while let Some((a, b)) = re.find(&cmd) {
let key: String = cmd.chars().skip(a + 1).take(b - a - 1).collect();
let val = env.get(&key).map_or("", String::as_str);
cmd = cmd.replace(&format!("${}", key), &val);
let val = config.env.get(&key).map_or("", String::as_str);
cmd = cmd.replace(&format!("${}", key), val);
}
// Set env var like `foo=42` or `bar = "Hello, World!"
if Regex::new("^\\w+ *= *\\S*$").is_match(&cmd) {
let mut iter = cmd.splitn(2, '=');
let key = iter.next().unwrap_or("").trim().to_string();
let val = iter.next().unwrap_or("").trim().to_string();
env.insert(key, val);
return ExitCode::CommandSuccessful
let mut args = split_args(&cmd);
// Replace command alias
if let Some(alias) = config.aliases.get(&args[0]) {
args.remove(0);
for arg in alias.split(' ').rev() {
args.insert(0, arg.to_string())
}
}
let args = split_args(&cmd);
let mut args: Vec<&str> = args.iter().map(String::as_str).collect();
// Redirections like `print hello => /tmp/hello`
@ -326,62 +389,52 @@ pub fn exec(cmd: &str, env: &mut BTreeMap<String, String>) -> ExitCode {
}
let res = match args[0] {
"" => ExitCode::CommandSuccessful,
"a" => ExitCode::CommandUnknown,
"b" => ExitCode::CommandUnknown,
"c" | "copy" => usr::copy::main(&args),
"d" | "del" | "delete" => usr::delete::main(&args),
"e" | "edit" => usr::editor::main(&args),
"f" | "find" => usr::find::main(&args),
"g" | "go" | "goto" => change_dir(&args, env),
"h" | "help" => usr::help::main(&args),
"i" => ExitCode::CommandUnknown,
"j" => ExitCode::CommandUnknown,
"k" => ExitCode::CommandUnknown,
"l" | "list" => usr::list::main(&args),
"m" | "move" => usr::r#move::main(&args),
"n" => ExitCode::CommandUnknown,
"o" => ExitCode::CommandUnknown,
"q" | "quit" | "exit" => ExitCode::ShellExit,
"r" | "read" => usr::read::main(&args),
"s" => ExitCode::CommandUnknown,
"t" => ExitCode::CommandUnknown,
"u" => ExitCode::CommandUnknown,
"v" => ExitCode::CommandUnknown,
"w" | "write" => usr::write::main(&args),
"x" => ExitCode::CommandUnknown,
"y" => ExitCode::CommandUnknown,
"z" => ExitCode::CommandUnknown,
"vga" => usr::vga::main(&args),
"sh" | "shell" => usr::shell::main(&args),
"calc" => usr::calc::main(&args),
"base64" => usr::base64::main(&args),
"date" => usr::date::main(&args),
"env" => usr::env::main(&args),
"hex" => usr::hex::main(&args),
"net" => usr::net::main(&args),
"dhcp" => usr::dhcp::main(&args),
"http" => usr::http::main(&args),
"httpd" => usr::httpd::main(&args),
"socket" => usr::socket::main(&args),
"tcp" => usr::tcp::main(&args),
"host" => usr::host::main(&args),
"install" => usr::install::main(&args),
"geotime" => usr::geotime::main(&args),
"colors" => usr::colors::main(&args),
"dsk" | "disk" => usr::disk::main(&args),
"user" => usr::user::main(&args),
"mem" | "memory" => usr::memory::main(&args),
"kb" | "keyboard" => usr::keyboard::main(&args),
"lisp" => usr::lisp::main(&args),
"chess" => usr::chess::main(&args),
"beep" => usr::beep::main(&args),
"elf" => usr::elf::main(&args),
"pci" => usr::pci::main(&args),
"2048" => usr::pow::main(&args),
"time" => usr::time::main(&args),
"proc" => proc(&args),
_ => {
"" => ExitCode::CommandSuccessful,
"2048" => usr::pow::main(&args),
"alias" => cmd_alias(&args, config),
"base64" => usr::base64::main(&args),
"beep" => usr::beep::main(&args),
"calc" => usr::calc::main(&args),
"chess" => usr::chess::main(&args),
"colors" => usr::colors::main(&args),
"copy" => usr::copy::main(&args),
"date" => usr::date::main(&args),
"delete" => usr::delete::main(&args),
"dhcp" => usr::dhcp::main(&args),
"disk" => usr::disk::main(&args),
"edit" => usr::editor::main(&args),
"elf" => usr::elf::main(&args),
"env" => usr::env::main(&args),
"find" => usr::find::main(&args),
"geotime" => usr::geotime::main(&args),
"goto" => cmd_change_dir(&args, config),
"help" => usr::help::main(&args),
"hex" => usr::hex::main(&args),
"host" => usr::host::main(&args),
"http" => usr::http::main(&args),
"httpd" => usr::httpd::main(&args),
"install" => usr::install::main(&args),
"keyboard" => usr::keyboard::main(&args),
"lisp" => usr::lisp::main(&args),
"list" => usr::list::main(&args),
"memory" => usr::memory::main(&args),
"move" => usr::r#move::main(&args),
"net" => usr::net::main(&args),
"pci" => usr::pci::main(&args),
"proc" => cmd_proc(&args),
"quit" => ExitCode::ShellExit,
"read" => usr::read::main(&args),
"set" => cmd_set(&args, config),
"shell" => usr::shell::main(&args),
"socket" => usr::socket::main(&args),
"tcp" => usr::tcp::main(&args),
"time" => usr::time::main(&args),
"unalias" => cmd_unalias(&args, config),
"unset" => cmd_unset(&args, config),
"user" => usr::user::main(&args),
"vga" => usr::vga::main(&args),
"write" => usr::write::main(&args),
_ => {
let mut path = fs::realpath(args[0]);
if path.len() > 1 {
path = path.trim_end_matches('/').into();
@ -389,7 +442,7 @@ pub fn exec(cmd: &str, env: &mut BTreeMap<String, String>) -> ExitCode {
match syscall::info(&path).map(|info| info.kind()) {
Some(FileType::Dir) => {
sys::process::set_dir(&path);
env.insert("DIR".to_string(), sys::process::dir());
config.env.insert("DIR".to_string(), sys::process::dir());
ExitCode::CommandSuccessful
}
Some(FileType::File) => {
@ -402,12 +455,7 @@ pub fn exec(cmd: &str, env: &mut BTreeMap<String, String>) -> ExitCode {
}
}
_ => {
// TODO: add aliases command instead of hardcoding them
let name = match args[0] {
"p" => "print",
arg => arg,
};
if api::process::spawn(&format!("/bin/{}", name), &args).is_ok() {
if api::process::spawn(&format!("/bin/{}", args[0]), &args).is_ok() {
ExitCode::CommandSuccessful
} else {
error!("Could not execute '{}'", cmd);
@ -428,7 +476,7 @@ pub fn exec(cmd: &str, env: &mut BTreeMap<String, String>) -> ExitCode {
res
}
fn repl(env: &mut BTreeMap<String, String>) -> usr::shell::ExitCode {
fn repl(config: &mut Config) -> usr::shell::ExitCode {
println!();
let mut prompt = Prompt::new();
@ -438,7 +486,7 @@ fn repl(env: &mut BTreeMap<String, String>) -> usr::shell::ExitCode {
let mut success = true;
while let Some(cmd) = prompt.input(&prompt_string(success)) {
match exec(&cmd, env) {
match exec_with_config(&cmd, config) {
ExitCode::CommandSuccessful => {
success = true;
},
@ -458,26 +506,37 @@ fn repl(env: &mut BTreeMap<String, String>) -> usr::shell::ExitCode {
ExitCode::CommandSuccessful
}
pub fn exec(cmd: &str) -> ExitCode {
let mut config = Config::new();
exec_with_config(cmd, &mut config)
}
pub fn main(args: &[&str]) -> ExitCode {
let mut env = default_env();
let mut config = Config::new();
if let Ok(rc) = fs::read_to_string("/ini/shell.sh") {
for cmd in rc.split('\n') {
exec_with_config(cmd, &mut config);
}
}
if args.len() < 2 {
env.insert(0.to_string(), args[0].to_string());
config.env.insert(0.to_string(), args[0].to_string());
repl(&mut env)
repl(&mut config)
} else {
env.insert(0.to_string(), args[1].to_string());
config.env.insert(0.to_string(), args[1].to_string());
// Add script arguments to the environment as `$1`, `$2`, `$3`, ...
for (i, arg) in args[2..].iter().enumerate() {
env.insert((i + 1).to_string(), arg.to_string());
config.env.insert((i + 1).to_string(), arg.to_string());
}
let pathname = args[1];
if let Ok(contents) = api::fs::read_to_string(pathname) {
for line in contents.split('\n') {
if !line.is_empty() {
exec(line, &mut env);
exec_with_config(line, &mut config);
}
}
ExitCode::CommandSuccessful
@ -496,26 +555,25 @@ fn test_shell() {
sys::fs::format_mem();
usr::install::copy_files(false);
let mut env = default_env();
// Redirect standard output
exec("print test1 => /test", &mut env);
exec("print test1 => /test");
assert_eq!(api::fs::read_to_string("/test"), Ok("test1\n".to_string()));
// Overwrite content of existing file
exec("print test2 => /test", &mut env);
exec("print test2 => /test");
assert_eq!(api::fs::read_to_string("/test"), Ok("test2\n".to_string()));
// Redirect standard output explicitely
exec("print test3 1=> /test", &mut env);
exec("print test3 1=> /test");
assert_eq!(api::fs::read_to_string("/test"), Ok("test3\n".to_string()));
// Redirect standard error explicitely
exec("hex /nope 2=> /test", &mut env);
exec("hex /nope 2=> /test");
assert!(api::fs::read_to_string("/test").unwrap().contains("File not found '/nope'"));
exec("b = 42", &mut env);
exec("print a $b $c d => /test", &mut env);
let mut config = Config::new();
exec_with_config("set b 42", &mut config);
exec_with_config("print a $b $c d => /test", &mut config);
assert_eq!(api::fs::read_to_string("/test"), Ok("a 42 d\n".to_string()));
sys::fs::dismount();

View File

@ -6,9 +6,8 @@ pub fn main(args: &[&str]) -> usr::shell::ExitCode {
let csi_color = Style::color("LightBlue");
let csi_reset = Style::reset();
let cmd = args[1..].join(" ");
let mut env = usr::shell::default_env();
let start = clock::realtime();
usr::shell::exec(&cmd, &mut env);
usr::shell::exec(&cmd);
let duration = clock::realtime() - start;
println!("{}Executed '{}' in {:.6}s{}", csi_color, cmd, duration, csi_reset);
usr::shell::ExitCode::CommandSuccessful