diff --git a/README.md b/README.md index 3625c3b..8fa1579 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,90 @@ # Walk.js (@root/walk) -A port of Go's [`filepath.Walk`](https://golang.org/pkg/path/filepath/#Walk) for Node.js v10+ (which introduced `fs.readdir` `withFileTypes`). +Walk a directory recursively and handle each entity (files, directories, symlnks, etc). ```js -await Walk.walk(rootpath, walkFunc); +await Walk.walk(pathname, walkFunc); + +function walkFunc(err, pathname, dirent) { + // ... +} ``` -# Example +This is a port of Go's [`filepath.Walk`](https://golang.org/pkg/path/filepath/#Walk) +for Node.js v10+ (which introduced `fs.readdir` `withFileTypes`) and ES 2021. + +# Examples + +You can use this with Node v12+ using Vanilla JS or ES2021. + +## ES 2021 Modules + +`@root/walk` can be used with async/await or Promises. ```js -import Walk from "walk"; +import { walk } from "@root/walk"; +import path from "path"; -Walk.walk("./", function (err, pathname, dirent) { +await walk("./", async (err, pathname, dirent) => { + if (err) { + // throw an error to stop walking + // (or return to ignore and keep going) + console.warn("fs stat error for %s: %s", pathname, err.message); + return; + } + + // return false to skip a directory + // (ex: skipping "dot files") + if (dirent.isDirectory() && dirent.name.startsWith(".")) { + return false; + } + + // fs.Dirent is a slimmed-down, faster version of fs.Stat + console.log("name:", dirent.name, "in", path.dirname(pathname)); + // (only one of these will be true) + console.log("is file?", dirent.isFile()); + console.log("is link?", dirent.isSymbolicLink()); +}); + +console.log("Done"); +``` + +## Vanilla JS (ES5) + +```js +var Walk = require("@root/walk"); +var path = require("path"); + +Walk.walk("./", function walkFunc(err, pathname, dirent) { if (err) { throw err; } - // ignore dot files - if (dirent.name.startsWith(".")) { - return Walk.skipDir; + if (dirent.isDirectory() && dirent.name.startsWith(".")) { + return Promise.resolve(false); } + console.log("name:", dirent.name, "in", path.dirname(pathname)); + + return Promise.resolve(); +}).then(function () { + console.log("Done"); }); ``` # API Documentation -`Walk.walk` walks `rootpath` (inclusive) and calls `walkFunc` for each file system entry. +`Walk.walk` walks `pathname` (inclusive) and calls `walkFunc` for each file system entry. It can be used with Promises: ```js -Walk.walk(rootpath, promiseWalker).then(doMore); +Walk.walk(pathname, promiseWalker).then(doMore); ``` Or with async / await: ```js -await Walk.walk(rootpath, asyncWalker); +await Walk.walk(pathname, asyncWalker); ``` The behavior should exactly match Go's diff --git a/bin/walk.mjs b/bin/walk.js similarity index 70% rename from bin/walk.mjs rename to bin/walk.js index 39c57c0..9cabda7 100644 --- a/bin/walk.mjs +++ b/bin/walk.js @@ -1,4 +1,4 @@ -import Walk from "../walk.mjs"; +import Walk from "../index.js"; import path from "path"; var rootpath = process.argv[2] || "."; @@ -8,6 +8,10 @@ Walk.walk(rootpath, async function (err, pathname, dirent) { throw err; } + if (dirent.name.startsWith(".")) { + return false; + } + var entType; if (dirent.isDirectory()) { entType = " dir"; @@ -18,7 +22,7 @@ Walk.walk(rootpath, async function (err, pathname, dirent) { } else { entType = "----"; } - console.info("[%s] %s", entType, path.dirname(pathname), dirent.name); + console.info("[%s] %s", entType, path.dirname(path.resolve(pathname)), dirent.name); }).catch(function (err) { console.error(err.stack); }); diff --git a/create.js b/create.js new file mode 100644 index 0000000..a47630f --- /dev/null +++ b/create.js @@ -0,0 +1,110 @@ +// a port of Go's filepath.Walk + +import { promises as fs } from "fs"; +import { skipDir } from "./walk.js"; +import path from "path"; + +var Walk = {}; +var _withFileTypes = { withFileTypes: true }; +var _noopts = {}; + +function pass(err) { + return err; +} + +function skipOrThrow(err) { + if (!(err instanceof Error)) { + // go + return false; + } + if (false === err || skipDir === err) { + // skip + return true; + } + // throw + throw err; +} + +Walk.skipDir = skipDir; + +Walk.create = function (opts) { + async function _walk(root, walker) { + if (!opts) { + opts = _noopts; + } + + // Special case of the very first item, root + var err; + var stat = await fs.lstat(root).catch(function (e) { + err = e; + return null; + }); + if (stat) { + stat.name = path.basename(path.resolve(root)); + } + + /* similar to function in main walk loop */ + var uerr = await walker(err, root, stat).then(pass).catch(pass); + if (skipOrThrow(uerr) || err) { + return; + } + + if (false === opts.withFileTypes) { + stat = stat.name; + } + if (opts.sort) { + stat = (opts.sort([stat]) || [])[0]; + } + + if (stat && stat.isDirectory()) { + return _walkHelper(root, stat, walker, opts); + } + /* end */ + + async function _walkHelper(root, prevEnt, walker, opts) { + var err; + var _readdirOpts; + if (false !== opts.withFileTypes) { + _readdirOpts = _withFileTypes; + } + var dirents = await fs.readdir(root, _readdirOpts).catch(function (e) { + err = e; + }); + if (err) { + return walker(err, root, prevEnt); + } + if (opts.sort) { + dirents = opts.sort(dirents); + } + + var dirent; + var pathname; + for (dirent of dirents) { + if ("string" === typeof dirent) { + pathname = path.join(root, dirent); + dirent = await fs.lstat(path.join(root)).catch(function (e) { + err = e; + }); + } else { + pathname = path.join(root, dirent.name); + } + + /* main inner loop */ + err = await walker(err, pathname, dirent).then(pass).catch(pass); + if (skipOrThrow(err)) { + continue; + } + + // "walk does not follow symbolic links" + if (dirent.isDirectory()) { + await _walkHelper(path.join(root, dirent.name), dirent, walker, opts); + } + /* end */ + } + } + } + + return _walk; +}; + +export default Walk; diff --git a/create.mjs b/create.mjs deleted file mode 100644 index 9dd945e..0000000 --- a/create.mjs +++ /dev/null @@ -1,106 +0,0 @@ -// a port of Go's filepath.Walk - -import { promises as fs } from "fs"; -import path from "path"; - -var Walk = {}; -var _withFileTypes = { withFileTypes: true }; -var _skipDir = new Error("skip this directory"); -var _noopts = {}; - -function pass(err) { - return err; -} - -function skipOrThrow(err) { - if (!(err instanceof Error)) { - // go - return false; - } - if (_skipDir === err) { - // skip - return true; - } - // throw - throw err; -} - -Walk.skipDir = _skipDir; - -Walk.walk = async function _walk(root, walker, opts) { - if (!opts) { - opts = _noopts; - } - - // Special case of the very first item, root - var err; - var stat = await fs.lstat(root).catch(function (e) { - err = e; - return null; - }); - if (stat) { - stat.name = path.basename(path.resolve(root)); - } - - /* similar to function in main walk loop */ - var uerr = await walker(err, root, stat).then(pass).catch(pass); - if (skipOrThrow(uerr) || err) { - return; - } - - if (false === opts.withFileTypes) { - stat = stat.name; - } - if (opts.sort) { - stat = (opts.sort([stat]) || [])[0]; - } - - if (stat && stat.isDirectory()) { - return _walkHelper(root, stat, walker, opts); - } - /* end */ -}; - -async function _walkHelper(root, prevEnt, walker, opts) { - var err; - var _readdirOpts; - if (false !== opts.withFileTypes) { - _readdirOpts = _withFileTypes; - } - var dirents = await fs.readdir(root, _readdirOpts).catch(function (e) { - err = e; - }); - if (err) { - return walker(err, root, prevEnt); - } - if (opts.sort) { - dirents = opts.sort(dirents); - } - - var dirent; - var pathname; - for (dirent of dirents) { - if ("string" === typeof dirent) { - pathname = path.join(root, dirent); - dirent = await fs.lstat(path.join(root)).catch(function (e) { - err = e; - }); - } else { - pathname = path.join(root, dirent.name); - } - - /* main inner loop */ - err = await walker(err, pathname, dirent).then(pass).catch(pass); - if (skipOrThrow(err)) { - continue; - } - - // "walk does not follow symbolic links" - if (dirent.isDirectory()) { - await _walkHelper(path.join(root, dirent.name), dirent, walker, opts); - } - /* end */ - } -} - -export default Walk; diff --git a/index.js b/index.js new file mode 100644 index 0000000..6361d57 --- /dev/null +++ b/index.js @@ -0,0 +1,3 @@ +import Walk from "./walk.js"; + +export default Walk; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ebedafc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "@root/walk", + "version": "1.0.0", + "lockfileVersion": 1 +} diff --git a/package.json b/package.json index 007b146..0acccd0 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,26 @@ { - "name": "walk", + "name": "@root/walk", "version": "1.0.0", - "description": "", + "description": "fs.walk for node (as a port of Go's filepath.Walk)", + "homepage": "https://git.rootprojects.org/root/walk.js", "main": "index.js", + "type": "module", + "files": [ + "walk.js" + ], "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, - "author": "AJ ONeal (https://coolaj86.com/)", - "license": "MIT" + "repository": { + "type": "git", + "url": "https://git.rootprojects.org/root/walk.js.git" + }, + "keywords": [ + "fs", + "walk", + "filepath", + "os" + ], + "author": "AJ ONeal (https://rootprojects.org/)", + "license": "MPL-2.0" } diff --git a/walk.js b/walk.js new file mode 100644 index 0000000..5402f57 --- /dev/null +++ b/walk.js @@ -0,0 +1,48 @@ +import { promises as fs } from "fs"; +import path from "path"; + +const _withFileTypes = { withFileTypes: true }; +const skipDir = new Error("skip this directory"); +const pass = (err) => err; + +// a port of Go's filepath.Walk +const walk = async (root, walkFunc, _dirent) => { + let err; + + // special case of the very first run + if (!_dirent) { + let _name = path.basename(path.resolve(root)); + _dirent = await fs.lstat(root).catch(pass); + if (_dirent instanceof Error) { + err = _dirent; + } else { + _dirent.name = _name; + } + } + + // run the user-supplied function and either skip, bail, or continue + err = await walkFunc(err, root, _dirent).catch(pass); + if (false === err || skipDir === err) { + return; + } + if (err instanceof Error) { + throw err; + } + + // "walk does not follow symbolic links" + if (!_dirent.isDirectory()) { + return; + } + let result = await fs.readdir(root, _withFileTypes).catch(pass); + if (result instanceof Error) { + return walkFunc(result, root, _dirent); + } + for (let dirent of result) { + await walk(path.join(root, dirent.name), walkFunc, dirent); + } +}; + +export default { + walk, + skipDir, +}; diff --git a/walk.mjs b/walk.mjs deleted file mode 100644 index 52fe01b..0000000 --- a/walk.mjs +++ /dev/null @@ -1,88 +0,0 @@ -// a port of Go's filepath.Walk - -import { promises as fs } from "fs"; -import path from "path"; - -var Walk = {}; -var _withFileTypes = { withFileTypes: true }; -var _skipDir = new Error("skip this directory"); - -function pass(err) { - return err; -} - -function skipOrThrow(err) { - if (!(err instanceof Error)) { - // go - return false; - } - if (_skipDir === err) { - // skip - return true; - } - // throw - throw err; -} - -Walk.skipDir = _skipDir; - -Walk.walk = async function _walk(root, walker) { - // Special case of the very first item, root - var err; - var stat = await fs.lstat(root).catch(function (e) { - err = e; - return null; - }); - stat.name = path.basename(path.resolve(root)); - - /* similar to function in main walk loop */ - var uerr = await walker(err, root, stat).then(pass).catch(pass); - if (skipOrThrow(uerr) || err) { - return; - } - - if (stat.isDirectory()) { - return _walkHelper(root, stat, walker); - } - /* end */ -}; - -async function _walkHelper(root, prevEnt, walker) { - var err; - var dirents = await fs.readdir(root, _withFileTypes).catch(function (e) { - err = e; - }); - if (err) { - return walker(err, root, prevEnt); - } - - var dirent; - var pathname; - var _name; - for (dirent of dirents) { - if ("string" === typeof dirent) { - _name = dirent; - pathname = path.join(root, _name); - dirent = await fs.lstat(pathname).catch(function (e) { - err = e; - }); - dirent.name = _name; - } else { - pathname = path.join(root, dirent.name); - } - - /* main inner loop */ - err = await walker(err, pathname, dirent).then(pass).catch(pass); - if (skipOrThrow(err)) { - continue; - } - - // "walk does not follow symbolic links" - if (dirent.isDirectory()) { - await _walkHelper(path.join(root, dirent.name), dirent, walker); - } - /* end */ - } -} - -export default Walk;