Skip to content

Commit 57429a6

Browse files
committed
v1.0.0
0 parents  commit 57429a6

File tree

6 files changed

+317
-0
lines changed

6 files changed

+317
-0
lines changed

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package-lock=false

README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# action-walk
2+
3+
Minimal utility to walk directory trees performing actions on each directory
4+
entry. `action-walk` has no production dependencies other than
5+
node core modules and has only one strong opinion - don't presume anything
6+
about why the directory tree is being walked.
7+
8+
No presumptions means that this does little more than walk the tree. There
9+
are two options to facilitate implementing your code on top of `action-walk`.
10+
If the boolean option `stat` is truthy `action-walk` will execute `fs.stat`
11+
on the entry and pass that to you action handler. If the option `own` is
12+
present `action-walk` will pass that to the action functions in a context
13+
object.
14+
15+
### usage
16+
17+
`npm install action-walk`
18+
19+
### examples
20+
21+
```
22+
const walk = require('action-walk');
23+
24+
function dirAction (path, context) {
25+
const {dirent, stat, own} = context;
26+
if (own.skipDirs && own.skipDirs.indexOf(dirent.name) >= 0) {
27+
return 'skip';
28+
}
29+
own.total += stat.size;
30+
}
31+
function fileAction (path, context) {
32+
const {stat, own} = context;
33+
own.total += stat.size;
34+
}
35+
36+
const own = {total: 0, skipDirs: ['node_modules']};
37+
const options = {
38+
dirAction,
39+
fileAction,
40+
own,
41+
stat: true
42+
};
43+
44+
await walk('.', options);
45+
46+
console.log('total bytes in "."', ctx.total);
47+
48+
// executed in the await-walk package root it will print something like
49+
// total bytes in "." 14778
50+
```
51+
52+
see `test/basics.test.js` for another example.
53+
54+
### api
55+
56+
`await walk(directory, options = {})`
57+
58+
options
59+
- `dirAction` - called for each directory.
60+
- `fileAction` - called for each file.
61+
- `otherAction` - called for non-file, non-directory entries.
62+
- `stat` - call `fs.stat` on the entry and add it to the action context.
63+
- `own` - add this to the action context.
64+
65+
It's possible to call `walk()` with no options but probably not useful unless
66+
all you're wanting to do is seed the disk cache with directory entries. The
67+
action functions are where task-specific work is done.
68+
69+
Each of the action function (`dirAction`, `fileAction`, `otherAction`) is
70+
called with two arguments:
71+
- `filepath` for the entry starting with the `directory`, e.g., if
72+
`directory` is `test` and the entry is `basics.test.js` then `filepath`
73+
will be `test/basics.test.js`. (It is created using node's `path.join` so
74+
note that if `directory` is `.` it will *not* be present in `filepath`.)
75+
- `context` is an object as follows.
76+
```
77+
{
78+
dirent, // the fs.Dirent object for the directory entry
79+
stat, // if `options.stat` the object returned by `fs.stat`
80+
own // `options.own` if provided.
81+
}
82+
```
83+
84+
`dirAction` is the only function with return value that matters. If
85+
`dirAction` returns the string `'skip'` (either directly or via a
86+
Promise) then `walk()` will not walk that branch of the directory tree.
87+
88+
All the action functions can return a promise if they need to perform
89+
asynchronous work but only the value of `dirAction` is meaningful.
90+
91+
### todo
92+
93+
- add error handling
94+
- let otherAction return indicator that a symbolic link should be followed.

action-walk.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const fsp = require('fs').promises;
2+
const path = require('path');
3+
4+
async function walk (dir, options = {}) {
5+
const noop = async () => undefined;
6+
let fileAction = noop;
7+
let dirAction = noop;
8+
let otherAction = noop;
9+
10+
if (options.fileAction) {
11+
fileAction = async (filepath, ctx) => options.fileAction(filepath, ctx);
12+
}
13+
if (options.dirAction) {
14+
dirAction = async (filepath, ctx) => options.dirAction(filepath, ctx);
15+
}
16+
if (options.otherAction) {
17+
otherAction = async (filepath, ctx) => options.otherAction(filepath, ctx);
18+
}
19+
20+
//
21+
// walk through a directory tree calling user functions for each entry.
22+
//
23+
async function walker (dir) {
24+
for await (const dirent of await fsp.opendir(dir)) {
25+
const entry = path.join(dir, dirent.name);
26+
const ctx = {dirent};
27+
if (options.own) {
28+
ctx.own = options.own;
29+
}
30+
if (options.stat) {
31+
ctx.stat = await fsp.stat(entry);
32+
}
33+
if (dirent.isDirectory() && await dirAction(entry, ctx) !== 'skip') {
34+
await walker(entry);
35+
} else if (dirent.isFile()) {
36+
await fileAction(entry, ctx);
37+
} else {
38+
await otherAction(entry, ctx);
39+
}
40+
}
41+
return undefined;
42+
}
43+
44+
return walker(dir);
45+
}
46+
47+
module.exports = walk;

package.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "action-walk",
3+
"version": "1.0.0",
4+
"description": "walk a directory tree performing actions",
5+
"main": "action-walk.js",
6+
"directories": {
7+
"test": "test"
8+
},
9+
"scripts": {
10+
"test": "mocha test/*.test.js"
11+
},
12+
"keywords": [
13+
"directory",
14+
"tree",
15+
"action",
16+
"iterate",
17+
"general",
18+
"utility",
19+
"recursive",
20+
"walk",
21+
"flexible"
22+
],
23+
"author": "Bruce A. MacNaughton",
24+
"license": "ISC",
25+
"dependencies": {
26+
},
27+
"devDependencies": {
28+
"mocha": "^8.1.3",
29+
"chai": "^4.2.0"
30+
}
31+
}

test/basics.test.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
const fsp = require('fs').promises;
2+
const {execCommandLine} = require('./utilities/exec');
3+
const walk = require('../action-walk');
4+
const {expect} = require('chai');
5+
6+
const testdir = '.';
7+
let testdirStat;
8+
const duOutput = {
9+
wo_node: {},
10+
w_node: {},
11+
}
12+
13+
describe('verify that action-walk works as expected', function () {
14+
// tests need to account for du counting the target directory itself
15+
// while walk treats that as a starting point and only counts the
16+
// contents of the directory.
17+
before(function getTestDirSize () {
18+
return fsp.stat(testdir)
19+
.then(s => {
20+
testdirStat = s;
21+
});
22+
})
23+
before(function getDuOutput () {
24+
// output is size-in-bytes <tab> path-starting-with-dir-name
25+
const p = [
26+
execCommandLine(`du -ab --exclude=node_modules ${testdir}`),
27+
execCommandLine(`du -ab ${testdir}`),
28+
];
29+
return Promise.all(p)
30+
.then(r => {
31+
expect(r[0].stderr).equal('');
32+
expect(r[1].stderr).equal('');
33+
duOutput.wo_node = parseDuOutput(r[0].stdout);
34+
duOutput.w_node = parseDuOutput(r[1].stdout);
35+
});
36+
});
37+
38+
it('should match du -ab output', function () {
39+
const own = {total: 0};
40+
const options = {dirAction, fileAction, own, stat: true};
41+
return walk(testdir, options)
42+
.then(() => {
43+
expect(own.total + testdirStat.size).equal(duOutput.w_node[testdir]);
44+
})
45+
});
46+
47+
it('should match du -ab --exclude=node_modules', function () {
48+
const own = {total: 0, skipDirs: ['node_modules']};
49+
const options = {dirAction, fileAction, own, stat: true};
50+
return walk(testdir, options)
51+
.then(() => {
52+
expect(own.total + testdirStat.size).equal(duOutput.wo_node[testdir]);
53+
})
54+
});
55+
56+
it('should execute recursively matching du -b --exclude=node_modules', function () {
57+
const own = {total: 0, dirTotals: {}, skipDirs: ['node_modules']};
58+
const options = {dirAction: daDirOnly, fileAction, own, stat: true};
59+
return walk(testdir, options)
60+
.then(() => {
61+
expect(own.total + testdirStat.size).equal(duOutput.wo_node[testdir]);
62+
for (const dir in own.dirTotals) {
63+
expect(own.dirTotals[dir]).equal(duOutput.w_node[`./${dir}`]);
64+
}
65+
});
66+
});
67+
68+
it('should execute recursively matching du -b', function () {
69+
const own = {total: 0, dirTotals: {}, skipDirs: []};
70+
const options = {dirAction: daDirOnly, fileAction, own, stat: true};
71+
return walk(testdir, options)
72+
.then(() => {
73+
expect(own.total + testdirStat.size).equal(duOutput.w_node[testdir]);
74+
for (const dir in own.dirTotals) {
75+
expect(own.dirTotals[dir]).equal(duOutput.w_node[`./${dir}`]);
76+
}
77+
});
78+
});
79+
80+
81+
});
82+
83+
84+
//
85+
// utilities
86+
//
87+
function dirAction (path, ctx) {
88+
const {dirent, stat, own} = ctx;
89+
if (own.skipDirs && own.skipDirs.indexOf(dirent.name) >= 0) {
90+
return 'skip';
91+
}
92+
own.total += stat.size;
93+
}
94+
function fileAction (path, ctx) {
95+
const {stat, own} = ctx;
96+
own.total += stat.size;
97+
}
98+
99+
async function daDirOnly (path, ctx) {
100+
const {dirent, stat, own} = ctx;
101+
if (own.skipDirs && own.skipDirs.indexOf(dirent.name) >= 0) {
102+
return 'skip';
103+
}
104+
own.dirTotals[path] = 0;
105+
const newown = {total: 0, dirTotals: own.dirTotals};
106+
const options = {
107+
dirAction: daDirOnly,
108+
fileAction,
109+
own: newown,
110+
stat: true,
111+
};
112+
await walk(path, options);
113+
own.dirTotals[path] = newown.total + stat.size;
114+
own.total += newown.total + stat.size;
115+
116+
// skip it because the recursive call counted the subtree.
117+
return 'skip';
118+
}
119+
120+
function parseDuOutput (text) {
121+
const o = {};
122+
for (const m of text.matchAll(/(?<size>\d+)\s+(?<path>.+)/g)) {
123+
o[m.groups.path] = +m.groups.size;
124+
}
125+
return o;
126+
}

test/utilities/exec.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const exec = require('child_process').exec;
2+
3+
async function execCommandLine (cmdline, options = {}) {
4+
return new Promise((resolve, reject) => {
5+
// eslint-disable-next-line no-unused-vars
6+
const cp = exec(cmdline, options, function (error, stdout, stderr) {
7+
if (error) {
8+
reject({error, stdout, stderr});
9+
} else {
10+
resolve({stdout, stderr});
11+
}
12+
});
13+
});
14+
}
15+
16+
module.exports = {
17+
execCommandLine,
18+
}

0 commit comments

Comments
 (0)