Compare commits

...

3 Commits

Author SHA1 Message Date
AJ ONeal f87c69c25e docs: add linkback in README 2022-01-03 17:55:20 +00:00
AJ ONeal 22e686a414 1.1.0 2020-12-09 15:59:16 -07:00
AJ ONeal ee74fc4465 cleanup 2020-12-09 15:58:53 -07:00
8 changed files with 257 additions and 140 deletions

226
README.md
View File

@ -1,4 +1,4 @@
# Walk.js (@root/walk)
# [Walk.js](https://git.rootprojects.org/root/walk.js) (@root/walk)
Walk a directory recursively and handle each entity (files, directories, symlnks, etc).
@ -8,27 +8,80 @@ using Node.js v10+'s `fs.readdir`'s `withFileTypes` and ES 2021)
```js
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
- `pathname` may be relative
- `dirent` is an `fs.Dirent` that has
- `dirent.name`
- `dirent.isFile()`
- `dirent.isDirectory()`
- `dirent.isSymbolicLink()`
- etc
- Install
- Usage
- CommonJS
- ES Modules
- API
- Walk.walk
- walkFunc
- 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.
@ -36,55 +89,28 @@ You can use this with Node v12+ using Vanilla JS or ES2021.
import { walk } from "@root/walk";
import path from "path";
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) {
const walkFunc = async (err, pathname, dirent) => {
if (err) {
throw err;
}
if (dirent.isDirectory() && dirent.name.startsWith(".")) {
return Promise.resolve(false);
return false;
}
console.log("name:", dirent.name, "in", path.dirname(pathname));
return Promise.resolve();
}).then(function () {
console.log("Done");
});
console.log("name:", dirent.name, "in", path.dirname(pathname));
};
await walk("./", walkFunc);
console.log("Done");
```
# 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:
@ -99,21 +125,18 @@ await Walk.walk(pathname, asyncWalker);
```
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
- 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
- can be created with `options` to change default behaviors
-->
## walkFunc
### walkFunc
Handles each directory entry
```js
function walker(err, pathname, dirent) {
async function walkFunc(err, pathname, dirent) {
// `err` is a file system stat error
// `pathname` is the full pathname, including the file name
// `dirent` is an fs.Dirent with a `name`, `isDirectory`, `isFile`, etc
@ -121,11 +144,82 @@ function walker(err, pathname, dirent) {
}
```
<!-- TODO
## create(options)
## Walk.create(options)
Create a custom walker with these options:
- `withFileTypes: false` walkFunc will receive String[] instead of fs.Dirent[]
- `sort: sortFunc`
-->
- `withFileStats: true` walkFunc will receive fs.Stats[] from fs.lstat instead of fs.Dirent[]
- `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";
//import { walk } from "../index.js";
"use strict";
import Walk from "../index.js";
var walk = Walk.create({
var path = require("path");
var Walk = require("../index.js");
var walk = Walk.walk;
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] || ".";
@ -17,23 +23,21 @@ walk(rootpath, async function (err, pathname, dirent) {
throw err;
}
/*
if (!alt) {
if (dirent.name.startsWith(".")) {
return false;
}
*/
var entType;
if (dirent.isDirectory()) {
entType = " dir";
} else if (dirent.isFile()) {
entType = "file";
} else if (dirent.isSymbolicLink()) {
entType = "link";
} else {
entType = "----";
}
console.info("[%s] %s", entType, path.dirname(path.resolve(pathname)), dirent.name);
var entType = "?";
if (dirent.isDirectory()) {
entType = "d";
} else if (dirent.isFile()) {
entType = "f";
} else if (dirent.isSymbolicLink()) {
entType = "@";
}
console.info("%s %s", entType, path.dirname(path.resolve(pathname)), dirent.name);
}).catch(function (err) {
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
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) {
if (!opts) {
opts = _noopts;
@ -28,7 +25,7 @@ Walk.create = function (opts) {
// the first run, or if false === withFileTypes
if ("string" === typeof _dirent) {
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) {
err = _dirent;
} else {
@ -37,7 +34,7 @@ Walk.create = function (opts) {
}
// 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) {
return;
}
@ -58,7 +55,7 @@ Walk.create = function (opts) {
// TODO check if the error is "not a directory"
// (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) {
return walkFunc(result, pathname, _dirent);
}
@ -73,4 +70,4 @@ Walk.create = function (opts) {
return _walk;
};
export default Walk;
module.exports = Walk;

View File

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

2
package-lock.json generated
View File

@ -1,5 +1,5 @@
{
"name": "@root/walk",
"version": "1.0.0",
"version": "1.1.0",
"lockfileVersion": 1
}

View File

@ -1,15 +1,15 @@
{
"name": "@root/walk",
"version": "1.0.0",
"version": "1.1.0",
"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",
"create.js"
],
"scripts": {
"prettier": "npx prettier --write '**/*.{md,js,mjs,cjs}'",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {

View File

@ -1,55 +1,77 @@
// ECMAScript 2021
// (or Vanilla JS)
import { promises as fs } from "fs";
// or let fs = require("fs").promises;
import path from "path";
// or let path = require("path");
/**
* @license
* walk.js - fs.walk for node.js (a port of Go's filepath.Walk)
*
* Written in 2020 by AJ ONeal <coolaj86@gmail.com>
* 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;
function pass(e) {
return e;
}
// special case of the very first run
if (!_dirent) {
let _name = path.basename(path.resolve(pathname));
_dirent = await fs.lstat(pathname).catch(pass);
if (_dirent instanceof Error) {
err = _dirent;
// special case: walk the very first file or folder
if (!dirent) {
let filename = path.basename(path.resolve(pathname));
dirent = await fs.lstat(pathname).catch(_pass);
if (dirent instanceof Error) {
err = dirent;
} else {
_dirent.name = _name;
dirent.name = filename;
}
}
// 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) {
// walkFunc can return false to skip
return;
}
if (err instanceof Error) {
// if walkFunc throws, we throw
throw err;
}
// "walk does not follow symbolic links"
if (!_dirent.isDirectory()) {
// (doing so could cause infinite loops)
if (!dirent.isDirectory()) {
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) {
return walkFunc(result, pathname, _dirent);
// notify on directory read error
return walkFunc(result, pathname, dirent);
}
for (let dirent of result) {
await walk(path.join(pathname, dirent.name), walkFunc, dirent);
for (let entity of result) {
await walk(path.join(pathname, entity.name), walkFunc, entity);
}
}
// Example Usage:
const path = require("path");
walk("./", function (err, pathname, dirent) {
if (dirent.name.startsWith(".")) {
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);
});

10
walk.js
View File

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