#!/usr/bin/env node import { readFileSync } from 'node:fs'; const HELP = `Usage: tally [options] Count literal-substring occurrences of in . Options: -i, --ignore-case Case-insensitive matching --lines Count lines containing pattern (not total occurrences) -r, --regex Interpret as a JavaScript regular expression --json Emit a JSON object on stdout -h, --help Show this help and exit Exit codes: 0=success 1=file error 2=invalid argv `; function fail(msg, code = 2) { process.stderr.write(`tally: ${msg}\n`); process.exit(code); } function parseArgs(argv) { const positional = []; const flags = { json: false, ignoreCase: false, lines: false, regex: false }; for (const a of argv) { if (a === '--json') flags.json = true; else if (a === '-i' || a === '--ignore-case') flags.ignoreCase = true; else if (a === '--lines') flags.lines = true; else if (a === '--regex' || a === '-r') flags.regex = true; else if (a === '-h' || a === '--help') { process.stdout.write(HELP); process.exit(0); } else if (a.startsWith('-')) fail(`unknown flag: ${a}`); else positional.push(a); } if (positional.length !== 2) fail('expected '); return { pattern: positional[0], file: positional[1], flags }; } function compileRegex(pattern) { try { return new RegExp(pattern, 'g'); } catch (e) { fail(`invalid regex: ${e.message}`); } } function countOccurrences(text, pattern, ignoreCase) { if (pattern.length === 0) return 0; const haystack = ignoreCase ? text.toLowerCase() : text; const needle = ignoreCase ? pattern.toLowerCase() : pattern; let count = 0, idx = 0; while ((idx = haystack.indexOf(needle, idx)) !== -1) { count++; idx += needle.length; } return count; } function countLines(text, pattern, ignoreCase) { if (pattern.length === 0) return 0; const needle = ignoreCase ? pattern.toLowerCase() : pattern; let count = 0; for (const line of text.split('\n')) { const haystack = ignoreCase ? line.toLowerCase() : line; if (haystack.includes(needle)) count++; } return count; } function main() { const { pattern, file, flags } = parseArgs(process.argv.slice(2)); let text; try { text = readFileSync(file, 'utf8'); } catch (err) { const what = err.code === 'ENOENT' ? 'file not found' : 'read error'; process.stderr.write(`tally: ${what}: ${file}\n`); process.exit(1); } let count; if (flags.regex) { const re = compileRegex(pattern); count = (text.match(re) || []).length; } else if (flags.lines) { count = countLines(text, pattern, flags.ignoreCase); } else { count = countOccurrences(text, pattern, flags.ignoreCase); } if (flags.json) { process.stdout.write(JSON.stringify({ pattern, file, count, flags: { json: flags.json, ignoreCase: flags.ignoreCase, lines: flags.lines, regex: flags.regex }, }) + '\n'); } else { process.stdout.write(count + '\n'); } } main();