const fs = require('fs'); const path = require('path'); function usage() { console.log('Usage: node sort-logs.js [--write]'); console.log(' : .txt file or directory containing .txt logs'); console.log(' --write : rewrite files in chronological order'); } function parseDateFromFileName(filePath) { const match = path.basename(filePath).match(/(\d{4})_(\d{2})_(\d{2})/); if (!match) { return null; } return { year: Number(match[1]), month: Number(match[2]), day: Number(match[3]), }; } function parseTimestamp(line, fileDate) { const match = line.match(/\((?:(\d{4})\/)?(\d{1,2})\/(\d{1,2})\s+(\d{1,2}):(\d{2}):(\d{2})\)\s*$/); if (!match) { return null; } const year = match[1] ? Number(match[1]) : fileDate?.year; if (!year) { return null; } const month = Number(match[2]); const day = Number(match[3]); const hour = Number(match[4]); const minute = Number(match[5]); const second = Number(match[6]); return new Date(year, month - 1, day, hour, minute, second).getTime(); } function getTargetFiles(targetPath) { const resolvedPath = path.resolve(targetPath); const stat = fs.statSync(resolvedPath); if (stat.isFile()) { return [resolvedPath]; } if (stat.isDirectory()) { return fs.readdirSync(resolvedPath) .filter((entry) => entry.endsWith('.txt')) .map((entry) => path.join(resolvedPath, entry)) .sort(); } throw new Error(`Unsupported target: ${resolvedPath}`); } function sortFile(filePath, writeBack) { const content = fs.readFileSync(filePath, 'utf8'); const hasTrailingNewline = content.endsWith('\n'); const lines = content.split(/\r?\n/).filter((line) => line.length > 0); const fileDate = parseDateFromFileName(filePath); const decorated = lines.map((line, index) => ({ line, index, timestamp: parseTimestamp(line, fileDate), })); const sortable = decorated.filter((entry) => entry.timestamp !== null); if (sortable.length === 0) { return { changed: false, reason: 'no timestamps found' }; } const sorted = [...decorated].sort((left, right) => { if (left.timestamp === null && right.timestamp === null) { return left.index - right.index; } if (left.timestamp === null) { return 1; } if (right.timestamp === null) { return -1; } if (left.timestamp !== right.timestamp) { return left.timestamp - right.timestamp; } return left.index - right.index; }); const sortedContent = sorted.map((entry) => entry.line).join('\n') + (hasTrailingNewline ? '\n' : ''); if (sortedContent === content) { return { changed: false, reason: 'already sorted' }; } if (writeBack) { fs.writeFileSync(filePath, sortedContent, 'utf8'); } return { changed: true, reason: writeBack ? 'rewritten' : 'would rewrite', firstBefore: decorated[0]?.line ?? '', firstAfter: sorted[0]?.line ?? '', lastBefore: decorated[decorated.length - 1]?.line ?? '', lastAfter: sorted[sorted.length - 1]?.line ?? '', }; } function main() { const args = process.argv.slice(2); const writeBack = args.includes('--write'); const target = args.find((arg) => !arg.startsWith('-')); if (!target) { usage(); process.exitCode = 1; return; } let files; try { files = getTargetFiles(target); } catch (error) { console.error(error.message); process.exitCode = 1; return; } let changedCount = 0; for (const filePath of files) { const result = sortFile(filePath, writeBack); if (result.changed) { changedCount += 1; } console.log(`${path.relative(process.cwd(), filePath)}: ${result.reason}`); } console.log(`${files.length} file(s) checked, ${changedCount} file(s) ${writeBack ? 'rewritten' : 'need sorting'}.`); } main();