xref: /freebsd/contrib/libxo/xo/xo.c (revision a0409676120c1e558d0ade943019934e0f15118d)
1 /*
2  * Copyright (c) 2014-2019, Juniper Networks, Inc.
3  * All rights reserved.
4  * This SOFTWARE is licensed under the LICENSE provided in the
5  * ../Copyright file. By downloading, installing, copying, or otherwise
6  * using the SOFTWARE, you agree to be bound by the terms of that
7  * LICENSE.
8  * Phil Shafer, July 2014
9  */
10 
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <stdarg.h>
14 #include <string.h>
15 
16 #include "xo_config.h"
17 #include "xo.h"
18 #include "xo_explicit.h"
19 
20 #include <getopt.h>		/* Include after xo.h for testing */
21 
22 #ifndef UNUSED
23 #define UNUSED __attribute__ ((__unused__))
24 #endif /* UNUSED */
25 
26 static int opt_warn;		/* Enable warnings */
27 
28 static char **save_argv;
29 static char **checkpoint_argv;
30 
31 static char *
32 next_arg (void)
33 {
34     char *cp = *save_argv;
35 
36     if (cp == NULL)
37 	xo_errx(1, "missing argument");
38 
39     save_argv += 1;
40     return cp;
41 }
42 
43 static void
44 prep_arg (char *fmt)
45 {
46     char *cp, *fp;
47 
48     for (cp = fp = fmt; *cp; cp++, fp++) {
49 	if (*cp != '\\') {
50 	    if (cp != fp)
51 		*fp = *cp;
52 	    continue;
53 	}
54 
55 	switch (*++cp) {
56 	case 'n':
57 	    *fp = '\n';
58 	    break;
59 
60 	case 'r':
61 	    *fp = '\r';
62 	    break;
63 
64 	case 'b':
65 	    *fp = '\b';
66 	    break;
67 
68 	case 'e':
69 	    *fp = '\e';
70 	    break;
71 
72 	default:
73 	    *fp = *cp;
74 	}
75     }
76 
77     *fp = '\0';
78 }
79 
80 static void
81 checkpoint (xo_handle_t *xop UNUSED, va_list vap UNUSED, int restore)
82 {
83     if (restore)
84 	save_argv = checkpoint_argv;
85     else
86 	checkpoint_argv = save_argv;
87 }
88 
89 /*
90  * Our custom formatter is responsible for combining format string pieces
91  * with our command line arguments to build strings.  This involves faking
92  * some printf-style logic.
93  */
94 static xo_ssize_t
95 formatter (xo_handle_t *xop, char *buf, xo_ssize_t bufsiz,
96 	   const char *fmt, va_list vap UNUSED)
97 {
98     int lflag UNUSED = 0;	/* Parse long flag, though currently ignored */
99     int hflag = 0, jflag = 0, tflag = 0,
100 	zflag = 0, qflag = 0, star1 = 0, star2 = 0;
101     int rc = 0;
102     int w1 = 0, w2 = 0;
103     const char *cp;
104 
105     for (cp = fmt + 1; *cp; cp++) {
106 	if (*cp == 'l')
107 	    lflag += 1;
108 	else if (*cp == 'h')
109 	    hflag += 1;
110 	else if (*cp == 'j')
111 	    jflag += 1;
112 	else if (*cp == 't')
113 	    tflag += 1;
114 	else if (*cp == 'z')
115 	    zflag += 1;
116 	else if (*cp == 'q')
117 	    qflag += 1;
118 	else if (*cp == '*') {
119 	    if (star1 == 0)
120 		star1 = 1;
121 	    else
122 		star2 = 1;
123 	} else if (strchr("diouxXDOUeEfFgGaAcCsSp", *cp) != NULL)
124 	    break;
125 	else if (*cp == 'n' || *cp == 'v') {
126 	    if (opt_warn)
127 		xo_error_h(xop, "unsupported format: '%s'", fmt);
128 	    return -1;
129 	}
130     }
131 
132     char fc = *cp;
133 
134     /* Handle "%*.*s" */
135     if (star1)
136 	w1 = strtol(next_arg(), NULL, 0);
137     if (star2 > 1)
138 	w2 = strtol(next_arg(), NULL, 0);
139 
140     if (fc == 'D' || fc == 'O' || fc == 'U')
141 	lflag = 1;
142 
143     if (strchr("diD", fc) != NULL) {
144 	long long value = strtoll(next_arg(), NULL, 0);
145 	if (star1 && star2)
146 	    rc = snprintf(buf, bufsiz, fmt, w1, w2, value);
147 	else if (star1)
148 	    rc = snprintf(buf, bufsiz, fmt, w1, value);
149 	else
150 	    rc = snprintf(buf, bufsiz, fmt, value);
151 
152     } else if (strchr("ouxXOUp", fc) != NULL) {
153 	unsigned long long value = strtoull(next_arg(), NULL, 0);
154 	if (star1 && star2)
155 	    rc = snprintf(buf, bufsiz, fmt, w1, w2, value);
156 	else if (star1)
157 	    rc = snprintf(buf, bufsiz, fmt, w1, value);
158 	else
159 	    rc = snprintf(buf, bufsiz, fmt, value);
160 
161     } else if (strchr("eEfFgGaA", fc) != NULL) {
162 	double value = strtold(next_arg(), NULL);
163 	if (star1 && star2)
164 	    rc = snprintf(buf, bufsiz, fmt, w1, w2, value);
165 	else if (star1)
166 	    rc = snprintf(buf, bufsiz, fmt, w1, value);
167 	else
168 	    rc = snprintf(buf, bufsiz, fmt, value);
169 
170     } else if (fc == 'C' || fc == 'c' || fc == 'S' || fc == 's') {
171 	char *value = next_arg();
172 	if (star1 && star2)
173 	    rc = snprintf(buf, bufsiz, fmt, w1, w2, value);
174 	else if (star1)
175 	    rc = snprintf(buf, bufsiz, fmt, w1, value);
176 	else
177 	    rc = snprintf(buf, bufsiz, fmt, value);
178     }
179 
180     return rc;
181 }
182 
183 static void
184 print_version (void)
185 {
186     fprintf(stderr, "libxo version %s%s\n",
187 	    xo_version, xo_version_extra);
188     fprintf(stderr, "xo version %s%s\n",
189 	    LIBXO_VERSION, LIBXO_VERSION_EXTRA);
190 }
191 
192 static void
193 print_help (void)
194 {
195     fprintf(stderr,
196 "Usage: xo [options] format [fields]\n"
197 "    --close <path>        Close tags for the given path\n"
198 "    --close-instance <name> Close an open instance name\n"
199 "    --close-list <name>   Close an open list name\n"
200 "    --continuation OR -C  Output belongs on same line as previous output\n"
201 "    --depth <num>         Set the depth for pretty printing\n"
202 "    --help                Display this help text\n"
203 "    --html OR -H          Generate HTML output\n"
204 "    --instance OR -I <name> Wrap in an instance of the given name\n"
205 "    --json OR -J          Generate JSON output\n"
206 "    --leading-xpath <path> OR -l <path> "
207 	    "Add a prefix to generated XPaths (HTML)\n"
208 "    --not-first           Indicate this object is not the first (JSON)\n"
209 "    --open <path>         Open tags for the given path\n"
210 "    --open-instance <name> Open an instance given by name\n"
211 "    --open-list <name>   Open a list given by name\n"
212 "    --option <opts> -or -O <opts>  Give formatting options\n"
213 "    --pretty OR -p        Make 'pretty' output (add indent, newlines)\n"
214 "    --style <style> OR -s <style>  "
215 	    "Generate given style (xml, json, text, html)\n"
216 "    --text OR -T          Generate text output (the default style)\n"
217 "    --top-wrap            Generate a top-level object wrapper (JSON)\n"
218 "    --version             Display version information\n"
219 "    --warn OR -W          Display warnings in text on stderr\n"
220 "    --warn-xml            Display warnings in xml on stdout\n"
221 "    --wrap <path>         Wrap output in a set of containers\n"
222 "    --xml OR -X           Generate XML output\n"
223 "    --xpath               Add XPath data to HTML output\n");
224 }
225 
226 static struct opts {
227     int o_close_instance;
228     int o_close_list;
229     int o_depth;
230     int o_help;
231     int o_not_first;
232     int o_open_instance;
233     int o_open_list;
234     int o_top_wrap;
235     int o_version;
236     int o_warn_xml;
237     int o_wrap;
238     int o_xpath;
239 } opts;
240 
241 static struct option long_opts[] = {
242     { "close", required_argument, NULL, 'c' },
243     { "close-instance", required_argument, &opts.o_close_instance, 1 },
244     { "close-list", required_argument, &opts.o_close_list, 1 },
245     { "continuation", no_argument, NULL, 'C' },
246     { "depth", required_argument, &opts.o_depth, 1 },
247     { "help", no_argument, &opts.o_help, 1 },
248     { "html", no_argument, NULL, 'H' },
249     { "instance", required_argument, NULL, 'I' },
250     { "json", no_argument, NULL, 'J' },
251     { "leading-xpath", required_argument, NULL, 'l' },
252     { "not-first", no_argument, &opts.o_not_first, 1 },
253     { "open", required_argument, NULL, 'o' },
254     { "open-instance", required_argument, &opts.o_open_instance, 1 },
255     { "open-list", required_argument, &opts.o_open_list, 1 },
256     { "option", required_argument, NULL, 'O' },
257     { "pretty", no_argument, NULL, 'p' },
258     { "style", required_argument, NULL, 's' },
259     { "text", no_argument, NULL, 'T' },
260     { "top-wrap", no_argument, &opts.o_top_wrap, 1 },
261     { "xml", no_argument, NULL, 'X' },
262     { "xpath", no_argument, &opts.o_xpath, 1 },
263     { "version", no_argument, &opts.o_version, 1 },
264     { "warn", no_argument, NULL, 'W' },
265     { "warn-xml", no_argument, &opts.o_warn_xml, 1 },
266     { "wrap", required_argument, &opts.o_wrap, 1 },
267     { NULL, 0, NULL, 0 }
268 };
269 
270 int
271 main (int argc UNUSED, char **argv)
272 {
273     char *fmt = NULL, *cp, *np;
274     char *opt_opener = NULL, *opt_closer = NULL, *opt_wrapper = NULL;
275     char *opt_options = NULL;
276     char *opt_instance = NULL;
277     char *opt_name = NULL;
278     xo_state_t new_state = 0;
279     int opt_depth = 0;
280     int opt_not_first = 0;
281     int opt_top_wrap = 0;
282     int rc;
283 
284     argc = xo_parse_args(argc, argv);
285     if (argc < 0)
286 	return 1;
287 
288     while ((rc = getopt_long(argc, argv, "Cc:HJl:O:o:ps:TXW",
289 				long_opts, NULL)) != -1) {
290 	switch (rc) {
291 	case 'C':
292 	    xo_set_flags(NULL, XOF_CONTINUATION);
293 	    break;
294 
295 	case 'c':
296 	    opt_closer = optarg;
297 	    xo_set_flags(NULL, XOF_IGNORE_CLOSE);
298 	    break;
299 
300 	case 'H':
301 	    xo_set_style(NULL, XO_STYLE_HTML);
302 	    break;
303 
304 	case 'I':
305 	    opt_instance = optarg;
306 	    break;
307 
308 	case 'J':
309 	    xo_set_style(NULL, XO_STYLE_JSON);
310 	    break;
311 
312 	case 'l':
313 	    xo_set_leading_xpath(NULL, optarg);
314 	    break;
315 
316 	case 'O':
317 	    opt_options = optarg;
318 	    break;
319 
320 	case 'o':
321 	    opt_opener = optarg;
322 	    break;
323 
324 	case 'p':
325 	    xo_set_flags(NULL, XOF_PRETTY);
326 	    break;
327 
328 	case 's':
329 	    if (xo_set_style_name(NULL, optarg) < 0)
330 		xo_errx(1, "unknown style: %s", optarg);
331 	    break;
332 
333 	case 'T':
334 	    xo_set_style(NULL, XO_STYLE_TEXT);
335 	    break;
336 
337 	case 'X':
338 	    xo_set_style(NULL, XO_STYLE_XML);
339 	    break;
340 
341 	case 'W':
342 	    opt_warn = 1;
343 	    xo_set_flags(NULL, XOF_WARN);
344 	    break;
345 
346 	case ':':
347 	    xo_errx(1, "missing argument");
348 	    break;
349 
350 	case 0:
351 	    if (opts.o_depth) {
352 		opt_depth = atoi(optarg);
353 
354 	    } else if (opts.o_help) {
355 		print_help();
356 		return 1;
357 
358 	    } else if (opts.o_not_first) {
359 		opt_not_first = 1;
360 
361 	    } else if (opts.o_xpath) {
362 		xo_set_flags(NULL, XOF_XPATH);
363 
364 	    } else if (opts.o_version) {
365 		print_version();
366 		return 0;
367 
368 	    } else if (opts.o_warn_xml) {
369 		opt_warn = 1;
370 		xo_set_flags(NULL, XOF_WARN | XOF_WARN_XML);
371 
372 	    } else if (opts.o_wrap) {
373 		opt_wrapper = optarg;
374 
375 	    } else if (opts.o_top_wrap) {
376 		opt_top_wrap = 1;
377 
378 	    } else if (opts.o_open_list) {
379 		if (opt_name)
380 		    xo_errx(1, "only one open/close list/instance allowed: %s",
381 			    optarg);
382 
383 		opt_name = optarg;
384 		new_state = XSS_OPEN_LIST;
385 
386 	    } else if (opts.o_open_instance) {
387 		if (opt_name)
388 		    xo_errx(1, "only one open/close list/instance allowed: %s",
389 			    optarg);
390 
391 		opt_name = optarg;
392 		new_state = XSS_OPEN_INSTANCE;
393 
394 	    } else if (opts.o_close_list) {
395 		if (opt_name)
396 		    xo_errx(1, "only one open/close list/instance allowed: %s",
397 			    optarg);
398 
399 		opt_name = optarg;
400 		new_state = XSS_CLOSE_LIST;
401 
402 	    } else if (opts.o_close_instance) {
403 		if (opt_name)
404 		    xo_errx(1, "only one open/close list/instance allowed: %s",
405 			    optarg);
406 
407 		opt_name = optarg;
408 		new_state = XSS_CLOSE_INSTANCE;
409 
410 	    } else {
411 		print_help();
412 		return 1;
413 	    }
414 
415 	    bzero(&opts, sizeof(opts)); /* Reset all the options */
416 	    break;
417 
418 	default:
419 	    print_help();
420 	    return 1;
421 	}
422     }
423 
424     argc -= optind;
425     argv += optind;
426 
427     if (opt_options) {
428 	rc = xo_set_options(NULL, opt_options);
429 	if (rc < 0)
430 	    xo_errx(1, "invalid options: %s", opt_options);
431     }
432 
433     xo_set_formatter(NULL, formatter, checkpoint);
434     xo_set_flags(NULL, XOF_NO_VA_ARG | XOF_NO_TOP | XOF_NO_CLOSE);
435 
436     /*
437      * If we have some explicit state change, handle it
438      */
439     if (new_state) {
440 	if (opt_depth > 0)
441 	    xo_set_depth(NULL, opt_depth);
442 
443 	if (opt_not_first)
444 	    xo_set_flags(NULL, XOF_NOT_FIRST);
445 
446 	xo_explicit_transition(NULL, new_state, opt_name, 0);
447 	xo_finish();
448 	exit(0);
449     }
450 
451     fmt = *argv++;
452     if (opt_opener == NULL && opt_closer == NULL && fmt == NULL) {
453 	print_help();
454 	return 1;
455     }
456 
457     if (opt_top_wrap) {
458 	/* If we have a closing path, we'll be one extra level deeper */
459 	if (opt_closer && xo_get_style(NULL) == XO_STYLE_JSON)
460 	    opt_depth += 1;
461 	else
462 	    xo_clear_flags(NULL, XOF_NO_TOP);
463     }
464 
465     if (opt_closer) {
466 	opt_depth += 1;
467 	for (cp = opt_closer; cp && *cp; cp = np) {
468 	    np = strchr(cp, '/');
469 	    if (np == NULL)
470 		break;
471 	    np += 1;
472 	    opt_depth += 1;
473 	}
474     }
475 
476     if (opt_depth > 0)
477 	xo_set_depth(NULL, opt_depth);
478 
479     if (opt_not_first)
480 	xo_set_flags(NULL, XOF_NOT_FIRST);
481 
482     /* If there's an opening hierarchy, open each element as a container */
483     if (opt_opener) {
484 	for (cp = opt_opener; cp && *cp; cp = np) {
485 	    np = strchr(cp, '/');
486 	    if (np)
487 		*np = '\0';
488 	    xo_open_container(cp);
489 	    if (np)
490 		np += 1;
491 	}
492     }
493 
494     /* If there's an wrapper hierarchy, open each element as a container */
495     if (opt_wrapper) {
496 	for (cp = opt_wrapper; cp && *cp; cp = np) {
497 	    np = strchr(cp, '/');
498 	    if (np)
499 		*np = '\0';
500 	    xo_open_container(cp);
501 	    if (np)
502 		*np++ = '/';	/* Put it back */
503 	}
504     }
505 
506     if (opt_instance)
507 	xo_open_instance(opt_instance);
508 
509     /* If there's a format string, call xo_emit to emit the contents */
510     if (fmt && *fmt) {
511 	save_argv = argv;
512 	prep_arg(fmt);
513 	xo_emit(fmt);		/* This call does the real formatting */
514     }
515 
516     if (opt_instance)
517 	xo_close_instance(opt_instance);
518 
519     /* If there's an wrapper hierarchy, close each element's container */
520     while (opt_wrapper) {
521 	np = strrchr(opt_wrapper, '/');
522 	xo_close_container(np ? np + 1 : opt_wrapper);
523 	if (np)
524 	    *np = '\0';
525 	else
526 	    opt_wrapper = NULL;
527     }
528 
529     /* Remember to undo the depth before calling xo_finish() */
530     opt_depth = (opt_closer && opt_top_wrap) ? -1 : 0;
531 
532     /* If there's an closing hierarchy, close each element's container */
533     while (opt_closer) {
534 	np = strrchr(opt_closer, '/');
535 	xo_close_container(np ? np + 1 : opt_closer);
536 	if (np)
537 	    *np = '\0';
538 	else
539 	    opt_closer = NULL;
540     }
541 
542     /* If there's a closer and a wrapper, we need to clean it up */
543     if (opt_depth) {
544 	xo_set_depth(NULL, opt_depth);
545 	xo_clear_flags(NULL, XOF_NO_TOP);
546     }
547 
548     /* If we're wrapping the entire content, skip the closer */
549     if (opt_top_wrap && opt_opener)
550 	xo_set_flags(NULL, XOF_NO_TOP);
551 
552     xo_finish();
553 
554     return 0;
555 }
556