Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add shell globbing #352

Merged
merged 6 commits into from
Jun 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions doc/shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,15 @@ And accessing that variable is done with the `$` operator:
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.

## Globbing

MOROS Shell support filename expansion or globbing for `*` and `?` wildcard
characters, where a pattern given in an argument of a command will be replaced
by files matching the pattern.

- `*` means zero or more chars except `/`
- `?` means any char except `/`

For example `/tmp/*.txt` will match any files with the `txt` extension inside
`/tmp`, and `a?c.txt` will match a file named `abc.txt`.
43 changes: 22 additions & 21 deletions src/usr/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,35 @@ use crate::api::syscall;
use crate::api::fs;

pub fn main(args: &[&str]) -> usr::shell::ExitCode {
if args.len() != 2 {
if args.len() < 2 {
return usr::shell::ExitCode::CommandError;
}

let mut pathname = args[1];
for arg in &args[1..] {
let mut pathname = arg.clone();

// The commands `delete /usr/alice/` and `delete /usr/alice` are equivalent,
// but `delete /` should not be modified.
if pathname.len() > 1 {
pathname = pathname.trim_end_matches('/');
}

if !fs::exists(pathname) {
error!("File not found '{}'", pathname);
return usr::shell::ExitCode::CommandError;
}
// The commands `delete /usr/alice/` and `delete /usr/alice` are equivalent,
// but `delete /` should not be modified.
if pathname.len() > 1 {
pathname = pathname.trim_end_matches('/');
}

if let Some(info) = syscall::info(pathname) {
if info.is_dir() && info.size() > 0 {
error!("Directory '{}' not empty", pathname);
if !fs::exists(pathname) {
error!("File not found '{}'", pathname);
return usr::shell::ExitCode::CommandError;
}
}

if fs::delete(pathname).is_ok() {
usr::shell::ExitCode::CommandSuccessful
} else {
error!("Could not delete file '{}'", pathname);
usr::shell::ExitCode::CommandError
if let Some(info) = syscall::info(pathname) {
if info.is_dir() && info.size() > 0 {
error!("Directory '{}' not empty", pathname);
return usr::shell::ExitCode::CommandError;
}
}

if fs::delete(pathname).is_err() {
error!("Could not delete file '{}'", pathname);
return usr::shell::ExitCode::CommandError;
}
}
usr::shell::ExitCode::CommandSuccessful
}
9 changes: 5 additions & 4 deletions src/usr/find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ pub fn main(args: &[&str]) -> usr::shell::ExitCode {
path = path.trim_end_matches('/');
}

if name.is_some() {
todo!();
if name.is_some() { // TODO
error!("`--name` is not implemented");
return usr::shell::ExitCode::CommandError;
}

let mut state = PrintingState::new();
Expand Down Expand Up @@ -153,7 +154,7 @@ fn help() -> usr::shell::ExitCode {
println!("{}Usage:{} find {}<options> <path>{1}", csi_title, csi_reset, csi_option);
println!();
println!("{}Options:{}", csi_title, csi_reset);
println!(" {0}-n{1},{0} --name <pattern>{1} Find file name matching {0}<pattern>{1}", csi_option, csi_reset);
println!(" {0}-l{1},{0} --line <pattern>{1} Find lines matching {0}<pattern>{1}", csi_option, csi_reset);
println!(" {0}-n{1},{0} --name \"<pattern>\"{1} Find file name matching {0}<pattern>{1}", csi_option, csi_reset);
println!(" {0}-l{1},{0} --line \"<pattern>\"{1} Find lines matching {0}<pattern>{1}", csi_option, csi_reset);
usr::shell::ExitCode::CommandSuccessful
}
90 changes: 81 additions & 9 deletions src/usr/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ fn shell_completer(line: &str) -> Vec<String> {
let i = args.len() - 1;
if args.len() == 1 && !args[0].starts_with('/') { // Autocomplete command
for cmd in autocomplete_commands() {
if let Some(entry) = cmd.strip_prefix(args[i]) {
if let Some(entry) = cmd.strip_prefix(&args[i]) {
entries.push(entry.into());
}
}
} else { // Autocomplete path
let pathname = fs::realpath(args[i]);
let pathname = fs::realpath(&args[i]);
let dirname = fs::dirname(&pathname);
let filename = fs::filename(&pathname);
let sep = if dirname.ends_with('/') { "" } else { "/" };
Expand Down Expand Up @@ -90,8 +90,62 @@ pub fn default_env() -> BTreeMap<String, String> {
env
}

pub fn split_args(cmd: &str) -> Vec<&str> {
let mut args: Vec<&str> = Vec::new();
fn is_globbing(arg: &str) -> bool {
let arg: Vec<char> = arg.chars().collect();
let n = arg.len();
if n == 0 {
return false;
}
if arg[0] == '"' && arg[n - 1] == '"' {
return false;
}
if arg[0] == '\'' && arg[n - 1] == '\'' {
return false;
}
for i in 0..n {
if arg[i] == '*' || arg[i] == '?' {
return true;
}
}
false
}

fn glob_to_regex(pattern: &str) -> String {
format!("^{}$", pattern
.replace('\\', "\\\\") // `\` string literal
.replace('.', "\\.") // `.` string literal
.replace('*', ".*") // `*` match zero or more chars except `/`
.replace('?', ".") // `?` match any char except `/`
)
}

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())
} else {
(sys::process::dir().clone(), arg.to_string())
};

let re = Regex::new(&glob_to_regex(&pattern));

if let Ok(files) = fs::read_dir(&dir) {
for file in files {
let name = file.name();
if re.is_match(&name) {
matches.push(format!("{}/{}", dir, name));
}
}
}
} else {
matches.push(arg.to_string());
}
matches
}

pub fn split_args(cmd: &str) -> Vec<String> {
let mut args = Vec::new();
let mut i = 0;
let mut n = cmd.len();
let mut is_quote = false;
Expand All @@ -102,13 +156,17 @@ pub fn split_args(cmd: &str) -> Vec<&str> {
break;
} else if c == ' ' && !is_quote {
if i != j {
args.push(&cmd[i..j]);
if args.is_empty() {
args.push(cmd[i..j].to_string())
} else {
args.extend(glob(&cmd[i..j]))
}
}
i = j + 1;
} else if c == '"' {
is_quote = !is_quote;
if !is_quote {
args.push(&cmd[i..j]);
args.push(cmd[i..j].to_string());
}
i = j + 1;
}
Expand All @@ -117,12 +175,16 @@ pub fn split_args(cmd: &str) -> Vec<&str> {
if i < n {
if is_quote {
n -= 1;
args.push(cmd[i..n].to_string());
} else if args.is_empty() {
args.push(cmd[i..n].to_string());
} else {
args.extend(glob(&cmd[i..n]))
}
args.push(&cmd[i..n]);
}

if n == 0 || cmd.ends_with(' ') {
args.push("");
args.push("".to_string());
}

args
Expand Down Expand Up @@ -203,7 +265,8 @@ pub fn exec(cmd: &str, env: &mut BTreeMap<String, String>) -> ExitCode {
return ExitCode::CommandSuccessful
}

let mut args = split_args(&cmd);
let args = split_args(&cmd);
let mut args: Vec<&str> = args.iter().map(String::as_str).collect();

// Redirections like `print hello => /tmp/hello`
// Pipes like `print hello -> write /tmp/hello` or `p hello > w /tmp/hello`
Expand Down Expand Up @@ -453,3 +516,12 @@ fn test_shell() {

sys::fs::dismount();
}

#[test_case]
fn test_glob_to_regex() {
assert_eq!(glob_to_regex("hello.txt"), "^hello\\.txt$");
assert_eq!(glob_to_regex("h?llo.txt"), "^h.llo\\.txt$");
assert_eq!(glob_to_regex("h*.txt"), "^h.*\\.txt$");
assert_eq!(glob_to_regex("*.txt"), "^.*\\.txt$");
assert_eq!(glob_to_regex("\\w*.txt"), "^\\\\w.*\\.txt$");
}