Skip to content

Commit

Permalink
Merge branch 'trunk' into feature/doc
Browse files Browse the repository at this point in the history
  • Loading branch information
vinc committed Jun 1, 2024
2 parents fb80ab3 + 946d7a2 commit ae58f15
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 70 deletions.
Binary file modified doc/images/find.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions src/api/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ pub fn is_dir(path: &str) -> bool {
}
}

pub fn is_file(path: &str) -> bool {
if let Some(info) = syscall::info(path) {
info.is_file()
} else {
false
}
}

pub fn delete(path: &str) -> Result<(), ()> {
syscall::delete(path)
}
Expand Down
20 changes: 20 additions & 0 deletions src/api/regex.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::ops::RangeBounds;
Expand Down Expand Up @@ -64,6 +65,16 @@ impl Regex {
Self(re.to_string())
}

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

pub fn is_match(&self, text: &str) -> bool {
self.find(text).is_some()
}
Expand Down Expand Up @@ -300,3 +311,12 @@ fn test_regex() {
assert_eq!(Regex::new("a\\w*?d").find("abcdabcd"), Some((0, 4)));
assert_eq!(Regex::new("\\$\\w+").find("test $test test"), Some((5, 10)));
}

#[test_case]
fn test_regex_from_glob() {
assert_eq!(Regex::from_glob("hello.txt").0, "^hello\\.txt$");
assert_eq!(Regex::from_glob("h?llo.txt").0, "^h.llo\\.txt$");
assert_eq!(Regex::from_glob("h*.txt").0, "^h.*\\.txt$");
assert_eq!(Regex::from_glob("*.txt").0, "^.*\\.txt$");
assert_eq!(Regex::from_glob("\\w*.txt").0, "^\\\\w.*\\.txt$");
}
161 changes: 111 additions & 50 deletions src/usr/find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,29 @@ use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::iter::FromIterator;

struct PrintingState {
struct Options {
is_first_match: bool,
is_recursive: bool,
file: String,
line: String,
trim: String,
}

impl PrintingState {
impl Options {
fn new() -> Self {
Self {
is_first_match: true,
is_recursive: false,
file: "*".into(),
line: "".into(),
trim: "".into(),
}
}
}

// > find /tmp -name *.txt -line hello
pub fn main(args: &[&str]) -> Result<(), ExitCode> {
let mut path: &str = &sys::process::dir(); // TODO: use '.'
let mut name = None;
let mut line = None;
let mut path = String::new();
let mut options = Options::new();
let mut i = 1;
let n = args.len();
while i < n {
Expand All @@ -36,85 +40,98 @@ pub fn main(args: &[&str]) -> Result<(), ExitCode> {
usage();
return Ok(());
}
"-n" | "--name" => {
"-f" | "--file" => {
if i + 1 < n {
i += 1;
name = Some(args[i]);
options.file = args[i].into();
} else {
error!("Missing name");
error!("Missing file pattern");
return Err(ExitCode::UsageError);
}
}
"-l" | "--line" => {
if i + 1 < n {
i += 1;
line = Some(args[i]);
options.line = args[i].into();
} else {
error!("Missing line");
error!("Missing line pattern");
return Err(ExitCode::UsageError);
}
}
_ => {
if path.is_empty() {
path = args[i].into();
} else {
error!("Multiple paths not supported");
return Err(ExitCode::UsageError);
}
}
_ => path = args[i],
}
i += 1;
}

if path.len() > 1 {
path = path.trim_end_matches('/');
}

if name.is_none() && line.is_none() {
usage();
return Err(ExitCode::UsageError);
if path.is_empty() {
path = sys::process::dir();
options.trim = format!("{}/", path);
}

if name.is_some() {
// TODO
error!("`--name` is not implemented");
return Err(ExitCode::Failure);
if path.len() > 1 {
path = path.trim_end_matches('/').into();
}

let mut state = PrintingState::new();
if let Some(pattern) = line {
print_matching_lines(path, pattern, &mut state);
if fs::is_dir(&path) || (fs::is_file(&path) && !options.line.is_empty()) {
search_files(&path, &mut options);
} else {
error!("Invalid path");
return Err(ExitCode::UsageError);
}

Ok(())
}

fn print_matching_lines(path: &str, pattern: &str, state: &mut PrintingState) {
if let Ok(files) = fs::read_dir(path) {
state.is_recursive = true;
fn search_files(path: &str, options: &mut Options) {
if let Ok(mut files) = fs::read_dir(path) {
files.sort_by_key(|f| f.name());
options.is_recursive = true;
for file in files {
let mut file_path = path.to_string();
if !file_path.ends_with('/') {
file_path.push('/');
}
file_path.push_str(&file.name());
if file.is_dir() {
print_matching_lines(&file_path, pattern, state);
} else if file.is_device() {
// Skip devices
} else {
print_matching_lines_in_file(&file_path, pattern, state);
search_files(&file_path, options);
} else if is_matching_file(&file_path, &options.file) {
if options.line == "" {
println!("{}", file_path.trim_start_matches(&options.trim));
} else {
print_matching_lines(&file_path, options);
}
}

}
} else if fs::exists(path) {
print_matching_lines_in_file(path, pattern, state);
} else {
print_matching_lines(path, options);
}
}

fn print_matching_lines_in_file(
path: &str,
pattern: &str,
state: &mut PrintingState
) {
let name_color = Style::color("yellow");
fn is_matching_file(path: &str, pattern: &str) -> bool {
let file = fs::filename(&path);
let re = Regex::from_glob(pattern);
re.is_match(file)
}

fn print_matching_lines(path: &str, options: &mut Options) {
if !fs::is_file(path) {
return;
}

let file_color = Style::color("yellow");
let line_color = Style::color("aqua");
let match_color = Style::color("red");
let reset = Style::reset();

let re = Regex::new(pattern);
let re = Regex::new(&options.line);
if let Ok(lines) = fs::read_to_string(path) {
let mut matches = Vec::new();
for (i, line) in lines.split('\n').enumerate() {
Expand Down Expand Up @@ -143,13 +160,13 @@ fn print_matching_lines_in_file(
}
}
if !matches.is_empty() {
if state.is_recursive {
if state.is_first_match {
state.is_first_match = false;
if options.is_recursive {
if options.is_first_match {
options.is_first_match = false;
} else {
println!();
}
println!("{}{}{}", name_color, path, reset);
println!("{}{}{}", file_color, path, reset);
}
let width = matches[matches.len() - 1].0.to_string().len();
for (i, line) in matches {
Expand All @@ -171,14 +188,14 @@ fn usage() {
let csi_title = Style::color("yellow");
let csi_reset = Style::reset();
println!(
"{}Usage:{} find {}<options> <path>{1}",
"{}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}",
" {0}-f{1}, {0}--file \"<pattern>\"{1} \
Find files matching {0}<pattern>{1}",
csi_option, csi_reset
);
println!(
Expand All @@ -187,3 +204,47 @@ fn usage() {
csi_option, csi_reset
);
}

#[test_case]
fn test_find() {
use crate::{api, usr, sys};
use crate::usr::shell::exec;

sys::fs::mount_mem();
sys::fs::format_mem();
usr::install::copy_files(false);

exec("find / => /tmp/find.log").ok();
assert!(api::fs::read_to_string("/tmp/find.log").unwrap().
contains("/tmp/alice.txt"));

exec("find /dev => /tmp/find.log").ok();
assert!(api::fs::read_to_string("/tmp/find.log").unwrap().
contains("/dev/random"));

exec("find /tmp/alice.txt --line Alice => /tmp/find.log").ok();
assert!(api::fs::read_to_string("/tmp/find.log").unwrap().
contains("Alice"));

exec("find nope 2=> /tmp/find.log").ok();
assert!(api::fs::read_to_string("/tmp/find.log").unwrap().
contains("Invalid path"));

exec("find /tmp/alice.txt 2=> /tmp/find.log").ok();
assert!(api::fs::read_to_string("/tmp/find.log").unwrap().
contains("Invalid path"));

exec("find /dev/random --line nope 2=> /tmp/find.log").ok();
assert!(api::fs::read_to_string("/tmp/find.log").unwrap().
contains("Invalid path"));

exec("find /tmp --line list => /tmp/find.log").ok();
assert!(api::fs::read_to_string("/tmp/find.log").unwrap().
contains("alice.txt"));

exec("find /tmp --file \"*.lsp\" --line list => /tmp/find.log").ok();
assert!(!api::fs::read_to_string("/tmp/find.log").unwrap().
contains("alice.txt"));

sys::fs::dismount();
}
21 changes: 1 addition & 20 deletions src/usr/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,6 @@ fn is_globbing(arg: &str) -> bool {
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) {
Expand All @@ -160,7 +150,7 @@ fn glob(arg: &str) -> Vec<String> {
} else {
(sys::process::dir(), arg.to_string(), false)
};
let re = Regex::new(&glob_to_regex(&pattern));
let re = Regex::from_glob(&pattern);
let sep = if dir == "/" { "" } else { "/" };
if let Ok(files) = fs::read_dir(&dir) {
for file in files {
Expand Down Expand Up @@ -764,15 +754,6 @@ fn test_split_args() {
assert_eq!(split_args("print foo \"\" "), vec!["print", "foo", ""]);
}

#[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$");
}

#[test_case]
fn test_variables_expansion() {
let mut config = Config::new();
Expand Down

0 comments on commit ae58f15

Please sign in to comment.