print-gtfs-rt-cli
Version:
Read a GTFS Realtime feed from stdin, print human-readable or as JSON.
162 lines (144 loc) • 4.15 kB
JavaScript
#!/usr/bin/env node
'use strict'
const {parseArgs} = require('util')
const {isatty} = require('tty')
const pkg = require('./package.json')
const {
values: flags,
} = parseArgs({
options: {
help: {
type: 'boolean',
short: 'h',
},
version: {
type: 'boolean',
short: 'v',
},
'length-prefixed': {
type: 'boolean',
short: 'l',
},
json: {
type: 'boolean',
short: 'j',
},
'single-json': {
type: 'boolean',
short: 's',
},
'depth': {
type: 'string',
short: 'd',
},
'include-all': {
type: 'boolean',
short: 'a',
},
'gtfs-rt-bindings': {
type: 'string',
},
},
})
if (flags.help) {
process.stdout.write(`
Usage:
cat gtfs-rt-feed.pbf | print-gtfs-rt
Options:
--length-prefixed -l Read input as length-prefixed.
See https://www.npmjs.com/package/length-prefixed-stream
--json -j Output newline-delimeted JSON (http://ndjson.org).
--single-json -s Output a single JSON array.
--depth -d Number of nested levels to print. Default: infinite
--include-all -a Print the entire FeedMessage, including its header.
--gtfs-rt-bindings Path to GTFS-RT bindings. Must be compatible with
those generated by protobufjs.
Examples:
curl 'https://example.org/gtfs-rt.pbf' | print-gtfs-rt
\n`)
process.exit(0)
}
if (flags.version) {
process.stdout.write(`print-gtfs-rt v${pkg.version}\n`)
process.exit(0)
}
const showError = (err) => {
if (!err) return;
if (err.code === 'EPIPE') return; // todo: refine this
if (process.env.NODE_ENV === 'dev') console.error(err)
else console.error(err.message || (err + ''))
process.exit(1)
}
if (isatty(process.stdin.fd)) showError('You must pipe into print-gtfs-rt.')
const {decode: decodeLengthPrefixed} = require('length-prefixed-stream')
const {pipeline} = require('stream')
const {resolve: pathResolve} = require('path')
const defaultBindings = require('gtfs-rt-bindings')
const {inspect} = require('util')
const read = (readable) => {
return new Promise((resolve, reject) => {
const chunks = []
readable
.once('error', reject)
.on('data', chunk => chunks.push(chunk))
.once('end', () => resolve(chunks))
})
}
const isLengthPrefixed = flags['length-prefixed']
const printAsNDJSON = flags.json
const printAsJSON = flags['single-json']
const includeAll = flags['include-all']
const printWithColors = isatty(process.stdout.fd)
const depth = flags.depth ? parseInt(flags.depth) : null
const bindings = flags['gtfs-rt-bindings']
? require(pathResolve(process.cwd(), flags['gtfs-rt-bindings']))
: defaultBindings
const {FeedMessage} = bindings.transit_realtime || bindings
const onFeedMessage = (buf) => {
const data = FeedMessage.toObject(FeedMessage.decode(buf))
if (!data) throw new Error('invalid feed')
if (!data.header) throw new Error('invalid feed: missing header')
// Protocol buffers don't encode empty arrays, so .entity is missing with 0 FeedEntitys.
if (!('entity' in data)) {
data.entity = []
} else if (!Array.isArray(data.entity)) {
throw new Error('invalid feed: missing entity[]')
}
const inspectOptions = {depth, colors: printWithColors};
if (includeAll) {
const msg = printAsNDJSON || printAsJSON
? JSON.stringify(data) + '\n'
: inspect(data, inspectOptions);
process.stdout.write(msg);
return;
}
if (printAsJSON) {
process.stdout.write('[\n')
}
for (var i = 0; i < data.entity.length; ++i) {
const entity = data.entity[i];
const msg = printAsNDJSON || printAsJSON
? JSON.stringify(entity)
: inspect(entity, inspectOptions)
const isLastEntity = i == data.entity.length - 1;
const delimeter = (printAsJSON && !isLastEntity) ? ',\n' : '\n'
process.stdout.write(msg + delimeter)
}
if (printAsJSON) {
process.stdout.write(']\n')
}
}
if (isLengthPrefixed) {
const decoder = decodeLengthPrefixed()
pipeline(
process.stdin,
decoder,
showError,
)
decoder.on('data', onFeedMessage)
} else {
read(process.stdin)
.then(chunks => onFeedMessage(Buffer.concat(chunks)))
.catch(showError)
}
process.stdout.on('error', showError)