Created
March 17, 2025 20:03
-
-
Save kadamwhite/f34ccfe1c3c123b53e1cc95463b564b1 to your computer and use it in GitHub Desktop.
[Node based] CLI script to take a nested tree of files and render it into a flat list without breaking relative ordering
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 | |
const minimist = function(t,e){var r={bools:{},strings:{}};"boolean"==typeof(e=e||{}).boolean&&e.boolean?r.allBools=!0:[].concat(e.boolean).filter(Boolean).forEach(function(t){r.bools[t]=!0});var s={};Object.keys(e.alias||{}).forEach(function(t){s[t]=[].concat(e.alias[t]),s[t].forEach(function(e){s[e]=[t].concat(s[t].filter(function(t){return e!==t}))})}),[].concat(e.string).filter(Boolean).forEach(function(t){r.strings[t]=!0,s[t]&&(r.strings[s[t]]=!0)});var o=e.default||{},n={_:[]};Object.keys(r.bools).forEach(function(t){a(t,void 0!==o[t]&&o[t])});var i=[];function a(t,e){var o=!r.strings[t]&&isNumber(e)?Number(e):e;setKey(n,t.split("."),o),(s[t]||[]).forEach(function(t){setKey(n,t.split("."),o)})}-1!==t.indexOf("--")&&(i=t.slice(t.indexOf("--")+1),t=t.slice(0,t.indexOf("--")));for(var c=0;c<t.length;c++){var f=t[c];if(/^--.+=/.test(f)){var l=f.match(/^--([^=]+)=([\s\S]*)$/);a(l[1],l[2])}else if(/^--no-.+/.test(f))a(u=f.match(/^--no-(.+)/)[1],!1);else if(/^--.+/.test(f)){var u=f.match(/^--(.+)/)[1];void 0===(p=t[c+1])||/^-/.test(p)||r.bools[u]||r.allBools||s[u]&&r.bools[s[u]]?/^(true|false)$/.test(p)?(a(u,"true"===p),c++):a(u,!r.strings[u]||""):(a(u,p),c++)}else if(/^-[^-]+/.test(f)){for(var p,b=f.slice(1,-1).split(""),h=!1,y=0;y<b.length;y++)if("-"!==(p=f.slice(y+2))){if(/[A-Za-z]/.test(b[y])&&/-?\d+(\.\d*)?(e-?\d+)?$/.test(p)){a(b[y],p),h=!0;break}if(b[y+1]&&b[y+1].match(/\W/)){a(b[y],f.slice(y+2)),h=!0;break}a(b[y],!r.strings[b[y]]||"")}else a(b[y],p);u=f.slice(-1)[0];h||"-"===u||(!t[c+1]||/^(-|--)[^-]/.test(t[c+1])||r.bools[u]||s[u]&&r.bools[s[u]]?t[c+1]&&/true|false/.test(t[c+1])?(a(u,"true"===t[c+1]),c++):a(u,!r.strings[u]||""):(a(u,t[c+1]),c++))}else n._.push(r.strings._||!isNumber(f)?f:Number(f))}return Object.keys(o).forEach(function(e){hasKey(n,e.split("."))||(setKey(n,e.split("."),o[e]),(s[e]||[]).forEach(function(t){setKey(n,t.split("."),o[e])}))}),e["--"]?(n["--"]=new Array,i.forEach(function(t){n["--"].push(t)})):i.forEach(function(t){n._.push(t)}),n};function hasKey(t,e){var o=t;return e.slice(0,-1).forEach(function(t){o=o[t]||{}}),e[e.length-1]in o}function setKey(t,e,o){for(var r,s=t,n=0;n<e.length-1;n++){if("__proto__"===(r=e[n]))return;void 0===s[r]&&(s[r]={}),s[r]!==Object.prototype&&s[r]!==Number.prototype&&s[r]!==String.prototype||(s[r]={}),s[r]===Array.prototype&&(s[r]=[]),s=s[r]}"__proto__"!==(r=e[e.length-1])&&(s!==Object.prototype&&s!==Number.prototype&&s!==String.prototype||(s={}),s===Array.prototype&&(s=[]),void 0===s[r]||"boolean"==typeof s[r]?s[r]=o:Array.isArray(s[r])?s[r].push(o):s[r]=[s[r],o])}function isNumber(t){return"number"==typeof t||(!!/^0x[0-9a-f]+$/i.test(t)||/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(t))}; | |
const { promisify } = require( 'util' ); | |
const { relative, resolve } = require( 'path' ); | |
const fs = require( 'fs' ); | |
const cp = require( 'child_process' ); | |
const argv = minimist( process.argv.slice(2), {} ); | |
const targetDir = resolve( process.cwd(), argv._[0] || '.' ); | |
/** | |
* Default mode for spawn invocation. Output to stdio and stderr, resolve when | |
* child process exits, but do not resolve with any specific value. Reject with | |
* error object or code, depending on error state. | |
* | |
* @private | |
* @param {cp.ChildProcess} spawnedProcess Invoked child process. | |
* @param {(value: any) => void} resolve Promise constructor resolve function. | |
* @param {(reason?: any) => void} reject Promise constructor reject function. | |
*/ | |
const useInheritedStdIO = ( spawnedProcess, resolve, reject ) => { | |
spawnedProcess.on( 'error', ( err ) => reject( err ) ); | |
spawnedProcess.on( 'close', ( code, signal ) => { | |
if ( code ) { | |
reject(); | |
return; | |
} | |
resolve(); | |
} ); | |
}; | |
/** | |
* Execute a command as a spawned process. | |
* | |
* @param {string} command A bash command string, excluding arguments. | |
* @param {Array<string|number>} args Array of argument strings for the provided command. | |
* @param {cp.SpawnOptions} [options] cp.spawn options object. | |
* @param {( | |
* spawnedProcess: cp.ChildProcess, | |
* resolve: (value: any) => void, | |
* reject: (reason?: any) => void | |
* ) => void} [callback] Handler function to control processing and resolution. | |
* | |
* @returns {Promise} Promise that resolves depending on actions within callback(). | |
*/ | |
const spawn = ( command, args, options = {}, callback = useInheritedStdIO ) => { | |
return new Promise( ( resolve, reject ) => { | |
const spawnedProcess = cp.spawn( command, args, { | |
stdio: 'inherit', | |
} ); | |
spawnedProcess.on( 'error', ( err ) => reject( err ) ); | |
spawnedProcess.on( 'close', ( code, signal ) => { | |
if ( code ) { | |
reject(); | |
return; | |
} | |
resolve(); | |
} ); | |
} ); | |
}; | |
/** | |
* List the files in a directory, either as a list of file and subdir names or | |
* a list of absolute file system paths. | |
* | |
* @param {String} inputDir The file system path to the directory to read. | |
* @returns {Promise} A promise to an array of file system path strings. | |
*/ | |
const ls = async ( inputDir ) => promisify( fs.readdir )( inputDir ); | |
const getAllNonDirectoryFiles = async ( rootDir ) => { | |
const nonHiddenFiles = await ls( rootDir ).then( files => files | |
.filter( ( file ) => ! /^\./.test( file ) ) | |
.map( ( file ) => resolve( rootDir, file ) ) | |
); | |
let files = []; | |
for ( let file of nonHiddenFiles ) { | |
let isDirectory = false; | |
try { | |
if ( fs.statSync( file ).isDirectory() ) { | |
isDirectory = true; | |
} | |
} catch ( e ) {} | |
if ( isDirectory ) { | |
files = files.concat( await getAllNonDirectoryFiles( file ) ); | |
} else { | |
files.push( file ); | |
} | |
} | |
return files; | |
}; | |
const normalizeFilenames = ( file, rootDir ) => { | |
const fileWithoutRootDir = relative( rootDir, file ); | |
return fileWithoutRootDir.replace( /\//g, '_' ).replace( /\s+/g, '' ); | |
} | |
/** | |
* Get the number of nested, non-directory files in a directory tree. | |
* | |
* @param {string} rootDir Absolute file system path. | |
* @returns {number} Count of non-directory, non-hidden files within this path. | |
*/ | |
const countFiles = async ( rootDir ) => { | |
const nonHiddenFiles = await ls( rootDir ).then( files => files | |
.filter( ( file ) => ! /^\./.test( file ) ) | |
.map( ( file ) => resolve( rootDir, file ) ) | |
); | |
let count = 0; | |
for ( let file of nonHiddenFiles ) { | |
let isDirectory = false; | |
try { | |
if ( fs.statSync( file ).isDirectory() ) { | |
isDirectory = true; | |
} | |
} catch ( e ) {} | |
if ( isDirectory ) { | |
count += await countFiles( file ); | |
} else { | |
count += 1; | |
} | |
} | |
return count; | |
}; | |
async function run() { | |
let files; | |
try { | |
files = await getAllNonDirectoryFiles( targetDir ); | |
} catch ( e ) { | |
console.error( `Could not enumerate files in ${ targetDir }` ); | |
process.exit( 1 ); | |
} | |
for ( let file of files ) { | |
const normalizedFile = normalizeFilenames( file, targetDir ); | |
console.log( `mv "${ relative( targetDir, file ) }" "${ normalizedFile }` ); | |
await spawn( 'mv', [ file, resolve( targetDir, normalizedFile ) ] ); | |
} | |
} | |
run(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment