diff --git a/MANIFEST b/MANIFEST
index 63fdc2053..f8207316f 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -384,6 +384,7 @@ plugins/sudoers/toke_util.c
 plugins/sudoers/tsgetgrpw.c
 plugins/sudoers/tsgetgrpw.h
 plugins/sudoers/visudo.c
+plugins/sudoers/visudo_json.c
 plugins/system_group/Makefile.in
 plugins/system_group/system_group.c
 plugins/system_group/system_group.exp
diff --git a/doc/visudo.cat b/doc/visudo.cat
index 72ab6979e..84f09609a 100644
--- a/doc/visudo.cat
+++ b/doc/visudo.cat
@@ -4,7 +4,7 @@ NNAAMMEE
      vviissuuddoo - edit the sudoers file
 
 SSYYNNOOPPSSIISS
-     vviissuuddoo [--cchhqqssVV] [--ff _s_u_d_o_e_r_s]
+     vviissuuddoo [--cchhqqssVVxx] [--ff _s_u_d_o_e_r_s]
 
 DDEESSCCRRIIPPTTIIOONN
      vviissuuddoo edits the _s_u_d_o_e_r_s file in a safe fashion, analogous to vipw(1m).
@@ -72,6 +72,13 @@ DDEESSCCRRIIPPTTIIOONN
      --VV, ----vveerrssiioonn
                  Print the vviissuuddoo and _s_u_d_o_e_r_s grammar versions and exit.
 
+     --xx, ----eexxppoorrtt
+                 Export _s_u_d_o_e_r_s in JSON format to the standard output.  The
+                 result is intended to be easier for third-party applications
+                 to parse that the normal _s_u_d_o_e_r_s format.  The various values
+                 have explicit types which removes some of the ambiguity of
+                 the _s_u_d_o_e_r_s format.
+
 EENNVVIIRROONNMMEENNTT
      The following environment variables may be consulted depending on the
      value of the _e_d_i_t_o_r and _e_n_v___e_d_i_t_o_r _s_u_d_o_e_r_s settings:
@@ -147,4 +154,4 @@ DDIISSCCLLAAIIMMEERR
      file distributed with ssuuddoo or http://www.sudo.ws/sudo/license.html for
      complete details.
 
-Sudo 1.8.8                      August 14, 2013                     Sudo 1.8.8
+Sudo 1.8.9                     November 15, 2013                    Sudo 1.8.9
diff --git a/doc/visudo.man.in b/doc/visudo.man.in
index f705cc1b3..bfdf30b3e 100644
--- a/doc/visudo.man.in
+++ b/doc/visudo.man.in
@@ -21,7 +21,7 @@
 .\" Agency (DARPA) and Air Force Research Laboratory, Air Force
 .\" Materiel Command, USAF, under agreement number F39502-99-1-0512.
 .\"
-.TH "VISUDO" "@mansectsu@" "August 14, 2013" "Sudo @PACKAGE_VERSION@" "System Manager's Manual"
+.TH "VISUDO" "@mansectsu@" "November 15, 2013" "Sudo @PACKAGE_VERSION@" "System Manager's Manual"
 .nh
 .if n .ad l
 .SH "NAME"
@@ -30,7 +30,7 @@
 .SH "SYNOPSIS"
 .HP 7n
 \fBvisudo\fR
-[\fB\-chqsV\fR]
+[\fB\-chqsVx\fR]
 [\fB\-f\fR\ \fIsudoers\fR]
 .SH "DESCRIPTION"
 \fBvisudo\fR
@@ -203,6 +203,19 @@ Print the
 and
 \fIsudoers\fR
 grammar versions and exit.
+.TP 12n
+\fB\-x\fR, \fB\--export\fR
+Export
+\fIsudoers\fR
+in JSON format to the standard output.
+The result is intended to be easier for third-party applications
+to parse that the normal
+\fIsudoers\fR
+format.
+The various values have explicit types which removes some of the
+ambiguity of the
+\fIsudoers\fR
+format.
 .SH "ENVIRONMENT"
 The following environment variables may be consulted depending on
 the value of the
diff --git a/doc/visudo.mdoc.in b/doc/visudo.mdoc.in
index a092f5624..3f44184a0 100644
--- a/doc/visudo.mdoc.in
+++ b/doc/visudo.mdoc.in
@@ -19,7 +19,7 @@
 .\" Agency (DARPA) and Air Force Research Laboratory, Air Force
 .\" Materiel Command, USAF, under agreement number F39502-99-1-0512.
 .\"
-.Dd August 14, 2013
+.Dd November 15, 2013
 .Dt VISUDO @mansectsu@
 .Os Sudo @PACKAGE_VERSION@
 .Sh NAME
@@ -27,7 +27,7 @@
 .Nd edit the sudoers file
 .Sh SYNOPSIS
 .Nm visudo
-.Op Fl chqsV
+.Op Fl chqsVx
 .Bk -words
 .Op Fl f Ar sudoers
 .Ek
@@ -197,6 +197,18 @@ Print the
 and
 .Em sudoers
 grammar versions and exit.
+.It Fl x , -export
+Export
+.Em sudoers
+in JSON format to the standard output.
+The result is intended to be easier for third-party applications
+to parse that the normal
+.Em sudoers
+format.
+The various values have explicit types which removes some of the
+ambiguity of the
+.Em sudoers
+format.
 .El
 .Sh ENVIRONMENT
 The following environment variables may be consulted depending on
diff --git a/plugins/sudoers/Makefile.in b/plugins/sudoers/Makefile.in
index e5f0a9f01..368a8c97a 100644
--- a/plugins/sudoers/Makefile.in
+++ b/plugins/sudoers/Makefile.in
@@ -138,7 +138,7 @@ SUDOERS_OBJS = $(AUTH_OBJS) boottime.lo check.lo env.lo find_path.lo \
 	       policy.lo prompt.lo set_perms.lo sudo_nss.lo sudoers.lo \
 	       timestamp.lo @SUDOERS_OBJS@
 
-VISUDO_OBJS = find_path.o goodpath.o locale.o visudo.o
+VISUDO_OBJS = find_path.o goodpath.o locale.o visudo.o visudo_json.o
 
 REPLAY_OBJS = getdate.o locale.o sudoreplay.o
 
@@ -889,3 +889,12 @@ visudo.o: $(srcdir)/visudo.c $(devdir)/def_data.h $(devdir)/gram.h \
           $(top_builddir)/pathnames.h $(top_srcdir)/compat/getopt.h \
           $(top_srcdir)/compat/stdbool.h
 	$(CC) -c $(CPPFLAGS) $(CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(DEFS) $(srcdir)/visudo.c
+visudo_json.o: $(srcdir)/visudo_json.c $(devdir)/def_data.h $(devdir)/gram.h \
+               $(incdir)/alloc.h $(incdir)/fatal.h $(incdir)/fileops.h \
+               $(incdir)/gettext.h $(incdir)/missing.h $(incdir)/queue.h \
+               $(incdir)/sudo_debug.h $(incdir)/sudo_plugin.h \
+               $(srcdir)/defaults.h $(srcdir)/logging.h $(srcdir)/parse.h \
+               $(srcdir)/sudo_nss.h $(srcdir)/sudoers.h \
+               $(top_builddir)/config.h $(top_builddir)/pathnames.h \
+               $(top_srcdir)/compat/stdbool.h
+	$(CC) -c $(CPPFLAGS) $(CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(DEFS) $(srcdir)/visudo_json.c
diff --git a/plugins/sudoers/visudo.c b/plugins/sudoers/visudo.c
index e7c3997ea..a9bd08406 100644
--- a/plugins/sudoers/visudo.c
+++ b/plugins/sudoers/visudo.c
@@ -114,6 +114,8 @@ static void help(void) __attribute__((__noreturn__));
 static void usage(int);
 static void visudo_cleanup(void);
 
+extern bool export_sudoers(char *, bool, bool);
+
 extern void sudoerserror(const char *);
 extern void sudoersrestart(FILE *);
 
@@ -134,9 +136,10 @@ struct passwd *list_pw;
 static struct sudoersfile_list sudoerslist = TAILQ_HEAD_INITIALIZER(sudoerslist);
 static struct rbtree *alias_freelist;
 static bool checkonly;
-static const char short_opts[] =  "cf:hqsV";
+static const char short_opts[] =  "cf:hqsVx";
 static struct option long_opts[] = {
     { "check",		no_argument,		NULL,	'c' },
+    { "export",		no_argument,		NULL,	'x' },
     { "file",		required_argument,	NULL,	'f' },
     { "help",		no_argument,		NULL,	'h' },
     { "quiet",		no_argument,		NULL,	'q' },
@@ -153,7 +156,7 @@ main(int argc, char *argv[])
     struct sudoersfile *sp;
     char *args, *editor, *sudoers_path;
     int ch, exitcode = 0;
-    bool quiet, strict, oldperms;
+    bool quiet, strict, oldperms, export;
     debug_decl(main, SUDO_DEBUG_MAIN)
 
 #if defined(SUDO_DEVEL) && defined(__OpenBSD__)
@@ -183,7 +186,7 @@ main(int argc, char *argv[])
     /*
      * Arg handling.
      */
-    checkonly = oldperms = quiet = strict = false;
+    checkonly = oldperms = quiet = strict = export = false;
     sudoers_path = _PATH_SUDOERS;
     while ((ch = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) {
 	switch (ch) {
@@ -209,6 +212,9 @@ main(int argc, char *argv[])
 	    case 'q':
 		quiet = true;		/* quiet mode */
 		break;
+	    case 'x':
+		export = true;		/* export mode */
+		break;
 	    default:
 		usage(1);
 	}
@@ -221,7 +227,7 @@ main(int argc, char *argv[])
     sudo_setgrent();
 
     /* Mock up a fake sudo_user struct. */
-    user_cmnd = "";
+    user_cmnd = user_base = "";
     if ((sudo_user.pw = sudo_getpwuid(getuid())) == NULL)
 	fatalx(_("you do not exist in the %s database"), "passwd");
     get_hostname();
@@ -233,6 +239,10 @@ main(int argc, char *argv[])
 	exitcode = check_syntax(sudoers_path, quiet, strict, oldperms) ? 0 : 1;
 	goto done;
     }
+    if (export) {
+	exitcode = export_sudoers(sudoers_path, quiet, strict) ? 0 : 1;
+	goto done;
+    }
 
     /*
      * Parse the existing sudoers file(s) to highlight any existing
@@ -272,7 +282,7 @@ main(int argc, char *argv[])
     }
 
 done:
-    sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, exitcode);                
+    sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, exitcode);
     exit(exitcode);
 }
 
@@ -1319,7 +1329,7 @@ static void
 usage(int fatal)
 {
     (void) fprintf(fatal ? stderr : stdout,
-	"usage: %s [-chqsV] [-f sudoers]\n", getprogname());
+	"usage: %s [-chqsVx] [-f sudoers]\n", getprogname());
     if (fatal)
 	exit(1);
 }
@@ -1335,6 +1345,7 @@ help(void)
 	"  -h, --help       display help message and exit\n"
 	"  -q, --quiet      less verbose (quiet) syntax error messages\n"
 	"  -s, --strict     strict syntax checking\n"
-	"  -V, --version    display version information and exit"));
+	"  -V, --version    display version information and exit"
+	"  -x, --export     export sudoers in JSON format"));
     exit(0);
 }
diff --git a/plugins/sudoers/visudo_json.c b/plugins/sudoers/visudo_json.c
new file mode 100644
index 000000000..13f7e2a5e
--- /dev/null
+++ b/plugins/sudoers/visudo_json.c
@@ -0,0 +1,986 @@
+/*
+ * Copyright (c) 2013 Todd C. Miller <Todd.Miller@courtesan.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <config.h>
+
+#include <sys/types.h>
+#include <stdio.h>
+#ifdef STDC_HEADERS
+# include <stdlib.h>
+# include <stddef.h>
+#else
+# ifdef HAVE_STDLIB_H
+#  include <stdlib.h>
+# endif
+#endif /* STDC_HEADERS */
+#ifdef HAVE_STRING_H
+# include <string.h>
+#endif /* HAVE_STRING_H */
+#ifdef HAVE_STRINGS_H
+# include <strings.h>
+#endif /* HAVE_STRINGS_H */
+#ifdef HAVE_UNISTD_H
+#include <unistd.h>
+#endif /* HAVE_UNISTD_H */
+#include <stdarg.h>
+#include <ctype.h>
+
+#include "sudoers.h"
+#include "parse.h"
+#include "gettext.h"
+#include <gram.h>
+
+/*
+ * External globals exported by the parser
+ */
+extern FILE *sudoersin;
+extern char *sudoers, *errorfile;
+extern int errorlineno;
+extern bool parse_error;
+
+/*
+ * JSON values may be of the following types.
+ */
+enum json_value_type {
+    JSON_STRING,
+    JSON_NUMBER,
+    JSON_OBJECT,
+    JSON_ARRAY,
+    JSON_BOOL,
+    JSON_NULL
+};
+
+/*
+ * JSON value suitable for printing.
+ * Note: this does not support object or array values.
+ */
+struct json_value {
+    enum json_value_type type;
+    union {
+	char *string;
+	int number;
+	bool boolean;
+    } u;
+};
+
+/*
+ * Closure used to store state when iterating over all aliases.
+ */
+struct json_alias_closure {
+    const char *title;
+    unsigned int count;
+    int alias_type;
+    int indent;
+    bool need_comma;
+};
+
+/*
+ * Type values used to disambiguate the generic WORD and ALIAS types.
+ */
+enum word_type {
+    TYPE_COMMAND,
+    TYPE_HOSTNAME,
+    TYPE_RUNASGROUP,
+    TYPE_RUNASUSER,
+    TYPE_USERNAME
+};
+
+/*
+ * SUDO_DIGEST_* number to name mapping.
+ * XXX - should go somewhere else.
+ */
+static const char *digest_names[] = {
+    "sha224",
+    "sha256",
+    "sha384",
+    "sha512",
+    "invalid digest"
+};
+
+/*
+ * Print "indent" number of blank characters.
+ */
+static void
+print_indent(int indent)
+{
+    while (indent--)
+	putchar(' ');
+}
+
+/*
+ * Print a JSON string, escaping special characters.
+ * Does not support unicode escapes.
+ */
+static void
+print_string_json_unquoted(const char *str)
+{
+    char ch;
+
+    while ((ch = *str++) != '\0') {
+	switch (ch) {
+	case '"':
+	case '\\':
+	case '/':
+	    putchar('\\');
+	    break;
+	case '\b':
+	    ch = 'b';
+	    putchar('\\');
+	    break;
+	case '\f':
+	    ch = 'f';
+	    putchar('\\');
+	    break;
+	case '\n':
+	    ch = 'n';
+	    putchar('\\');
+	    break;
+	case '\r':
+	    ch = 'r';
+	    putchar('\\');
+	    break;
+	case '\t':
+	    ch = 't';
+	    putchar('\\');
+	    break;
+	}
+	putchar(ch);
+    }
+}
+
+/*
+ * Print a quoted JSON string, escaping special characters.
+ * Does not support unicode escapes.
+ */
+static void
+print_string_json(const char *str)
+{
+    putchar('\"');
+    print_string_json_unquoted(str);
+    putchar('\"');
+}
+
+/*
+ * Print a JSON name: value pair with proper quoting and escaping.
+ */
+static void
+print_pair_json(const char *pre, const char *name,
+    const struct json_value *value, const char *post, int indent)
+{
+    debug_decl(print_pair_json, SUDO_DEBUG_UTIL)
+
+    print_indent(indent);
+
+    /* prefix */
+    if (pre != NULL)
+	fputs(pre, stdout);
+
+    /* name */
+    print_string_json(name);
+    putchar(':');
+    putchar(' ');
+
+    /* value */
+    switch (value->type) {
+    case JSON_STRING:
+	print_string_json(value->u.string);
+	break;
+    case JSON_NUMBER:
+	printf("%d", value->u.number);
+	break;
+    case JSON_NULL:
+	fputs("null", stdout);
+	break;
+    case JSON_BOOL:
+	fputs(value->u.boolean ? "true" : "false", stdout);
+	break;
+    case JSON_OBJECT:
+	fatalx("internal error: can't print JSON_OBJECT");
+	break;
+    case JSON_ARRAY:
+	fatalx("internal error: can't print JSON_ARRAY");
+	break;
+    }
+
+    /* postfix */
+    if (post != NULL)
+	fputs(post, stdout);
+
+    debug_return;
+}
+
+/*
+ * Print a JSON string with optional prefix and postfix to stdout.
+ * Strings are not quoted but are escaped as per the JSON spec.
+ */
+static void
+printstr_json(const char *pre, const char *str, const char *post, int indent)
+{
+    debug_decl(printstr_json, SUDO_DEBUG_UTIL)
+
+    print_indent(indent);
+    if (pre != NULL)
+	fputs(pre, stdout);
+    if (str != NULL) {
+	print_string_json_unquoted(str);
+    }
+    if (post != NULL)
+	fputs(post, stdout);
+    debug_return;
+}
+
+/*
+ * Print struct sudo_command in JSON format, with specified indentation.
+ * If last_one is false, a comma will be printed before the newline
+ * that closes the object.
+ */
+static void
+print_command_json(struct sudo_command *c, int indent, bool last_one)
+{
+    struct json_value value;
+    const char *digest_type;
+    debug_decl(print_command_json, SUDO_DEBUG_UTIL)
+
+    printstr_json("{", NULL, NULL, indent);
+    if (c->digest != NULL) {
+	putchar('\n');
+	indent += 4;
+	if (c->digest->digest_type < SUDO_DIGEST_INVALID) {
+	    digest_type = digest_names[c->digest->digest_type];
+	} else {
+	    digest_type = digest_names[SUDO_DIGEST_INVALID];
+	}
+	value.type = JSON_STRING;
+	value.u.string = c->digest->digest_str;
+	print_pair_json(NULL, digest_type, &value, ",\n", indent);
+    } else {
+	putchar(' ');
+	indent = 0;
+    }
+    if (c->args != NULL) {
+	printstr_json("\"", "command", "\": ", indent);
+	printstr_json("\"", c->cmnd, " ", 0);
+	printstr_json(NULL, c->args, "\"", 0);
+    } else {
+	value.type = JSON_STRING;
+	value.u.string = c->cmnd;
+	print_pair_json(NULL, "command", &value, NULL, indent);
+    }
+    if (c->digest != NULL) {
+	indent -= 4;
+	putchar('\n');
+	print_indent(indent);
+    } else {
+	putchar(' ');
+    }
+    putchar('}');
+    if (!last_one)
+	putchar(',');
+    putchar('\n');
+
+    debug_return;
+}
+
+/*
+ * Map an alias type to enum word_type.
+ */
+static enum word_type
+alias_to_word_type(int alias_type)
+{
+    switch (alias_type) {
+    case CMNDALIAS:
+	return TYPE_COMMAND;
+    case HOSTALIAS:
+	return TYPE_HOSTNAME;
+    case RUNASALIAS:
+	return TYPE_RUNASUSER;
+    case USERALIAS:
+	return TYPE_USERNAME;
+    default:
+	fatalx_nodebug("unexpected alias type %d", alias_type);
+    }
+}
+
+/*
+ * Map a Defaults type to enum word_type.
+ */
+static enum word_type
+defaults_to_word_type(int defaults_type)
+{
+    switch (defaults_type) {
+    case DEFAULTS_CMND:
+	return TYPE_COMMAND;
+    case DEFAULTS_HOST:
+	return TYPE_HOSTNAME;
+    case DEFAULTS_RUNAS:
+	return TYPE_RUNASUSER;
+    case DEFAULTS_USER:
+	return TYPE_USERNAME;
+    default:
+	fatalx_nodebug("unexpected defaults type %d", defaults_type);
+    }
+}
+
+/*
+ * Print struct member in JSON format, with specified indentation.
+ * If last_one is false, a comma will be printed before the newline
+ * that closes the object.
+ */
+static void
+print_member_json(struct member *m, enum word_type word_type, bool last_one,
+     int indent)
+{
+    struct json_value value;
+    const char *typestr;
+    debug_decl(print_member_json, SUDO_DEBUG_UTIL)
+
+    /* Most of the time we print a string. */
+    value.type = JSON_STRING;
+    value.u.string = m->name;
+
+    switch (m->type) {
+    case USERGROUP:
+	value.u.string++; /* skip leading '%' */
+	if (*value.u.string == '#') {
+	    value.type = JSON_NUMBER;
+	    value.u.number = atoi(m->name + 2); /* XXX - use atoid? */
+	    typestr = "usergid";
+	} else {
+	    typestr = "usergroup";
+	}
+	break;
+    case NETGROUP:
+	typestr = "netgroup";
+	value.u.string++; /* skip leading '+' */
+	break;
+    case NTWKADDR:
+	typestr = "networkaddr";
+	break;
+    case COMMAND:
+	print_command_json((struct sudo_command *)m->name, indent, last_one);
+	debug_return;
+    case WORD:
+	switch (word_type) {
+	case TYPE_HOSTNAME:
+	    typestr = "hostname";
+	    break;
+	case TYPE_RUNASGROUP:
+	    typestr = "runasgroup";
+	    break;
+	case TYPE_RUNASUSER:
+	    typestr = "runasuser";
+	    break;
+	case TYPE_USERNAME:
+	    if (*value.u.string == '#') {
+		value.type = JSON_NUMBER;
+		value.u.number = atoi(m->name + 1); /* XXX - use atoid? */
+		typestr = "userid";
+	    } else {
+		typestr = "username";
+	    }
+	    break;
+	default:
+	    fatalx("unexpected word type %d", word_type);
+	}
+	break;
+    case ALL:
+	value.u.string = "ALL";
+	/* FALLTHROUGH */
+    case ALIAS:
+	switch (word_type) {
+	case TYPE_COMMAND:
+	    typestr = "cmndalias";
+	    break;
+	case TYPE_HOSTNAME:
+	    typestr = "hostalias";
+	    break;
+	case TYPE_RUNASGROUP:
+	case TYPE_RUNASUSER:
+	    typestr = "runasalias";
+	    break;
+	case TYPE_USERNAME:
+	    typestr = "useralias";
+	    break;
+	default:
+	    fatalx("unexpected word type %d", word_type);
+	}
+	break;
+    default:
+	fatalx("unexpected member type %d", m->type);
+    }
+    print_pair_json("{ ", typestr, &value, " }", indent);
+    if (!last_one)
+	putchar(',');
+    putchar('\n');
+
+    debug_return;
+}
+
+/*
+ * Callback for alias_apply() to print an alias entry if it matches
+ * the type specified in the closure.
+ */
+int
+print_alias_json(void *v1, void *v2)
+{
+    struct alias *a = v1;
+    struct json_alias_closure *closure = v2;
+    struct member *m;
+    debug_decl(print_alias_json, SUDO_DEBUG_UTIL)
+
+    if (a->type != closure->alias_type)
+	debug_return_int(0);
+
+    /* Open the aliases object or close the last entry, then open new one. */
+    if (closure->count++ == 0) {
+	printf("%s\n%*s\"%s\": {\n", closure->need_comma ? "," : "",
+	    closure->indent, "", closure->title);
+	closure->indent += 4;
+    } else {
+	printf("%*s],\n", closure->indent, "");
+    }
+    printstr_json(NULL, a->name, ": [\n", closure->indent);
+
+    closure->indent += 4;
+    TAILQ_FOREACH(m, &a->members, entries) {
+	print_member_json(m, alias_to_word_type(closure->alias_type),
+	    TAILQ_NEXT(m, entries) == NULL, closure->indent);
+    }
+    closure->indent -= 4;
+    debug_return_int(0);
+}
+
+/*
+ * Print the binding for a Defaults entry of the specified type.
+ */
+static void
+print_binding_json(struct member_list *binding, int type, int indent)
+{
+    struct member *m;
+    debug_decl(print_binding_json, SUDO_DEBUG_UTIL)
+
+    if (TAILQ_EMPTY(binding))
+	debug_return;
+
+    printf("%*s\"Binding\": [\n", indent, "");
+    indent += 4;
+
+    /* Print each member object in binding. */
+    TAILQ_FOREACH(m, binding, entries) {
+	print_member_json(m, defaults_to_word_type(type),
+	     TAILQ_NEXT(m, entries) == NULL, indent);
+    }
+
+    indent -= 4;
+    printf("%*s],\n", indent, "");
+
+    debug_return;
+}
+
+/*
+ * Print a Defaults list JSON format.
+ */
+static void
+print_defaults_list_json(struct defaults *def, int indent)
+{
+    char savech, *start, *end = def->val;
+    debug_decl(print_defaults_list_json, SUDO_DEBUG_UTIL)
+
+    printstr_json("\"", def->var, "\": {\n", indent);
+    indent += 4;
+    switch (def->op) {
+    case '+':
+	printstr_json("\"", "list_add", "\": [\n", indent);
+	break;
+    case '-':
+	printstr_json("\"", "list_remove", "\": [\n", indent);
+	break;
+    case true:
+	printstr_json("\"", "list_assign", "\": [\n", indent);
+	break;
+    default:
+	warningx("internal error: unexpected list op %d", def->op);
+	printstr_json("\"", "unsupported", "\": [\n", indent);
+	break;
+    }
+    indent += 4;
+    print_indent(indent);
+    /* Split value into multiple space-separated words. */
+    do {
+	/* Remove leading blanks, must have a non-empty string. */
+	for (start = end; isblank((unsigned char)*start); start++)
+	    ;
+	if (*start == '\0')
+	    break;
+
+	/* Find the end and print it. */
+	for (end = start; *end && !isblank((unsigned char)*end); end++)
+	    ;
+	savech = *end;
+	*end = '\0';
+	print_string_json(start);
+	if (savech != '\0')
+	    putchar(',');
+	*end = savech;
+    } while (*end++ != '\0');
+    putchar('\n');
+    indent -= 4;
+    printf("%*s]\n", indent, "");
+    indent -= 4;
+    printf("%*s}", indent, "");
+
+    debug_return;
+}
+
+/*
+ * Export all Defaults in JSON format.
+ */
+static bool
+print_defaults_json(int indent, bool need_comma)
+{
+    struct sudo_defs_types *cur;
+    struct json_value value;
+    struct defaults *def, *next;
+    int num;
+    debug_decl(print_defaults_json, SUDO_DEBUG_UTIL)
+
+    if (TAILQ_EMPTY(&defaults))
+	debug_return_bool(need_comma);
+
+    printf("%s\n%*s\"Defaults\": [\n", need_comma ? "," : "", indent, "");
+
+    TAILQ_FOREACH_SAFE(def, &defaults, entries, next) {
+	/* Look up def in table to find its type. */
+	for (cur = sudo_defs_table, num = 0; cur->name; cur++, num++) {
+	    if (strcmp(def->var, cur->name) == 0)
+		break;
+	}
+	if (!cur->name) {
+	    warningx(_("unknown defaults entry `%s'"), def->var);
+	    /* XXX - just pass it through as a string anyway? */
+	    continue;
+	}
+
+	/* Found it, print object container and binding (if any). */
+	indent = 8;
+	printf("%*s{\n", indent, "");
+	indent = 12;
+	print_binding_json(def->binding, def->type, indent);
+
+	/* Validation checks. */
+	/* XXX - validate values in addition to names? */
+
+	/* Print options, merging ones with the same binding. */
+	printf("%*s\"Options\": [\n", indent, "");
+	indent += 4;
+	for (;;) {
+	    next = TAILQ_NEXT(def, entries);
+	    if ((cur->type & T_MASK) == T_FLAG || def->val == NULL) {
+		value.type = JSON_BOOL;
+		value.u.boolean = def->op;
+		print_pair_json("{ ", def->var, &value, " }", indent);
+	    } else if ((cur->type & T_MASK) == T_LIST) {
+		print_defaults_list_json(def, indent);
+	    } else {
+		value.type = JSON_STRING;
+		value.u.string = def->val;
+		print_pair_json("{ ", def->var, &value, " }", indent);
+	    }
+	    if (next == NULL || def->binding != next->binding)
+		break;
+	    def = next;
+	    fputs(",\n", stdout);
+	}
+	putchar('\n');
+	indent -= 4;
+	print_indent(indent);
+	fputs("]\n", stdout);
+	indent -= 4;
+	print_indent(indent);
+	printf("}%s\n", next != NULL ? "," : "");
+    }
+
+    /* Close Defaults array; comma (if any) & newline will be printer later. */
+    indent -= 4;
+    print_indent(indent);
+    fputs("]", stdout);
+
+    debug_return_bool(true);
+}
+
+/*
+ * Export all aliases of the specified type in JSON format.
+ * Iterates through the entire aliases tree.
+ */
+static bool
+print_aliases_by_type_json(int alias_type, const char *title, int indent,
+    bool need_comma)
+{
+    struct json_alias_closure closure;
+    debug_decl(print_aliases_by_type_json, SUDO_DEBUG_UTIL)
+
+    closure.indent = indent;
+    closure.count = 0;
+    closure.alias_type = alias_type;
+    closure.title = title;
+    closure.need_comma = need_comma;
+    alias_apply(print_alias_json, &closure);
+    if (closure.count != 0) {
+	print_indent(closure.indent);
+	fputs("]\n", stdout);
+	closure.indent -= 4;
+	print_indent(closure.indent);
+	putchar('}');
+	need_comma = true;
+    }
+
+    debug_return_bool(need_comma);
+}
+
+/*
+ * Export all aliases in JSON format.
+ */
+static bool
+print_aliases_json(int indent, bool need_comma)
+{
+    debug_decl(print_aliases_json, SUDO_DEBUG_UTIL)
+
+    need_comma = print_aliases_by_type_json(USERALIAS, "User_Aliases",
+	indent, need_comma);
+    need_comma = print_aliases_by_type_json(RUNASALIAS, "Runas_Aliases",
+	indent, need_comma);
+    need_comma = print_aliases_by_type_json(HOSTALIAS, "Host_Aliases",
+	indent, need_comma);
+    need_comma = print_aliases_by_type_json(CMNDALIAS, "Command_Aliases",
+	indent, need_comma);
+
+    debug_return_bool(need_comma);
+}
+
+/* XXX these are all duplicated w/ parse.c */
+#define RUNAS_CHANGED(cs1, cs2) \
+	(cs1 == NULL || cs2 == NULL || \
+	 cs1->runasuserlist != cs2->runasuserlist || \
+	 cs1->runasgrouplist != cs2->runasgrouplist)
+
+#define TAG_SET(tt) \
+	((tt) != UNSPEC && (tt) != IMPLIED)
+
+#define TAGS_CHANGED(ot, nt) \
+	((TAG_SET((nt).setenv) && (nt).setenv != (ot).setenv) || \
+	 (TAG_SET((nt).noexec) && (nt).noexec != (ot).noexec) || \
+	 (TAG_SET((nt).nopasswd) && (nt).nopasswd != (ot).nopasswd) || \
+	 (TAG_SET((nt).log_input) && (nt).log_input != (ot).log_input) || \
+	 (TAG_SET((nt).log_output) && (nt).log_output != (ot).log_output))
+
+/*
+ * Print a Cmnd_Spec in JSON format at the specified indent level.
+ * A pointer to the next Cmnd_Spec is passed in to make it possible to
+ * merge adjacent entries that are identical in all but the command.
+ */
+static void
+print_cmndspec_json(struct cmndspec *cs, struct cmndspec **nextp, int indent)
+{
+    struct cmndspec *next = *nextp;
+    struct json_value value;
+    struct member *m;
+    bool last_one;
+    debug_decl(print_cmndspec_json, SUDO_DEBUG_UTIL)
+
+    /* Open Cmnd_Spec object. */
+    printf("%*s{\n", indent, "");
+    indent += 4;
+
+    /* Print runasuserlist */
+    if (cs->runasuserlist != NULL) {
+	printf("%*s\"runasusers\": [\n", indent, "");
+	indent += 4;
+	TAILQ_FOREACH(m, cs->runasuserlist, entries) {
+	    print_member_json(m, TYPE_RUNASUSER,
+		TAILQ_NEXT(m, entries) == NULL, indent);
+	}
+	indent -= 4;
+	printf("%*s],\n", indent, "");
+    }
+
+    /* Print runasgrouplist */
+    if (cs->runasgrouplist != NULL) {
+	printf("%*s\"runasgroup\": [\n", indent, "");
+	indent += 4;
+	TAILQ_FOREACH(m, cs->runasgrouplist, entries) {
+	    print_member_json(m, TYPE_RUNASGROUP,
+		TAILQ_NEXT(m, entries) == NULL, indent);
+	}
+	indent -= 4;
+	printf("%*s],\n", indent, "");
+    }
+
+    /* Print tags */
+    if (cs->tags.nopasswd != UNSPEC || cs->tags.noexec != UNSPEC ||
+	cs->tags.setenv != UNSPEC || cs->tags.log_input != UNSPEC ||
+	cs->tags.log_output != UNSPEC) {
+	printf("%*s\"Options\": [\n", indent, "");
+	indent += 4;
+	if (cs->tags.nopasswd != UNSPEC) {
+	    value.type = JSON_BOOL;
+	    value.u.boolean = !cs->tags.nopasswd;
+	    last_one = cs->tags.noexec == UNSPEC &&
+		cs->tags.setenv == UNSPEC && cs->tags.log_input == UNSPEC &&
+		cs->tags.log_output == UNSPEC;
+	    print_pair_json(NULL, "authenticate", &value,
+		last_one ? "\n" : ",\n", indent);
+	}
+	if (cs->tags.noexec != UNSPEC) {
+	    value.type = JSON_BOOL;
+	    value.u.boolean = cs->tags.noexec;
+	    last_one = cs->tags.setenv == UNSPEC &&
+		cs->tags.log_input == UNSPEC && cs->tags.log_output == UNSPEC;
+	    print_pair_json(NULL, "noexec", &value,
+		last_one ? "\n" : ",\n", indent);
+	}
+	if (cs->tags.setenv != UNSPEC) {
+	    value.type = JSON_BOOL;
+	    value.u.boolean = cs->tags.setenv;
+	    last_one = cs->tags.log_input == UNSPEC &&
+		cs->tags.log_output == UNSPEC;
+	    print_pair_json(NULL, "setenv", &value,
+		last_one ? "\n" : ",\n", indent);
+	}
+	if (cs->tags.log_input != UNSPEC) {
+	    value.type = JSON_BOOL;
+	    value.u.boolean = cs->tags.log_input;
+	    last_one = cs->tags.log_output == UNSPEC;
+	    print_pair_json(NULL, "log_input", &value,
+		last_one ? "\n" : ",\n", indent);
+	}
+	if (cs->tags.log_output != UNSPEC) {
+	    value.type = JSON_BOOL;
+	    value.u.boolean = cs->tags.log_output;
+	    print_pair_json(NULL, "log_output", &value, "\n", indent);
+	}
+	indent -= 4;
+	printf("%*s],\n", indent, "");
+    }
+
+#ifdef HAVE_SELINUX
+    /* Print SELinux role/type */
+    if (cs->role != NULL && cs->type != NULL) {
+	printf("%*s\"SELinux_Spec\": [\n", indent, "");
+	indent += 4;
+	value.type = JSON_STRING;
+	value.u.string = cs->role;
+	print_pair_json(NULL, "role", &value, ",\n", indent);
+	value.u.string = cs->type;
+	print_pair_json(NULL, "type", &value, "\n", indent);
+	indent -= 4;
+	printf("%*s],\n", indent, "");
+    }
+#endif /* HAVE_SELINUX */
+
+#ifdef HAVE_PRIV_SET
+    /* Print Solaris privs/limitprivs */
+    if (cs->privs != NULL || cs->limitprivs != NULL) {
+	printf("%*s\"Solaris_Priv_Spec\": [\n", indent, "");
+	indent += 4;
+	value.type = JSON_STRING;
+	if (cs->privs != NULL) {
+	    value.u.string = cs->privs;
+	    print_pair_json(NULL, "privs", &value,
+		cs->limitprivs != NULL ? ",\n" : "\n", indent);
+	}
+	if (cs->limitprivs != NULL) {
+	    value.u.string = cs->limitprivs;
+	    print_pair_json(NULL, "limitprivs", &value, "\n", indent);
+	}
+	indent -= 4;
+	printf("%*s],\n", indent, "");
+    }
+#endif /* HAVE_PRIV_SET */
+
+    /*
+     * Merge adjacent commands with matching tags, runas, SELinux
+     * role/type and Solaris priv settings.
+     */
+    printf("%*s\"Commands\": [\n", indent, "");
+    indent += 4;
+    for (;;) {
+	/* Does the next entry differ only in the command itself? */
+	/* XXX - move into a function that returns bool */
+	last_one = next == NULL ||
+	    RUNAS_CHANGED(cs, next) || TAGS_CHANGED(cs->tags, next->tags)
+#ifdef HAVE_PRIV_SET
+	    || cs->privs != next->privs || cs->limitprivs != next->limitprivs
+#endif /* HAVE_PRIV_SET */
+#ifdef HAVE_SELINUX
+	    || cs->role != next->role || cs->type != next->type
+#endif /* HAVE_SELINUX */
+	    ;
+
+	print_member_json(cs->cmnd, TYPE_COMMAND, last_one, indent);
+	if (last_one)
+	    break;
+	cs = next;
+	next = TAILQ_NEXT(cs, entries);
+    }
+    indent -= 4;
+    printf("%*s]\n", indent, "");
+
+    /* Close Cmnd_Spec object. */
+    indent -= 4;
+    printf("%*s}%s\n", indent, "", TAILQ_NEXT(cs, entries) != NULL ? "," : "");
+
+    *nextp = next;
+
+    debug_return;
+}
+
+/*
+ * Print a User_Spec in JSON format at the specified indent level.
+ */
+static void
+print_userspec_json(struct userspec *us, int indent)
+{
+    struct privilege *priv;
+    struct member *m;
+    struct cmndspec *cs, *next;
+    debug_decl(print_userspec_json, SUDO_DEBUG_UTIL)
+
+    /*
+     * Each userspec struct may contain multiple privileges for
+     * a user.  We export each privilege as a separate User_Spec
+     * object for simplicity's sake.
+     */
+    TAILQ_FOREACH(priv, &us->privileges, entries) {
+	/* Open User_Spec object. */
+	printf("%*s{\n", indent, "");
+	indent += 4;
+
+	/* Print users list. */
+	printf("%*s\"User_List\": [\n", indent, "");
+	indent += 4;
+	TAILQ_FOREACH(m, &us->users, entries) {
+	    print_member_json(m, TYPE_USERNAME,
+		TAILQ_NEXT(m, entries) == NULL, indent);
+	}
+	indent -= 4;
+	printf("%*s]\n", indent, "");
+
+	/* Print hosts list. */
+	printf("%*s\"Host_List\": [\n", indent, "");
+	indent += 4;
+	TAILQ_FOREACH(m, &priv->hostlist, entries) {
+	    print_member_json(m, TYPE_HOSTNAME,
+		TAILQ_NEXT(m, entries) == NULL, indent);
+	}
+	indent -= 4;
+	printf("%*s]\n", indent, "");
+
+	/* Print commands. */
+	printf("%*s\"Cmnd_Specs\": [\n", indent, "");
+	indent += 4;
+	TAILQ_FOREACH_SAFE(cs, &priv->cmndlist, entries, next) {
+	    print_cmndspec_json(cs, &next, indent);
+	}
+	indent -= 4;
+	printf("%*s]\n", indent, "");
+
+	/* Close User_Spec object. */
+	indent -= 4;
+	printf("%*s}%s\n", indent, "", TAILQ_NEXT(priv, entries) != NULL ||
+	    TAILQ_NEXT(us, entries) != NULL ? "," : "");
+    }
+
+    debug_return;
+}
+
+static bool
+print_userspecs_json(int indent, bool need_comma)
+{
+    struct userspec *us;
+    debug_decl(print_userspecs_json, SUDO_DEBUG_UTIL)
+
+    if (TAILQ_EMPTY(&userspecs))
+	debug_return_bool(need_comma);
+
+    printf("%s\n%*s\"User_Specs\": [\n", need_comma ? "," : "", indent, "");
+    indent += 4;
+    TAILQ_FOREACH(us, &userspecs, entries) {
+	print_userspec_json(us, indent);
+    }
+    indent -= 4;
+    printf("%*s]", indent, "");
+
+    debug_return_bool(true);
+}
+
+/*
+ * Export the parsed sudoers file in JSON format.
+ * XXX - ignores strict flag and doesn't pass through quiet flag
+ * XXX - pass indent=4 to other json functions
+ */
+bool
+export_sudoers(char *sudoers_path, bool quiet, bool strict)
+{
+    bool ok = false, need_comma = false;
+    const int indent = 4;
+    debug_decl(export_sudoers, SUDO_DEBUG_UTIL)
+
+    if (strcmp(sudoers_path, "-") == 0) {
+	sudoersin = stdin;
+	sudoers_path = "stdin";
+    } else if ((sudoersin = fopen(sudoers_path, "r")) == NULL) {
+	if (!quiet)
+	    warning(_("unable to open %s"), sudoers_path);
+	goto done;
+    }
+    init_parser(sudoers_path, quiet);
+    if (sudoersparse() && !parse_error) {
+	if (!quiet)
+	    warningx(_("failed to parse %s file, unknown error"), sudoers_path);
+	parse_error = true;
+	errorfile = sudoers_path;
+    }
+    ok = !parse_error;
+
+    if (parse_error) {
+	if (!quiet) {
+	    if (errorlineno != -1)
+		printf(_("parse error in %s near line %d\n"),
+		    errorfile, errorlineno);
+	    else if (errorfile != NULL)
+		printf(_("parse error in %s\n"), errorfile);
+	}
+	goto done;
+    }
+
+    /* Open JSON output. */
+    putchar('{');
+
+    /* Dump Defaults in JSON format. */
+    need_comma = print_defaults_json(indent, need_comma);
+
+    /* Dump Aliases in JSON format. */
+    need_comma = print_aliases_json(indent, need_comma);
+
+    /* Dump User_Specs in JSON format. */
+    print_userspecs_json(indent, need_comma);
+
+    /* Close JSON output. */
+    puts("\n}");
+
+done:
+    debug_return_bool(ok);
+}
