Skip to content

Instantly share code, notes, and snippets.

@chaimleib
Created January 6, 2020 22:12
Show Gist options
  • Save chaimleib/0cb4b7221f60f4495a0adddd42184610 to your computer and use it in GitHub Desktop.
Save chaimleib/0cb4b7221f60f4495a0adddd42184610 to your computer and use it in GitHub Desktop.
List differences between folders in ls output
#!/usr/bin/env node
/*
Requirements:
node v7.6.0
node-getopt
*/
const path = require('path');
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));
lsdiff.defaults = {
ignore: [
'.DS_Store',
'node_modules',
'.git',
],
};
// command-line interface
if (require.main === module) {
const Getopt = require('node-getopt');
async function main() {
try {
const gotten = getopts();
const cfg = buildConfig(gotten);
await lsdiff(cfg);
} catch (err) {
console.error(err);
process.exit(1);
}
}
function getopts() {
let optConfig = new Getopt([
['c', 'command', 'print the commands to synchronize the folders. To execute them, add "| sh" at the end of the line.'],
['q', 'quiet', 'in command mode, silence stdout from all commands'],
['f', 'forward-only', 'only analyze in the forward direction, from origin to dest'],
['n', 'no-conflicts', 'skip over files that exist in both the origin and destination'],
['i', 'ignore=ARG+', 'ignore files and directories with this name'],
['h', 'help', 'display this help'],
]).bindHelp();
optConfig.setHelp(
"Usage: node "+path.basename(__filename)+" [OPTION] originDir destDir\n"+
"Compare the files in originDir against destDir.\n"+
"\n"+
"[[OPTIONS]]\n"
);
const gotten = optConfig.parseSystem();
const opts = gotten.options;
if (!opts.ignore) opts.ignore = [];
const argv = gotten.argv;
if (opts.help) {
optConfig.showHelp();
process.exit(0);
}
if (argv.length < 2) {
throw 'missing arguments, require origin and destination paths';
} else if (argv.length > 2) {
throw 'too many arguments, require origin and destination paths';
}
return gotten;
}
function buildConfig(gotten) {
let argv = gotten.argv;
let opts = gotten.options;
let cfg = {};
cfg.command = opts.command;
cfg.verbose = !opts.quiet;
cfg.noConflicts = opts['no-conflicts'];
cfg.forwardOnly = opts['forward-only'];
cfg.ignore = [...lsdiff.defaults.ignore, ...opts.ignore];
[cfg.origin, cfg.dest] = argv;
return cfg;
}
main();
}
async function lsdiff(cfg) {
const task = {
cfg,
shouldIgnore: shouldIgnoreBuilder(cfg),
};
[task.originFiles, task.destFiles] = await Promise.all([
getFileStats(cfg.origin, task),
getFileStats(cfg.dest, task),
]);
task.pathsToCheck = keyUnion(task.originFiles, task.destFiles);
const diff = calculateDiff(task);
const diffpaths = Object.keys(diff)
.reduce((diffpaths, key) => {
diffpaths[key] = Object.keys(diff[key]).sort();
return diffpaths;
}, {});
const result = Object.assign({}, diffpaths);
if (cfg.forwardOnly) delete result.dest;
if (cfg.noConflicts) delete result.both;
if (!cfg.command) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(getCpCommands(result.origin, cfg.origin, cfg.dest, cfg));
console.log(getCpCommands(result.dest, cfg.dest, cfg.origin, cfg));
// TODO: resolve conflicts here and print appropiate commands
}
async function getFileStats(dirpath, task) {
const statTable = {};
const helper = getFileStatsHelperBuilder(statTable, task, dirpath);
let pathsToExplore = [dirpath];
while (pathsToExplore.length != 0) {
let pathPromises = pathsToExplore.map(helper);
let pathLists = await Promise.all(pathPromises);
pathsToExplore = pathLists.reduce((flatList, list) => {
if (!list) return flatList;
return flatList.concat(list);
}, []);
}
return statTable;
}
function getFileStatsHelperBuilder(statTable, task, rootpath) {
return async function(dirpath) {
if (task.shouldIgnore(dirpath)) return null;
stats = await fs.lstatAsync(dirpath);
if (!stats.isDirectory()) {
statTable[path.relative(rootpath, dirpath)] = stats;
return null;
}
let files = await fs.readdirAsync(dirpath);
return files.map(file => path.join(dirpath, file));
};
}
function keyUnion(obj1, obj2) {
[obj1, obj2] = [obj1, obj2]
.sort((a, b) => Object.keys(a).length - Object.keys(b).length);
const addedKeys = Object.keys(obj1)
.filter(key => !obj2.hasOwnProperty(key));
const result = Object.keys(obj2).concat(addedKeys);
return result;
}
function keyDifference(obj1, obj2) {
const result = Object.keys(obj1)
.filter(key => !obj2.hasOwnProperty(key));
return result;
}
function keyIntersection(obj1, obj2) {
[obj1, obj2] = [obj1, obj2]
.sort((a, b) => Object.keys(a).length - Object.keys(b).length);
const result = Object.keys(obj1)
.filter(obj2.hasOwnProperty.bind(obj2));
return result;
}
function objectFilter(obj, f) {
const result = Object.keys(obj)
.filter(key => f(key, obj[key]))
.reduce((result, key) => {
result[key] = obj[key];
return result;
}, []);
return result;
}
function objectFilterKeylist(obj, keys) {
const result = keys
.reduce((result, key) => {
result[key] = obj[key];
return result;
}, {});
return result;
}
function shouldIgnoreBuilder(cfg) {
const ignores = {};
for (ignore of cfg.ignore) {
ignores[ignore] = true;
}
return dirpath => {
return ignores[path.basename(dirpath)];
};
}
function calculateDiff(task) {
const originKeys = keyDifference(task.originFiles, task.destFiles);
const destKeys = keyDifference(task.destFiles, task.originFiles);
const bothKeys = keyIntersection(task.originFiles, task.destFiles);
const result = {
origin: objectFilterKeylist(task.originFiles, originKeys),
dest: objectFilterKeylist(task.destFiles, destKeys),
both: bothKeys.map(key => [key, {
origin: task.originFiles[key],
dest: task.destFiles[key],
}])
.reduce((both, [key, val]) => {
both[key] = val;
return both;
}, {}),
};
return result;
}
function getCpCommands(files, frompath, topath, cfg) {
if (!files || files.length == 0) {
return (
"# No files to copy: "+
JSON.stringify(frompath)+
" => "+
JSON.stringify(topath)+
"\n"
);
}
const resultLines = [];
resultLines.push(
"# "+
JSON.stringify(frompath)+
" => "+
JSON.stringify(topath)
);
for (fpath of files) {
resultLines.push(
(cfg.verbose ? "cp -v " : "cp ")+
JSON.stringify(path.join(frompath, fpath))+
" "+
JSON.stringify(path.join(topath, fpath))+
(cfg.verbose ? "" : " >/dev/null")
);
}
const result = resultLines.join("\n") + "\n";
return result;
}
module.exports = lsdiff;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment