This commit is contained in:
AJ ONeal 2020-12-09 15:58:53 -07:00
parent 67df50bb1b
commit ee74fc4465
7 changed files with 254 additions and 137 deletions

224
README.md
View File

@ -8,27 +8,80 @@ using Node.js v10+'s `fs.readdir`'s `withFileTypes` and ES 2021)
```js ```js
await Walk.walk(pathname, walkFunc); await Walk.walk(pathname, walkFunc);
function walkFunc(err, pathname, dirent) { async function walkFunc(err, pathname, dirent) {
// ... // err is failure to lstat a file or directory
// pathname is relative path, including the file or folder name
// dirent = { name, isDirectory(), isFile(), isSymbolicLink(), ... }
if (err) {
return false;
}
console.log(pathname);
} }
``` ```
Where # Table of Contents
- `err` is a failure to lstat a file or directory - Install
- `pathname` may be relative - Usage
- `dirent` is an `fs.Dirent` that has - CommonJS
- `dirent.name` - ES Modules
- `dirent.isFile()` - API
- `dirent.isDirectory()` - Walk.walk
- `dirent.isSymbolicLink()` - walkFunc
- etc - Example: filter dotfiles
- Walk.create
- withFileStats
- sort (and filter)
- [Node walk in <50 Lines of Code](https://therootcompany.com/blog/fs-walk-for-node-js/)
- License (MPL-2.0)
# Examples # Install
You can use this with Node v12+ using Vanilla JS or ES2021. ```bash
npm install --save @root/walk
```
## ES 2021 Modules # Usage
You can use this with Node v12+ using Vanilla JS (CommonJS) or ES2021 (ES Modules).
## CommonJS (Vanilla JS / ES5)
```js
var Walk = require("@root/walk");
var path = require("path");
Walk.walk("./", walkFunc).then(function () {
console.log("Done");
});
// walkFunc must be async, or return a Promise
function walkFunc(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 Promise.resolve();
}
// return false to skip a directory
// (ex: skipping "dot file" directories)
if (dirent.isDirectory() && dirent.name.startsWith(".")) {
return Promise.resolve(false);
}
// fs.Dirent is a slimmed-down, faster version of fs.Stats
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());
return Promise.resolve();
}
```
## ECMAScript 2021 (ES Modules)
`@root/walk` can be used with async/await or Promises. `@root/walk` can be used with async/await or Promises.
@ -36,55 +89,28 @@ You can use this with Node v12+ using Vanilla JS or ES2021.
import { walk } from "@root/walk"; import { walk } from "@root/walk";
import path from "path"; import path from "path";
await walk("./", async (err, pathname, dirent) => { const walkFunc = 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) { if (err) {
throw err; throw err;
} }
if (dirent.isDirectory() && dirent.name.startsWith(".")) { if (dirent.isDirectory() && dirent.name.startsWith(".")) {
return Promise.resolve(false); return false;
} }
console.log("name:", dirent.name, "in", path.dirname(pathname));
return Promise.resolve(); console.log("name:", dirent.name, "in", path.dirname(pathname));
}).then(function () { };
console.log("Done");
}); await walk("./", walkFunc);
console.log("Done");
``` ```
# API Documentation # API Documentation
`Walk.walk` walks `pathname` (inclusive) and calls `walkFunc` for each file system entry. ## Walk.walk(pathname, walkFunc)
`Walk.walk` walks `pathname` (inclusive) and calls `walkFunc` for each file system entity.
It can be used with Promises: It can be used with Promises:
@ -99,21 +125,18 @@ await Walk.walk(pathname, asyncWalker);
``` ```
The behavior should exactly match Go's The behavior should exactly match Go's
[`filepath.Walk`](https://golang.org/pkg/path/filepath/#Walk) with 3 exceptions: [`filepath.Walk`](https://golang.org/pkg/path/filepath/#Walk) with a few exceptions:
- uses JavaScript Promises/async/await - uses JavaScript Promises/async/await
- receives `dirent` rather than `lstat` (for performance) - receives `dirent` rather than `lstat` (for performance, see `withFileStats`)
- optional parameters to change stat behavior and sort order
<!-- TODO ### walkFunc
- can be created with `options` to change default behaviors
-->
## walkFunc
Handles each directory entry Handles each directory entry
```js ```js
function walker(err, pathname, dirent) { async function walkFunc(err, pathname, dirent) {
// `err` is a file system stat error // `err` is a file system stat error
// `pathname` is the full pathname, including the file name // `pathname` is the full pathname, including the file name
// `dirent` is an fs.Dirent with a `name`, `isDirectory`, `isFile`, etc // `dirent` is an fs.Dirent with a `name`, `isDirectory`, `isFile`, etc
@ -121,11 +144,82 @@ function walker(err, pathname, dirent) {
} }
``` ```
<!-- TODO ## Walk.create(options)
## create(options)
Create a custom walker with these options: Create a custom walker with these options:
- `withFileTypes: false` walkFunc will receive String[] instead of fs.Dirent[] - `withFileStats: true` walkFunc will receive fs.Stats[] from fs.lstat instead of fs.Dirent[]
- `sort: sortFunc` - `sort: (entities) => entities.sort()` sort and/or filter entities before walking them
-->
```js
const walk = Walk.create({
withFileStats: true,
sort: (entities) => entities.sort()),
});
```
## withFileStats
By default `walk` will use `fs.readdir(pathname, { withFileTypes: true })` which returns `fs.Dirent[]`,
which only has name and file type info, but is much faster when you don't need the complete `fs.Stats`.
Enable `withFileStats` to use get full `fs.Stats`. This will use `fs.readdir(pathname)` (returning `String[]`)
and then call `fs.lstat(pathname)` - including `mtime`, `birthtime`, `uid`, etc - right after.
```js
const walk = Walk.create({
withFileStats: true,
});
walk(".", async function (err, pathname, stat) {
console.log(stat.name, stat.uid, stat.birthtime, stat.isDirectory());
});
```
## sort (and filter)
Sometimes you want to give priority to walking certain directories first.
The `sort` option allows you to specify a funciton that modifies the `fs.Dirent[]` entities (default) or `String[]` filenames (`withFileStats: true`).
Since you must return the sorted array, you can also filter here if you'd prefer.
```js
const byNameWithoutDotFiles = (entities) => {
// sort by name
// filter dot files
return entities
.sort((a, b) => {
if (a.name > b.name) {
return 1;
}
if (a.name < b.name) {
return -1;
}
return 0;
})
.filter((ent) => !ent.name.startsWith("."));
};
const walk = Walk.create({ sort: byNameWithoutDotFiles });
walk(".", async function (err, pathname, stat) {
// each directories contents will be listed alphabetically
console.log(pathname);
});
```
Note: this gets the result of `fs.readdir()`. If `withFileStats` is `true` you will get a `String[]` of filenames - because this hapens BEFORE `fs.lstat()` is called - otherwise you will get `fs.Dirent[]`.
# node walk in 50 lines of code
If you're like me and you hate dependencies,
here's the bare minimum node fs walk function:
See [snippet.js](/snippet.js) or <https://therootcompany.com/blog/fs-walk-for-node-js/>.
# License
The main module, as published to NPM, is licensed the MPL-2.0.
The ~50 line snippet is licensed CC0-1.0 (Public Domain).

View File

@ -1,14 +1,20 @@
import path from "path"; "use strict";
//import { walk } from "../index.js";
import Walk from "../index.js"; var path = require("path");
var walk = Walk.create({ var Walk = require("../index.js");
sort: function (ents) {
return ents.filter(function (ent) { var walk = Walk.walk;
return !ent.name.startsWith("."); var alt = process.argv[2];
});
}, if (alt) {
}); walk = Walk.create({
sort: function (ents) {
return ents.filter(function (ent) {
return !ent.name.startsWith(".");
});
},
});
}
var rootpath = process.argv[2] || "."; var rootpath = process.argv[2] || ".";
@ -17,23 +23,21 @@ walk(rootpath, async function (err, pathname, dirent) {
throw err; throw err;
} }
/* if (!alt) {
if (dirent.name.startsWith(".")) { if (dirent.name.startsWith(".")) {
return false; return false;
}
} }
*/
var entType; var entType = "?";
if (dirent.isDirectory()) { if (dirent.isDirectory()) {
entType = " dir"; entType = "d";
} else if (dirent.isFile()) { } else if (dirent.isFile()) {
entType = "file"; entType = "f";
} else if (dirent.isSymbolicLink()) { } else if (dirent.isSymbolicLink()) {
entType = "link"; entType = "@";
} else {
entType = "----";
} }
console.info("[%s] %s", entType, path.dirname(path.resolve(pathname)), dirent.name); console.info("%s %s", entType, path.dirname(path.resolve(pathname)), dirent.name);
}).catch(function (err) { }).catch(function (err) {
console.error(err.stack); console.error(err.stack);
}); });

View File

@ -1,16 +1,13 @@
"use strict";
const fs = require("fs").promises;
const Walk = require("./walk.js");
const path = require("path");
const _withFileTypes = { withFileTypes: true };
const _noopts = {};
const _pass = (err) => err;
// a port of Go's filepath.Walk // a port of Go's filepath.Walk
import { promises as fs } from "fs";
import Walk from "./walk.js";
import path from "path";
var _withFileTypes = { withFileTypes: true };
var _noopts = {};
function pass(err) {
return err;
}
Walk.create = function (opts) { Walk.create = function (opts) {
if (!opts) { if (!opts) {
opts = _noopts; opts = _noopts;
@ -28,7 +25,7 @@ Walk.create = function (opts) {
// the first run, or if false === withFileTypes // the first run, or if false === withFileTypes
if ("string" === typeof _dirent) { if ("string" === typeof _dirent) {
let _name = path.basename(path.resolve(pathname)); let _name = path.basename(path.resolve(pathname));
_dirent = await fs.lstat(pathname).catch(pass); _dirent = await fs.lstat(pathname).catch(_pass);
if (_dirent instanceof Error) { if (_dirent instanceof Error) {
err = _dirent; err = _dirent;
} else { } else {
@ -37,7 +34,7 @@ Walk.create = function (opts) {
} }
// run the user-supplied function and either skip, bail, or continue // run the user-supplied function and either skip, bail, or continue
err = await walkFunc(err, pathname, _dirent).catch(pass); err = await walkFunc(err, pathname, _dirent).catch(_pass);
if (false === err || Walk.skipDir === err) { if (false === err || Walk.skipDir === err) {
return; return;
} }
@ -58,7 +55,7 @@ Walk.create = function (opts) {
// TODO check if the error is "not a directory" // TODO check if the error is "not a directory"
// (and thus allow false === opts.withFileTypes) // (and thus allow false === opts.withFileTypes)
let result = await fs.readdir(pathname, _readdirOpts).catch(pass); let result = await fs.readdir(pathname, _readdirOpts).catch(_pass);
if (result instanceof Error) { if (result instanceof Error) {
return walkFunc(result, pathname, _dirent); return walkFunc(result, pathname, _dirent);
} }
@ -73,4 +70,4 @@ Walk.create = function (opts) {
return _walk; return _walk;
}; };
export default Walk; module.exports = Walk;

View File

@ -1,6 +1,4 @@
import Walk from "./walk.js"; "use strict";
import Walk2 from "./create.js";
Walk.create = Walk2.create; module.exports = require("./walk.js");
module.exports.create = require("./create.js").create;
export default Walk;

View File

@ -4,12 +4,12 @@
"description": "fs.walk for node (as a port of Go's filepath.Walk)", "description": "fs.walk for node (as a port of Go's filepath.Walk)",
"homepage": "https://git.rootprojects.org/root/walk.js", "homepage": "https://git.rootprojects.org/root/walk.js",
"main": "index.js", "main": "index.js",
"type": "module",
"files": [ "files": [
"walk.js", "walk.js",
"create.js" "create.js"
], ],
"scripts": { "scripts": {
"prettier": "npx prettier --write '**/*.{md,js,mjs,cjs}'",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": { "repository": {

View File

@ -1,55 +1,77 @@
// ECMAScript 2021 /**
// (or Vanilla JS) * @license
import { promises as fs } from "fs"; * walk.js - fs.walk for node.js (a port of Go's filepath.Walk)
// or let fs = require("fs").promises; *
import path from "path"; * Written in 2020 by AJ ONeal <coolaj86@gmail.com>
// or let path = require("path"); * To the extent possible under law, the author(s) have dedicated all copyright
* and related and neighboring rights to this software to the public domain
* worldwide. This software is distributed without any warranty.
*
* You should have received a copy of the CC0 Public Domain Dedication along with
* this software. If not, see <https://creativecommons.org/publicdomain/zero/1.0/>.
*/
"use strict";
async function walk(pathname, walkFunc, dirent) {
const fs = require("fs").promises;
const path = require("path");
const _pass = (err) => err;
// a port of Go's filepath.Walk
async function walk(pathname, walkFunc, _dirent) {
let err; let err;
function pass(e) {
return e;
}
// special case of the very first run // special case: walk the very first file or folder
if (!_dirent) { if (!dirent) {
let _name = path.basename(path.resolve(pathname)); let filename = path.basename(path.resolve(pathname));
_dirent = await fs.lstat(pathname).catch(pass); dirent = await fs.lstat(pathname).catch(_pass);
if (_dirent instanceof Error) { if (dirent instanceof Error) {
err = _dirent; err = dirent;
} else { } else {
_dirent.name = _name; dirent.name = filename;
} }
} }
// run the user-supplied function and either skip, bail, or continue // run the user-supplied function and either skip, bail, or continue
err = await walkFunc(err, pathname, _dirent).catch(pass); err = await walkFunc(err, pathname, dirent).catch(_pass);
if (false === err) { if (false === err) {
// walkFunc can return false to skip
return; return;
} }
if (err instanceof Error) { if (err instanceof Error) {
// if walkFunc throws, we throw
throw err; throw err;
} }
// "walk does not follow symbolic links" // "walk does not follow symbolic links"
if (!_dirent.isDirectory()) { // (doing so could cause infinite loops)
if (!dirent.isDirectory()) {
return; return;
} }
let result = await fs.readdir(pathname, { withFileTypes: true }).catch(pass); let result = await fs.readdir(pathname, { withFileTypes: true }).catch(_pass);
if (result instanceof Error) { if (result instanceof Error) {
return walkFunc(result, pathname, _dirent); // notify on directory read error
return walkFunc(result, pathname, dirent);
} }
for (let dirent of result) { for (let entity of result) {
await walk(path.join(pathname, dirent.name), walkFunc, dirent); await walk(path.join(pathname, entity.name), walkFunc, entity);
} }
} }
// Example Usage:
const path = require("path");
walk("./", function (err, pathname, dirent) { walk("./", function (err, pathname, dirent) {
if (dirent.name.startsWith(".")) { if (dirent.name.startsWith(".")) {
return Promise.resolve(false); return Promise.resolve(false);
} }
console.log(path.resolve(pathname)); var typ = "-";
if (dirent.isFile()) {
typ = "f";
} else if (dirent.isDirectory()) {
typ = "d";
} else if (dirent.isSymbolicLink()) {
typ = "@";
}
console.info(typ, path.resolve(pathname));
return Promise.resolve(true); return Promise.resolve(true);
}); });

10
walk.js
View File

@ -1,8 +1,10 @@
import { promises as fs } from "fs"; "use strict";
import path from "path";
const fs = require("fs").promises;
const path = require("path");
const _withFileTypes = { withFileTypes: true };
const skipDir = new Error("skip this directory"); const skipDir = new Error("skip this directory");
const _withFileTypes = { withFileTypes: true };
const pass = (err) => err; const pass = (err) => err;
// a port of Go's filepath.Walk // a port of Go's filepath.Walk
@ -42,7 +44,7 @@ const walk = async (pathname, walkFunc, _dirent) => {
} }
}; };
export default { module.exports = {
walk, walk,
skipDir, skipDir,
}; };