plistutil: Add a --nodepath option to allow selecting a specific node
diff --git a/docs/plistutil.1 b/docs/plistutil.1
index b1f7773..f322bab 100644
--- a/docs/plistutil.1
+++ b/docs/plistutil.1
@@ -29,6 +29,52 @@
 .B \-p, \-\-print FILE
 Print PList in human-readable format.
 .TP
+.B \-n, \-\-nodepath PATH
+Restrict output to nodepath defined by
+.I PATH
+which selects a specific node within the plist document.
+A node path describes traversal through dictionaries and arrays using / to
+separate the components.
+Traversal begins at the root node and proceeds from left to right.
+Each component selects a child of the current node.
+If the current node is a dictionary, the component is interpreted as a
+dictionary key name.
+If the current node is an array, the component must be a non-negative decimal
+integer specifying the array index (0-based).
+
+Given the following structure:
+.PP
+.RS
+.nf
+<plist version="1.0">
+<dict>
+  <key>Users</key>
+  <array>
+    <dict>
+      <key>Name</key>
+      <string>Alice</string>
+    </dict>
+    <dict>
+      <key>Name</key>
+      <string>Bob</string>
+    </dict>
+  </array>
+</dict>
+</plist>
+.fi
+
+A nodepath of:
+.TP
+\f[B]Users\f[] resolves to the entire array.
+.TP
+\f[B]Users/0\f[] resolves to the first dictionary in the array.
+.TP
+\f[B]Users/0/Name\f[] resolves to the string value "Alice".
+.TP
+\f[B]Users/1/Name\f[] resolves to the string value "Bob".
+.RE
+
+.TP
 .B \-c, \-\-compact
 JSON and OpenStep only: Print output in compact form. By default, the output
 will be pretty-printed.
diff --git a/tools/plistutil.c b/tools/plistutil.c
index 84e7add..6e697cf 100644
--- a/tools/plistutil.c
+++ b/tools/plistutil.c
@@ -40,11 +40,12 @@
 #ifdef _MSC_VER
 #pragma warning(disable:4996)
 #define STDIN_FILENO _fileno(stdin)
+#define strtok_r strtok_s
 #endif
 
 typedef struct _options
 {
-    char *in_file, *out_file;
+    char *in_file, *out_file, *nodepath;
     uint8_t in_fmt, out_fmt; // fmts 0 = undef, 1 = bin, 2 = xml, 3 = json, 4 = openstep
     uint8_t flags;
 } options_t;
@@ -70,6 +71,7 @@
     printf("                       If omitted, XML will be converted to binary,\n");
     printf("                       and binary to XML.\n");
     printf("  -p, --print FILE     Print the PList in human-readable format.\n");
+    printf("  -n, --nodepath PATH  Restrict output to nodepath defined by PATH.\n");
     printf("  -c, --compact        JSON and OpenStep only: Print output in compact form.\n");
     printf("                       By default, the output will be pretty-printed.\n");
     printf("  -s, --sort           Sort all dictionary nodes lexicographically by key\n");
@@ -96,6 +98,7 @@
         { "compact",  no_argument,       0, 'c' },
         { "sort",     no_argument,       0, 's' },
         { "print",    required_argument, 0, 'p' },
+        { "nodepath", required_argument, 0, 'n' },
         { "debug",    no_argument,       0, 'd' },
         { "help",     no_argument,       0, 'h' },
         { "version",  no_argument,       0, 'v' },
@@ -103,7 +106,7 @@
     };
 
     int c;
-    while ((c = getopt_long(argc, argv, "i:o:f:csp:dhv", long_options, NULL)) != -1)
+    while ((c = getopt_long(argc, argv, "i:o:f:csp:n:dhv", long_options, NULL)) != -1)
     {
         switch (c)
         {
@@ -175,6 +178,15 @@
                 break;
             }
 
+            case 'n':
+                if (!optarg || optarg[0] == '\0') {
+                    fprintf(stderr, "ERROR: --extract needs a node path\n");
+                    free(options);
+                    return NULL;
+                }
+                options->nodepath = optarg;
+                break;
+
             case 'd':
                 options->flags |= OPT_DEBUG;
                 break;
@@ -326,6 +338,60 @@
     {
         input_res = plist_from_memory(plist_entire, read_size, &root_node, NULL);
         if (input_res == PLIST_ERR_SUCCESS) {
+
+            if (options->nodepath) {
+                char *copy = strdup(options->nodepath);
+                char *tok, *saveptr = NULL;
+                if (!copy) {
+                    plist_free(root_node);
+                    free(plist_entire);
+                    free(options);
+                    return 1;
+                }
+
+                plist_t current = root_node;
+                for (tok = strtok_r(copy, "/", &saveptr); tok; tok = strtok_r(NULL, "/", &saveptr)) {
+                    if (*tok == '\0') continue;
+                    switch (plist_get_node_type(current)) {
+                        case PLIST_DICT:
+                            current = plist_dict_get_item(current, tok);
+                            break;
+                        case PLIST_ARRAY: {
+                            char* endp = NULL;
+                            uint32_t idx = strtoul(tok, &endp, 10);
+                            if (endp == tok || *endp != '\0') {
+                                current = NULL;
+                                break;
+                            }
+                            if (idx >= plist_array_get_size(current)) {
+                                current = NULL;
+                                break;
+                            }
+                            current = plist_array_get_item(current, idx);
+                            break;
+                        }
+                        default:
+                            current = NULL;
+                            break;
+                    }
+                    if (!current) {
+                        break;
+                    }
+                }
+                free(copy);
+                if (current) {
+                    plist_t destnode = plist_copy(current);
+                    plist_free(root_node);
+                    root_node = destnode;
+                } else {
+                    fprintf(stderr, "ERROR: nodepath '%s' is invalid\n", options->nodepath);
+                    plist_free(root_node);
+                    free(plist_entire);
+                    free(options);
+                    return 1;
+                }
+            }
+
             if (options->flags & OPT_SORT) {
                 plist_sort(root_node);
             }