From a689641c1b9bbfea6eb917e8b322d01a5ef6244b Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Mon, 22 Aug 2022 17:10:46 +0300 Subject: [PATCH] os: rewrite os.walk and os.walk_with_context to use iteration, instead of recursion --- vlib/os/os.c.v | 35 +++++++++++++++++++++++ vlib/os/os.v | 69 +++++++++++++++++++++++++++++----------------- vlib/os/os_js.js.v | 18 ++++++++++++ 3 files changed, 97 insertions(+), 25 deletions(-) diff --git a/vlib/os/os.c.v b/vlib/os/os.c.v index 8d2869195..241e15603 100644 --- a/vlib/os/os.c.v +++ b/vlib/os/os.c.v @@ -738,6 +738,41 @@ pub fn is_link(path string) bool { } } +struct PathKind { +mut: + is_dir bool + is_link bool +} + +fn kind_of_existing_path(path string) PathKind { + mut res := PathKind{} + $if windows { + attr := C.GetFileAttributesW(path.to_wide()) + if attr != u32(C.INVALID_FILE_ATTRIBUTES) { + if (int(attr) & C.FILE_ATTRIBUTE_DIRECTORY) != 0 { + res.is_dir = true + } + if (int(attr) & 0x400) != 0 { + res.is_link = true + } + } + } $else { + statbuf := C.stat{} + // ref: https://code.woboq.org/gcc/include/sys/stat.h.html + res_stat := unsafe { C.lstat(&char(path.str), &statbuf) } + if res_stat == 0 { + kind := (int(statbuf.st_mode) & s_ifmt) + if kind == s_ifdir { + res.is_dir = true + } + if kind == s_iflnk { + res.is_link = true + } + } + } + return res +} + // chdir changes the current working directory to the new directory in `path`. pub fn chdir(path string) ? { ret := $if windows { C._wchdir(path.to_wide()) } $else { C.chdir(&char(path.str)) } diff --git a/vlib/os/os.v b/vlib/os/os.v index f011bb2eb..43ece4346 100644 --- a/vlib/os/os.v +++ b/vlib/os/os.v @@ -569,8 +569,9 @@ fn impl_walk_ext(path string, ext string, mut out []string) { } // walk traverses the given directory `path`. -// When a file is encountred it will call the -// callback function `f` with current file as argument. +// When a file is encountred, it will call the callback `f` with current file as argument. +// Note: walk can be called even for deeply nested folders, +// since it does not recurse, but processes them iteratively. pub fn walk(path string, f fn (string)) { if path.len == 0 { return @@ -578,29 +579,36 @@ pub fn walk(path string, f fn (string)) { if !is_dir(path) { return } - mut files := ls(path) or { return } - mut local_path_separator := path_separator - if path.ends_with(path_separator) { - local_path_separator = '' + mut remaining := []string{cap: 1000} + clean_path := path.trim_right(path_separator) + $if windows { + remaining << clean_path.replace('/', '\\') + } $else { + remaining << clean_path } - for file in files { - p := path + local_path_separator + file - if is_dir(p) && !is_link(p) { - walk(p, f) - } else if exists(p) { - f(p) + for remaining.len > 0 { + cpath := remaining.pop() + pkind := kind_of_existing_path(cpath) + if pkind.is_link || !pkind.is_dir { + f(cpath) + continue + } + mut files := ls(cpath) or { continue } + for idx := files.len - 1; idx >= 0; idx-- { + remaining << cpath + path_separator + files[idx] } } - return } // FnWalkContextCB is used to define the callback functions, passed to os.walk_context pub type FnWalkContextCB = fn (voidptr, string) // walk_with_context traverses the given directory `path`. -// For each encountred file and directory, it will call your `fcb` callback, +// For each encountred file *and* directory, it will call your `fcb` callback, // passing it the arbitrary `context` in its first parameter, // and the path to the file in its second parameter. +// Note: walk_with_context can be called even for deeply nested folders, +// since it does not recurse, but processes them iteratively. pub fn walk_with_context(path string, context voidptr, fcb FnWalkContextCB) { if path.len == 0 { return @@ -608,19 +616,30 @@ pub fn walk_with_context(path string, context voidptr, fcb FnWalkContextCB) { if !is_dir(path) { return } - mut files := ls(path) or { return } - mut local_path_separator := path_separator - if path.ends_with(path_separator) { - local_path_separator = '' - } - for file in files { - p := path + local_path_separator + file - fcb(context, p) - if is_dir(p) && !is_link(p) { - walk_with_context(p, context, fcb) + mut remaining := []string{cap: 1000} + clean_path := path.trim_right(path_separator) + $if windows { + remaining << clean_path.replace('/', '\\') + } $else { + remaining << clean_path + } + mut loops := 0 + for remaining.len > 0 { + loops++ + cpath := remaining.pop() + // call `fcb` for everything, but the initial folder: + if loops > 1 { + fcb(context, cpath) + } + pkind := kind_of_existing_path(cpath) + if pkind.is_link || !pkind.is_dir { + continue + } + mut files := ls(cpath) or { continue } + for idx := files.len - 1; idx >= 0; idx-- { + remaining << cpath + path_separator + files[idx] } } - return } // log will print "os.log: "+`s` ... diff --git a/vlib/os/os_js.js.v b/vlib/os/os_js.js.v index ad14be843..f4d4500a6 100644 --- a/vlib/os/os_js.js.v +++ b/vlib/os/os_js.js.v @@ -29,6 +29,24 @@ pub fn is_link(path string) bool { return res } +struct PathKind { + is_dir bool + is_link bool +} + +fn kind_of_existing_path(path string) PathKind { + is_link := false + is_dir := false + $if js_node { + #is_link.val = $fs.existsSync(path.str) && $fs.lstatSync(path.str).isSymbolicLink() + #is_dir.val = $fs.existsSync(path,str) && $fs.lstatSync(path.str).isDirectory() + } + return PathKind{ + is_dir: is_dir + is_link: is_link + } +} + pub fn exists(path string) bool { res := false $if js_node { -- 2.30.2