Add shell variables (#348)

* Add env vars to shell

* Add shell env

* Rename shell::run to shell::repl

* Add script pathname to shell env

* Add shell variables

* Add test

* Add doc

* Fix doc
This commit is contained in:
Vincent Ollivier 2022-06-08 20:51:02 +02:00 committed by GitHub
parent 9ad5f874e4
commit 0831045c64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 102 additions and 32 deletions

View File

@ -121,3 +121,25 @@ Which is more efficient than doing:
> print hello -> write /tmp/hello
## Variables
- Name of the shell or the script: `$0`
- Script arguments: `$1`, `$2`, `$3`, `$4`, ...
- Process environment variable: `$HOME`, ...
- Shell environment variable: `$foo`, ...
Setting a variable in the shell environment is done with the following command:
> foo = "world"
And accessing that variable is done with the `$` operator:
> print $foo
world
> print "hello $foo"
hello world
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.

View File

@ -62,10 +62,10 @@ pub fn delete(path: &str) -> isize
pub fn stop(code: usize)
```
The system will reboot with `0xcafe` and halt with `0xdead`.
## SLEEP (0xB)
```rust
pub fn sleep(seconds: f64)
```
The system will reboot with `0xcafe` and halt with `0xdead`.

View File

@ -64,9 +64,11 @@ impl Regex {
pub fn new(re: &str) -> Self {
Self(re.to_string())
}
pub fn is_match(&self, text: &str) -> bool {
self.find(text).is_some()
}
pub fn find(&self, text: &str) -> Option<(usize, usize)> {
let text: Vec<char> = text.chars().collect(); // UTF-32
let re: Vec<char> = self.0.chars().collect(); // UTF-32
@ -264,4 +266,5 @@ fn test_regex() {
assert_eq!(Regex::new("b.*?c").find("aaabbbcccddd"), Some((3, 7)));
assert_eq!(Regex::new("a\\w*d").find("abcdabcd"), Some((0, 8)));
assert_eq!(Regex::new("a\\w*?d").find("abcdabcd"), Some((0, 4)));
assert_eq!(Regex::new("\\$\\w+").find("test $test test"), Some((5, 10)));
}

View File

@ -14,9 +14,10 @@ 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);
usr::shell::exec(cmd, &mut env);
sys::acpi::shutdown();
} else {
user_boot();

View File

@ -294,7 +294,8 @@ 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 res = usr::shell::exec(&cmd);
let mut env = usr::shell::default_env();
let res = usr::shell::exec(&cmd, &mut env);
Ok(Exp::Num(res as u8 as f64))
}));
data.insert("print".to_string(), Exp::Func(|args: &[Exp]| -> Result<Exp, Err> {

View File

@ -3,9 +3,11 @@ use crate::api::fs;
use crate::api::regex::Regex;
use crate::api::prompt::Prompt;
use crate::api::console::Style;
use alloc::collections::btree_map::BTreeMap;
use alloc::format;
use alloc::vec::Vec;
use alloc::string::String;
use alloc::string::{String, ToString};
// TODO: Scan /bin
const AUTOCOMPLETE_COMMANDS: [&str; 40] = [
@ -61,6 +63,17 @@ 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
}
pub fn split_args(cmd: &str) -> Vec<&str> {
let mut args: Vec<&str> = Vec::new();
let mut i = 0;
@ -154,8 +167,27 @@ fn change_dir(args: &[&str]) -> ExitCode {
}
}
pub fn exec(cmd: &str) -> ExitCode {
let mut args = split_args(cmd);
pub fn exec(cmd: &str, env: &mut BTreeMap<String, String>) -> 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);
}
// 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);
// Redirections like `print hello => /tmp/hello`
// Pipes like `print hello -> write /tmp/hello` or `p hello > w /tmp/hello`
@ -291,7 +323,7 @@ pub fn exec(cmd: &str) -> ExitCode {
res
}
pub fn run() -> usr::shell::ExitCode {
fn repl(env: &mut BTreeMap<String, String>) -> usr::shell::ExitCode {
println!();
let mut prompt = Prompt::new();
@ -301,7 +333,7 @@ pub fn run() -> usr::shell::ExitCode {
let mut success = true;
while let Some(cmd) = prompt.input(&prompt_string(success)) {
match exec(&cmd) {
match exec(&cmd, env) {
ExitCode::CommandSuccessful => {
success = true;
},
@ -322,27 +354,32 @@ pub fn run() -> usr::shell::ExitCode {
}
pub fn main(args: &[&str]) -> ExitCode {
match args.len() {
1 => {
run()
},
2 => {
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);
}
let mut env = default_env();
if args.len() < 2 {
env.insert(0.to_string(), args[0].to_string());
repl(&mut env)
} else {
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());
}
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);
}
ExitCode::CommandSuccessful
} else {
println!("File not found '{}'", pathname);
ExitCode::CommandError
}
},
_ => {
ExitCode::CommandSuccessful
} else {
println!("File not found '{}'", pathname);
ExitCode::CommandError
},
}
}
}
@ -354,22 +391,27 @@ fn test_shell() {
sys::fs::format_mem();
usr::install::copy_files(false);
let mut env = default_env();
// Redirect standard output
exec("print test1 => /test");
exec("print test1 => /test", &mut env);
assert_eq!(api::fs::read_to_string("/test"), Ok("test1\n".to_string()));
// Overwrite content of existing file
exec("print test2 => /test");
exec("print test2 => /test", &mut env);
assert_eq!(api::fs::read_to_string("/test"), Ok("test2\n".to_string()));
// Redirect standard output explicitely
exec("print test3 1=> /test");
exec("print test3 1=> /test", &mut env);
assert_eq!(api::fs::read_to_string("/test"), Ok("test3\n".to_string()));
// Redirect standard error explicitely
exec("hex /nope 2=> /test");
exec("hex /nope 2=> /test", &mut env);
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);
assert_eq!(api::fs::read_to_string("/test"), Ok("a 42 d\n".to_string()));
sys::fs::dismount();
}

View File

@ -6,8 +6,9 @@ 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);
usr::shell::exec(&cmd, &mut env);
let duration = clock::realtime() - start;
println!("{}Executed '{}' in {:.6}s{}", csi_color, cmd, duration, csi_reset);
usr::shell::ExitCode::CommandSuccessful