Import old 'virt-mem-new' program.
authorRichard Jones <rjones@redhat.com>
Mon, 15 Mar 2010 11:58:12 +0000 (11:58 +0000)
committerRichard Jones <rjones@redhat.com>
Mon, 15 Mar 2010 11:59:24 +0000 (11:59 +0000)
.gitignore
Makefile.am
configure.ac
src/Makefile.am
src/dmesg.c [new file with mode: 0644]
src/internal.h [new file with mode: 0644]
src/main.c [new file with mode: 0644]
src/output.c [new file with mode: 0644]

index 6415d1a..a478abd 100644 (file)
@@ -1,13 +1,19 @@
 *~
+*.o
+.deps
 Makefile
 Makefile.in
 aclocal.m4
 autom4te.cache
 compile
+config.h
+config.h.in
 config.log
 config.status
 configure
+depcomp
 install-sh
 missing
 src/Makefile
-src/Makefile.in
\ No newline at end of file
+src/Makefile.in
+stamp-h1
\ No newline at end of file
index 9051206..2aca109 100644 (file)
@@ -15,4 +15,6 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 
+ACLOCAL_AMFLAGS = -I m4
+
 SUBDIRS = src
index 2c41680..5dd76ed 100644 (file)
@@ -33,7 +33,10 @@ AM_PROG_CC_C_O
 dnl Check support for 64 bit file offsets.
 AC_SYS_LARGEFILE
 
+AC_GNU_SOURCE
+
 # Generate output files.
+AC_CONFIG_HEADERS([config.h])
 AC_CONFIG_FILES([Makefile
                  src/Makefile])
 AC_OUTPUT
index 1ee3bc7..74f48dd 100644 (file)
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 
+bin_PROGRAMS = virt-kernel-info
+virt_kernel_info_SOURCES = \
+       internal.h \
+       main.c \
+       output.c
+
+# Plugins:
+virt_kernel_info_SOURCES += \
+       dmesg.c
+
+virt_kernel_info_CFLAGS = -DDATADIR=\"$(datadir)\"
diff --git a/src/dmesg.c b/src/dmesg.c
new file mode 100644 (file)
index 0000000..74f91dd
--- /dev/null
@@ -0,0 +1,51 @@
+/* Kernel info for virtual domains.
+ * (C) Copyright 2008-2010 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "internal.h"
+
+static void init (void) ATTRIBUTE_CONSTRUCTOR;
+static void run (/*struct kimage * */ void);
+
+static struct tool me = {
+  .name = "dmesg",
+  .external_cmd = 1,
+  .run_fn = run,
+};
+
+static void
+init (void)
+{
+  me.summary = _("show kernel messages from a Linux virtual machine");
+  me.description =
+    _("\
+This tool prints the kernel messages ('dmesg') from a running
+Linux virtual machine.\n\
+");
+  register_tool (&me);
+}
+
+static void
+run (/*struct kimage *kimage*/)
+{
+  NOT_IMPL;
+}
diff --git a/src/internal.h b/src/internal.h
new file mode 100644 (file)
index 0000000..7bc4832
--- /dev/null
@@ -0,0 +1,164 @@
+/* Kernel info for virtual domains.
+ * (C) Copyright 2008-2010 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+
+#ifndef INTERNAL_H
+#define INTERNAL_H
+
+#include <libintl.h>
+
+#ifdef __GNUC__
+
+#ifndef __GNUC_PREREQ
+#define __GNUC_PREREQ(maj,min) 0
+#endif
+
+#define ATTRIBUTE_UNUSED __attribute__((__unused__))
+#define ATTRIBUTE_FORMAT(args...) __attribute__((__format__ (args)))
+#define ATTRIBUTE_NORETURN __attribute__((__noreturn__))
+#define ATTRIBUTE_CONSTRUCTOR __attribute__((__constructor__))
+
+#if __GNUC_PREREQ (3, 4)
+#define ATTRIBUTE_RETURN_CHECK __attribute__((__warn_unused_result__))
+#else
+#define ATTRIBUTE_RETURN_CHECK
+#endif
+
+#else  /* !__GNUC__ */
+#define ATTRIBUTE_UNUSED
+#define ATTRIBUTE_FORMAT(...)
+#define ATTRIBUTE_NORETURN
+#error "we must be able to define attribute((constructor)) for this compiler"
+#define ATTRIBUTE_RETURN_CHECK
+#endif /* __GNUC__ */
+
+/* Gettext functions.
+ *
+ * PACKAGE (usually the string "virt-kernel-info") should be the same as the
+ * $(DOMAIN) variable declared in po/Makevars.
+ */
+#define _(str) dgettext(PACKAGE, (str))
+#define N_(str) dgettext(PACKAGE, (str))
+
+/* String equality tests, suggested by Jim Meyering. */
+#define STREQ(a,b) (strcmp((a),(b)) == 0)
+#define STRCASEEQ(a,b) (strcasecmp((a),(b)) == 0)
+#define STRNEQ(a,b) (strcmp((a),(b)) != 0)
+#define STRCASENEQ(a,b) (strcasecmp((a),(b)) != 0)
+#define STREQLEN(a,b,n) (strncmp((a),(b),(n)) == 0)
+#define STRCASEEQLEN(a,b,n) (strncasecmp((a),(b),(n)) == 0)
+#define STRNEQLEN(a,b,n) (strncmp((a),(b),(n)) != 0)
+#define STRCASENEQLEN(a,b,n) (strncasecmp((a),(b),(n)) != 0)
+#define STRPREFIX(a,b) (strncmp((a),(b),strlen((b))) == 0)
+
+/* Each tool describes itself to the main program by
+ * defining and registering this structure.
+ */
+typedef void (*tool_run_fn) (void); /* XXX */
+
+struct tool {
+  const char *name;            /* Tool name, eg. "ps" */
+
+  int external_cmd; /* Is the tool an external command, eg. "virt-ps" */
+
+  const char *summary;         /* Tool-specific one line summary. */
+  const char *description;     /* Long description. */
+
+  tool_run_fn run_fn;          /* Tool entry point. */
+
+  /* The main program links tools after they are registered
+   * through this field.
+   */
+  struct tool *next;
+};
+
+/* Register a tool with the main program. */
+extern void register_tool (struct tool *me);
+
+/* Error printing functions:
+ *
+ * error() will print an error and exit.  Only use this if the error
+ * really is fatal.  NOT usually used inside tools, because they can
+ * be called repeatedly for different domains.
+ *
+ * warning() will print a warning message and continue.  Use this
+ * inside tools (and return), instead of exiting, because the tool
+ * can be called repeatedly for different domains.
+ *
+ * internal_error() is like error() but used for errors that shouldn't
+ * happen.
+ *
+ * INTERNAL_ERROR is also used for errors which shouldn't happen, but
+ * prints only file and line number.
+ *
+ * NOT_IMPL is used to mark functions and code sections which are not
+ * implemented.
+ *
+ * All of these functions append \n after the message.
+ */
+extern void error (const char *fs, ...)
+  ATTRIBUTE_FORMAT(printf, 1, 2) ATTRIBUTE_NORETURN;
+extern void warning (const char *fs, ...)
+  ATTRIBUTE_FORMAT(printf, 1, 2);
+extern void internal_error (const char *fs, ...)
+  ATTRIBUTE_FORMAT(printf, 1, 2) ATTRIBUTE_NORETURN;
+
+#define INTERNAL_ERROR                                                 \
+  internal_error (_("%s:%d: internal error"),                          \
+                 __FILE__, __LINE__)
+
+#define NOT_IMPL                                                       \
+  internal_error (_("%s:%d: this feature is not implemented"), \
+                   __FILE__, __LINE__)
+
+/* Output functions.  These handle CSV output automatically, but
+ * must be used with care.
+ *
+ * output_heading() is used to write the heading (title) row.
+ * The format string is converted such that any %-sequence which
+ * _isn't_ %s is turned into %s.  (So only string fields are
+ * expected in headings).
+ *
+ * output_row() is used to write each row of data.  The format
+ * string is used directly for non-CSV output, so this is mostly
+ * equivalent to printf().  However for CSV output, the format
+ * string is used to tell the types of each field, other characters
+ * being ignored.
+ *
+ * Only fairly simple format strings are understood.
+ *
+ * Usual usage is:
+ *
+ *   const char *fs = "%-9d %s";
+ *
+ *   output_heading (fs, _("PID"), _("NAME"));
+ *   for (i = 0; i < nr_procs; ++i) {
+ *     output_row (fs, proc[i].pid, proc[i].name);
+ *   }
+ *
+ * A \n is added automatically after each heading/row.
+ */
+extern void output_heading (const char *fs, ...)
+  /* not ATTRIBUTE_FORMAT */;
+extern void output_row (const char *fs, ...)
+  ATTRIBUTE_FORMAT(printf, 1, 2);
+
+/* Global flags. */
+extern int csv;
+extern int debug;
+
+#endif /* INTERNAL_H */
diff --git a/src/main.c b/src/main.c
new file mode 100644 (file)
index 0000000..5de205a
--- /dev/null
@@ -0,0 +1,383 @@
+/* Kernel info for virtual domains.
+ * (C) Copyright 2008-2010 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <assert.h>
+#include <getopt.h>
+
+#include <libvirt/libvirt.h>
+#include <libvirt/virterror.h>
+
+#include "internal.h"
+
+/* Global flags. */
+int csv = 0;
+int debug = 0;
+
+/* Linked list of registered tools. */
+static struct tool *tools = NULL;
+
+/* Currently selected tool (may be NULL). */
+struct tool *tool = NULL;
+
+/* External DB location. */
+static const char *externaldb = DATADIR "/" PACKAGE_NAME "/kerneldb";
+static int fail_if_no_externaldb = 0;
+
+/* URI and libvirt connection. */
+static const char *uri = NULL;
+static virConnectPtr conn = NULL;
+
+/* If -t option was passed, this is the filename of the test image. */
+static const char *test_image = NULL;
+
+/* If --list-kernels was passed. */
+static int list_kernels = 0;
+
+/* Local functions. */
+static void load_externaldb (const char *, int);
+static void do_list_kernels (void) ATTRIBUTE_NORETURN;
+static void usage (void) ATTRIBUTE_NORETURN;
+
+static virDomainPtr *get_named_domains (char * const*, int);
+static virDomainPtr *get_all_domains (void);
+
+int
+main (int argc, char *argv[])
+{
+  int c;
+
+  /* argv[0] can be the name of the tool, or if not recognized then
+   * the name of the tool must be the first anonymous argument.
+   */
+  if (argv[0]) {
+    const char *prog = strrchr (argv[0], '/');
+
+    if (!prog)
+      prog = argv[0];
+
+    if (STRCASEEQLEN (prog+1, "virt-", 5)) {
+      struct tool *t;
+
+      prog += 6;
+
+      for (t = tools; t != NULL; t = t->next) {
+       if (STRCASEEQ (prog, t->name)) {
+         tool = t;
+         break;
+       }
+      }
+    }
+  }
+
+  /* Parse command line parameters. */
+  while (1) {
+    static const char *shortopts = "A:E:T:W:c:dt:x:?";
+    static struct option longopts[] = {
+      { "arch", required_argument, 0, 'A' },
+      { "endian", required_argument, 0, 'E' },
+      { "kernel-min", required_argument, 0, 0 },
+      { "kernel-max", required_argument, 0, 0 },
+      { "text", required_argument, 0, 'T' },
+      { "wordsize", required_argument, 0, 'W' },
+      { "connect", required_argument, 0, 'c' },
+      { "csv", no_argument, 0, 0 },
+      { "debug", no_argument, 0, 'd' },
+      { "help", no_argument, 0, '?' },
+      { "list-kernels", no_argument, 0, 0 },
+      { "image", required_argument, 0, 't' },
+      { "externaldb", required_argument, 0, 'x' },
+      { "version", no_argument, 0, 0 }
+    };
+    int option_index = 0;
+
+    c = getopt_long (argc, argv, shortopts, longopts, &option_index);
+    if (c == -1) break;
+
+    switch (c) {
+    case 0: {                  /* longopt without short equivalent */
+      const char *longopt_name = longopts[option_index].name;
+
+      if (STRCASEEQ (longopt_name, "csv")) /* csv output */
+       csv = 1;
+                               /* list kernels and exit */
+      else if (STRCASEEQ (longopt_name, "list-kernels"))
+       list_kernels = 1;
+                               /* set kernel-min */
+      else if (STRCASEEQ (longopt_name, "kernel-min"))
+       NOT_IMPL;
+                               /* set kernel-max */
+      else if (STRCASEEQ (longopt_name, "kernel-max"))
+       NOT_IMPL;
+                               /* display version and exit */
+      else if (STRCASEEQ (longopt_name, "version")) {
+       printf ("%s %s\n", PACKAGE_NAME, PACKAGE_VERSION);
+       exit (0);
+      }
+      else
+       INTERNAL_ERROR;         /* this shouldn't happen */
+
+      break;
+    }
+
+    case 'A':                  /* set architecture */
+      NOT_IMPL;
+    case 'E':                  /* set endianness */
+      NOT_IMPL;
+    case 'T':                  /* set text address */
+      NOT_IMPL;
+    case 'W':                  /* set wordsize */
+      NOT_IMPL;
+
+    case 'c':                  /* connect to URI */
+      uri = optarg;
+      break;
+
+    case 'd':                  /* enable debugging */
+      debug = 1;
+      break;
+
+    case 't':                  /* load kernel image */
+      test_image = optarg;
+      break;
+
+    case 'x':                  /* location of external database */
+      externaldb = optarg;
+      fail_if_no_externaldb = 1;
+      break;
+
+    case '?':                  /* print help */
+      usage ();
+
+    default:
+      INTERNAL_ERROR;          /* this shouldn't happen */
+    }
+  } /* while */
+
+  /* Load the external database / kernel updates, if necessary. */
+  load_externaldb (externaldb, fail_if_no_externaldb);
+
+  /* If asked, list kernels and exit. */
+  if (list_kernels)
+    do_list_kernels ();
+
+  /* If we haven't got a tool name yet, then the first anon parameter
+   * must be a tool name.
+   */
+  if (!tool) {
+    if (optind < argc) {
+      const char *p;
+      struct tool *t;
+
+      p = argv[optind++];
+
+      for (t = tools; t != NULL; t = t->next) {
+       if (STRCASEEQ (p, t->name)) {
+         tool = t;
+         goto found_tool;
+       }
+      }
+
+      error (_("'%s' is not a recognized virt-kernel-info tool.\n\nMake sure you specify the virt-kernel-info tool as the first argument on the command line.  To get a list of recognized tools, use 'virt-kernel-info --help'."), p);
+    } else
+      error (_("Could not work out which virt-kernel-info tool you are trying to use.\n\nMake sure you specify the virt-kernel-info tool as the first argument on the command line.  To get a list of recognized tools, use 'virt-kernel-info --help'."));
+
+  found_tool: ;
+  }
+
+  /* We should have worked out which tool it is by now. */
+  assert (tool != NULL);
+
+  /* If -t was passed, then we load that forensic image, else we have
+   * to connect to libvirt.
+   */
+  if (test_image) {
+    if (optind < argc)
+      error (_("If '-t' is passed then there shouldn't be any additional command line arguments."));
+
+    NOT_IMPL;
+  }
+  else {
+    virDomainPtr *doms;
+
+    /* Connect to libvirt. */
+    conn = virConnectOpenReadOnly (uri);
+    if (!conn)
+      error (_("failed to connect to hypervisor '%s'.\nWhen connecting to some hypervisors you may need to be running as root.\nThere may be a more detailed error message above this, but if not then we didn't print one because libvirt's virterror mechanism is next to useless."),
+            uri != NULL ? uri : _("default"));
+
+    /* Extra parameters are a list of domain names, IDs or UUIDs. */
+    if (optind < argc)
+      doms = get_named_domains (&argv[optind], argc - optind);
+    else
+      doms = get_all_domains ();
+
+    /* Act on each domain. */
+    NOT_IMPL;
+  }
+
+  exit (0);
+}
+
+/* Usage. */
+static void usage (void)
+{
+  struct tool *t;
+
+  printf (_("\
+virt-kernel-info: Tools for providing information about virtual machines\n\
+\n\
+General usage is:\n\
+  <tool> [-options]\n\
+  <tool> [-options] [domain-name|ID|UUID ...]\n\
+  <tool> [-options] -t <image>\n\
+where <tool> is 'virt-ps', 'virt-dmesg' etc. (full list below) or a\n\
+subtool such as 'virt-kernel-info ps', 'virt-kernel-info capture' etc.\n\
+\n\
+General options:\n\
+  --connect | -c <libvirt-uri>   Connect to URI (default: autodetect)\n\
+  --csv                          Output in CSV format for spreadsheets etc.\n\
+  --debug                        Print extra debugging information\n\
+  --list-kernels                 List known kernels, then exit\n\
+  --image | -t <image>           Examine saved image (see: virt-kernel-info capture)\n\
+  --externaldb | -x <externaldb> Load/override external kernels database\n\
+  --version                      Print program name and version, then exit\n\
+\n\
+These options may be used to override automatic detection of guest\n\
+architecture, endianness, kernel location, etc.:\n\
+  -A | --arch      auto | <arch> | ...\n\
+  -E | --endian    auto | le | be\n\
+  --kernel-min     auto | <arch> | ... | 0x<addr>\n\
+  --kernel-max     auto | <arch> | ... | 0x<addr>\n\
+  -T | --text      auto | <arch> | ... | 0x<addr>\n\
+  -W | --wordsize  auto | 32 | 64\n\
+where <arch> is the name of an architecture such as i386, x86-64, etc.\n\
+\n\
+List of tools:\n\
+"));
+
+  for (t = tools; t != NULL; t = t->next) {
+    printf ("\n");
+
+    if (t->external_cmd)
+      printf ("  virt-%-12s - %s\n", t->name, t->summary);
+    else
+      printf ("  virt-kernel-info %8s - %s\n", t->name, t->summary);
+
+    printf ("%s", t->description);
+    if (t->next) printf ("\n");
+  }
+
+  exit (0);
+}
+
+static void
+load_externaldb (const char *externaldb, int fail_if_no_externaldb)
+{
+
+
+
+
+
+
+
+
+  NOT_IMPL;
+}
+
+static void
+do_list_kernels (void)
+{
+  NOT_IMPL;
+}
+
+static virDomainPtr *
+get_named_domains (char * const *domains, int nr_domains)
+{
+  NOT_IMPL;
+}
+
+static virDomainPtr *
+get_all_domains (void)
+{
+  NOT_IMPL;
+}
+
+/* Tools register themselves by calling this function.  Note that the
+ * function is called from constructors.  In particular it is called
+ * before main().  Also can be called in unspecified order.
+ */
+void
+register_tool (struct tool *tool)
+{
+  struct tool *t, **p;
+
+  /* Insertion sort. */
+  p = &tools;
+  for (t = tools; t != NULL; t = t->next) {
+    if (strcmp (t->name, tool->name) < 0)
+      goto insert;
+    p = &t->next;
+  }
+ insert:
+  tool->next = *p;
+  *p = tool;
+}
+
+/* Warning, error message functions.  See internal.h for usage. */
+static void
+message (const char *pre, const char *fs, va_list args)
+{
+  fprintf (stderr, "%s-%s: %s: ", PACKAGE_NAME, PACKAGE_VERSION, pre);
+  vfprintf (stderr, fs, args);
+  fprintf (stderr, "\n");
+}
+
+void
+error (const char *fs, ...)
+{
+  va_list args;
+  va_start (args, fs);
+  message (_("error"), fs, args);
+  va_end (args);
+  exit (1);
+}
+
+void
+warning (const char *fs, ...)
+{
+  va_list args;
+  va_start (args, fs);
+  message (_("warning"), fs, args);
+  va_end (args);
+}
+
+void
+internal_error (const char *fs, ...)
+{
+  va_list args;
+  va_start (args, fs);
+  message (_("internal error"), fs, args);
+  va_end (args);
+  abort ();
+}
diff --git a/src/output.c b/src/output.c
new file mode 100644 (file)
index 0000000..0c15472
--- /dev/null
@@ -0,0 +1,174 @@
+/* Kernel info for virtual domains.
+ * (C) Copyright 2008-2010 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+
+/* Generic output routines which can write either plain
+ * text tables or CSV files.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+
+#include "internal.h"
+
+static char *convert_fs_to_strings (const char *fs);
+static void output_to_csv (int to_strings, const char *fs, va_list args);
+
+void
+output_heading (const char *fs, ...)
+{
+  va_list args;
+
+  va_start (args, fs);
+
+  if (!csv) {
+    /* Convert each %..x in the format string into %..s */
+    char *fs2 = convert_fs_to_strings (fs);
+    vprintf (fs2, args);
+    free (fs2);
+    printf ("\n");
+  } else
+    output_to_csv (1, fs, args);
+
+  va_end (args);
+}
+
+void
+output_row (const char *fs, ...)
+{
+  va_list args;
+
+  va_start (args, fs);
+
+  if (!csv) {
+    vprintf (fs, args);
+    printf ("\n");
+  }
+  else
+    output_to_csv (0, fs, args);
+
+  va_end (args);
+}
+
+/* Return a newly allocated format string with each '%..x' sequence
+ * converted to '%..s'.  We only attempt to parse simple format
+ * strings, and we assume the length of the format string won't
+ * change.
+ */
+static const char *end_of_specifier = "diouxXeEfFgGaAcsp%";
+
+static char *
+convert_fs_to_strings (const char *fs)
+{
+  char *fs2;
+  int i, len;
+
+  len = strlen (fs);
+  fs2 = strdup (fs);
+  for (i = 0; i < len; ++i) {
+    if (fs2[i] == '%') {
+      /* Search for the end of the %-sequence. */
+      do { i++; }
+      while (i < len && strchr (end_of_specifier, fs2[i]) == NULL);
+
+      if (i == len)
+       internal_error (_("unrecognized format string in output_heading function"));
+
+      fs2[i] = 's';
+    }
+  }
+
+  return fs2; /* caller frees */
+}
+
+/* Output a row to a CSV file.  The format string is just used to
+ * get the correct types for each parameter - any non-%-specifiers
+ * in the format string are ignored.  If to_strings is true, then
+ * each parameter is assumed to be a string.
+ *
+ * The CSV output is very conservative, designed to be correct
+ * rather than concise.
+ */
+static void
+output_to_csv (int to_strings, const char *fs, va_list args)
+{
+  int i, j, len, comma = 0;
+  char fs2[16];
+
+  len = strlen (fs);
+
+  for (i = 0; i < len; ++i) {
+    /* Look for the next %-specifier. */
+    if (fs[i] == '%') {
+      j = 0;
+      do { fs2[j++] = fs[i++]; }
+      while (j < sizeof (fs2) - 1 &&
+            i < len &&
+            strchr (end_of_specifier, fs[i]) == NULL);
+
+      if (j == sizeof (fs2) - 1 || i == len)
+       internal_error (_("unrecognized format string in output_* function"));
+
+      fs2[j] = fs[i++];
+      fs2[j+1] = '\0';
+    }
+
+    if (fs2[j] != '%') {
+      char *str;
+      int len2;
+
+      if (to_strings)
+       fs2[j] = 's';
+
+      /* Convert the next parameter into a string. */
+      if (vasprintf (&str, fs2, args) == -1)
+       internal_error (_("unable to convert next argument to string using '%s'"),
+                       fs2);
+      len2 = strlen (str);
+
+      /* Output the next parameter as a CSV field. */
+      if (comma)
+       putchar (',');
+      comma = 1;
+
+      putchar ('"');
+      for (j = 0; j < len2; ++j) {
+       switch (str[j]) {
+       case '"':
+         putchar ('"');
+         putchar ('"');
+         break;
+       case '\0':
+         putchar ('"');
+         putchar ('0');
+         break;
+       default:
+         putchar (str[j]);
+       }
+      }
+      putchar ('"');
+
+      free (str);
+    }
+  }
+
+  putchar ('\n');
+}