Skip to content

Commit

Permalink
Merge pull request #11 from bmacnaughton/top-level-action
Browse files Browse the repository at this point in the history
optionally invoke actions on the top-level directory
  • Loading branch information
bmacnaughton authored Dec 8, 2023
2 parents 7dae038 + 1a0b17b commit 2c1c81b
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 141 deletions.
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

![commit-test](https://github.com/bmacnaughton/action-walk/workflows/commit-test/badge.svg)

Minimal utility to walk directory trees performing actions on each directory
Framework to walk directory trees performing actions on each directory
entry. `action-walk` has no external production dependencies and has only one
strong opinion - don't presume anything about why the directory tree is being
walked.
Expand All @@ -11,9 +11,15 @@ No presumptions means that this does little more than walk the tree. There
are two options to facilitate implementing your code on top of `action-walk`.
If the boolean option `stat` is truthy `action-walk` will execute `fs.stat`
on the entry and pass that to you action handler. If the option `own` is
present `action-walk` will pass that to the action functions in a context
present `action-walk` will pass it to the action functions in a context
object.

There is one additional option `includeTopLevel`. By default, `action-walk` does
not call the action functions on the directory passed to `action-walk`; it just
starts walking that directory. If `includeTopLevel` is truthy, `action-walk` will
call the directory action function on the top level directory. This likely should
have been the default but it's not as it's a breaking change.

### usage

`action-walk` should run on any version of node that supports the `node:` prefix
Expand Down Expand Up @@ -43,7 +49,7 @@ function fileAction (path, context) {
own.total += stat.size;
}
const own = {total: 0, skipDirs: ['node_modules']};
const own = {total: 0, skipDirs: ['node_modules', '.git']};
const options = {
dirAction,
fileAction,
Expand All @@ -57,7 +63,7 @@ walk('.', options)
});
// executed in the action-walk package root it will print something like
// total bytes in "." 14778
// total bytes in "." 109558
```

see `test/basics.test.js` or `bin/walk.js` for other examples.
Expand All @@ -75,12 +81,13 @@ recurse into the directory.
- `stat` - if `'lstat'` call `fs.lstat` on the entry and add it to the action context as
the `stat` property. if otherwise truthy use `fs.stat`.
- `own` - add this to the action context. it is your context for the action functions.
- `includeTopLevel` - if truthy, the first call to `dirAction` will be for the the directory argument. if falsey, the first call to `dirAction` will be for the first entry in the directory.

It's possible to call `walk()` with no options but probably not useful unless
all you're wanting to do is seed the disk cache with directory entries. The
action functions are where task-specific work is done.

Each of the action function (`dirAction`, `fileAction`, `linkAction`, `otherAction`) is
Each of the action functions (`dirAction`, `fileAction`, `linkAction`, `otherAction`) is
called with two arguments:
- `filepath` for the entry starting with `directory`, e.g., if `directory` is `test` and
the entry is `basics.test.js` then `filepath` will be `test/basics.test.js`.
Expand Down
78 changes: 50 additions & 28 deletions action-walk.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,45 +45,67 @@ async function walk (dir, options = {}) {
otherAction = async (filepath, ctx) => options.otherAction(filepath, ctx);
}

async function dispatchAction (dir, dirent) {
let entry = path.join(dir, dirent.name);
// path.join refuses to start a path with '.'
if ((dir === '.' && entry) || dir.startsWith(`.${sep}`)) {
entry = `.${sep}${entry}`;
}
const ctx = { dirent, stack };
if (options.own) {
ctx.own = options.own;
}
if (stat) {
ctx.stat = await fsp[stat](entry);
}
if (dirent.isDirectory()) {
if (await dirAction(entry, ctx) !== 'skip') {
await walker(entry);
}
} else if (dirent.isFile()) {
fileAction && await fileAction(entry, ctx);
} else if (dirent.isSymbolicLink()) {
if (linkAction) {
await linkAction(entry, ctx);
} else {
fileAction && await fileAction(entry, ctx);
}
} else {
otherAction && await otherAction(entry, ctx);
}
}

//
// walk through a directory tree calling user functions for each entry.
//
async function walker (dir) {
stack.push(path.basename(dir));
for await (const dirent of await fsp.opendir(dir)) {
let entry = path.join(dir, dirent.name);
// path.join refuses to start a path with '.'
if (dir === '.' || dir.startsWith(`.${sep}`)) {
entry = `.${sep}${entry}`;
}
const ctx = {dirent, stack};
if (options.own) {
ctx.own = options.own;
}
if (stat) {
ctx.stat = await fsp[stat](entry);
}
if (dirent.isDirectory()) {
if (await dirAction(entry, ctx) !== 'skip') {
await walker(entry);
}
} else if (dirent.isFile()) {
fileAction && await fileAction(entry, ctx);
} else if (dirent.isSymbolicLink()) {
if (linkAction) {
await linkAction(entry, ctx);
} else {
fileAction && await fileAction(entry, ctx);
}
} else {
otherAction && await otherAction(entry, ctx);
}
await dispatchAction(dir, dirent);
}
stack.pop();
return undefined;
}

return walker(dir);
// do first level dir here. we fake the top level dir as '' if it is '.'
// because the user didn't specify a full path and we don't want the
// resolved path to show up in the results.
if (options.includeTopLevel) {
let name = dir;
if (dir === '.') {
dir = '';
} else {
const p = path.parse(dir);
name = p.base;
}
const dirent = {
name,
isDirectory: () => true,
}
return dispatchAction(dir, dirent);
} else {
return walker(dir);
}
}

module.exports = walk;
13 changes: 10 additions & 3 deletions bin/walk.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,25 @@ const walk = require('..');
/* eslint-disable no-console */

const args = process.argv.slice(2);
const options = {};
for (let i = 0; i < args.length; i++) {
if (args[i] === '-t' || args[i] === '--include-top-level') {
args.splice(i, 1);
options.includeTopLevel = true;
}
}

if (args.length < 1) {
console.log('usage: action-walk directory');
console.log('usage: action-walk [--include-top-level] directory');
process.exit(1);
}
const dir = args[0];

const own = {total: 0};
const options = {dirAction, fileAction, own, stat: 'lstat'};
Object.assign(options, {dirAction, fileAction, own, stat: 'lstat'});

async function main () {
return walk(dir, options);

}

main()
Expand Down
Loading

0 comments on commit 2c1c81b

Please sign in to comment.