UNPKG

14.5 kBJavaScriptView Raw
1var util = require("util"),
2 vm = require("vm");
3
4/*
5 JSON Command class
6*/
7
8JSON.Command = function(args) {
9 this.args = null;
10
11 this.debugOn = false;
12 this.fileNames = [];
13 this.files = null;
14 this.keys = [];
15 this.transformedKeys = [];
16 this.uglyOutput = false;
17 this.inspectOutput = false;
18 this.headerPassthrough = false;
19 this.columnOutput = false;
20 this.useObject = null;
21 this.inputIsArray = false;
22
23 this.conditionals = [];
24 this.executables = [];
25
26 this.stdin = null;
27 this.buffer = "";
28
29 if (args) { this.processArgs(args); }
30};
31
32JSON.Command.prototype.printhelp = function() {
33 console.log("usage: stdout_generator | json [options] [fields]");
34 console.log("");
35 console.log("json processes standard input and parses json objects. json currently handles a");
36 console.log("few different standard input formats and provides a number of options tailored");
37 console.log("toward inspecting and transforming the parsed json objects.");
38 console.log("");
39 console.log("options:\n");
40 console.log(" -h print this help info and exit\n");
41 console.log(" -v (-V | --version) print version number and exit\n");
42 console.log(" -u print ugly json output, each object on a single line\n");
43 console.log(" -d print debugging output including exception messages\n");
44 console.log(" -o object.path specify the path to an array to be iterated on\n");
45 console.log(" new.key=old_key move old_key to new.key in output object\n");
46 console.log(" -a input object is an array, process each element separately\n");
47 console.log(" -c \"js conditional\" js conditional to be run in the context of each object");
48 console.log(" that determines whether an object is printed\n");
49 console.log(" -C print the output fields as tab delimited columns in");
50 console.log(" the order specified by fields\n");
51 console.log(" -e \"js expression\" execute arbitrary js in the context of each object.\n");
52 console.log(" -i use node's util.inspect instead of JSON.stringify\n");
53 console.log(" -H print headers, if they are supplied.");
54 console.log(" Useful for output from curl -i.\n");
55
56 console.log("examples:\n");
57 console.log(" curl http://search.twitter.com/search.json?q=node.js 2> /dev/null |");
58 console.log(" json -o results\n");
59 console.log(" curl http://search.twitter.com/search.json?q=node.js 2> /dev/null |");
60 console.log(" json -o results new_id=id\n");
61 console.log(" curl http://search.twitter.com/search.json?q=node.js 2> /dev/null |");
62 console.log(" json -o results -C from_user from_user_id text\n");
63 console.log("more help:\n");
64 console.log(" use \"man json\" or visit http://github.com/zpoley/json-command\n");
65 process.exit();
66};
67
68JSON.Command.prototype.printversion = function() {
69 var npm = require("npm");
70 npm.load([], function(er) {
71 console.log("json command line toolkit\n version: ");
72 npm.commands.view([ "json", "version" ], function(er, data) {
73 process.exit();
74 });
75 });
76};
77
78
79JSON.Command.prototype.stringify = function(obj) {
80 return( this.inspectOutput ? util.inspect(obj, false, Infinity, true)
81 : this.uglyOutput ? JSON.stringify(obj)
82 : JSON.stringify(obj, null, 2) );
83};
84
85JSON.Command.prototype.debug = function(msg) {
86 if (this.debugOn) { console.log(msg); }
87};
88
89JSON.Command.prototype.printex = function(ex) {
90 this.debug("ex: " + JSON.stringify(ex, null, 2));
91};
92
93/*
94 Process Command line arguments to JSON Command
95*/
96
97JSON.Command.prototype.processArgs = function processArgs(args) {
98
99 // copy argv to chop it up
100 var a = args.slice(0);
101 // turn -iH into -i -H
102 // nb: don't cache length. it may change.
103 for (var i = 0; i < a.length; i ++) {
104 if (a[i].charAt(0) === "-" && a[i].length > 2) {
105 var arg = a[i].replace(/^-+/, "").split("").map(function (a) {
106 return "-" + a;
107 });
108 a.splice.apply(a, [i, 1].concat(arg));
109 }
110 }
111
112 while (a.length > 0) {
113 var arg = a.shift();
114 switch(arg) {
115 case "-h": // display help and exit
116 this.printhelp();
117 break;
118 case "-v": // display version and exit
119 case "-V":
120 case "--version":
121 this.printversion();
122 break;
123 case "-f": // file
124 this.fileNames.push(a.shift());
125 break;
126 case "-d": // debug
127 this.debugOn = true;
128 break;
129 case "-u": // pretty printing (turn off)
130 this.uglyOutput = true;
131 break;
132 case "-c": // conditional
133 this.conditionals.push(a.shift());
134 break;
135 case "-C": // column output
136 this.columnOutput = true;
137 break;
138 case "-e": // executable (transform data)
139 this.executables.push(a.shift());
140 break;
141 case "-o": // use object
142 this.useObject = a.shift();
143 break;
144 case "-a": // array
145 this.inputIsArray = true;
146 break;
147 case "-i": // use util.inspect
148 this.inspectOutput = true;
149 break;
150 case "-H": // header passthrough
151 this.headerPassthrough = true;
152 break;
153 default: // json object keys
154 if (arg.match("=")) {
155 var kk = arg.split("=");
156 this.keys.push(kk[0]);
157 this.transformedKeys.push({
158 newKey : kk[0],
159 oldKey : kk[1]
160 });
161 }
162 else {
163 this.keys.push(arg);
164 }
165 break;
166 }
167 }
168};
169
170/*
171 Create any reuested keys that don't already exist. Init values with null.
172 The default value could be an option.
173*/
174
175JSON.Command.prototype.createRequestedKeys = function(parsedObject) {
176 // instantiate any requested keys
177 for(var j = 0; (j < this.keys.length); j++) {
178 if (typeof(parsedObject[this.keys[j]]) === 'undefined') {
179 parsedObject[this.keys[j]] = null;
180 }
181 }
182};
183
184/*
185 Check conditionals against object.
186*/
187
188JSON.Command.prototype.checkConditionals = function(parsedObject) {
189 if (this.conditionals.length) {
190 try {
191 var conditionsFailed = false;
192 for(var i = 0; (i < this.conditionals.length); i++) {
193 if (!vm.runInNewContext(this.conditionals[i], parsedObject)) {
194 conditionsFailed = true;
195 }
196 }
197 // if any conditions failed return false
198 if (conditionsFailed) { return false; }
199 }
200 catch(ex) {
201 // if any conditional fails, return false,
202 // the conditional may access something not present, etc..
203 this.printex(ex);
204 return false;
205 }
206 }
207 // all conditionals passed
208 return true;
209};
210
211/*
212 Process key transforms against object.
213*/
214
215JSON.Command.prototype.processKeyTransforms = function(parsedObject) {
216 if (this.transformedKeys.length) {
217 for(var i = 0; (i < this.transformedKeys.length); i++) {
218 try {
219 vm.runInNewContext(this.transformedKeys[i].newKey +
220 " = " + this.transformedKeys[i].oldKey, parsedObject)
221 }
222 catch (ex) {
223 this.printex(ex);
224 }
225 }
226 }
227};
228
229/*
230 Process executables against object.
231*/
232
233JSON.Command.prototype.processExecutables = function(parsedObject) {
234 if (this.executables.length) {
235 for(var i = 0; (i < this.executables.length); i++) {
236 try {
237 vm.runInNewContext(this.executables[i], parsedObject);
238 }
239 catch (ex) {
240 // stop catstrophic failure if any executable fails.
241 // TODO: this may not be the desired behavior.
242 this.printex(ex);
243 }
244 }
245 }
246};
247
248/*
249 Process requested keys against parsedObject.
250 This is one of the most complicated parts of this code, and it may
251 very well not need to be. If you have a better idea of how to do this
252 please let me know: zpoley@gmail.com.
253
254 What's happening here is:
255 1. Create a new object to replace the old one since we don't want all
256 the keys from the old object.
257 2. Create each object necessary in the chain in order for the resulting
258 object to retain the same structure of the parsedObject.
259 (arrays and dictionaries require separate handling)
260 3. Assign each requested key value from the parsedObject into the new
261 object. (arrays and dictionaries require separate handling)
262
263*/
264
265JSON.Command.prototype.processKeys = function(parsedObject) {
266 if (this.keys.length) {
267 var hsh = {}, cols = [], evalStr = null, instStr = null, simpleKey = null;
268 function instantiateArrayAndPush(key) {
269 simpleKey = key.split("[").shift();
270 // instantiate array in new object if not exists
271 instStr = "if (!hsh." + simpleKey + ") { hsh." + simpleKey + " = []; }";
272 eval(instStr);
273 // push new value into array (command line order matters)
274 evalStr = "hsh." + simpleKey + ".push(" + "parsedObject." + key + ")";
275 eval(evalStr);
276 cols.push(eval("parsedObject." + key));
277 }
278 for (var i = 0; (i < this.keys.length); i++) {
279 try {
280 if ((this.keys[i].indexOf(".") > -1) || (this.keys[i].indexOf("[") > -1)) {
281 // create any keys that don't exist in the object chain
282 if (this.keys[i].indexOf(".") > -1) {
283 var s = this.keys[i].split(".");
284 for (var j = 1; (j < s.length); j++) {
285 // create necessary keys
286 evalStr = "hsh." + s.slice(0,j).join(".");
287 if (!eval(evalStr)) {
288 eval("hsh." + s.slice(0,j).join(".") + " = {};");
289 }
290 }
291 if (this.keys[i].indexOf("[") > -1) {
292 instantiateArrayAndPush(this.keys[i]);
293 }
294 else {
295 evalStr = "hsh." + s.join(".") + " = " + "parsedObject." + s.join(".");
296 eval(evalStr);
297 cols.push(eval("parsedObject." + s.join(".")));
298 }
299 }
300 else if (this.keys[i].indexOf("[") > -1) {
301 instantiateArrayAndPush(this.keys[i]);
302 }
303 }
304 else {
305 // no expansion
306 hsh[this.keys[i]] = parsedObject[this.keys[i]];
307 cols.push(parsedObject[this.keys[i]]);
308 }
309 }
310 catch(ex) {
311 this.debug("Failed to read property " + this.keys[i] + " from object: " + JSON.stringify(parsedObject));
312 ex.message = "Failed to read property";
313 throw ex;
314 }
315 }
316 return this.columnOutput ? cols : hsh;
317 }
318 else {
319 return parsedObject;
320 }
321};
322
323/*
324 Process input objects.
325*/
326
327JSON.Command.prototype.processObjects = function(objects) {
328
329 var rawObject = null, parsedObject = null;
330
331 try {
332 if (this.useObject && objects && objects.length > 0 && typeof(objects[0]) == 'string' && objects[0].trim().length > 0) {
333 objects = vm.runInNewContext(this.useObject, JSON.parse(objects[0]));
334 }
335 }
336 catch(ex) {
337 this.printex(ex);
338 }
339
340 if (this.inputIsArray && objects && objects.length > 0 && typeof(objects[0]) == 'string') {
341 var trimmed = objects[0].trim();
342 if (trimmed.length > 0 && trimmed[0] == '[') {
343 objects = JSON.parse(objects[0]);
344 }
345 }
346
347 try {
348 for (var i = 0; (i < (objects.length)); i++) {
349 // if there's no object, there's nothing to do
350 // (null object is not the same as string null)
351 if ((objects[i] == null) || (objects[i] == undefined)) { continue; }
352
353 try {
354 if (typeof(objects[i]) == "string") {
355 rawObject = objects[i];
356 if (rawObject.trim().length == 0) continue;
357 parsedObject = JSON.parse(rawObject);
358 }
359 else {
360 rawObject = JSON.stringify(objects[i]);
361 parsedObject = objects[i];
362 }
363 } catch(ex) {
364 // discard bad records
365 this.printex(ex);
366 continue;
367 }
368
369 // create any requested keys that are missing
370 this.createRequestedKeys(parsedObject);
371
372 // process key transformations on this parsedObject
373 this.processKeyTransforms(parsedObject);
374
375 // process executables on this parsedObject
376 this.processExecutables(parsedObject);
377
378 // continue if any conditionals fail on this parsedObject
379 if (!this.checkConditionals(parsedObject)) {
380 continue;
381 }
382
383 try {
384 // process requested keys on the parsed object
385 outputObject = this.processKeys(parsedObject);
386 }
387 catch(ex) { continue; }
388
389 // finally, print output
390 if (this.columnOutput) {
391 process.stdout.write(outputObject.join("\t") + "\n");
392 }
393 else {
394 process.stdout.write(this.stringify(outputObject) + "\n");
395 }
396 }
397 }
398 catch(ex) {
399 this.printex(ex);
400 }
401};
402
403/*
404 Process input.
405*/
406
407JSON.Command.prototype.processInput = function() {
408 if (this.files) {
409 // TODO: implement file support?
410 }
411 else {
412 this.stdin = process.openStdin();
413 this.stdin.setEncoding("utf8");
414 this.stdin.jsonC = this; // context for closure
415
416 this.stdin.on("data", function(chunk) {
417 this.jsonC.buffer += chunk;
418 if (this.jsonC.inputIsArray) return;
419
420 var objects = null;
421 if (this.jsonC.buffer.match(/\n/g) ||
422 this.jsonC.buffer.match(/\r\n/g) ||
423 this.jsonC.buffer.match(/\0/g) ||
424 this.jsonC.buffer.match("}{")) {
425 if (this.jsonC.buffer.match(/\n/g)) {
426 objects = this.jsonC.buffer.split("\n");
427 }
428 if (this.jsonC.buffer.match(/\r\n/g)) {
429 objects = this.jsonC.buffer.split("\r\n");
430 }
431 if (this.jsonC.buffer.match(/\0/g)) {
432 objects = this.jsonC.buffer.split("\0");
433 }
434 if (this.jsonC.buffer.match("}{")) {
435 objects = this.jsonC.buffer.split("}{").join("}\n{").split("\n");
436 }
437
438 this.jsonC.buffer = objects.pop();
439
440 if (this.jsonC.headerPassthrough) {
441 for (var i = 0, l = objects.length; i < l; i ++) {
442 process.stdout.write(objects[i]+"\r\n");
443 if (objects[i] === "") {
444 this.jsonC.headerPassthrough = false;
445 break;
446 }
447 }
448 objects.splice(0, i);
449 }
450
451 if (objects.length) this.jsonC.processObjects(objects);
452 }
453 });
454
455 var handleEPIPE = function(e) {
456 if (e.code !== "EPIPE") {
457 process.emit("error", e);
458 }
459 };
460 process.stdout.on("error", handleEPIPE);
461
462 this.stdin.on("end", function() {
463 this.jsonC.processObjects([this.jsonC.buffer, null]);
464 process.stdout.removeListener("error", handleEPIPE);
465 });
466 }
467};
468
469