comment-strip-cli
Version:
A powerful CLI tool to strip comments from source code files while preserving strings and important metadata
341 lines (287 loc) • 10.7 kB
JavaScript
const fs = require('fs').promises;
const path = require('path');
function parseArgs(args) {
const options = {
input: null,
type: ['all'],
dryRun: false,
backup: false,
recursive: true,
extensions: [
'js', 'mjs', 'jsx', 'ts', 'tsx', 'c', 'h', 'cpp', 'cxx', 'cc', 'hpp', 'hxx',
'py', 'pyw', 'sol', 'rs', 'go', 'java', 'kt', 'kts', 'swift', 'dart', 'scala', 'sc',
'php', 'phtml', 'css', 'scss', 'rb', 'rbw', 'pl', 'pm', 'r', 'R', 'sh', 'bash', 'zsh',
'dockerfile', 'cmake', 'toml', 'yml', 'yaml', 'ini', 'cfg', 'conf', 'makefile', 'mk',
'asm', 's', 'sql', 'lua', 'hs', 'lhs', 'json', 'jsonc'
]
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--help' || arg === '-h') {
showHelp();
return { help: true };
}
else if (arg.startsWith('--type=') || arg.startsWith('-t=')) {
const typeValue = arg.split('=')[1];
if (typeValue) {
if (typeValue.includes(',')) {
options.type = typeValue.split(',').map(t => t.trim());
} else {
options.type = [typeValue];
}
} else {
console.error('❌Error: --type flag requires a value');
return { error: true };
}
}
else if (arg === '--type' || arg === '-t') {
if (i + 1 < args.length) {
i++;
const typeValue = args[i];
if (typeValue.includes(',')) {
options.type = typeValue.split(',').map(t => t.trim());
} else {
options.type = [typeValue];
}
} else {
console.error('❌Error: --type flag requires a value');
return { error: true };
}
}
else if (arg === '--dry-run' || arg === '-d') {
options.dryRun = true;
}
else if (arg === '--backup' || arg === '-b') {
options.backup = true;
}
else if (arg.startsWith('--extensions=') || arg.startsWith('-e=')) {
const extensionsValue = arg.split('=')[1];
if (extensionsValue) {
options.extensions = extensionsValue.split(',').map(ext => ext.trim());
} else {
console.error('❌Error: --extensions flag requires a value');
return { error: true };
}
}
else if (arg === '--extensions' || arg === '-e') {
if (i + 1 < args.length) {
i++;
const extensionsValue = args[i];
options.extensions = extensionsValue.split(',').map(ext => ext.trim());
} else {
console.error('❌Error: --extensions flag requires a value');
return { error: true };
}
}
else if (!arg.startsWith('--') && !arg.startsWith('-') && !options.input) {
options.input = arg;
}
}
return options;
}
function showHelp() {
console.log(`
Comment Stripper CLI Tool
Usage: comment-strip <file|directory> [options]
Options:
--type, -t <type> Comment type to strip: '//', '#', '/* */', '--', ';', etc. (default: all)
--dry-run, -d Preview changes without modifying files
--backup, -b Create backup files before modification
--extensions, -e <ext> File extensions to process (comma-separated)
--help, -h Show this help message
Examples:
comment-strip app.js # Strip all comments from app.js
comment-strip src/ --type="//" # Strip only // comments from src/
comment-strip . --dry-run # Preview all changes
comment-strip code/ --backup --type="#" # Strip # comments with backup
comment-strip project/ --extensions=js,py,cpp # Process only specific extensions
Supported Languages:
Programming: JavaScript/TypeScript, C/C++, Java, Python, Solidity, Rust, Go,
Kotlin, Swift, Dart, Scala, PHP, Ruby, Perl, R, Lua, Haskell
Web: CSS, SCSS, HTML, JSON
Config: Docker, CMake, TOML, YAML, INI, Makefile
Database: SQL
Assembly: ASM, S
And many more!
`);
}
async function main(args) {
try {
const options = parseArgs(args);
if (options.help) {
return { success: true };
}
if (options.error) {
return { success: false };
}
if (!options.input) {
console.error('❌Error: Please provide a file or directory to process');
console.error('❌Use --help for usage information');
return { success: false };
}
const typeDisplay = options.type.join(', ');
console.log(`------> Options: type=${typeDisplay}, backup=${options.backup}`);
// Resolve the input path to absolute path
const inputPath = path.resolve(options.input);
console.log(`------> Checking path: ${inputPath}`);
// Dry-run specific message
if (options.dryRun) {
console.log(`------> Comments with type ${typeDisplay} will be stripped and the code will be autoformatted`);
}
// Check if input exists
try {
const stats = await fs.stat(inputPath);
if (stats.isDirectory()) {
return await processDirectory(inputPath, options);
} else {
return await processFile(inputPath, options);
}
} catch (error) {
console.error(`❌Error: File or directory '${options.input}' not found`);
console.error(`❌Resolved path: ${inputPath}`);
console.error(`❌Error details: ${error.message}`);
return { success: false };
}
} catch (error) {
console.error('❌Unexpected error:', error.message);
return { success: false };
}
}
async function processDirectory(dirPath, options) {
let processedCount = 0;
let skippedCount = 0;
let errorCount = 0;
let noChangesCount = 0;
console.log(`------> Processing directory: ${dirPath}`);
try {
const files = await getFilesRecursively(dirPath, options.extensions);
if (files.length === 0) {
console.log(`------> No files found with supported extensions or special files`);
return { success: true, processedCount: 0, skippedCount: 0, errorCount: 0 };
}
console.log(`------> Found ${files.length} files to process`);
for (const file of files) {
try {
const result = await processFile(file, options);
if (result.skipped) {
skippedCount++;
} else if (result.modified === false) {
noChangesCount++;
} else if (result.modified !== false) {
processedCount++;
}
} catch (error) {
console.error(`❌Error processing ${file}: ${error.message}`);
errorCount++;
}
}
console.log(`------> Summary: ${processedCount} files processed, ${noChangesCount} unchanged, ${skippedCount} skipped, ${errorCount} errors`);
return { success: errorCount === 0, processedCount, noChangesCount, skippedCount, errorCount };
} catch (error) {
console.error(`❌Error reading directory: ${error.message}`);
return { success: false };
}
}
async function processFile(filePath, options) {
try {
// Read file content
const content = await fs.readFile(filePath, 'utf8');
// Get file extension and filename to determine language
const ext = path.extname(filePath).slice(1).toLowerCase();
const filename = path.basename(filePath);
// Check if this file should be processed
if (!shouldProcessFile(filename, ext, options.extensions)) {
return { success: true, skipped: true };
}
// Import and use the parser
const { stripComments } = require('./parser.js');
let strippedContent;
try {
const commentTypes = Array.isArray(options.type) ? options.type : [options.type];
strippedContent = stripComments(content, ext, commentTypes, filename);
} catch (parserError) {
throw new Error(`Parser error: ${parserError.message}`);
}
if (typeof strippedContent !== 'string') {
throw new Error('Parser returned invalid result format');
}
const typeDisplay = options.type.join(', ');
if (content === strippedContent) {
if (!options.dryRun) {
console.log(`>>>>>> No comments found at ${filePath} of type ${typeDisplay}`);
}
return { success: true, modified: false };
}
if (options.dryRun) {
return { success: true, preview: true };
}
// Create backup if requested
if (options.backup) {
try {
const backupPath = filePath + '.bak';
await fs.writeFile(backupPath, content, 'utf8');
console.log(`======> Backup for your file ${filePath} has been created`);
} catch (backupError) {
console.warn(`Warning: Could not create backup for ${filePath}: ${backupError.message}`);
}
}
// Write the modified content
await fs.writeFile(filePath, strippedContent, 'utf8');
console.log(`------> Comments of type ${typeDisplay} have been stripped and the code has been formatted`);
return { success: true, modified: true };
} catch (error) {
throw new Error(`Failed to process ${filePath}: ${error.message}`);
}
}
function shouldProcessFile(filename, ext, allowedExtensions) {
// Check for special files without extensions
const specialFiles = [
'dockerfile', 'Dockerfile',
'makefile', 'Makefile', 'GNUmakefile',
'CMakeLists.txt', 'cmakelists.txt'
];
const lowerName = filename.toLowerCase();
// Check if it's a special file
if (specialFiles.some(special => lowerName === special.toLowerCase())) {
return true;
}
// Check if it starts with special patterns
if (lowerName.startsWith('dockerfile.') ||
lowerName.startsWith('makefile.')) {
return true;
}
// Check normal extensions
if (ext && allowedExtensions.includes(ext)) {
return true;
}
return false;
}
async function getFilesRecursively(dir, extensions) {
const files = [];
async function scanDir(currentDir) {
const entries = await fs.readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
// Skip node_modules, .git, and other hidden directories
if (!entry.name.startsWith('.') &&
entry.name !== 'node_modules' &&
entry.name !== 'dist' &&
entry.name !== 'build' &&
entry.name !== 'coverage' &&
entry.name !== 'target' &&
entry.name !== 'vendor') {
await scanDir(fullPath);
}
} else if (entry.isFile()) {
const ext = path.extname(entry.name).slice(1).toLowerCase();
if (shouldProcessFile(entry.name, ext, extensions)) {
files.push(fullPath);
}
}
}
}
await scanDir(dir);
return files;
}
module.exports = { main };