Skip to content

Instantly share code, notes, and snippets.

@mokshchadha
Created August 7, 2025 10:35
Show Gist options
  • Save mokshchadha/c9016846898108c36b94dedcf38f7fa4 to your computer and use it in GitHub Desktop.
Save mokshchadha/c9016846898108c36b94dedcf38f7fa4 to your computer and use it in GitHub Desktop.
#!/usr/bin/env node
const { exec } = require('child_process');
const util = require('util');
const path = require('path');
const fs = require('fs').promises;
const execAsync = util.promisify(exec);
// Console colors
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
gray: '\x1b[90m'
};
// Parse command line arguments
function parseArgs() {
const args = process.argv.slice(2);
const options = {
srcPath: './src',
sortBy: 'physical',
order: 'desc',
limit: null,
showHelp: false,
output: null,
pretty: false
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case '-h':
case '--help':
options.showHelp = true;
break;
case '-p':
case '--path':
if (i + 1 < args.length) {
options.srcPath = args[++i];
}
break;
case '-s':
case '--sort':
if (i + 1 < args.length) {
const sortBy = args[++i].toLowerCase();
if (['physical', 'source', 'comment', 'empty'].includes(sortBy)) {
options.sortBy = sortBy;
} else {
console.error(`Invalid sort option: ${sortBy}. Use: physical, source, comment, empty`);
process.exit(1);
}
}
break;
case '-o':
case '--order':
if (i + 1 < args.length) {
const order = args[++i].toLowerCase();
if (['asc', 'desc'].includes(order)) {
options.order = order;
} else {
console.error(`Invalid order: ${order}. Use: asc or desc`);
process.exit(1);
}
}
break;
case '-l':
case '--limit':
if (i + 1 < args.length) {
const limit = parseInt(args[++i]);
if (!isNaN(limit) && limit > 0) {
options.limit = limit;
} else {
console.error(`Invalid limit: ${args[i]}. Must be a positive number`);
process.exit(1);
}
}
break;
case '--output':
if (i + 1 < args.length) {
options.output = args[++i];
// Ensure .csv extension
if (!options.output.endsWith('.csv')) {
options.output += '.csv';
}
}
break;
case '--pretty':
options.pretty = true;
break;
default:
// If no flag is provided, treat as path
if (!arg.startsWith('-')) {
options.srcPath = arg;
} else {
console.error(`Unknown option: ${arg}`);
showHelp();
process.exit(1);
}
}
}
return options;
}
function showHelp() {
console.log(`
SLOC File Sorter - Sort files by lines of code statistics
Usage: node sort-sloc.js [path] [options]
Arguments:
path Source directory path (default: ./src)
Options:
-h, --help Show this help message
-p, --path <path> Source directory path
-s, --sort <metric> Sort by metric (physical, source, comment, empty) [default: physical]
-o, --order <order> Sort order (asc, desc) [default: desc]
-l, --limit <number> Limit number of files shown
--output <filename> Save results to CSV file (auto-adds .csv extension)
--pretty Show colorized single-line output
Examples:
node sort-sloc.js # Use ./src, sort by physical lines (desc)
node sort-sloc.js ./lib # Use ./lib directory
node sort-sloc.js -p ./src -s source # Sort by source lines
node sort-sloc.js -p ./src -o asc -l 10 # Show top 10, ascending order
node sort-sloc.js --path ./src --sort comment --limit 5
node sort-sloc.js --output results # Save to results.csv
node sort-sloc.js --pretty # Pretty colorized output
node sort-sloc.js --pretty --output stats # Pretty output + save to CSV
`);
}
async function runSlocAndSort() {
const options = parseArgs();
if (options.showHelp) {
showHelp();
return;
}
try {
console.log(`Running sloc command on: ${options.srcPath}\n`);
// Execute the sloc command with the specified path
const { stdout, stderr } = await execAsync(`sloc "${options.srcPath}" --details`);
if (stderr) {
console.error('Error:', stderr);
return;
}
// Parse the output to extract file information
const files = parseSloc(stdout);
// Sort files by the specified metric and order
files.sort((a, b) => {
const aValue = a[options.sortBy];
const bValue = b[options.sortBy];
if (options.order === 'asc') {
return aValue - bValue;
} else {
return bValue - aValue;
}
});
// Apply limit if specified
const displayFiles = options.limit ? files.slice(0, options.limit) : files;
// Handle CSV output
if (options.output) {
await saveToCsv(displayFiles, options.output);
console.log(`${colors.green}✓ Results saved to ${options.output}${colors.reset}`);
}
// Display results (unless only CSV output is requested)
if (!options.output || options.pretty) {
if (options.pretty) {
displayPrettyResults(displayFiles, options);
} else {
displayDetailedResults(displayFiles, options);
}
}
} catch (error) {
console.error('Failed to execute sloc command:', error.message);
console.log('\nMake sure:');
console.log('1. sloc is installed globally (npm install -g sloc)');
console.log(`2. The ${options.srcPath} directory exists`);
console.log('3. You have the necessary permissions');
}
}
async function saveToCsv(files, filename) {
const header = 'File Path,Physical,Source,Comment,Single-line Comment,Block Comment,Mixed,Empty Block Comment,Empty,To Do\n';
const rows = files.map(file =>
`"${file.path}",${file.physical},${file.source},${file.comment},${file.singleLineComment},${file.blockComment},${file.mixed},${file.emptyBlockComment},${file.empty},${file.todo}`
).join('\n');
const csvContent = header + rows;
await fs.writeFile(filename, csvContent, 'utf8');
}
function displayPrettyResults(files, options) {
const sortMetricDisplay = options.sortBy.charAt(0).toUpperCase() + options.sortBy.slice(1);
const orderDisplay = options.order === 'asc' ? 'lowest to highest' : 'highest to lowest';
console.log(`${colors.bright}${colors.blue}📊 Files sorted by ${sortMetricDisplay} lines (${orderDisplay})${colors.reset}`);
if (options.limit) {
console.log(`${colors.dim}Showing top ${Math.min(options.limit, files.length)} of ${files.length} files${colors.reset}`);
}
console.log();
files.forEach((file, index) => {
const rank = `${colors.dim}${(index + 1).toString().padStart(3, ' ')}.${colors.reset}`;
const filename = `${colors.cyan}${path.basename(file.path)}${colors.reset}`;
const filepath = `${colors.gray}(${file.path})${colors.reset}`;
// Color code the main metric value
let mainValue;
const value = file[options.sortBy];
if (value > 1000) {
mainValue = `${colors.red}${colors.bright}${value}${colors.reset}`;
} else if (value > 500) {
mainValue = `${colors.yellow}${value}${colors.reset}`;
} else if (value > 100) {
mainValue = `${colors.green}${value}${colors.reset}`;
} else {
mainValue = `${colors.dim}${value}${colors.reset}`;
}
const stats = [
`${colors.blue}Physical:${colors.reset} ${file.physical}`,
`${colors.green}Source:${colors.reset} ${file.source}`,
`${colors.yellow}Comment:${colors.reset} ${file.comment}`,
`${colors.gray}Empty:${colors.reset} ${file.empty}`
].join(' | ');
console.log(`${rank} ${filename} ${filepath}`);
console.log(` ${stats}`);
console.log();
});
console.log(`${colors.dim}Total files processed: ${files.length}${colors.reset}`);
}
function displayDetailedResults(files, options) {
const sortMetricDisplay = options.sortBy.charAt(0).toUpperCase() + options.sortBy.slice(1);
const orderDisplay = options.order === 'asc' ? 'lowest to highest' : 'highest to lowest';
console.log(`Files sorted by ${sortMetricDisplay} lines (${orderDisplay}):`);
if (options.limit) {
console.log(`Showing top ${Math.min(options.limit, files.length)} of ${files.length} files`);
}
console.log();
files.forEach(file => {
console.log(`--- ${file.path}`);
console.log('');
console.log(` Physical : ${file.physical}`);
console.log(` Source : ${file.source}`);
console.log(` Comment : ${file.comment}`);
console.log(` Single-line comment : ${file.singleLineComment}`);
console.log(` Block comment : ${file.blockComment}`);
console.log(` Mixed : ${file.mixed}`);
console.log(` Empty block comment : ${file.emptyBlockComment}`);
console.log(` Empty : ${file.empty}`);
console.log(` To Do : ${file.todo}`);
console.log('');
});
console.log(`\nTotal files processed: ${files.length}`);
if (options.limit && files.length > options.limit) {
console.log(`Showing: ${files.length}`);
}
}
function parseSloc(output) {
const files = [];
const lines = output.split('\n');
let currentFile = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Check if this line starts a new file entry
if (line.startsWith('--- ')) {
// If we have a previous file, save it
if (currentFile) {
files.push(currentFile);
}
// Start a new file entry
currentFile = {
path: line.substring(4), // Remove "--- " prefix
physical: 0,
source: 0,
comment: 0,
singleLineComment: 0,
blockComment: 0,
mixed: 0,
emptyBlockComment: 0,
empty: 0,
todo: 0
};
}
// Parse the statistics lines
else if (currentFile && line.includes(':')) {
const parts = line.split(':');
if (parts.length === 2) {
const key = parts[0].trim();
const value = parseInt(parts[1].trim()) || 0;
switch (key) {
case 'Physical':
currentFile.physical = value;
break;
case 'Source':
currentFile.source = value;
break;
case 'Comment':
currentFile.comment = value;
break;
case 'Single-line comment':
currentFile.singleLineComment = value;
break;
case 'Block comment':
currentFile.blockComment = value;
break;
case 'Mixed':
currentFile.mixed = value;
break;
case 'Empty block comment':
currentFile.emptyBlockComment = value;
break;
case 'Empty':
currentFile.empty = value;
break;
case 'To Do':
currentFile.todo = value;
break;
}
}
}
}
// Don't forget the last file
if (currentFile) {
files.push(currentFile);
}
return files;
}
// Run the script
runSlocAndSort();
@mokshchadha
Copy link
Author

need to npm i sloc -g

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment