From 4e6259ad7fcf4e913721c69cf66690bd408dd084 Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Sun, 5 Oct 2025 23:28:57 +0700 Subject: [PATCH] [feat] Add new CLI feature: processing batch user-agent data from file and output as JSON --- CHANGELOG.md | 8 +++- package.json | 2 +- script/cli.js | 94 ++++++++++++++++++++++++++++++++++++++- test/unit/cli/cli.spec.js | 37 +++++++++++++++ test/unit/cli/input.txt | 2 + test/unit/cli/output.json | 32 +++++++++++++ 6 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 test/unit/cli/cli.spec.js create mode 100644 test/unit/cli/input.txt create mode 100644 test/unit/cli/output.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ea4bb8..fb55cf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,8 +23,12 @@ - **Support for Custom/Predefined Extensions:** - Pass custom regexes or predefined extensions as a list to `UAParser()` -- **Support for CLI Parsing:** - - Parse a user-agent directly from the command line using `npx ua-parser-js "[User-Agent]"` +- **Support for CLI Processing:** + - Directly parse user-agent strings from the command line: + `npx ua-parser-js ""` + - Process batch data from files: + `npx ua-parser-js --input-file=log.txt >> result.json` or + `npx ua-parser-js --input-file=log.txt --output-file=result.json` - **Enhanced Detection with Client Hints:** - `withClientHints()`: Improves detection accuracy by leveraging client hints diff --git a/package.json b/package.json index 580bbcc..06b4b7b 100755 --- a/package.json +++ b/package.json @@ -220,7 +220,7 @@ "test:eslint": "eslint src && eslint script", "test:jshint": "jshint src/main", "test:lockfile-lint": "npx lockfile-lint -p package-lock.json", - "test:mocha": "mocha test/unit", + "test:mocha": "mocha --recursive test/unit", "test:playwright": "npx playwright install && playwright test test/e2e --browser all" }, "dependencies": { diff --git a/script/cli.js b/script/cli.js index 785a104..368d6c5 100755 --- a/script/cli.js +++ b/script/cli.js @@ -1,4 +1,94 @@ #!/usr/bin/env node -const UAParser = require('ua-parser-js'); -console.log(JSON.stringify(process.argv.slice(2).map(ua => UAParser(ua)), null, 4)); \ No newline at end of file +try { + const fs = require('node:fs'); + const path = require('node:path'); + const { performance } = require('node:perf_hooks'); + const readline = require('node:readline'); + const { parseArgs } = require('node:util'); + const UAParser = require('ua-parser-js'); + const { Bots, Emails, ExtraDevices, InApps, Vehicles } = require('ua-parser-js/extensions'); + + if (!process.argv[2].startsWith('-')) { + + const results = process.argv.slice(2).map(ua => UAParser(ua)); + console.log(JSON.stringify(results, null, 4)); + process.exit(0); + + } else if (['-h', '--help'].includes(process.argv[2])) { + + console.log('Usage: npx ua-parser-js '); + console.log(' or npx ua-parser-js --input-file [--output-file ]'); + console.log('-i, --input-file'); + console.log('-o, --output-file'); + process.exit(0); + + } else { + + const startPerf = performance.now(); + const { + values: { + 'input-file': inputFile, + 'output-file': outputFile + }, + } = parseArgs({ + options: { + 'input-file': { type: 'string', short: 'i' }, + 'output-file': { type: 'string', short: 'o' } + } + }); + + if (!inputFile) { + console.error('Input file must be present'); + process.exit(1); + } + + const inputPath = path.resolve(__dirname, inputFile); + const outputPath = outputFile ? path.resolve(__dirname, outputFile) : null; + + if (!fs.existsSync(inputPath)) { + console.error(`Input file not found: ${inputPath}`); + process.exit(1); + } + + const inputStream = fs.createReadStream(inputPath, 'utf8'); + const rl = readline.createInterface({ + input: inputStream, + crlfDelay: Infinity + }); + + const outputStream = outputPath ? fs.createWriteStream(outputPath, { encoding : 'utf8' }) : process.stdout; + + const uap = new UAParser([Bots, Emails, ExtraDevices, InApps, Vehicles]); + let lineNumber = 0; + + outputStream.write('[\n'); + + rl.on('line', line => { + const result = uap.setUA(line).getResult(); + const json = JSON.stringify(result, null, 4); + if (lineNumber > 0) outputStream.write(',\n'); + outputStream.write(json); + lineNumber++; + }); + + rl.on('close', () => { + outputStream.write('\n]'); + if (outputPath) { + outputStream.end(() => { + const finishPerf = performance.now(); + console.log(`Done!`); + console.log(`Number of lines found: ${lineNumber}`); + console.log(`Task finished in: ${(finishPerf - startPerf).toFixed(3)}ms`); + console.log(`Output written to: ${outputPath}`); + process.exit(0); + }); + } else { + process.exit(0); + } + }); + } +} catch (err) { + console.error(err); + process.exit(1); +} \ No newline at end of file diff --git a/test/unit/cli/cli.spec.js b/test/unit/cli/cli.spec.js new file mode 100644 index 0000000..7365fe4 --- /dev/null +++ b/test/unit/cli/cli.spec.js @@ -0,0 +1,37 @@ +const assert = require('node:assert'); +const { exec } = require('node:child_process'); +const fs = require('node:fs'); +const { UAParser } = require('../../../src/main/ua-parser'); +const uap = new UAParser(); + +describe('npx ua-parser-js ', () => { + it ('print result to stdout', () => { + exec('npx ua-parser-js "TEST"', (err, stdout, stderr) => { + assert.deepEqual(JSON.parse(stdout), JSON.parse(JSON.stringify([uap.setUA("TEST").getResult()]))); + }); + }) +}); + +describe('npx ua-parser-js --input-file=', () => { + it ('load file and print result to stdout', () => { + exec('npx ua-parser-js --input-file="../test/unit/cli/input.txt"', (err, stdout, stderr) => { + assert.deepEqual(JSON.parse(stdout), JSON.parse(JSON.stringify([ + uap.setUA('Opera/9.25 (Windows NT 6.0; U; ru)').getResult(), + uap.setUA('Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)').getResult() + ]))); + }); + }); +}); + +describe('npx ua-parser-js --input-file= --output-file=', () => { + it ('load file and save result to file', () => { + exec('npx ua-parser-js --input-file="../test/unit/cli/input.txt" --output-file="../test/unit/cli/output.json"', (err, stdout, stderr) => { + fs.readFile('test/unit/cli/output.json', (err, data) => { + assert.deepEqual(JSON.parse(data), JSON.parse(JSON.stringify([ + uap.setUA('Opera/9.25 (Windows NT 6.0; U; ru)').getResult(), + uap.setUA('Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)').getResult() + ]))); + }); + }); + }); +}); diff --git a/test/unit/cli/input.txt b/test/unit/cli/input.txt new file mode 100644 index 0000000..2d50fb8 --- /dev/null +++ b/test/unit/cli/input.txt @@ -0,0 +1,2 @@ +Opera/9.25 (Windows NT 6.0; U; ru) +Mozilla/4.0 (compatible; MSIE 5.5; Windows NT) \ No newline at end of file diff --git a/test/unit/cli/output.json b/test/unit/cli/output.json new file mode 100644 index 0000000..d5b92c8 --- /dev/null +++ b/test/unit/cli/output.json @@ -0,0 +1,32 @@ +[ +{ + "ua": "Opera/9.25 (Windows NT 6.0; U; ru)", + "browser": { + "name": "Opera", + "version": "9.25", + "major": "9" + }, + "cpu": {}, + "device": {}, + "engine": {}, + "os": { + "name": "Windows", + "version": "Vista" + } +}, +{ + "ua": "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)", + "browser": { + "name": "IE", + "version": "5.5", + "major": "5" + }, + "cpu": {}, + "device": {}, + "engine": {}, + "os": { + "name": "Windows", + "version": "NT" + } +} +] \ No newline at end of file