New API: guestfs_inspect_get_product_variant
[libguestfs.git] / inspector / virt-inspector.c
1 /* virt-inspector
2  * Copyright (C) 2010-2011 Red Hat Inc.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17  */
18
19 #include <config.h>
20
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <inttypes.h>
24 #include <unistd.h>
25 #include <getopt.h>
26 #include <locale.h>
27 #include <assert.h>
28
29 #include <libxml/xmlIO.h>
30 #include <libxml/xmlwriter.h>
31
32 #include "progname.h"
33 #include "c-ctype.h"
34
35 #include "guestfs.h"
36 #include "options.h"
37
38 /* Currently open libguestfs handle. */
39 guestfs_h *g;
40
41 int read_only = 1;
42 int live = 0;
43 int verbose = 0;
44 int keys_from_stdin = 0;
45 int echo_keys = 0;
46 const char *libvirt_uri = NULL;
47 int inspector = 1;
48
49 static void output (char **roots);
50 static void output_roots (xmlTextWriterPtr xo, char **roots);
51 static void output_root (xmlTextWriterPtr xo, char *root);
52 static void output_mountpoints (xmlTextWriterPtr xo, char *root);
53 static void output_filesystems (xmlTextWriterPtr xo, char *root);
54 static void output_applications (xmlTextWriterPtr xo, char *root);
55 static void canonicalize (char *dev);
56 static void free_strings (char **argv);
57 static int count_strings (char *const*argv);
58
59 static inline char *
60 bad_cast (char const *s)
61 {
62   return (char *) s;
63 }
64
65 static void __attribute__((noreturn))
66 usage (int status)
67 {
68   if (status != EXIT_SUCCESS)
69     fprintf (stderr, _("Try `%s --help' for more information.\n"),
70              program_name);
71   else {
72     fprintf (stdout,
73            _("%s: display information about a virtual machine\n"
74              "Copyright (C) 2010 Red Hat Inc.\n"
75              "Usage:\n"
76              "  %s [--options] -d domname file [file ...]\n"
77              "  %s [--options] -a disk.img [-a disk.img ...] file [file ...]\n"
78              "Options:\n"
79              "  -a|--add image       Add image\n"
80              "  -c|--connect uri     Specify libvirt URI for -d option\n"
81              "  -d|--domain guest    Add disks from libvirt guest\n"
82              "  --echo-keys          Don't turn off echo for passphrases\n"
83              "  --format[=raw|..]    Force disk format for -a option\n"
84              "  --help               Display brief help\n"
85              "  --keys-from-stdin    Read passphrases from stdin\n"
86              "  -v|--verbose         Verbose messages\n"
87              "  -V|--version         Display version and exit\n"
88              "  -x                   Trace libguestfs API calls\n"
89              "For more information, see the manpage %s(1).\n"),
90              program_name, program_name, program_name,
91              program_name);
92   }
93   exit (status);
94 }
95
96 int
97 main (int argc, char *argv[])
98 {
99   /* Set global program name that is not polluted with libtool artifacts.  */
100   set_program_name (argv[0]);
101
102   setlocale (LC_ALL, "");
103   bindtextdomain (PACKAGE, LOCALEBASEDIR);
104   textdomain (PACKAGE);
105
106   enum { HELP_OPTION = CHAR_MAX + 1 };
107
108   static const char *options = "a:c:d:vVx";
109   static const struct option long_options[] = {
110     { "add", 1, 0, 'a' },
111     { "connect", 1, 0, 'c' },
112     { "domain", 1, 0, 'd' },
113     { "echo-keys", 0, 0, 0 },
114     { "format", 2, 0, 0 },
115     { "help", 0, 0, HELP_OPTION },
116     { "keys-from-stdin", 0, 0, 0 },
117     { "verbose", 0, 0, 'v' },
118     { "version", 0, 0, 'V' },
119     { 0, 0, 0, 0 }
120   };
121   struct drv *drvs = NULL;
122   struct drv *drv;
123   const char *format = NULL;
124   int c;
125   int option_index;
126
127   g = guestfs_create ();
128   if (g == NULL) {
129     fprintf (stderr, _("guestfs_create: failed to create handle\n"));
130     exit (EXIT_FAILURE);
131   }
132
133   argv[0] = bad_cast (program_name);
134
135   for (;;) {
136     c = getopt_long (argc, argv, options, long_options, &option_index);
137     if (c == -1) break;
138
139     switch (c) {
140     case 0:                     /* options which are long only */
141       if (STREQ (long_options[option_index].name, "keys-from-stdin")) {
142         keys_from_stdin = 1;
143       } else if (STREQ (long_options[option_index].name, "echo-keys")) {
144         echo_keys = 1;
145       } else if (STREQ (long_options[option_index].name, "format")) {
146         if (!optarg || STREQ (optarg, ""))
147           format = NULL;
148         else
149           format = optarg;
150       } else {
151         fprintf (stderr, _("%s: unknown long option: %s (%d)\n"),
152                  program_name, long_options[option_index].name, option_index);
153         exit (EXIT_FAILURE);
154       }
155       break;
156
157     case 'a':
158       OPTION_a;
159       break;
160
161     case 'c':
162       OPTION_c;
163       break;
164
165     case 'd':
166       OPTION_d;
167       break;
168
169     case 'h':
170       usage (EXIT_SUCCESS);
171
172     case 'v':
173       OPTION_v;
174       break;
175
176     case 'V':
177       OPTION_V;
178       break;
179
180     case 'x':
181       OPTION_x;
182       break;
183
184     case HELP_OPTION:
185       usage (EXIT_SUCCESS);
186
187     default:
188       usage (EXIT_FAILURE);
189     }
190   }
191
192   /* Old-style syntax?  There were no -a or -d options in the old
193    * virt-inspector which is how we detect this.
194    */
195   if (drvs == NULL) {
196     while (optind < argc) {
197       if (strchr (argv[optind], '/') ||
198           access (argv[optind], F_OK) == 0) { /* simulate -a option */
199         drv = malloc (sizeof (struct drv));
200         if (!drv) {
201           perror ("malloc");
202           exit (EXIT_FAILURE);
203         }
204         drv->type = drv_a;
205         drv->a.filename = argv[optind];
206         drv->a.format = NULL;
207         drv->next = drvs;
208         drvs = drv;
209       } else {                  /* simulate -d option */
210         drv = malloc (sizeof (struct drv));
211         if (!drv) {
212           perror ("malloc");
213           exit (EXIT_FAILURE);
214         }
215         drv->type = drv_d;
216         drv->d.guest = argv[optind];
217         drv->next = drvs;
218         drvs = drv;
219       }
220
221       optind++;
222     }
223   }
224
225   /* These are really constants, but they have to be variables for the
226    * options parsing code.  Assert here that they have known-good
227    * values.
228    */
229   assert (read_only == 1);
230   assert (inspector == 1);
231   assert (live == 0);
232
233   /* Must be no extra arguments on the command line. */
234   if (optind != argc)
235     usage (EXIT_FAILURE);
236
237   /* User must have specified some drives. */
238   if (drvs == NULL)
239     usage (EXIT_FAILURE);
240
241   /* Add drives, inspect and mount.  Note that inspector is always true,
242    * and there is no -m option.
243    */
244   add_drives (drvs, 'a');
245
246   if (guestfs_launch (g) == -1)
247     exit (EXIT_FAILURE);
248
249   /* Free up data structures, no longer needed after this point. */
250   free_drives (drvs);
251
252   /* NB. Can't call inspect_mount () here (ie. normal processing of
253    * the -i option) because it can only handle a single root.  So we
254    * use low-level APIs.
255    */
256   inspect_do_decrypt ();
257
258   char **roots = guestfs_inspect_os (g);
259   if (roots == NULL) {
260     fprintf (stderr, _("%s: no operating system could be detected inside this disk image.\n\nThis may be because the file is not a disk image, or is not a virtual machine\nimage, or because the OS type is not understood by libguestfs.\n\nNOTE for Red Hat Enterprise Linux 6 users: for Windows guest support you must\ninstall the separate libguestfs-winsupport package.\n\nIf you feel this is an error, please file a bug report including as much\ninformation about the disk image as possible.\n"),
261              program_name);
262     exit (EXIT_FAILURE);
263   }
264
265   output (roots);
266
267   free_strings (roots);
268
269   guestfs_close (g);
270
271   exit (EXIT_SUCCESS);
272 }
273
274 #define DISABLE_GUESTFS_ERRORS_FOR(stmt) do {                           \
275     guestfs_error_handler_cb old_error_cb;                              \
276     void *old_error_data;                                               \
277     old_error_cb = guestfs_get_error_handler (g, &old_error_data);      \
278     guestfs_set_error_handler (g, NULL, NULL);                          \
279     stmt;                                                               \
280     guestfs_set_error_handler (g, old_error_cb, old_error_data);        \
281   } while (0)
282
283 #define XMLERROR(code,e) do {                                           \
284     if ((e) == (code)) {                                                \
285       fprintf (stderr, _("%s: XML write error at \"%s\": %m\n"),        \
286                #e, program_name);                                       \
287       exit (EXIT_FAILURE);                                              \
288     }                                                                   \
289   } while (0)
290
291 static void
292 output (char **roots)
293 {
294   xmlOutputBufferPtr ob = xmlOutputBufferCreateFd (1, NULL);
295   if (ob == NULL) {
296     fprintf (stderr,
297              _("%s: xmlOutputBufferCreateFd: failed to open stdout\n"),
298              program_name);
299     exit (EXIT_FAILURE);
300   }
301
302   xmlTextWriterPtr xo = xmlNewTextWriter (ob);
303   if (xo == NULL) {
304     fprintf (stderr,
305              _("%s: xmlNewTextWriter: failed to create libxml2 writer\n"),
306              program_name);
307     exit (EXIT_FAILURE);
308   }
309
310   /* Pretty-print the output. */
311   XMLERROR (-1, xmlTextWriterSetIndent (xo, 1));
312   XMLERROR (-1, xmlTextWriterSetIndentString (xo, BAD_CAST "  "));
313
314   XMLERROR (-1, xmlTextWriterStartDocument (xo, NULL, NULL, NULL));
315   output_roots (xo, roots);
316   XMLERROR (-1, xmlTextWriterEndDocument (xo));
317
318   /* 'ob' is freed by this too. */
319   xmlFreeTextWriter (xo);
320 }
321
322 static void
323 output_roots (xmlTextWriterPtr xo, char **roots)
324 {
325   size_t i;
326
327   XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "operatingsystems"));
328   for (i = 0; roots[i] != NULL; ++i)
329     output_root (xo, roots[i]);
330   XMLERROR (-1, xmlTextWriterEndElement (xo));
331 }
332
333 static void
334 output_root (xmlTextWriterPtr xo, char *root)
335 {
336   char *str;
337   int i, r;
338   char buf[32];
339   char canonical_root[strlen (root) + 1];
340
341   XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "operatingsystem"));
342
343   strcpy (canonical_root, root);
344   canonicalize (canonical_root);
345   XMLERROR (-1,
346     xmlTextWriterWriteElement (xo, BAD_CAST "root", BAD_CAST canonical_root));
347
348   str = guestfs_inspect_get_type (g, root);
349   if (!str) exit (EXIT_FAILURE);
350   if (STRNEQ (str, "unknown"))
351     XMLERROR (-1,
352       xmlTextWriterWriteElement (xo, BAD_CAST "name", BAD_CAST str));
353   free (str);
354
355   str = guestfs_inspect_get_arch (g, root);
356   if (!str) exit (EXIT_FAILURE);
357   if (STRNEQ (str, "unknown"))
358     XMLERROR (-1,
359       xmlTextWriterWriteElement (xo, BAD_CAST "arch", BAD_CAST str));
360   free (str);
361
362   str = guestfs_inspect_get_distro (g, root);
363   if (!str) exit (EXIT_FAILURE);
364   if (STRNEQ (str, "unknown"))
365     XMLERROR (-1,
366       xmlTextWriterWriteElement (xo, BAD_CAST "distro", BAD_CAST str));
367   free (str);
368
369   str = guestfs_inspect_get_product_name (g, root);
370   if (!str) exit (EXIT_FAILURE);
371   if (STRNEQ (str, "unknown"))
372     XMLERROR (-1,
373       xmlTextWriterWriteElement (xo, BAD_CAST "product_name", BAD_CAST str));
374   free (str);
375
376   str = guestfs_inspect_get_product_variant (g, root);
377   if (!str) exit (EXIT_FAILURE);
378   if (STRNEQ (str, "unknown"))
379     XMLERROR (-1,
380       xmlTextWriterWriteElement (xo, BAD_CAST "product_variant", BAD_CAST str));
381   free (str);
382
383   i = guestfs_inspect_get_major_version (g, root);
384   snprintf (buf, sizeof buf, "%d", i);
385   XMLERROR (-1,
386     xmlTextWriterWriteElement (xo, BAD_CAST "major_version", BAD_CAST buf));
387   i = guestfs_inspect_get_minor_version (g, root);
388   snprintf (buf, sizeof buf, "%d", i);
389   XMLERROR (-1,
390     xmlTextWriterWriteElement (xo, BAD_CAST "minor_version", BAD_CAST buf));
391
392   str = guestfs_inspect_get_package_format (g, root);
393   if (!str) exit (EXIT_FAILURE);
394   if (STRNEQ (str, "unknown"))
395     XMLERROR (-1,
396       xmlTextWriterWriteElement (xo, BAD_CAST "package_format", BAD_CAST str));
397   free (str);
398
399   str = guestfs_inspect_get_package_management (g, root);
400   if (!str) exit (EXIT_FAILURE);
401   if (STRNEQ (str, "unknown"))
402     XMLERROR (-1,
403       xmlTextWriterWriteElement (xo, BAD_CAST "package_management",
404                                  BAD_CAST str));
405   free (str);
406
407   /* inspect-get-windows-systemroot will fail with non-windows guests,
408    * or if the systemroot could not be determined for a windows guest.
409    * Disable error output around this call.
410    */
411   DISABLE_GUESTFS_ERRORS_FOR (
412     str = guestfs_inspect_get_windows_systemroot (g, root);
413     if (str)
414       XMLERROR (-1,
415                 xmlTextWriterWriteElement (xo, BAD_CAST "windows_systemroot",
416                                            BAD_CAST str));
417     free (str);
418   );
419
420   str = guestfs_inspect_get_format (g, root);
421   if (!str) exit (EXIT_FAILURE);
422   if (STRNEQ (str, "unknown"))
423     XMLERROR (-1,
424       xmlTextWriterWriteElement (xo, BAD_CAST "format",
425                                  BAD_CAST str));
426   free (str);
427
428   r = guestfs_inspect_is_live (g, root);
429   if (r > 0) {
430     XMLERROR (-1,
431               xmlTextWriterStartElement (xo, BAD_CAST "live"));
432     XMLERROR (-1, xmlTextWriterEndElement (xo));
433   }
434
435   r = guestfs_inspect_is_netinst (g, root);
436   if (r > 0) {
437     XMLERROR (-1,
438               xmlTextWriterStartElement (xo, BAD_CAST "netinst"));
439     XMLERROR (-1, xmlTextWriterEndElement (xo));
440   }
441
442   r = guestfs_inspect_is_multipart (g, root);
443   if (r > 0) {
444     XMLERROR (-1,
445               xmlTextWriterStartElement (xo, BAD_CAST "multipart"));
446     XMLERROR (-1, xmlTextWriterEndElement (xo));
447   }
448
449   output_mountpoints (xo, root);
450
451   output_filesystems (xo, root);
452
453   output_applications (xo, root);
454
455   XMLERROR (-1, xmlTextWriterEndElement (xo));
456 }
457
458 static int
459 compare_keys (const void *p1, const void *p2)
460 {
461   const char *key1 = * (char * const *) p1;
462   const char *key2 = * (char * const *) p2;
463
464   return strcmp (key1, key2);
465 }
466
467 static int
468 compare_keys_len (const void *p1, const void *p2)
469 {
470   const char *key1 = * (char * const *) p1;
471   const char *key2 = * (char * const *) p2;
472   int c;
473
474   c = strlen (key1) - strlen (key2);
475   if (c != 0)
476     return c;
477
478   return compare_keys (p1, p2);
479 }
480
481 static void
482 output_mountpoints (xmlTextWriterPtr xo, char *root)
483 {
484   char **mountpoints;
485   size_t i;
486
487   mountpoints = guestfs_inspect_get_mountpoints (g, root);
488   if (mountpoints == NULL)
489     exit (EXIT_FAILURE);
490
491   /* Sort by key length, shortest key first, and then name, so the
492    * output is stable.
493    */
494   qsort (mountpoints, count_strings (mountpoints) / 2, 2 * sizeof (char *),
495          compare_keys_len);
496
497   XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "mountpoints"));
498
499   for (i = 0; mountpoints[i] != NULL; i += 2) {
500     canonicalize (mountpoints[i+1]);
501
502     XMLERROR (-1,
503               xmlTextWriterStartElement (xo, BAD_CAST "mountpoint"));
504     XMLERROR (-1,
505               xmlTextWriterWriteAttribute (xo, BAD_CAST "dev",
506                                            BAD_CAST mountpoints[i+1]));
507     XMLERROR (-1,
508               xmlTextWriterWriteString (xo, BAD_CAST mountpoints[i]));
509     XMLERROR (-1, xmlTextWriterEndElement (xo));
510   }
511
512   XMLERROR (-1, xmlTextWriterEndElement (xo));
513
514   free_strings (mountpoints);
515 }
516
517 static void
518 output_filesystems (xmlTextWriterPtr xo, char *root)
519 {
520   char **filesystems;
521   char *str;
522   size_t i;
523
524   filesystems = guestfs_inspect_get_filesystems (g, root);
525   if (filesystems == NULL)
526     exit (EXIT_FAILURE);
527
528   /* Sort by name so the output is stable. */
529   qsort (filesystems, count_strings (filesystems), sizeof (char *),
530          compare_keys);
531
532   XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "filesystems"));
533
534   for (i = 0; filesystems[i] != NULL; ++i) {
535     canonicalize (filesystems[i]);
536
537     XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "filesystem"));
538     XMLERROR (-1,
539               xmlTextWriterWriteAttribute (xo, BAD_CAST "dev",
540                                            BAD_CAST filesystems[i]));
541
542     DISABLE_GUESTFS_ERRORS_FOR (
543       str = guestfs_vfs_type (g, filesystems[i]);
544       if (str && str[0])
545         XMLERROR (-1,
546                   xmlTextWriterWriteElement (xo, BAD_CAST "type",
547                                              BAD_CAST str));
548       free (str);
549     );
550
551     DISABLE_GUESTFS_ERRORS_FOR (
552       str = guestfs_vfs_label (g, filesystems[i]);
553       if (str && str[0])
554         XMLERROR (-1,
555                   xmlTextWriterWriteElement (xo, BAD_CAST "label",
556                                              BAD_CAST str));
557       free (str);
558     );
559
560     DISABLE_GUESTFS_ERRORS_FOR (
561       str = guestfs_vfs_uuid (g, filesystems[i]);
562       if (str && str[0])
563         XMLERROR (-1,
564                   xmlTextWriterWriteElement (xo, BAD_CAST "uuid",
565                                              BAD_CAST str));
566       free (str);
567     );
568
569     XMLERROR (-1, xmlTextWriterEndElement (xo));
570   }
571
572   XMLERROR (-1, xmlTextWriterEndElement (xo));
573
574   free_strings (filesystems);
575 }
576
577 static void
578 output_applications (xmlTextWriterPtr xo, char *root)
579 {
580   struct guestfs_application_list *apps;
581   size_t i;
582
583   /* We need to mount everything up in order to read out the list of
584    * applications.
585    */
586   inspect_mount_root (root);
587
588   /* This returns an empty list if we simply couldn't determine the
589    * applications, so if it returns NULL then it's a real error.
590    */
591   apps = guestfs_inspect_list_applications (g, root);
592   if (apps == NULL)
593     exit (EXIT_FAILURE);
594   if (guestfs_umount_all (g) == -1)
595     exit (EXIT_FAILURE);
596
597   XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "applications"));
598
599   for (i = 0; i < apps->len; ++i) {
600     XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "application"));
601
602     assert (apps->val[i].app_name && apps->val[i].app_name[0]);
603     XMLERROR (-1,
604               xmlTextWriterWriteElement (xo, BAD_CAST "name",
605                                          BAD_CAST apps->val[i].app_name));
606
607     if (apps->val[i].app_display_name && apps->val[i].app_display_name[0])
608       XMLERROR (-1,
609         xmlTextWriterWriteElement (xo, BAD_CAST "display_name",
610                                    BAD_CAST apps->val[i].app_display_name));
611
612     if (apps->val[i].app_epoch != 0) {
613       char buf[32];
614
615       snprintf (buf, sizeof buf, "%d", apps->val[i].app_epoch);
616
617       XMLERROR (-1,
618         xmlTextWriterWriteElement (xo, BAD_CAST "epoch", BAD_CAST buf));
619     }
620
621     if (apps->val[i].app_version && apps->val[i].app_version[0])
622       XMLERROR (-1,
623         xmlTextWriterWriteElement (xo, BAD_CAST "version",
624                                    BAD_CAST apps->val[i].app_version));
625     if (apps->val[i].app_release && apps->val[i].app_release[0])
626       XMLERROR (-1,
627         xmlTextWriterWriteElement (xo, BAD_CAST "release",
628                                    BAD_CAST apps->val[i].app_release));
629     if (apps->val[i].app_install_path && apps->val[i].app_install_path[0])
630       XMLERROR (-1,
631         xmlTextWriterWriteElement (xo, BAD_CAST "install_path",
632                                    BAD_CAST apps->val[i].app_install_path));
633     if (apps->val[i].app_publisher && apps->val[i].app_publisher[0])
634       XMLERROR (-1,
635         xmlTextWriterWriteElement (xo, BAD_CAST "publisher",
636                                    BAD_CAST apps->val[i].app_publisher));
637     if (apps->val[i].app_url && apps->val[i].app_url[0])
638       XMLERROR (-1,
639         xmlTextWriterWriteElement (xo, BAD_CAST "url",
640                                    BAD_CAST apps->val[i].app_url));
641     if (apps->val[i].app_source_package && apps->val[i].app_source_package[0])
642       XMLERROR (-1,
643         xmlTextWriterWriteElement (xo, BAD_CAST "source_package",
644                                    BAD_CAST apps->val[i].app_source_package));
645     if (apps->val[i].app_summary && apps->val[i].app_summary[0])
646       XMLERROR (-1,
647         xmlTextWriterWriteElement (xo, BAD_CAST "summary",
648                                    BAD_CAST apps->val[i].app_summary));
649     if (apps->val[i].app_description && apps->val[i].app_description[0])
650       XMLERROR (-1,
651         xmlTextWriterWriteElement (xo, BAD_CAST "description",
652                                    BAD_CAST apps->val[i].app_description));
653
654     XMLERROR (-1, xmlTextWriterEndElement (xo));
655   }
656
657   XMLERROR (-1, xmlTextWriterEndElement (xo));
658
659   guestfs_free_application_list (apps);
660 }
661
662 /* "/dev/vda1" -> "/dev/sda1"
663  * See BLOCK DEVICE NAMING in guestfs(3).
664  */
665 static void
666 canonicalize (char *dev)
667 {
668   if (STRPREFIX (dev, "/dev/") &&
669       (dev[5] == 'h' || dev[5] == 'v') &&
670       dev[6] == 'd' &&
671       c_isalpha (dev[7]) &&
672       (c_isdigit (dev[8]) || dev[8] == '\0'))
673     dev[5] = 's';
674 }
675
676 static void
677 free_strings (char **argv)
678 {
679   int argc;
680
681   for (argc = 0; argv[argc] != NULL; ++argc)
682     free (argv[argc]);
683   free (argv);
684 }
685
686 static int
687 count_strings (char *const *argv)
688 {
689   int c;
690
691   for (c = 0; argv[c]; ++c)
692     ;
693   return c;
694 }