Created
January 6, 2020 22:12
-
-
Save chaimleib/0cb4b7221f60f4495a0adddd42184610 to your computer and use it in GitHub Desktop.
List differences between folders in ls output
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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