Skip to content

Instantly share code, notes, and snippets.

@kadamwhite
Created March 17, 2025 20:03
Show Gist options
  • Save kadamwhite/f34ccfe1c3c123b53e1cc95463b564b1 to your computer and use it in GitHub Desktop.
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
#!/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