Fix checking of generator being run from the right directory.
[libguestfs.git] / src / generator.ml
index f7057d0..66c272f 100755 (executable)
@@ -66,15 +66,16 @@ and ret =
     (* "RString" and "RStringList" are caller-frees. *)
   | RString of string
   | RStringList of string
-    (* Some limited tuples are possible: *)
-  | RIntBool of string * string
-    (* LVM PVs, VGs and LVs. *)
-  | RPVList of string
-  | RVGList of string
-  | RLVList of string
-    (* Stat buffers. *)
-  | RStat of string
-  | RStatVFS of string
+    (* "RStruct" is a function which returns a single named structure
+     * or an error indication (in C, a struct, and in other languages
+     * with varying representations, but usually very efficient).  See
+     * after the function list below for the structures. 
+     *)
+  | RStruct of string * string         (* name of retval, name of struct *)
+    (* "RStructList" is a function which returns either a list/array
+     * of structures (could be zero-length), or an error indication.
+     *)
+  | RStructList of string * string     (* name of retval, name of struct *)
     (* Key-value pairs of untyped strings.  Turns into a hashtable or
      * dictionary in languages which support it.  DON'T use this as a
      * general "bucket" for results.  Prefer a stronger typed return
@@ -116,9 +117,10 @@ type flags =
   | FishAlias of string          (* provide an alias for this cmd in guestfish *)
   | FishAction of string  (* call this function in guestfish *)
   | NotInFish            (* do not export via guestfish *)
+  | NotInDocs            (* do not add this function to documentation *)
 
 let protocol_limit_warning =
-  "Because of the message protocol, there is a transfer limit 
+  "Because of the message protocol, there is a transfer limit
 of somewhere between 2MB and 4MB.  To transfer large files you should use
 FTP."
 
@@ -129,27 +131,36 @@ can easily destroy all your data>."
 (* You can supply zero or as many tests as you want per API call.
  *
  * Note that the test environment has 3 block devices, of size 500MB,
- * 50MB and 10MB (respectively /dev/sda, /dev/sdb, /dev/sdc).
- * Note for partitioning purposes, the 500MB device has 63 cylinders.
+ * 50MB and 10MB (respectively /dev/sda, /dev/sdb, /dev/sdc), and
+ * a fourth squashfs block device with some known files on it (/dev/sdd).
+ *
+ * Note for partitioning purposes, the 500MB device has 1015 cylinders.
+ * Number of cylinders was 63 for IDE emulated disks with precisely
+ * the same size.  How exactly this is calculated is a mystery.
+ *
+ * The squashfs block device (/dev/sdd) comes from images/test.sqsh.
  *
  * To be able to run the tests in a reasonable amount of time,
  * the virtual machine and block devices are reused between tests.
  * So don't try testing kill_subprocess :-x
  *
- * Between each test we blockdev-setrw, umount-all, lvm-remove-all
- * (except InitNone).
- *
- * If the appliance is running an older Linux kernel (eg. RHEL 5) then
- * devices are named /dev/hda etc.  To cope with this, the test suite
- * adds some hairly logic to detect this case, and then automagically
- * replaces all strings which match "/dev/sd.*" with "/dev/hd.*".
- * When writing test cases you shouldn't have to worry about this
- * difference.
+ * Between each test we blockdev-setrw, umount-all, lvm-remove-all.
  *
  * Don't assume anything about the previous contents of the block
  * devices.  Use 'Init*' to create some initial scenarios.
+ *
+ * You can add a prerequisite clause to any individual test.  This
+ * is a run-time check, which, if it fails, causes the test to be
+ * skipped.  Useful if testing a command which might not work on
+ * all variations of libguestfs builds.  A test that has prerequisite
+ * of 'Always' is run unconditionally.
+ *
+ * In addition, packagers can skip individual tests by setting the
+ * environment variables:     eg:
+ *   SKIP_TEST_<CMD>_<NUM>=1  SKIP_TEST_COMMAND_3=1  (skips test #3 of command)
+ *   SKIP_TEST_<CMD>=1        SKIP_TEST_ZEROFREE=1   (skips all zerofree tests)
  *)
-type tests = (test_init * test) list
+type tests = (test_init * test_prereq * test) list
 and test =
     (* Run the command sequence and just expect nothing to fail. *)
   | TestRun of seq
@@ -162,6 +173,12 @@ and test =
      *)
   | TestOutputList of seq * string list
     (* Run the command sequence and expect the output of the final
+     * command to be the list of block devices (could be either
+     * "/dev/sd.." or "/dev/hd.." form - we don't check the 5th
+     * character of each string).
+     *)
+  | TestOutputListOfDevices of seq * string list
+    (* Run the command sequence and expect the output of the final
      * command to be the integer.
      *)
   | TestOutputInt of seq * int
@@ -193,6 +210,21 @@ and test_field_compare =
   | CompareFieldsIntEq of string * string
   | CompareFieldsStrEq of string * string
 
+(* Test prerequisites. *)
+and test_prereq =
+    (* Test always runs. *)
+  | Always
+    (* Test is currently disabled - eg. it fails, or it tests some
+     * unimplemented feature.
+     *)
+  | Disabled
+    (* 'string' is some C code (a function body) that should return
+     * true or false.  The test will run if the code returns true.
+     *)
+  | If of string
+    (* As for 'If' but the test runs _unless_ the code returns true. *)
+  | Unless of string
+
 (* Some initial scenarios for testing. *)
 and test_init =
     (* Do nothing, block devices could contain random stuff including
@@ -228,7 +260,77 @@ and cmd = string list
  * Apart from that, long descriptions are just perldoc paragraphs.
  *)
 
-let non_daemon_functions = [
+(* These test functions are used in the language binding tests. *)
+
+let test_all_args = [
+  String "str";
+  OptString "optstr";
+  StringList "strlist";
+  Bool "b";
+  Int "integer";
+  FileIn "filein";
+  FileOut "fileout";
+]
+
+let test_all_rets = [
+  (* except for RErr, which is tested thoroughly elsewhere *)
+  "test0rint",         RInt "valout";
+  "test0rint64",       RInt64 "valout";
+  "test0rbool",        RBool "valout";
+  "test0rconststring", RConstString "valout";
+  "test0rstring",      RString "valout";
+  "test0rstringlist",  RStringList "valout";
+  "test0rstruct",      RStruct ("valout", "lvm_pv");
+  "test0rstructlist",  RStructList ("valout", "lvm_pv");
+  "test0rhashtable",   RHashtable "valout";
+]
+
+let test_functions = [
+  ("test0", (RErr, test_all_args), -1, [NotInFish; NotInDocs],
+   [],
+   "internal test function - do not use",
+   "\
+This is an internal test function which is used to test whether
+the automatically generated bindings can handle every possible
+parameter type correctly.
+
+It echos the contents of each parameter to stdout.
+
+You probably don't want to call this function.");
+] @ List.flatten (
+  List.map (
+    fun (name, ret) ->
+      [(name, (ret, [String "val"]), -1, [NotInFish; NotInDocs],
+       [],
+       "internal test function - do not use",
+       "\
+This is an internal test function which is used to test whether
+the automatically generated bindings can handle every possible
+return type correctly.
+
+It converts string C<val> to the return type.
+
+You probably don't want to call this function.");
+       (name ^ "err", (ret, []), -1, [NotInFish; NotInDocs],
+       [],
+       "internal test function - do not use",
+       "\
+This is an internal test function which is used to test whether
+the automatically generated bindings can handle every possible
+return type correctly.
+
+This function always returns an error.
+
+You probably don't want to call this function.")]
+  ) test_all_rets
+)
+
+(* non_daemon_functions are any functions which don't get processed
+ * in the daemon, eg. functions for setting and getting local
+ * configuration values.
+ *)
+
+let non_daemon_functions = test_functions @ [
   ("launch", (RErr, []), -1, [FishAlias "run"; FishAction "launch"],
    [],
    "launch the qemu subprocess",
@@ -270,7 +372,13 @@ for whatever operations you want to perform (ie. read access if you
 just want to read the image or write access if you want to modify the
 image).
 
-This is equivalent to the qemu parameter C<-drive file=filename>.");
+This is equivalent to the qemu parameter
+C<-drive file=filename,cache=off,if=...>.
+
+Note that this call checks for the existence of C<filename>.  This
+stops you from specifying other types of drive which are supported
+by qemu such as C<nbd:> and C<http:> URLs.  To specify those, use
+the general C<guestfs_config> call instead.");
 
   ("add_cdrom", (RErr, [String "filename"]), -1, [FishAlias "cdrom"],
    [],
@@ -278,7 +386,33 @@ This is equivalent to the qemu parameter C<-drive file=filename>.");
    "\
 This function adds a virtual CD-ROM disk image to the guest.
 
-This is equivalent to the qemu parameter C<-cdrom filename>.");
+This is equivalent to the qemu parameter C<-cdrom filename>.
+
+Note that this call checks for the existence of C<filename>.  This
+stops you from specifying other types of drive which are supported
+by qemu such as C<nbd:> and C<http:> URLs.  To specify those, use
+the general C<guestfs_config> call instead.");
+
+  ("add_drive_ro", (RErr, [String "filename"]), -1, [FishAlias "add-ro"],
+   [],
+   "add a drive in snapshot mode (read-only)",
+   "\
+This adds a drive in snapshot mode, making it effectively
+read-only.
+
+Note that writes to the device are allowed, and will be seen for
+the duration of the guestfs handle, but they are written
+to a temporary file which is discarded as soon as the guestfs
+handle is closed.  We don't currently have any method to enable
+changes to be committed, although qemu can support this.
+
+This is equivalent to the qemu parameter
+C<-drive file=filename,snapshot=on,if=...>.
+
+Note that this call checks for the existence of C<filename>.  This
+stops you from specifying other types of drive which are supported
+by qemu such as C<nbd:> and C<http:> URLs.  To specify those, use
+the general C<guestfs_config> call instead.");
 
   ("config", (RErr, [String "qemuparam"; OptString "qemuvalue"]), -1, [],
    [],
@@ -305,9 +439,6 @@ configure script.
 You can also override this by setting the C<LIBGUESTFS_QEMU>
 environment variable.
 
-The string C<qemu> is stashed in the libguestfs handle, so the caller
-must make sure it remains valid for the lifetime of the handle.
-
 Setting C<qemu> to C<NULL> restores the default qemu binary.");
 
   ("get_qemu", (RConstString "qemu", []), -1, [],
@@ -328,9 +459,6 @@ Set the path that libguestfs searches for kernel and initrd.img.
 The default is C<$libdir/guestfs> unless overridden by setting
 C<LIBGUESTFS_PATH> environment variable.
 
-The string C<path> is stashed in the libguestfs handle, so the caller
-must make sure it remains valid for the lifetime of the handle.
-
 Setting C<path> to C<NULL> restores the default path.");
 
   ("get_path", (RConstString "path", []), -1, [],
@@ -342,6 +470,28 @@ Return the current search path.
 This is always non-NULL.  If it wasn't set already, then this will
 return the default path.");
 
+  ("set_append", (RErr, [String "append"]), -1, [FishAlias "append"],
+   [],
+   "add options to kernel command line",
+   "\
+This function is used to add additional options to the
+guest kernel command line.
+
+The default is C<NULL> unless overridden by setting
+C<LIBGUESTFS_APPEND> environment variable.
+
+Setting C<append> to C<NULL> means I<no> additional options
+are passed (libguestfs always adds a few of its own).");
+
+  ("get_append", (RConstString "append", []), -1, [],
+   [],
+   "get the additional kernel options",
+   "\
+Return the additional kernel options which are added to the
+guest kernel command line.
+
+If C<NULL> then no options are added.");
+
   ("set_autosync", (RErr, [Bool "autosync"]), -1, [FishAlias "autosync"],
    [],
    "set autosync mode",
@@ -448,12 +598,54 @@ actions using the low-level API.
 
 For more information on states, see L<guestfs(3)>.");
 
+  ("set_memsize", (RErr, [Int "memsize"]), -1, [FishAlias "memsize"],
+   [],
+   "set memory allocated to the qemu subprocess",
+   "\
+This sets the memory size in megabytes allocated to the
+qemu subprocess.  This only has any effect if called before
+C<guestfs_launch>.
+
+You can also change this by setting the environment
+variable C<LIBGUESTFS_MEMSIZE> before the handle is
+created.
+
+For more information on the architecture of libguestfs,
+see L<guestfs(3)>.");
+
+  ("get_memsize", (RInt "memsize", []), -1, [],
+   [],
+   "get memory allocated to the qemu subprocess",
+   "\
+This gets the memory size in megabytes allocated to the
+qemu subprocess.
+
+If C<guestfs_set_memsize> was not called
+on this handle, and if C<LIBGUESTFS_MEMSIZE> was not set,
+then this returns the compiled-in default value for memsize.
+
+For more information on the architecture of libguestfs,
+see L<guestfs(3)>.");
+
+  ("get_pid", (RInt "pid", []), -1, [FishAlias "pid"],
+   [],
+   "get PID of qemu subprocess",
+   "\
+Return the process ID of the qemu subprocess.  If there is no
+qemu subprocess, then this will return an error.
+
+This is an internal call used for debugging and testing.");
+
 ]
 
+(* daemon_functions are any functions which cause some action
+ * to take place in the daemon.
+ *)
+
 let daemon_functions = [
   ("mount", (RErr, [String "device"; String "mountpoint"]), 1, [],
-   [InitEmpty, TestOutput (
-      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+   [InitEmpty, Always, TestOutput (
+      [["sfdiskM"; "/dev/sda"; ","];
        ["mkfs"; "ext2"; "/dev/sda1"];
        ["mount"; "/dev/sda1"; "/"];
        ["write_file"; "/new"; "new file contents"; "0"];
@@ -478,7 +670,7 @@ The filesystem options C<sync> and C<noatime> are set with this
 call, in order to improve reliability.");
 
   ("sync", (RErr, []), 2, [],
-   [ InitEmpty, TestRun [["sync"]]],
+   [ InitEmpty, Always, TestRun [["sync"]]],
    "sync disks, writes are flushed through to the disk image",
    "\
 This syncs the disk, so that any writes are flushed through to the
@@ -488,7 +680,7 @@ You should always call this if you have modified a disk image, before
 closing the handle.");
 
   ("touch", (RErr, [String "path"]), 3, [],
-   [InitBasicFS, TestOutputTrue (
+   [InitBasicFS, Always, TestOutputTrue (
       [["touch"; "/new"];
        ["exists"; "/new"]])],
    "update file timestamps or create a new file",
@@ -498,7 +690,7 @@ update the timestamps on a file, or, if the file does not exist,
 to create a new zero-length file.");
 
   ("cat", (RString "content", [String "path"]), 4, [ProtocolLimitWarning],
-   [InitBasicFS, TestOutput (
+   [InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "new file contents"; "0"];
        ["cat"; "/new"]], "new file contents")],
    "list the contents of a file",
@@ -523,7 +715,7 @@ This command is mostly useful for interactive sessions.  It
 is I<not> intended that you try to parse the output string.");
 
   ("ls", (RStringList "listing", [String "directory"]), 6, [],
-   [InitBasicFS, TestOutputList (
+   [InitBasicFS, Always, TestOutputList (
       [["touch"; "/new"];
        ["touch"; "/newer"];
        ["touch"; "/newest"];
@@ -538,8 +730,8 @@ This command is mostly useful for interactive sessions.  Programs
 should probably use C<guestfs_readdir> instead.");
 
   ("list_devices", (RStringList "devices", []), 7, [],
-   [InitEmpty, TestOutputList (
-      [["list_devices"]], ["/dev/sda"; "/dev/sdb"; "/dev/sdc"])],
+   [InitEmpty, Always, TestOutputListOfDevices (
+      [["list_devices"]], ["/dev/sda"; "/dev/sdb"; "/dev/sdc"; "/dev/sdd"])],
    "list the block devices",
    "\
 List all the block devices.
@@ -547,10 +739,10 @@ List all the block devices.
 The full block device names are returned, eg. C</dev/sda>");
 
   ("list_partitions", (RStringList "partitions", []), 8, [],
-   [InitBasicFS, TestOutputList (
+   [InitBasicFS, Always, TestOutputListOfDevices (
       [["list_partitions"]], ["/dev/sda1"]);
-    InitEmpty, TestOutputList (
-      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+    InitEmpty, Always, TestOutputListOfDevices (
+      [["sfdiskM"; "/dev/sda"; ",100 ,200 ,"];
        ["list_partitions"]], ["/dev/sda1"; "/dev/sda2"; "/dev/sda3"])],
    "list the partitions",
    "\
@@ -562,10 +754,10 @@ This does not return logical volumes.  For that you will need to
 call C<guestfs_lvs>.");
 
   ("pvs", (RStringList "physvols", []), 9, [],
-   [InitBasicFSonLVM, TestOutputList (
+   [InitBasicFSonLVM, Always, TestOutputListOfDevices (
       [["pvs"]], ["/dev/sda1"]);
-    InitEmpty, TestOutputList (
-      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+    InitEmpty, Always, TestOutputListOfDevices (
+      [["sfdiskM"; "/dev/sda"; ",100 ,200 ,"];
        ["pvcreate"; "/dev/sda1"];
        ["pvcreate"; "/dev/sda2"];
        ["pvcreate"; "/dev/sda3"];
@@ -581,10 +773,10 @@ PVs (eg. C</dev/sda2>).
 See also C<guestfs_pvs_full>.");
 
   ("vgs", (RStringList "volgroups", []), 10, [],
-   [InitBasicFSonLVM, TestOutputList (
+   [InitBasicFSonLVM, Always, TestOutputList (
       [["vgs"]], ["VG"]);
-    InitEmpty, TestOutputList (
-      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+    InitEmpty, Always, TestOutputList (
+      [["sfdiskM"; "/dev/sda"; ",100 ,200 ,"];
        ["pvcreate"; "/dev/sda1"];
        ["pvcreate"; "/dev/sda2"];
        ["pvcreate"; "/dev/sda3"];
@@ -602,10 +794,10 @@ detected (eg. C<VolGroup00>).
 See also C<guestfs_vgs_full>.");
 
   ("lvs", (RStringList "logvols", []), 11, [],
-   [InitBasicFSonLVM, TestOutputList (
+   [InitBasicFSonLVM, Always, TestOutputList (
       [["lvs"]], ["/dev/VG/LV"]);
-    InitEmpty, TestOutputList (
-      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+    InitEmpty, Always, TestOutputList (
+      [["sfdiskM"; "/dev/sda"; ",100 ,200 ,"];
        ["pvcreate"; "/dev/sda1"];
        ["pvcreate"; "/dev/sda2"];
        ["pvcreate"; "/dev/sda3"];
@@ -625,21 +817,21 @@ This returns a list of the logical volume device names
 
 See also C<guestfs_lvs_full>.");
 
-  ("pvs_full", (RPVList "physvols", []), 12, [],
+  ("pvs_full", (RStructList ("physvols", "lvm_pv"), []), 12, [],
    [], (* XXX how to test? *)
    "list the LVM physical volumes (PVs)",
    "\
 List all the physical volumes detected.  This is the equivalent
 of the L<pvs(8)> command.  The \"full\" version includes all fields.");
 
-  ("vgs_full", (RVGList "volgroups", []), 13, [],
+  ("vgs_full", (RStructList ("volgroups", "lvm_vg"), []), 13, [],
    [], (* XXX how to test? *)
    "list the LVM volume groups (VGs)",
    "\
 List all the volumes groups detected.  This is the equivalent
 of the L<vgs(8)> command.  The \"full\" version includes all fields.");
 
-  ("lvs_full", (RLVList "logvols", []), 14, [],
+  ("lvs_full", (RStructList ("logvols", "lvm_lv"), []), 14, [],
    [], (* XXX how to test? *)
    "list the LVM logical volumes (LVs)",
    "\
@@ -647,10 +839,10 @@ List all the logical volumes detected.  This is the equivalent
 of the L<lvs(8)> command.  The \"full\" version includes all fields.");
 
   ("read_lines", (RStringList "lines", [String "path"]), 15, [],
-   [InitBasicFS, TestOutputList (
+   [InitBasicFS, Always, TestOutputList (
       [["write_file"; "/new"; "line1\r\nline2\nline3"; "0"];
        ["read_lines"; "/new"]], ["line1"; "line2"; "line3"]);
-    InitBasicFS, TestOutputList (
+    InitBasicFS, Always, TestOutputList (
       [["write_file"; "/new"; ""; "0"];
        ["read_lines"; "/new"]], [])],
    "read file as lines",
@@ -736,7 +928,7 @@ undefined.
 On success this returns the number of nodes in C<expr>, or
 C<0> if C<expr> evaluates to something which is not a nodeset.");
 
-  ("aug_defnode", (RIntBool ("nrnodes", "created"), [String "name"; String "expr"; String "val"]), 18, [],
+  ("aug_defnode", (RStruct ("nrnodescreated", "int_bool"), [String "name"; String "expr"; String "val"]), 18, [],
    [], (* XXX Augeas code needs tests. *)
    "define an Augeas node",
    "\
@@ -825,12 +1017,12 @@ This is just a shortcut for listing C<guestfs_aug_match>
 C<path/*> and sorting the resulting nodes into alphabetical order.");
 
   ("rm", (RErr, [String "path"]), 29, [],
-   [InitBasicFS, TestRun
+   [InitBasicFS, Always, TestRun
       [["touch"; "/new"];
        ["rm"; "/new"]];
-    InitBasicFS, TestLastFail
+    InitBasicFS, Always, TestLastFail
       [["rm"; "/new"]];
-    InitBasicFS, TestLastFail
+    InitBasicFS, Always, TestLastFail
       [["mkdir"; "/new"];
        ["rm"; "/new"]]],
    "remove a file",
@@ -838,12 +1030,12 @@ C<path/*> and sorting the resulting nodes into alphabetical order.");
 Remove the single file C<path>.");
 
   ("rmdir", (RErr, [String "path"]), 30, [],
-   [InitBasicFS, TestRun
+   [InitBasicFS, Always, TestRun
       [["mkdir"; "/new"];
        ["rmdir"; "/new"]];
-    InitBasicFS, TestLastFail
+    InitBasicFS, Always, TestLastFail
       [["rmdir"; "/new"]];
-    InitBasicFS, TestLastFail
+    InitBasicFS, Always, TestLastFail
       [["touch"; "/new"];
        ["rmdir"; "/new"]]],
    "remove a directory",
@@ -851,7 +1043,7 @@ Remove the single file C<path>.");
 Remove the single directory C<path>.");
 
   ("rm_rf", (RErr, [String "path"]), 31, [],
-   [InitBasicFS, TestOutputFalse
+   [InitBasicFS, Always, TestOutputFalse
       [["mkdir"; "/new"];
        ["mkdir"; "/new/foo"];
        ["touch"; "/new/foo/bar"];
@@ -864,25 +1056,32 @@ contents if its a directory.  This is like the C<rm -rf> shell
 command.");
 
   ("mkdir", (RErr, [String "path"]), 32, [],
-   [InitBasicFS, TestOutputTrue
+   [InitBasicFS, Always, TestOutputTrue
       [["mkdir"; "/new"];
        ["is_dir"; "/new"]];
-    InitBasicFS, TestLastFail
+    InitBasicFS, Always, TestLastFail
       [["mkdir"; "/new/foo/bar"]]],
    "create a directory",
    "\
 Create a directory named C<path>.");
 
   ("mkdir_p", (RErr, [String "path"]), 33, [],
-   [InitBasicFS, TestOutputTrue
+   [InitBasicFS, Always, TestOutputTrue
       [["mkdir_p"; "/new/foo/bar"];
        ["is_dir"; "/new/foo/bar"]];
-    InitBasicFS, TestOutputTrue
+    InitBasicFS, Always, TestOutputTrue
       [["mkdir_p"; "/new/foo/bar"];
        ["is_dir"; "/new/foo"]];
-    InitBasicFS, TestOutputTrue
+    InitBasicFS, Always, TestOutputTrue
       [["mkdir_p"; "/new/foo/bar"];
-       ["is_dir"; "/new"]]],
+       ["is_dir"; "/new"]];
+    (* Regression tests for RHBZ#503133: *)
+    InitBasicFS, Always, TestRun
+      [["mkdir"; "/new"];
+       ["mkdir_p"; "/new"]];
+    InitBasicFS, Always, TestLastFail
+      [["touch"; "/new"];
+       ["mkdir_p"; "/new"]]],
    "create a directory and parents",
    "\
 Create a directory named C<path>, creating any parent directories
@@ -906,10 +1105,10 @@ names, you will need to locate and parse the password file
 yourself (Augeas support makes this relatively easy).");
 
   ("exists", (RBool "existsflag", [String "path"]), 36, [],
-   [InitBasicFS, TestOutputTrue (
+   [InitBasicFS, Always, TestOutputTrue (
       [["touch"; "/new"];
        ["exists"; "/new"]]);
-    InitBasicFS, TestOutputTrue (
+    InitBasicFS, Always, TestOutputTrue (
       [["mkdir"; "/new"];
        ["exists"; "/new"]])],
    "test if file or directory exists",
@@ -920,10 +1119,10 @@ This returns C<true> if and only if there is a file, directory
 See also C<guestfs_is_file>, C<guestfs_is_dir>, C<guestfs_stat>.");
 
   ("is_file", (RBool "fileflag", [String "path"]), 37, [],
-   [InitBasicFS, TestOutputTrue (
+   [InitBasicFS, Always, TestOutputTrue (
       [["touch"; "/new"];
        ["is_file"; "/new"]]);
-    InitBasicFS, TestOutputFalse (
+    InitBasicFS, Always, TestOutputFalse (
       [["mkdir"; "/new"];
        ["is_file"; "/new"]])],
    "test if file exists",
@@ -935,10 +1134,10 @@ other objects like directories.
 See also C<guestfs_stat>.");
 
   ("is_dir", (RBool "dirflag", [String "path"]), 38, [],
-   [InitBasicFS, TestOutputFalse (
+   [InitBasicFS, Always, TestOutputFalse (
       [["touch"; "/new"];
        ["is_dir"; "/new"]]);
-    InitBasicFS, TestOutputTrue (
+    InitBasicFS, Always, TestOutputTrue (
       [["mkdir"; "/new"];
        ["is_dir"; "/new"]])],
    "test if file exists",
@@ -950,8 +1149,8 @@ other objects like files.
 See also C<guestfs_stat>.");
 
   ("pvcreate", (RErr, [String "device"]), 39, [],
-   [InitEmpty, TestOutputList (
-      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+   [InitEmpty, Always, TestOutputListOfDevices (
+      [["sfdiskM"; "/dev/sda"; ",100 ,200 ,"];
        ["pvcreate"; "/dev/sda1"];
        ["pvcreate"; "/dev/sda2"];
        ["pvcreate"; "/dev/sda3"];
@@ -963,8 +1162,8 @@ where C<device> should usually be a partition name such
 as C</dev/sda1>.");
 
   ("vgcreate", (RErr, [String "volgroup"; StringList "physvols"]), 40, [],
-   [InitEmpty, TestOutputList (
-      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+   [InitEmpty, Always, TestOutputList (
+      [["sfdiskM"; "/dev/sda"; ",100 ,200 ,"];
        ["pvcreate"; "/dev/sda1"];
        ["pvcreate"; "/dev/sda2"];
        ["pvcreate"; "/dev/sda3"];
@@ -977,8 +1176,8 @@ This creates an LVM volume group called C<volgroup>
 from the non-empty list of physical volumes C<physvols>.");
 
   ("lvcreate", (RErr, [String "logvol"; String "volgroup"; Int "mbytes"]), 41, [],
-   [InitEmpty, TestOutputList (
-      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+   [InitEmpty, Always, TestOutputList (
+      [["sfdiskM"; "/dev/sda"; ",100 ,200 ,"];
        ["pvcreate"; "/dev/sda1"];
        ["pvcreate"; "/dev/sda2"];
        ["pvcreate"; "/dev/sda3"];
@@ -998,8 +1197,8 @@ This creates an LVM volume group called C<logvol>
 on the volume group C<volgroup>, with C<size> megabytes.");
 
   ("mkfs", (RErr, [String "fstype"; String "device"]), 42, [],
-   [InitEmpty, TestOutput (
-      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+   [InitEmpty, Always, TestOutput (
+      [["sfdiskM"; "/dev/sda"; ","];
        ["mkfs"; "ext2"; "/dev/sda1"];
        ["mount"; "/dev/sda1"; "/"];
        ["write_file"; "/new"; "new file contents"; "0"];
@@ -1034,25 +1233,27 @@ information refer to the L<sfdisk(8)> manpage.
 
 To create a single partition occupying the whole disk, you would
 pass C<lines> as a single element list, when the single element being
-the string C<,> (comma).");
+the string C<,> (comma).
+
+See also: C<guestfs_sfdisk_l>, C<guestfs_sfdisk_N>");
 
   ("write_file", (RErr, [String "path"; String "content"; Int "size"]), 44, [ProtocolLimitWarning],
-   [InitBasicFS, TestOutput (
+   [InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "new file contents"; "0"];
        ["cat"; "/new"]], "new file contents");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "\nnew file contents\n"; "0"];
        ["cat"; "/new"]], "\nnew file contents\n");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "\n\n"; "0"];
        ["cat"; "/new"]], "\n\n");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; ""; "0"];
        ["cat"; "/new"]], "");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "\n\n\n"; "0"];
        ["cat"; "/new"]], "\n\n\n");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "\n"; "0"];
        ["cat"; "/new"]], "\n")],
    "create a file",
@@ -1071,13 +1272,13 @@ We hope to resolve this bug in a future version.  In the meantime
 use C<guestfs_upload>.");
 
   ("umount", (RErr, [String "pathordevice"]), 45, [FishAlias "unmount"],
-   [InitEmpty, TestOutputList (
-      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+   [InitEmpty, Always, TestOutputListOfDevices (
+      [["sfdiskM"; "/dev/sda"; ","];
        ["mkfs"; "ext2"; "/dev/sda1"];
        ["mount"; "/dev/sda1"; "/"];
        ["mounts"]], ["/dev/sda1"]);
-    InitEmpty, TestOutputList (
-      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+    InitEmpty, Always, TestOutputList (
+      [["sfdiskM"; "/dev/sda"; ","];
        ["mkfs"; "ext2"; "/dev/sda1"];
        ["mount"; "/dev/sda1"; "/"];
        ["umount"; "/"];
@@ -1089,7 +1290,7 @@ specified either by its mountpoint (path) or the device which
 contains the filesystem.");
 
   ("mounts", (RStringList "devices", []), 46, [],
-   [InitBasicFS, TestOutputList (
+   [InitBasicFS, Always, TestOutputListOfDevices (
       [["mounts"]], ["/dev/sda1"])],
    "show mounted filesystems",
    "\
@@ -1099,12 +1300,12 @@ the list of devices (eg. C</dev/sda1>, C</dev/VG/LV>).
 Some internal mounts are not shown.");
 
   ("umount_all", (RErr, []), 47, [FishAlias "unmount-all"],
-   [InitBasicFS, TestOutputList (
+   [InitBasicFS, Always, TestOutputList (
       [["umount_all"];
        ["mounts"]], []);
     (* check that umount_all can unmount nested mounts correctly: *)
-    InitEmpty, TestOutputList (
-      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+    InitEmpty, Always, TestOutputList (
+      [["sfdiskM"; "/dev/sda"; ",100 ,200 ,"];
        ["mkfs"; "ext2"; "/dev/sda1"];
        ["mkfs"; "ext2"; "/dev/sda2"];
        ["mkfs"; "ext2"; "/dev/sda3"];
@@ -1130,13 +1331,13 @@ This command removes all LVM logical volumes, volume groups
 and physical volumes.");
 
   ("file", (RString "description", [String "path"]), 49, [],
-   [InitBasicFS, TestOutput (
+   [InitBasicFS, Always, TestOutput (
       [["touch"; "/new"];
        ["file"; "/new"]], "empty");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "some content\n"; "0"];
        ["file"; "/new"]], "ASCII text");
-    InitBasicFS, TestLastFail (
+    InitBasicFS, Always, TestLastFail (
       [["file"; "/nofile"]])],
    "determine file type",
    "\
@@ -1148,8 +1349,55 @@ The exact command which runs is C<file -bsL path>.  Note in
 particular that the filename is not prepended to the output
 (the C<-b> option).");
 
-  ("command", (RString "output", [StringList "arguments"]), 50, [],
-   [], (* XXX how to test? *)
+  ("command", (RString "output", [StringList "arguments"]), 50, [ProtocolLimitWarning],
+   [InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command"; "/test-command 1"]], "Result1");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command"; "/test-command 2"]], "Result2\n");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command"; "/test-command 3"]], "\nResult3");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command"; "/test-command 4"]], "\nResult4\n");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command"; "/test-command 5"]], "\nResult5\n\n");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command"; "/test-command 6"]], "\n\nResult6\n\n");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command"; "/test-command 7"]], "");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command"; "/test-command 8"]], "\n");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command"; "/test-command 9"]], "\n\n");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command"; "/test-command 10"]], "Result10-1\nResult10-2\n");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command"; "/test-command 11"]], "Result11-1\nResult11-2");
+    InitBasicFS, Always, TestLastFail (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command"; "/test-command"]])],
    "run a command from the guest filesystem",
    "\
 This call runs a command from the guest filesystem.  The
@@ -1160,7 +1408,16 @@ or compatible processor architecture).
 The single parameter is an argv-style list of arguments.
 The first element is the name of the program to run.
 Subsequent elements are parameters.  The list must be
-non-empty (ie. must contain a program name).
+non-empty (ie. must contain a program name).  Note that
+the command runs directly, and is I<not> invoked via
+the shell (see C<guestfs_sh>).
+
+The return value is anything printed to I<stdout> by
+the command.
+
+If the command returns a non-zero exit status, then
+this function returns an error message.  The error message
+string is the content of I<stderr> from the command.
 
 The C<$PATH> environment variable will contain at least
 C</usr/bin> and C</bin>.  If you require a program from
@@ -1173,15 +1430,60 @@ correct places.  It is the caller's responsibility to ensure
 all filesystems that are needed are mounted at the right
 locations.");
 
-  ("command_lines", (RStringList "lines", [StringList "arguments"]), 51, [],
-   [], (* XXX how to test? *)
+  ("command_lines", (RStringList "lines", [StringList "arguments"]), 51, [ProtocolLimitWarning],
+   [InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command_lines"; "/test-command 1"]], ["Result1"]);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command_lines"; "/test-command 2"]], ["Result2"]);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command_lines"; "/test-command 3"]], ["";"Result3"]);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command_lines"; "/test-command 4"]], ["";"Result4"]);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command_lines"; "/test-command 5"]], ["";"Result5";""]);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command_lines"; "/test-command 6"]], ["";"";"Result6";""]);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command_lines"; "/test-command 7"]], []);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command_lines"; "/test-command 8"]], [""]);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command_lines"; "/test-command 9"]], ["";""]);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command_lines"; "/test-command 10"]], ["Result10-1";"Result10-2"]);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "0o755"; "/test-command"];
+       ["command_lines"; "/test-command 11"]], ["Result11-1";"Result11-2"])],
    "run a command, returning lines",
    "\
 This is the same as C<guestfs_command>, but splits the
-result into a list of lines.");
+result into a list of lines.
+
+See also: C<guestfs_sh_lines>");
 
-  ("stat", (RStat "statbuf", [String "path"]), 52, [],
-   [InitBasicFS, TestOutputStruct (
+  ("stat", (RStruct ("statbuf", "stat"), [String "path"]), 52, [],
+   [InitBasicFS, Always, TestOutputStruct (
       [["touch"; "/new"];
        ["stat"; "/new"]], [CompareWithInt ("size", 0)])],
    "get file information",
@@ -1190,8 +1492,8 @@ Returns file information for the given C<path>.
 
 This is the same as the C<stat(2)> system call.");
 
-  ("lstat", (RStat "statbuf", [String "path"]), 53, [],
-   [InitBasicFS, TestOutputStruct (
+  ("lstat", (RStruct ("statbuf", "stat"), [String "path"]), 53, [],
+   [InitBasicFS, Always, TestOutputStruct (
       [["touch"; "/new"];
        ["lstat"; "/new"]], [CompareWithInt ("size", 0)])],
    "get file information for a symbolic link",
@@ -1204,10 +1506,9 @@ refers to.
 
 This is the same as the C<lstat(2)> system call.");
 
-  ("statvfs", (RStatVFS "statbuf", [String "path"]), 54, [],
-   [InitBasicFS, TestOutputStruct (
-      [["statvfs"; "/"]], [CompareWithInt ("bfree", 487702);
-                          CompareWithInt ("blocks", 490020);
+  ("statvfs", (RStruct ("statbuf", "statvfs"), [String "path"]), 54, [],
+   [InitBasicFS, Always, TestOutputStruct (
+      [["statvfs"; "/"]], [CompareWithInt ("namemax", 255);
                           CompareWithInt ("bsize", 1024)])],
    "get file system statistics",
    "\
@@ -1230,7 +1531,7 @@ clearly defined, and depends on both the version of C<tune2fs>
 that libguestfs was built against, and the filesystem itself.");
 
   ("blockdev_setro", (RErr, [String "device"]), 56, [],
-   [InitEmpty, TestOutputTrue (
+   [InitEmpty, Always, TestOutputTrue (
       [["blockdev_setro"; "/dev/sda"];
        ["blockdev_getro"; "/dev/sda"]])],
    "set block device to read-only",
@@ -1240,7 +1541,7 @@ Sets the block device named C<device> to read-only.
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_setrw", (RErr, [String "device"]), 57, [],
-   [InitEmpty, TestOutputFalse (
+   [InitEmpty, Always, TestOutputFalse (
       [["blockdev_setrw"; "/dev/sda"];
        ["blockdev_getro"; "/dev/sda"]])],
    "set block device to read-write",
@@ -1250,7 +1551,7 @@ Sets the block device named C<device> to read-write.
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_getro", (RBool "ro", [String "device"]), 58, [],
-   [InitEmpty, TestOutputTrue (
+   [InitEmpty, Always, TestOutputTrue (
       [["blockdev_setro"; "/dev/sda"];
        ["blockdev_getro"; "/dev/sda"]])],
    "is block device set to read-only",
@@ -1261,7 +1562,7 @@ Returns a boolean indicating if the block device is read-only
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_getss", (RInt "sectorsize", [String "device"]), 59, [],
-   [InitEmpty, TestOutputInt (
+   [InitEmpty, Always, TestOutputInt (
       [["blockdev_getss"; "/dev/sda"]], 512)],
    "get sectorsize of block device",
    "\
@@ -1274,7 +1575,7 @@ for that).
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_getbsz", (RInt "blocksize", [String "device"]), 60, [],
-   [InitEmpty, TestOutputInt (
+   [InitEmpty, Always, TestOutputInt (
       [["blockdev_getbsz"; "/dev/sda"]], 4096)],
    "get blocksize of block device",
    "\
@@ -1297,7 +1598,7 @@ I<filesystem block size>).
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_getsz", (RInt64 "sizeinsectors", [String "device"]), 62, [],
-   [InitEmpty, TestOutputInt (
+   [InitEmpty, Always, TestOutputInt (
       [["blockdev_getsz"; "/dev/sda"]], 1024000)],
    "get total size of device in 512-byte sectors",
    "\
@@ -1311,7 +1612,7 @@ useful I<size in bytes>.
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_getsize64", (RInt64 "sizeinbytes", [String "device"]), 63, [],
-   [InitEmpty, TestOutputInt (
+   [InitEmpty, Always, TestOutputInt (
       [["blockdev_getsize64"; "/dev/sda"]], 524288000)],
    "get total size of device in bytes",
    "\
@@ -1322,7 +1623,7 @@ See also C<guestfs_blockdev_getsz>.
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_flushbufs", (RErr, [String "device"]), 64, [],
-   [InitEmpty, TestRun
+   [InitEmpty, Always, TestRun
       [["blockdev_flushbufs"; "/dev/sda"]]],
    "flush device buffers",
    "\
@@ -1332,7 +1633,7 @@ with C<device>.
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_rereadpt", (RErr, [String "device"]), 65, [],
-   [InitEmpty, TestRun
+   [InitEmpty, Always, TestRun
       [["blockdev_rereadpt"; "/dev/sda"]]],
    "reread partition table",
    "\
@@ -1341,9 +1642,9 @@ Reread the partition table on C<device>.
 This uses the L<blockdev(8)> command.");
 
   ("upload", (RErr, [FileIn "filename"; String "remotefilename"]), 66, [],
-   [InitBasicFS, TestOutput (
+   [InitBasicFS, Always, TestOutput (
       (* Pick a file from cwd which isn't likely to change. *)
-    [["upload"; "COPYING.LIB"; "/COPYING.LIB"];
+    [["upload"; "../COPYING.LIB"; "/COPYING.LIB"];
      ["checksum"; "md5"; "/COPYING.LIB"]], "e3eda01d9815f8d24aae2dbd89b68b06")],
    "upload a file from the local machine",
    "\
@@ -1355,9 +1656,9 @@ C<filename> can also be a named pipe.
 See also C<guestfs_download>.");
 
   ("download", (RErr, [String "remotefilename"; FileOut "filename"]), 67, [],
-   [InitBasicFS, TestOutput (
+   [InitBasicFS, Always, TestOutput (
       (* Pick a file from cwd which isn't likely to change. *)
-    [["upload"; "COPYING.LIB"; "/COPYING.LIB"];
+    [["upload"; "../COPYING.LIB"; "/COPYING.LIB"];
      ["download"; "/COPYING.LIB"; "testdownload.tmp"];
      ["upload"; "testdownload.tmp"; "/upload"];
      ["checksum"; "md5"; "/upload"]], "e3eda01d9815f8d24aae2dbd89b68b06")],
@@ -1371,29 +1672,35 @@ C<filename> can also be a named pipe.
 See also C<guestfs_upload>, C<guestfs_cat>.");
 
   ("checksum", (RString "checksum", [String "csumtype"; String "path"]), 68, [],
-   [InitBasicFS, TestOutput (
+   [InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "test\n"; "0"];
        ["checksum"; "crc"; "/new"]], "935282863");
-    InitBasicFS, TestLastFail (
+    InitBasicFS, Always, TestLastFail (
       [["checksum"; "crc"; "/new"]]);
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "test\n"; "0"];
        ["checksum"; "md5"; "/new"]], "d8e8fca2dc0f896fd7cb4cb0031ba249");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "test\n"; "0"];
        ["checksum"; "sha1"; "/new"]], "4e1243bd22c66e76c2ba9eddc1f91394e57f9f83");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "test\n"; "0"];
        ["checksum"; "sha224"; "/new"]], "52f1bf093f4b7588726035c176c0cdb4376cfea53819f1395ac9e6ec");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "test\n"; "0"];
        ["checksum"; "sha256"; "/new"]], "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "test\n"; "0"];
        ["checksum"; "sha384"; "/new"]], "109bb6b5b6d5547c1ce03c7a8bd7d8f80c1cb0957f50c4f7fda04692079917e4f9cad52b878f3d8234e1a170b154b72d");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "test\n"; "0"];
-       ["checksum"; "sha512"; "/new"]], "0e3e75234abc68f4378a86b3f4b32a198ba301845b0cd6e50106e874345700cc6663a86c1ea125dc5e92be17c98f9a0f85ca9d5f595db2012f7cc3571945c123")],
+       ["checksum"; "sha512"; "/new"]], "0e3e75234abc68f4378a86b3f4b32a198ba301845b0cd6e50106e874345700cc6663a86c1ea125dc5e92be17c98f9a0f85ca9d5f595db2012f7cc3571945c123");
+    InitBasicFS, Always, TestOutput (
+      (* RHEL 5 thinks this is an HFS+ filesystem unless we give
+       * the type explicitly.
+       *)
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["checksum"; "md5"; "/known-3"]], "46d6ca27ee07cdc6fa99c2e138cc522c")],
    "compute MD5, SHAx or CRC checksum of file",
    "\
 This call computes the MD5, SHAx or CRC checksum of the
@@ -1438,8 +1745,8 @@ Compute the SHA512 hash (using the C<sha512sum> program).
 The checksum is returned as a printable string.");
 
   ("tar_in", (RErr, [FileIn "tarfile"; String "directory"]), 69, [],
-   [InitBasicFS, TestOutput (
-      [["tar_in"; "images/helloworld.tar"; "/"];
+   [InitBasicFS, Always, TestOutput (
+      [["tar_in"; "../images/helloworld.tar"; "/"];
        ["cat"; "/hello"]], "hello\n")],
    "unpack tarfile to directory",
    "\
@@ -1458,8 +1765,8 @@ it to local file C<tarfile>.
 To download a compressed tarball, use C<guestfs_tgz_out>.");
 
   ("tgz_in", (RErr, [FileIn "tarball"; String "directory"]), 71, [],
-   [InitBasicFS, TestOutput (
-      [["tgz_in"; "images/helloworld.tar.gz"; "/"];
+   [InitBasicFS, Always, TestOutput (
+      [["tgz_in"; "../images/helloworld.tar.gz"; "/"];
        ["cat"; "/hello"]], "hello\n")],
    "unpack compressed tarball to directory",
    "\
@@ -1478,11 +1785,11 @@ it to local file C<tarball>.
 To download an uncompressed tarball, use C<guestfs_tar_out>.");
 
   ("mount_ro", (RErr, [String "device"; String "mountpoint"]), 73, [],
-   [InitBasicFS, TestLastFail (
+   [InitBasicFS, Always, TestLastFail (
       [["umount"; "/"];
        ["mount_ro"; "/dev/sda1"; "/"];
        ["touch"; "/new"]]);
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "data"; "0"];
        ["umount"; "/"];
        ["mount_ro"; "/dev/sda1"; "/"];
@@ -1521,23 +1828,26 @@ to look at the file C<daemon/debug.c> in the libguestfs source
 to find out what you can do.");
 
   ("lvremove", (RErr, [String "device"]), 77, [],
-   [InitEmpty, TestOutputList (
-      [["pvcreate"; "/dev/sda"];
-       ["vgcreate"; "VG"; "/dev/sda"];
+   [InitEmpty, Always, TestOutputList (
+      [["sfdiskM"; "/dev/sda"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["lvremove"; "/dev/VG/LV1"];
        ["lvs"]], ["/dev/VG/LV2"]);
-    InitEmpty, TestOutputList (
-      [["pvcreate"; "/dev/sda"];
-       ["vgcreate"; "VG"; "/dev/sda"];
+    InitEmpty, Always, TestOutputList (
+      [["sfdiskM"; "/dev/sda"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["lvremove"; "/dev/VG"];
        ["lvs"]], []);
-    InitEmpty, TestOutputList (
-      [["pvcreate"; "/dev/sda"];
-       ["vgcreate"; "VG"; "/dev/sda"];
+    InitEmpty, Always, TestOutputList (
+      [["sfdiskM"; "/dev/sda"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["lvremove"; "/dev/VG"];
@@ -1551,16 +1861,18 @@ You can also remove all LVs in a volume group by specifying
 the VG name, C</dev/VG>.");
 
   ("vgremove", (RErr, [String "vgname"]), 78, [],
-   [InitEmpty, TestOutputList (
-      [["pvcreate"; "/dev/sda"];
-       ["vgcreate"; "VG"; "/dev/sda"];
+   [InitEmpty, Always, TestOutputList (
+      [["sfdiskM"; "/dev/sda"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["vgremove"; "VG"];
        ["lvs"]], []);
-    InitEmpty, TestOutputList (
-      [["pvcreate"; "/dev/sda"];
-       ["vgcreate"; "VG"; "/dev/sda"];
+    InitEmpty, Always, TestOutputList (
+      [["sfdiskM"; "/dev/sda"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["vgremove"; "VG"];
@@ -1573,29 +1885,32 @@ This also forcibly removes all logical volumes in the volume
 group (if any).");
 
   ("pvremove", (RErr, [String "device"]), 79, [],
-   [InitEmpty, TestOutputList (
-      [["pvcreate"; "/dev/sda"];
-       ["vgcreate"; "VG"; "/dev/sda"];
+   [InitEmpty, Always, TestOutputListOfDevices (
+      [["sfdiskM"; "/dev/sda"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["vgremove"; "VG"];
-       ["pvremove"; "/dev/sda"];
+       ["pvremove"; "/dev/sda1"];
        ["lvs"]], []);
-    InitEmpty, TestOutputList (
-      [["pvcreate"; "/dev/sda"];
-       ["vgcreate"; "VG"; "/dev/sda"];
+    InitEmpty, Always, TestOutputListOfDevices (
+      [["sfdiskM"; "/dev/sda"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["vgremove"; "VG"];
-       ["pvremove"; "/dev/sda"];
+       ["pvremove"; "/dev/sda1"];
        ["vgs"]], []);
-    InitEmpty, TestOutputList (
-      [["pvcreate"; "/dev/sda"];
-       ["vgcreate"; "VG"; "/dev/sda"];
+    InitEmpty, Always, TestOutputListOfDevices (
+      [["sfdiskM"; "/dev/sda"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["vgremove"; "VG"];
-       ["pvremove"; "/dev/sda"];
+       ["pvremove"; "/dev/sda1"];
        ["pvs"]], [])],
    "remove an LVM physical volume",
    "\
@@ -1607,7 +1922,7 @@ wipe physical volumes that contain any volume groups, so you have
 to remove those first.");
 
   ("set_e2label", (RErr, [String "device"; String "label"]), 80, [],
-   [InitBasicFS, TestOutput (
+   [InitBasicFS, Always, TestOutput (
       [["set_e2label"; "/dev/sda1"; "testlabel"];
        ["get_e2label"; "/dev/sda1"]], "testlabel")],
    "set the ext2/3/4 filesystem label",
@@ -1627,16 +1942,16 @@ This returns the ext2/3/4 filesystem label of the filesystem on
 C<device>.");
 
   ("set_e2uuid", (RErr, [String "device"; String "uuid"]), 82, [],
-   [InitBasicFS, TestOutput (
+   [InitBasicFS, Always, TestOutput (
       [["set_e2uuid"; "/dev/sda1"; "a3a61220-882b-4f61-89f4-cf24dcc7297d"];
        ["get_e2uuid"; "/dev/sda1"]], "a3a61220-882b-4f61-89f4-cf24dcc7297d");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["set_e2uuid"; "/dev/sda1"; "clear"];
        ["get_e2uuid"; "/dev/sda1"]], "");
     (* We can't predict what UUIDs will be, so just check the commands run. *)
-    InitBasicFS, TestRun (
+    InitBasicFS, Always, TestRun (
       [["set_e2uuid"; "/dev/sda1"; "random"]]);
-    InitBasicFS, TestRun (
+    InitBasicFS, Always, TestRun (
       [["set_e2uuid"; "/dev/sda1"; "time"]])],
    "set the ext2/3/4 filesystem UUID",
    "\
@@ -1656,10 +1971,10 @@ This returns the ext2/3/4 filesystem UUID of the filesystem on
 C<device>.");
 
   ("fsck", (RInt "status", [String "fstype"; String "device"]), 84, [],
-   [InitBasicFS, TestOutputInt (
+   [InitBasicFS, Always, TestOutputInt (
       [["umount"; "/dev/sda1"];
        ["fsck"; "ext2"; "/dev/sda1"]], 0);
-    InitBasicFS, TestOutputInt (
+    InitBasicFS, Always, TestOutputInt (
       [["umount"; "/dev/sda1"];
        ["zero"; "/dev/sda1"];
        ["fsck"; "ext2"; "/dev/sda1"]], 8)],
@@ -1694,7 +2009,7 @@ Checking or repairing NTFS volumes is not supported
 This command is entirely equivalent to running C<fsck -a -t fstype device>.");
 
   ("zero", (RErr, [String "device"]), 85, [],
-   [InitBasicFS, TestOutput (
+   [InitBasicFS, Always, TestOutput (
       [["umount"; "/dev/sda1"];
        ["zero"; "/dev/sda1"];
        ["file"; "/dev/sda1"]], "data")],
@@ -1704,10 +2019,15 @@ This command writes zeroes over the first few blocks of C<device>.
 
 How many blocks are zeroed isn't specified (but it's I<not> enough
 to securely wipe the device).  It should be sufficient to remove
-any partition tables, filesystem superblocks and so on.");
+any partition tables, filesystem superblocks and so on.
+
+See also: C<guestfs_scrub_device>.");
 
   ("grub_install", (RErr, [String "root"; String "device"]), 86, [],
-   [InitBasicFS, TestOutputTrue (
+   (* Test disabled because grub-install incompatible with virtio-blk driver.
+    * See also: https://bugzilla.redhat.com/show_bug.cgi?id=479760
+    *)
+   [InitBasicFS, Disabled, TestOutputTrue (
       [["grub_install"; "/"; "/dev/sda1"];
        ["is_dir"; "/boot"]])],
    "install GRUB",
@@ -1716,15 +2036,15 @@ This command installs GRUB (the Grand Unified Bootloader) on
 C<device>, with the root directory being C<root>.");
 
   ("cp", (RErr, [String "src"; String "dest"]), 87, [],
-   [InitBasicFS, TestOutput (
+   [InitBasicFS, Always, TestOutput (
       [["write_file"; "/old"; "file content"; "0"];
        ["cp"; "/old"; "/new"];
        ["cat"; "/new"]], "file content");
-    InitBasicFS, TestOutputTrue (
+    InitBasicFS, Always, TestOutputTrue (
       [["write_file"; "/old"; "file content"; "0"];
        ["cp"; "/old"; "/new"];
        ["is_file"; "/old"]]);
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/old"; "file content"; "0"];
        ["mkdir"; "/dir"];
        ["cp"; "/old"; "/dir/new"];
@@ -1735,7 +2055,7 @@ This copies a file from C<src> to C<dest> where C<dest> is
 either a destination filename or destination directory.");
 
   ("cp_a", (RErr, [String "src"; String "dest"]), 88, [],
-   [InitBasicFS, TestOutput (
+   [InitBasicFS, Always, TestOutput (
       [["mkdir"; "/olddir"];
        ["mkdir"; "/newdir"];
        ["write_file"; "/olddir/file"; "file content"; "0"];
@@ -1747,11 +2067,11 @@ This copies a file or directory from C<src> to C<dest>
 recursively using the C<cp -a> command.");
 
   ("mv", (RErr, [String "src"; String "dest"]), 89, [],
-   [InitBasicFS, TestOutput (
+   [InitBasicFS, Always, TestOutput (
       [["write_file"; "/old"; "file content"; "0"];
        ["mv"; "/old"; "/new"];
        ["cat"; "/new"]], "file content");
-    InitBasicFS, TestOutputFalse (
+    InitBasicFS, Always, TestOutputFalse (
       [["write_file"; "/old"; "file content"; "0"];
        ["mv"; "/old"; "/new"];
        ["is_file"; "/old"]])],
@@ -1761,7 +2081,7 @@ This moves a file from C<src> to C<dest> where C<dest> is
 either a destination filename or destination directory.");
 
   ("drop_caches", (RErr, [Int "whattodrop"]), 90, [],
-   [InitEmpty, TestRun (
+   [InitEmpty, Always, TestRun (
       [["drop_caches"; "3"]])],
    "drop kernel page cache, dentries and inodes",
    "\
@@ -1776,7 +2096,7 @@ This automatically calls L<sync(2)> before the operation,
 so that the maximum guest memory is freed.");
 
   ("dmesg", (RString "kmsgs", []), 91, [],
-   [InitEmpty, TestRun (
+   [InitEmpty, Always, TestRun (
       [["dmesg"]])],
    "return kernel messages",
    "\
@@ -1790,7 +2110,7 @@ the environment variable C<LIBGUESTFS_DEBUG=1> before
 running the program.");
 
   ("ping_daemon", (RErr, []), 92, [],
-   [InitEmpty, TestRun (
+   [InitEmpty, Always, TestRun (
       [["ping_daemon"]])],
    "ping the guest daemon",
    "\
@@ -1800,15 +2120,15 @@ daemon responds to the ping message, without affecting the daemon
 or attached block device(s) in any other way.");
 
   ("equal", (RBool "equality", [String "file1"; String "file2"]), 93, [],
-   [InitBasicFS, TestOutputTrue (
+   [InitBasicFS, Always, TestOutputTrue (
       [["write_file"; "/file1"; "contents of a file"; "0"];
        ["cp"; "/file1"; "/file2"];
        ["equal"; "/file1"; "/file2"]]);
-    InitBasicFS, TestOutputFalse (
+    InitBasicFS, Always, TestOutputFalse (
       [["write_file"; "/file1"; "contents of a file"; "0"];
        ["write_file"; "/file2"; "contents of another file"; "0"];
        ["equal"; "/file1"; "/file2"]]);
-    InitBasicFS, TestLastFail (
+    InitBasicFS, Always, TestLastFail (
       [["equal"; "/file1"; "/file2"]])],
    "test if two files have equal contents",
    "\
@@ -1818,21 +2138,24 @@ true if their content is exactly equal, or false otherwise.
 The external L<cmp(1)> program is used for the comparison.");
 
   ("strings", (RStringList "stringsout", [String "path"]), 94, [ProtocolLimitWarning],
-   [InitBasicFS, TestOutputList (
+   [InitBasicFS, Always, TestOutputList (
       [["write_file"; "/new"; "hello\nworld\n"; "0"];
-       ["strings"; "/new"]], ["hello"; "world"])],
+       ["strings"; "/new"]], ["hello"; "world"]);
+    InitBasicFS, Always, TestOutputList (
+      [["touch"; "/new"];
+       ["strings"; "/new"]], [])],
    "print the printable strings in a file",
    "\
 This runs the L<strings(1)> command on a file and returns
 the list of printable strings found.");
 
   ("strings_e", (RStringList "stringsout", [String "encoding"; String "path"]), 95, [ProtocolLimitWarning],
-   [InitBasicFS, TestOutputList (
+   [InitBasicFS, Always, TestOutputList (
       [["write_file"; "/new"; "hello\nworld\n"; "0"];
        ["strings_e"; "b"; "/new"]], []);
-    (*InitBasicFS, TestOutputList (
+    InitBasicFS, Disabled, TestOutputList (
       [["write_file"; "/new"; "\000h\000e\000l\000l\000o\000\n\000w\000o\000r\000l\000d\000\n"; "24"];
-       ["strings_e"; "b"; "/new"]], ["hello"; "world"])*)],
+       ["strings_e"; "b"; "/new"]], ["hello"; "world"])],
    "print the printable strings in a file",
    "\
 This is like the C<guestfs_strings> command, but allows you to
@@ -1846,14 +2169,620 @@ show strings inside Windows/x86 files.
 The returned strings are transcoded to UTF-8.");
 
   ("hexdump", (RString "dump", [String "path"]), 96, [ProtocolLimitWarning],
-   [InitBasicFS, TestOutput (
+   [InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "hello\nworld\n"; "12"];
-       ["hexdump"; "/new"]], "00000000  68 65 6c 6c 6f 0a 77 6f  72 6c 64 0a              |hello.world.|\n0000000c\n")],
+       ["hexdump"; "/new"]], "00000000  68 65 6c 6c 6f 0a 77 6f  72 6c 64 0a              |hello.world.|\n0000000c\n");
+    (* Test for RHBZ#501888c2 regression which caused large hexdump
+     * commands to segfault.
+     *)
+    InitBasicFS, Always, TestRun (
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["hexdump"; "/100krandom"]])],
    "dump a file in hexadecimal",
    "\
 This runs C<hexdump -C> on the given C<path>.  The result is
 the human-readable, canonical hex dump of the file.");
 
+  ("zerofree", (RErr, [String "device"]), 97, [],
+   [InitNone, Always, TestOutput (
+      [["sfdiskM"; "/dev/sda"; ","];
+       ["mkfs"; "ext3"; "/dev/sda1"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["write_file"; "/new"; "test file"; "0"];
+       ["umount"; "/dev/sda1"];
+       ["zerofree"; "/dev/sda1"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["cat"; "/new"]], "test file")],
+   "zero unused inodes and disk blocks on ext2/3 filesystem",
+   "\
+This runs the I<zerofree> program on C<device>.  This program
+claims to zero unused inodes and disk blocks on an ext2/3
+filesystem, thus making it possible to compress the filesystem
+more effectively.
+
+You should B<not> run this program if the filesystem is
+mounted.
+
+It is possible that using this program can damage the filesystem
+or data on the filesystem.");
+
+  ("pvresize", (RErr, [String "device"]), 98, [],
+   [],
+   "resize an LVM physical volume",
+   "\
+This resizes (expands or shrinks) an existing LVM physical
+volume to match the new size of the underlying device.");
+
+  ("sfdisk_N", (RErr, [String "device"; Int "partnum";
+                      Int "cyls"; Int "heads"; Int "sectors";
+                      String "line"]), 99, [DangerWillRobinson],
+   [],
+   "modify a single partition on a block device",
+   "\
+This runs L<sfdisk(8)> option to modify just the single
+partition C<n> (note: C<n> counts from 1).
+
+For other parameters, see C<guestfs_sfdisk>.  You should usually
+pass C<0> for the cyls/heads/sectors parameters.");
+
+  ("sfdisk_l", (RString "partitions", [String "device"]), 100, [],
+   [],
+   "display the partition table",
+   "\
+This displays the partition table on C<device>, in the
+human-readable output of the L<sfdisk(8)> command.  It is
+not intended to be parsed.");
+
+  ("sfdisk_kernel_geometry", (RString "partitions", [String "device"]), 101, [],
+   [],
+   "display the kernel geometry",
+   "\
+This displays the kernel's idea of the geometry of C<device>.
+
+The result is in human-readable format, and not designed to
+be parsed.");
+
+  ("sfdisk_disk_geometry", (RString "partitions", [String "device"]), 102, [],
+   [],
+   "display the disk geometry from the partition table",
+   "\
+This displays the disk geometry of C<device> read from the
+partition table.  Especially in the case where the underlying
+block device has been resized, this can be different from the
+kernel's idea of the geometry (see C<guestfs_sfdisk_kernel_geometry>).
+
+The result is in human-readable format, and not designed to
+be parsed.");
+
+  ("vg_activate_all", (RErr, [Bool "activate"]), 103, [],
+   [],
+   "activate or deactivate all volume groups",
+   "\
+This command activates or (if C<activate> is false) deactivates
+all logical volumes in all volume groups.
+If activated, then they are made known to the
+kernel, ie. they appear as C</dev/mapper> devices.  If deactivated,
+then those devices disappear.
+
+This command is the same as running C<vgchange -a y|n>");
+
+  ("vg_activate", (RErr, [Bool "activate"; StringList "volgroups"]), 104, [],
+   [],
+   "activate or deactivate some volume groups",
+   "\
+This command activates or (if C<activate> is false) deactivates
+all logical volumes in the listed volume groups C<volgroups>.
+If activated, then they are made known to the
+kernel, ie. they appear as C</dev/mapper> devices.  If deactivated,
+then those devices disappear.
+
+This command is the same as running C<vgchange -a y|n volgroups...>
+
+Note that if C<volgroups> is an empty list then B<all> volume groups
+are activated or deactivated.");
+
+  ("lvresize", (RErr, [String "device"; Int "mbytes"]), 105, [],
+   [InitNone, Always, TestOutput (
+    [["sfdiskM"; "/dev/sda"; ","];
+     ["pvcreate"; "/dev/sda1"];
+     ["vgcreate"; "VG"; "/dev/sda1"];
+     ["lvcreate"; "LV"; "VG"; "10"];
+     ["mkfs"; "ext2"; "/dev/VG/LV"];
+     ["mount"; "/dev/VG/LV"; "/"];
+     ["write_file"; "/new"; "test content"; "0"];
+     ["umount"; "/"];
+     ["lvresize"; "/dev/VG/LV"; "20"];
+     ["e2fsck_f"; "/dev/VG/LV"];
+     ["resize2fs"; "/dev/VG/LV"];
+     ["mount"; "/dev/VG/LV"; "/"];
+     ["cat"; "/new"]], "test content")],
+   "resize an LVM logical volume",
+   "\
+This resizes (expands or shrinks) an existing LVM logical
+volume to C<mbytes>.  When reducing, data in the reduced part
+is lost.");
+
+  ("resize2fs", (RErr, [String "device"]), 106, [],
+   [], (* lvresize tests this *)
+   "resize an ext2/ext3 filesystem",
+   "\
+This resizes an ext2 or ext3 filesystem to match the size of
+the underlying device.
+
+I<Note:> It is sometimes required that you run C<guestfs_e2fsck_f>
+on the C<device> before calling this command.  For unknown reasons
+C<resize2fs> sometimes gives an error about this and sometimes not.
+In any case, it is always safe to call C<guestfs_e2fsck_f> before
+calling this function.");
+
+  ("find", (RStringList "names", [String "directory"]), 107, [],
+   [InitBasicFS, Always, TestOutputList (
+      [["find"; "/"]], ["lost+found"]);
+    InitBasicFS, Always, TestOutputList (
+      [["touch"; "/a"];
+       ["mkdir"; "/b"];
+       ["touch"; "/b/c"];
+       ["find"; "/"]], ["a"; "b"; "b/c"; "lost+found"]);
+    InitBasicFS, Always, TestOutputList (
+      [["mkdir_p"; "/a/b/c"];
+       ["touch"; "/a/b/c/d"];
+       ["find"; "/a/b/"]], ["c"; "c/d"])],
+   "find all files and directories",
+   "\
+This command lists out all files and directories, recursively,
+starting at C<directory>.  It is essentially equivalent to
+running the shell command C<find directory -print> but some
+post-processing happens on the output, described below.
+
+This returns a list of strings I<without any prefix>.  Thus
+if the directory structure was:
+
+ /tmp/a
+ /tmp/b
+ /tmp/c/d
+
+then the returned list from C<guestfs_find> C</tmp> would be
+4 elements:
+
+ a
+ b
+ c
+ c/d
+
+If C<directory> is not a directory, then this command returns
+an error.
+
+The returned list is sorted.");
+
+  ("e2fsck_f", (RErr, [String "device"]), 108, [],
+   [], (* lvresize tests this *)
+   "check an ext2/ext3 filesystem",
+   "\
+This runs C<e2fsck -p -f device>, ie. runs the ext2/ext3
+filesystem checker on C<device>, noninteractively (C<-p>),
+even if the filesystem appears to be clean (C<-f>).
+
+This command is only needed because of C<guestfs_resize2fs>
+(q.v.).  Normally you should use C<guestfs_fsck>.");
+
+  ("sleep", (RErr, [Int "secs"]), 109, [],
+   [InitNone, Always, TestRun (
+    [["sleep"; "1"]])],
+   "sleep for some seconds",
+   "\
+Sleep for C<secs> seconds.");
+
+  ("ntfs_3g_probe", (RInt "status", [Bool "rw"; String "device"]), 110, [],
+   [InitNone, Always, TestOutputInt (
+      [["sfdiskM"; "/dev/sda"; ","];
+       ["mkfs"; "ntfs"; "/dev/sda1"];
+       ["ntfs_3g_probe"; "true"; "/dev/sda1"]], 0);
+    InitNone, Always, TestOutputInt (
+      [["sfdiskM"; "/dev/sda"; ","];
+       ["mkfs"; "ext2"; "/dev/sda1"];
+       ["ntfs_3g_probe"; "true"; "/dev/sda1"]], 12)],
+   "probe NTFS volume",
+   "\
+This command runs the L<ntfs-3g.probe(8)> command which probes
+an NTFS C<device> for mountability.  (Not all NTFS volumes can
+be mounted read-write, and some cannot be mounted at all).
+
+C<rw> is a boolean flag.  Set it to true if you want to test
+if the volume can be mounted read-write.  Set it to false if
+you want to test if the volume can be mounted read-only.
+
+The return value is an integer which C<0> if the operation
+would succeed, or some non-zero value documented in the
+L<ntfs-3g.probe(8)> manual page.");
+
+  ("sh", (RString "output", [String "command"]), 111, [],
+   [], (* XXX needs tests *)
+   "run a command via the shell",
+   "\
+This call runs a command from the guest filesystem via the
+guest's C</bin/sh>.
+
+This is like C<guestfs_command>, but passes the command to:
+
+ /bin/sh -c \"command\"
+
+Depending on the guest's shell, this usually results in
+wildcards being expanded, shell expressions being interpolated
+and so on.
+
+All the provisos about C<guestfs_command> apply to this call.");
+
+  ("sh_lines", (RStringList "lines", [String "command"]), 112, [],
+   [], (* XXX needs tests *)
+   "run a command via the shell returning lines",
+   "\
+This is the same as C<guestfs_sh>, but splits the result
+into a list of lines.
+
+See also: C<guestfs_command_lines>");
+
+  ("glob_expand", (RStringList "paths", [String "pattern"]), 113, [],
+   [InitBasicFS, Always, TestOutputList (
+      [["mkdir_p"; "/a/b/c"];
+       ["touch"; "/a/b/c/d"];
+       ["touch"; "/a/b/c/e"];
+       ["glob_expand"; "/a/b/c/*"]], ["/a/b/c/d"; "/a/b/c/e"]);
+    InitBasicFS, Always, TestOutputList (
+      [["mkdir_p"; "/a/b/c"];
+       ["touch"; "/a/b/c/d"];
+       ["touch"; "/a/b/c/e"];
+       ["glob_expand"; "/a/*/c/*"]], ["/a/b/c/d"; "/a/b/c/e"]);
+    InitBasicFS, Always, TestOutputList (
+      [["mkdir_p"; "/a/b/c"];
+       ["touch"; "/a/b/c/d"];
+       ["touch"; "/a/b/c/e"];
+       ["glob_expand"; "/a/*/x/*"]], [])],
+   "expand a wildcard path",
+   "\
+This command searches for all the pathnames matching
+C<pattern> according to the wildcard expansion rules
+used by the shell.
+
+If no paths match, then this returns an empty list
+(note: not an error).
+
+It is just a wrapper around the C L<glob(3)> function
+with flags C<GLOB_MARK|GLOB_BRACE>.
+See that manual page for more details.");
+
+  ("scrub_device", (RErr, [String "device"]), 114, [DangerWillRobinson],
+   [InitNone, Always, TestRun (        (* use /dev/sdc because it's smaller *)
+      [["scrub_device"; "/dev/sdc"]])],
+   "scrub (securely wipe) a device",
+   "\
+This command writes patterns over C<device> to make data retrieval
+more difficult.
+
+It is an interface to the L<scrub(1)> program.  See that
+manual page for more details.");
+
+  ("scrub_file", (RErr, [String "file"]), 115, [],
+   [InitBasicFS, Always, TestRun (
+      [["write_file"; "/file"; "content"; "0"];
+       ["scrub_file"; "/file"]])],
+   "scrub (securely wipe) a file",
+   "\
+This command writes patterns over a file to make data retrieval
+more difficult.
+
+The file is I<removed> after scrubbing.
+
+It is an interface to the L<scrub(1)> program.  See that
+manual page for more details.");
+
+  ("scrub_freespace", (RErr, [String "dir"]), 116, [],
+   [], (* XXX needs testing *)
+   "scrub (securely wipe) free space",
+   "\
+This command creates the directory C<dir> and then fills it
+with files until the filesystem is full, and scrubs the files
+as for C<guestfs_scrub_file>, and deletes them.
+The intention is to scrub any free space on the partition
+containing C<dir>.
+
+It is an interface to the L<scrub(1)> program.  See that
+manual page for more details.");
+
+  ("mkdtemp", (RString "dir", [String "template"]), 117, [],
+   [InitBasicFS, Always, TestRun (
+      [["mkdir"; "/tmp"];
+       ["mkdtemp"; "/tmp/tmpXXXXXX"]])],
+   "create a temporary directory",
+   "\
+This command creates a temporary directory.  The
+C<template> parameter should be a full pathname for the
+temporary directory name with the final six characters being
+\"XXXXXX\".
+
+For example: \"/tmp/myprogXXXXXX\" or \"/Temp/myprogXXXXXX\",
+the second one being suitable for Windows filesystems.
+
+The name of the temporary directory that was created
+is returned.
+
+The temporary directory is created with mode 0700
+and is owned by root.
+
+The caller is responsible for deleting the temporary
+directory and its contents after use.
+
+See also: L<mkdtemp(3)>");
+
+  ("wc_l", (RInt "lines", [String "path"]), 118, [],
+   [InitBasicFS, Always, TestOutputInt (
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["wc_l"; "/10klines"]], 10000)],
+   "count lines in a file",
+   "\
+This command counts the lines in a file, using the
+C<wc -l> external command.");
+
+  ("wc_w", (RInt "words", [String "path"]), 119, [],
+   [InitBasicFS, Always, TestOutputInt (
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["wc_w"; "/10klines"]], 10000)],
+   "count words in a file",
+   "\
+This command counts the words in a file, using the
+C<wc -w> external command.");
+
+  ("wc_c", (RInt "chars", [String "path"]), 120, [],
+   [InitBasicFS, Always, TestOutputInt (
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["wc_c"; "/100kallspaces"]], 102400)],
+   "count characters in a file",
+   "\
+This command counts the characters in a file, using the
+C<wc -c> external command.");
+
+  ("head", (RStringList "lines", [String "path"]), 121, [ProtocolLimitWarning],
+   [InitBasicFS, Always, TestOutputList (
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["head"; "/10klines"]], ["0abcdefghijklmnopqrstuvwxyz";"1abcdefghijklmnopqrstuvwxyz";"2abcdefghijklmnopqrstuvwxyz";"3abcdefghijklmnopqrstuvwxyz";"4abcdefghijklmnopqrstuvwxyz";"5abcdefghijklmnopqrstuvwxyz";"6abcdefghijklmnopqrstuvwxyz";"7abcdefghijklmnopqrstuvwxyz";"8abcdefghijklmnopqrstuvwxyz";"9abcdefghijklmnopqrstuvwxyz"])],
+   "return first 10 lines of a file",
+   "\
+This command returns up to the first 10 lines of a file as
+a list of strings.");
+
+  ("head_n", (RStringList "lines", [Int "nrlines"; String "path"]), 122, [ProtocolLimitWarning],
+   [InitBasicFS, Always, TestOutputList (
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["head_n"; "3"; "/10klines"]], ["0abcdefghijklmnopqrstuvwxyz";"1abcdefghijklmnopqrstuvwxyz";"2abcdefghijklmnopqrstuvwxyz"]);
+    InitBasicFS, Always, TestOutputList (
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["head_n"; "-9997"; "/10klines"]], ["0abcdefghijklmnopqrstuvwxyz";"1abcdefghijklmnopqrstuvwxyz";"2abcdefghijklmnopqrstuvwxyz"]);
+    InitBasicFS, Always, TestOutputList (
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["head_n"; "0"; "/10klines"]], [])],
+   "return first N lines of a file",
+   "\
+If the parameter C<nrlines> is a positive number, this returns the first
+C<nrlines> lines of the file C<path>.
+
+If the parameter C<nrlines> is a negative number, this returns lines
+from the file C<path>, excluding the last C<nrlines> lines.
+
+If the parameter C<nrlines> is zero, this returns an empty list.");
+
+  ("tail", (RStringList "lines", [String "path"]), 123, [ProtocolLimitWarning],
+   [InitBasicFS, Always, TestOutputList (
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["tail"; "/10klines"]], ["9990abcdefghijklmnopqrstuvwxyz";"9991abcdefghijklmnopqrstuvwxyz";"9992abcdefghijklmnopqrstuvwxyz";"9993abcdefghijklmnopqrstuvwxyz";"9994abcdefghijklmnopqrstuvwxyz";"9995abcdefghijklmnopqrstuvwxyz";"9996abcdefghijklmnopqrstuvwxyz";"9997abcdefghijklmnopqrstuvwxyz";"9998abcdefghijklmnopqrstuvwxyz";"9999abcdefghijklmnopqrstuvwxyz"])],
+   "return last 10 lines of a file",
+   "\
+This command returns up to the last 10 lines of a file as
+a list of strings.");
+
+  ("tail_n", (RStringList "lines", [Int "nrlines"; String "path"]), 124, [ProtocolLimitWarning],
+   [InitBasicFS, Always, TestOutputList (
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["tail_n"; "3"; "/10klines"]], ["9997abcdefghijklmnopqrstuvwxyz";"9998abcdefghijklmnopqrstuvwxyz";"9999abcdefghijklmnopqrstuvwxyz"]);
+    InitBasicFS, Always, TestOutputList (
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["tail_n"; "-9998"; "/10klines"]], ["9997abcdefghijklmnopqrstuvwxyz";"9998abcdefghijklmnopqrstuvwxyz";"9999abcdefghijklmnopqrstuvwxyz"]);
+    InitBasicFS, Always, TestOutputList (
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["tail_n"; "0"; "/10klines"]], [])],
+   "return last N lines of a file",
+   "\
+If the parameter C<nrlines> is a positive number, this returns the last
+C<nrlines> lines of the file C<path>.
+
+If the parameter C<nrlines> is a negative number, this returns lines
+from the file C<path>, starting with the C<-nrlines>th line.
+
+If the parameter C<nrlines> is zero, this returns an empty list.");
+
+  ("df", (RString "output", []), 125, [],
+   [], (* XXX Tricky to test because it depends on the exact format
+       * of the 'df' command and other imponderables.
+       *)
+   "report file system disk space usage",
+   "\
+This command runs the C<df> command to report disk space used.
+
+This command is mostly useful for interactive sessions.  It
+is I<not> intended that you try to parse the output string.
+Use C<statvfs> from programs.");
+
+  ("df_h", (RString "output", []), 126, [],
+   [], (* XXX Tricky to test because it depends on the exact format
+       * of the 'df' command and other imponderables.
+       *)
+   "report file system disk space usage (human readable)",
+   "\
+This command runs the C<df -h> command to report disk space used
+in human-readable format.
+
+This command is mostly useful for interactive sessions.  It
+is I<not> intended that you try to parse the output string.
+Use C<statvfs> from programs.");
+
+  ("du", (RInt64 "sizekb", [String "path"]), 127, [],
+   [InitBasicFS, Always, TestOutputInt (
+      [["mkdir"; "/p"];
+       ["du"; "/p"]], 1 (* ie. 1 block, so depends on ext3 blocksize *))],
+   "estimate file space usage",
+   "\
+This command runs the C<du -s> command to estimate file space
+usage for C<path>.
+
+C<path> can be a file or a directory.  If C<path> is a directory
+then the estimate includes the contents of the directory and all
+subdirectories (recursively).
+
+The result is the estimated size in I<kilobytes>
+(ie. units of 1024 bytes).");
+
+  ("initrd_list", (RStringList "filenames", [String "path"]), 128, [],
+   [InitBasicFS, Always, TestOutputList (
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["initrd_list"; "/initrd"]], ["empty";"known-1";"known-2";"known-3"])],
+   "list files in an initrd",
+   "\
+This command lists out files contained in an initrd.
+
+The files are listed without any initial C</> character.  The
+files are listed in the order they appear (not necessarily
+alphabetical).  Directory names are listed as separate items.
+
+Old Linux kernels (2.4 and earlier) used a compressed ext2
+filesystem as initrd.  We I<only> support the newer initramfs
+format (compressed cpio files).");
+
+  ("mount_loop", (RErr, [String "file"; String "mountpoint"]), 129, [],
+   [],
+   "mount a file using the loop device",
+   "\
+This command lets you mount C<file> (a filesystem image
+in a file) on a mount point.  It is entirely equivalent to
+the command C<mount -o loop file mountpoint>.");
+
+  ("mkswap", (RErr, [String "device"]), 130, [],
+   [InitEmpty, Always, TestRun (
+      [["sfdiskM"; "/dev/sda"; ","];
+       ["mkswap"; "/dev/sda1"]])],
+   "create a swap partition",
+   "\
+Create a swap partition on C<device>.");
+
+  ("mkswap_L", (RErr, [String "label"; String "device"]), 131, [],
+   [InitEmpty, Always, TestRun (
+      [["sfdiskM"; "/dev/sda"; ","];
+       ["mkswap_L"; "hello"; "/dev/sda1"]])],
+   "create a swap partition with a label",
+   "\
+Create a swap partition on C<device> with label C<label>.");
+
+  ("mkswap_U", (RErr, [String "uuid"; String "device"]), 132, [],
+   [InitEmpty, Always, TestRun (
+      [["sfdiskM"; "/dev/sda"; ","];
+       ["mkswap_U"; "a3a61220-882b-4f61-89f4-cf24dcc7297d"; "/dev/sda1"]])],
+   "create a swap partition with an explicit UUID",
+   "\
+Create a swap partition on C<device> with UUID C<uuid>.");
+
+  ("mknod", (RErr, [Int "mode"; Int "devmajor"; Int "devminor"; String "path"]), 133, [],
+   [InitBasicFS, Always, TestOutputStruct (
+      [["mknod"; "0o10777"; "0"; "0"; "/node"];
+       (* NB: default umask 022 means 0777 -> 0755 in these tests *)
+       ["stat"; "/node"]], [CompareWithInt ("mode", 0o10755)]);
+    InitBasicFS, Always, TestOutputStruct (
+      [["mknod"; "0o60777"; "66"; "99"; "/node"];
+       ["stat"; "/node"]], [CompareWithInt ("mode", 0o60755)])],
+   "make block, character or FIFO devices",
+   "\
+This call creates block or character special devices, or
+named pipes (FIFOs).
+
+The C<mode> parameter should be the mode, using the standard
+constants.  C<devmajor> and C<devminor> are the
+device major and minor numbers, only used when creating block
+and character special devices.");
+
+  ("mkfifo", (RErr, [Int "mode"; String "path"]), 134, [],
+   [InitBasicFS, Always, TestOutputStruct (
+      [["mkfifo"; "0o777"; "/node"];
+       ["stat"; "/node"]], [CompareWithInt ("mode", 0o10755)])],
+   "make FIFO (named pipe)",
+   "\
+This call creates a FIFO (named pipe) called C<path> with
+mode C<mode>.  It is just a convenient wrapper around
+C<guestfs_mknod>.");
+
+  ("mknod_b", (RErr, [Int "mode"; Int "devmajor"; Int "devminor"; String "path"]), 135, [],
+   [InitBasicFS, Always, TestOutputStruct (
+      [["mknod_b"; "0o777"; "99"; "66"; "/node"];
+       ["stat"; "/node"]], [CompareWithInt ("mode", 0o60755)])],
+   "make block device node",
+   "\
+This call creates a block device node called C<path> with
+mode C<mode> and device major/minor C<devmajor> and C<devminor>.
+It is just a convenient wrapper around C<guestfs_mknod>.");
+
+  ("mknod_c", (RErr, [Int "mode"; Int "devmajor"; Int "devminor"; String "path"]), 136, [],
+   [InitBasicFS, Always, TestOutputStruct (
+      [["mknod_c"; "0o777"; "99"; "66"; "/node"];
+       ["stat"; "/node"]], [CompareWithInt ("mode", 0o20755)])],
+   "make char device node",
+   "\
+This call creates a char device node called C<path> with
+mode C<mode> and device major/minor C<devmajor> and C<devminor>.
+It is just a convenient wrapper around C<guestfs_mknod>.");
+
+  ("umask", (RInt "oldmask", [Int "mask"]), 137, [],
+   [], (* XXX umask is one of those stateful things that we should
+       * reset between each test.
+       *)
+   "set file mode creation mask (umask)",
+   "\
+This function sets the mask used for creating new files and
+device nodes to C<mask & 0777>.
+
+Typical umask values would be C<022> which creates new files
+with permissions like \"-rw-r--r--\" or \"-rwxr-xr-x\", and
+C<002> which creates new files with permissions like
+\"-rw-rw-r--\" or \"-rwxrwxr-x\".
+
+The default umask is C<022>.  This is important because it
+means that directories and device nodes will be created with
+C<0644> or C<0755> mode even if you specify C<0777>.
+
+See also L<umask(2)>, C<guestfs_mknod>, C<guestfs_mkdir>.
+
+This call returns the previous umask.");
+
+  ("readdir", (RStructList ("entries", "dirent"), [String "dir"]), 138, [],
+   [],
+   "read directories entries",
+   "\
+This returns the list of directory entries in directory C<dir>.
+
+All entries in the directory are returned, including C<.> and
+C<..>.  The entries are I<not> sorted, but returned in the same
+order as the underlying filesystem.
+
+This function is primarily intended for use by programs.  To
+get a simple list of names, use C<guestfs_ls>.  To get a printable
+directory for human consumption, use C<guestfs_ll>.");
+
+  ("sfdiskM", (RErr, [String "device"; StringList "lines"]), 139, [DangerWillRobinson],
+   [],
+   "create partitions on a block device",
+   "\
+This is a simplified interface to the C<guestfs_sfdisk>
+command, where partition sizes are specified in megabytes
+only (rounded to the nearest cylinder) and you don't need
+to specify the cyls, heads and sectors parameters which
+were rarely if ever used anyway.
+
+See also C<guestfs_sfdisk> and the L<sfdisk(8)> manpage.");
+
 ]
 
 let all_functions = non_daemon_functions @ daemon_functions
@@ -1865,102 +2794,174 @@ let all_functions_sorted =
   List.sort (fun (n1,_,_,_,_,_,_) (n2,_,_,_,_,_,_) ->
               compare n1 n2) all_functions
 
-(* Column names and types from LVM PVs/VGs/LVs. *)
-let pv_cols = [
-  "pv_name", `String;
-  "pv_uuid", `UUID;
-  "pv_fmt", `String;
-  "pv_size", `Bytes;
-  "dev_size", `Bytes;
-  "pv_free", `Bytes;
-  "pv_used", `Bytes;
-  "pv_attr", `String (* XXX *);
-  "pv_pe_count", `Int;
-  "pv_pe_alloc_count", `Int;
-  "pv_tags", `String;
-  "pe_start", `Bytes;
-  "pv_mda_count", `Int;
-  "pv_mda_free", `Bytes;
-(* Not in Fedora 10:
-  "pv_mda_size", `Bytes;
-*)
+(* Field types for structures. *)
+type field =
+  | FChar                      (* C 'char' (really, a 7 bit byte). *)
+  | FString                    (* nul-terminated ASCII string. *)
+  | FUInt32
+  | FInt32
+  | FUInt64
+  | FInt64
+  | FBytes                     (* Any int measure that counts bytes. *)
+  | FUUID                      (* 32 bytes long, NOT nul-terminated. *)
+  | FOptPercent                        (* [0..100], or -1 meaning "not present". *)
+
+(* Because we generate extra parsing code for LVM command line tools,
+ * we have to pull out the LVM columns separately here.
+ *)
+let lvm_pv_cols = [
+  "pv_name", FString;
+  "pv_uuid", FUUID;
+  "pv_fmt", FString;
+  "pv_size", FBytes;
+  "dev_size", FBytes;
+  "pv_free", FBytes;
+  "pv_used", FBytes;
+  "pv_attr", FString (* XXX *);
+  "pv_pe_count", FInt64;
+  "pv_pe_alloc_count", FInt64;
+  "pv_tags", FString;
+  "pe_start", FBytes;
+  "pv_mda_count", FInt64;
+  "pv_mda_free", FBytes;
+  (* Not in Fedora 10:
+     "pv_mda_size", FBytes;
+  *)
 ]
-let vg_cols = [
-  "vg_name", `String;
-  "vg_uuid", `UUID;
-  "vg_fmt", `String;
-  "vg_attr", `String (* XXX *);
-  "vg_size", `Bytes;
-  "vg_free", `Bytes;
-  "vg_sysid", `String;
-  "vg_extent_size", `Bytes;
-  "vg_extent_count", `Int;
-  "vg_free_count", `Int;
-  "max_lv", `Int;
-  "max_pv", `Int;
-  "pv_count", `Int;
-  "lv_count", `Int;
-  "snap_count", `Int;
-  "vg_seqno", `Int;
-  "vg_tags", `String;
-  "vg_mda_count", `Int;
-  "vg_mda_free", `Bytes;
-(* Not in Fedora 10:
-  "vg_mda_size", `Bytes;
-*)
+let lvm_vg_cols = [
+  "vg_name", FString;
+  "vg_uuid", FUUID;
+  "vg_fmt", FString;
+  "vg_attr", FString (* XXX *);
+  "vg_size", FBytes;
+  "vg_free", FBytes;
+  "vg_sysid", FString;
+  "vg_extent_size", FBytes;
+  "vg_extent_count", FInt64;
+  "vg_free_count", FInt64;
+  "max_lv", FInt64;
+  "max_pv", FInt64;
+  "pv_count", FInt64;
+  "lv_count", FInt64;
+  "snap_count", FInt64;
+  "vg_seqno", FInt64;
+  "vg_tags", FString;
+  "vg_mda_count", FInt64;
+  "vg_mda_free", FBytes;
+  (* Not in Fedora 10:
+     "vg_mda_size", FBytes;
+  *)
 ]
-let lv_cols = [
-  "lv_name", `String;
-  "lv_uuid", `UUID;
-  "lv_attr", `String (* XXX *);
-  "lv_major", `Int;
-  "lv_minor", `Int;
-  "lv_kernel_major", `Int;
-  "lv_kernel_minor", `Int;
-  "lv_size", `Bytes;
-  "seg_count", `Int;
-  "origin", `String;
-  "snap_percent", `OptPercent;
-  "copy_percent", `OptPercent;
-  "move_pv", `String;
-  "lv_tags", `String;
-  "mirror_log", `String;
-  "modules", `String;
+let lvm_lv_cols = [
+  "lv_name", FString;
+  "lv_uuid", FUUID;
+  "lv_attr", FString (* XXX *);
+  "lv_major", FInt64;
+  "lv_minor", FInt64;
+  "lv_kernel_major", FInt64;
+  "lv_kernel_minor", FInt64;
+  "lv_size", FBytes;
+  "seg_count", FInt64;
+  "origin", FString;
+  "snap_percent", FOptPercent;
+  "copy_percent", FOptPercent;
+  "move_pv", FString;
+  "lv_tags", FString;
+  "mirror_log", FString;
+  "modules", FString;
 ]
 
-(* Column names and types from stat structures.
- * NB. Can't use things like 'st_atime' because glibc header files
- * define some of these as macros.  Ugh.
+(* Names and fields in all structures (in RStruct and RStructList)
+ * that we support.
  *)
-let stat_cols = [
-  "dev", `Int;
-  "ino", `Int;
-  "mode", `Int;
-  "nlink", `Int;
-  "uid", `Int;
-  "gid", `Int;
-  "rdev", `Int;
-  "size", `Int;
-  "blksize", `Int;
-  "blocks", `Int;
-  "atime", `Int;
-  "mtime", `Int;
-  "ctime", `Int;
-]
-let statvfs_cols = [
-  "bsize", `Int;
-  "frsize", `Int;
-  "blocks", `Int;
-  "bfree", `Int;
-  "bavail", `Int;
-  "files", `Int;
-  "ffree", `Int;
-  "favail", `Int;
-  "fsid", `Int;
-  "flag", `Int;
-  "namemax", `Int;
+let structs = [
+  (* The old RIntBool return type, only ever used for aug_defnode.  Do
+   * not use this struct in any new code.
+   *)
+  "int_bool", [
+    "i", FInt32;               (* for historical compatibility *)
+    "b", FInt32;               (* for historical compatibility *)
+  ];
+
+  (* LVM PVs, VGs, LVs. *)
+  "lvm_pv", lvm_pv_cols;
+  "lvm_vg", lvm_vg_cols;
+  "lvm_lv", lvm_lv_cols;
+
+  (* Column names and types from stat structures.
+   * NB. Can't use things like 'st_atime' because glibc header files
+   * define some of these as macros.  Ugh.
+   *)
+  "stat", [
+    "dev", FInt64;
+    "ino", FInt64;
+    "mode", FInt64;
+    "nlink", FInt64;
+    "uid", FInt64;
+    "gid", FInt64;
+    "rdev", FInt64;
+    "size", FInt64;
+    "blksize", FInt64;
+    "blocks", FInt64;
+    "atime", FInt64;
+    "mtime", FInt64;
+    "ctime", FInt64;
+  ];
+  "statvfs", [
+    "bsize", FInt64;
+    "frsize", FInt64;
+    "blocks", FInt64;
+    "bfree", FInt64;
+    "bavail", FInt64;
+    "files", FInt64;
+    "ffree", FInt64;
+    "favail", FInt64;
+    "fsid", FInt64;
+    "flag", FInt64;
+    "namemax", FInt64;
+  ];
+
+  (* Column names in dirent structure. *)
+  "dirent", [
+    "ino", FInt64;
+    (* 'b' 'c' 'd' 'f' (FIFO) 'l' 'r' (regular file) 's' 'u' '?' *)
+    "ftyp", FChar;
+    "name", FString;
+  ];
+] (* end of structs *)
+
+(* Ugh, Java has to be different ..
+ * These names are also used by the Haskell bindings.
+ *)
+let java_structs = [
+  "int_bool", "IntBool";
+  "lvm_pv", "PV";
+  "lvm_vg", "VG";
+  "lvm_lv", "LV";
+  "stat", "Stat";
+  "statvfs", "StatVFS";
+  "dirent", "Dirent"
 ]
 
+(* Used for testing language bindings. *)
+type callt =
+  | CallString of string
+  | CallOptString of string option
+  | CallStringList of string list
+  | CallInt of int
+  | CallBool of bool
+
+(* Used to memoize the result of pod2text. *)
+let pod2text_memo_filename = "src/.pod2text.data"
+let pod2text_memo : ((int * string * string), string list) Hashtbl.t =
+  try
+    let chan = open_in pod2text_memo_filename in
+    let v = input_value chan in
+    close_in chan;
+    v
+  with
+    _ -> Hashtbl.create 13
+
 (* Useful functions.
  * Note we don't want to use any external OCaml libraries which
  * makes this a bit harder than it should be.
@@ -2076,8 +3077,20 @@ let name_of_argt = function
   | String n | OptString n | StringList n | Bool n | Int n
   | FileIn n | FileOut n -> n
 
+let java_name_of_struct typ =
+  try List.assoc typ java_structs
+  with Not_found ->
+    failwithf
+      "java_name_of_struct: no java_structs entry corresponding to %s" typ
+
+let cols_of_struct typ =
+  try List.assoc typ structs
+  with Not_found ->
+    failwithf "cols_of_struct: unknown struct %s" typ
+
 let seq_of_test = function
   | TestRun s | TestOutput (s, _) | TestOutputList (s, _)
+  | TestOutputListOfDevices (s, _)
   | TestOutputInt (s, _) | TestOutputTrue s | TestOutputFalse s
   | TestOutputLength (s, _) | TestOutputStruct (s, _)
   | TestLastFail s -> s
@@ -2102,8 +3115,10 @@ let check_functions () =
     fun (name, _, _, _, _, _, _) ->
       if String.length name >= 7 && String.sub name 0 7 = "guestfs" then
        failwithf "function name %s does not need 'guestfs' prefix" name;
-      if contains_uppercase name then
-       failwithf "function name %s should not contain uppercase chars" name;
+      if name = "" then
+       failwithf "function name is empty";
+      if name.[0] < 'a' || name.[0] > 'z' then
+       failwithf "function name %s must start with lowercase a-z" name;
       if String.contains name '-' then
        failwithf "function name %s should not contain '-', use '_' instead."
          name
@@ -2120,21 +3135,21 @@ let check_functions () =
          failwithf "%s param/ret %s should not contain '-' or '_'"
            name n;
        if n = "value" then
-         failwithf "%s has a param/ret called 'value', which causes conflicts in the OCaml bindings, use something like 'val' or a more descriptive name" n;
+         failwithf "%s has a param/ret called 'value', which causes conflicts in the OCaml bindings, use something like 'val' or a more descriptive name" name;
+       if n = "int" || n = "char" || n = "short" || n = "long" then
+         failwithf "%s has a param/ret which conflicts with a C type (eg. 'int', 'char' etc.)" name;
+       if n = "i" || n = "n" then
+         failwithf "%s has a param/ret called 'i' or 'n', which will cause some conflicts in the generated code" name;
        if n = "argv" || n = "args" then
-         failwithf "%s has a param/ret called 'argv' or 'args', which will cause some conflicts in the generated code" n
+         failwithf "%s has a param/ret called 'argv' or 'args', which will cause some conflicts in the generated code" name
       in
 
       (match fst style with
        | RErr -> ()
        | RInt n | RInt64 n | RBool n | RConstString n | RString n
-       | RStringList n | RPVList n | RVGList n | RLVList n
-       | RStat n | RStatVFS n
+       | RStringList n | RStruct (n, _) | RStructList (n, _)
        | RHashtable n ->
           check_arg_ret_name n
-       | RIntBool (n,m) ->
-          check_arg_ret_name n;
-          check_arg_ret_name m
       );
       List.iter (fun arg -> check_arg_ret_name (name_of_argt arg)) (snd style)
   ) all_functions;
@@ -2195,7 +3210,7 @@ let check_functions () =
     | name, _, _, _, tests, _, _ ->
        let funcs =
          List.map (
-           fun (_, test) ->
+           fun (_, _, test) ->
              match seq_of_test test with
              | [] ->
                  failwithf "%s has a test containing an empty sequence" name
@@ -2274,102 +3289,89 @@ let generate_header comment license =
 let rec generate_actions_pod () =
   List.iter (
     fun (shortname, style, _, flags, _, _, longdesc) ->
-      let name = "guestfs_" ^ shortname in
-      pr "=head2 %s\n\n" name;
-      pr " ";
-      generate_prototype ~extern:false ~handle:"handle" name style;
-      pr "\n\n";
-      pr "%s\n\n" longdesc;
-      (match fst style with
-       | RErr ->
-          pr "This function returns 0 on success or -1 on error.\n\n"
-       | RInt _ ->
-          pr "On error this function returns -1.\n\n"
-       | RInt64 _ ->
-          pr "On error this function returns -1.\n\n"
-       | RBool _ ->
-          pr "This function returns a C truth value on success or -1 on error.\n\n"
-       | RConstString _ ->
-          pr "This function returns a string, or NULL on error.
+      if not (List.mem NotInDocs flags) then (
+       let name = "guestfs_" ^ shortname in
+       pr "=head2 %s\n\n" name;
+       pr " ";
+       generate_prototype ~extern:false ~handle:"handle" name style;
+       pr "\n\n";
+       pr "%s\n\n" longdesc;
+       (match fst style with
+        | RErr ->
+            pr "This function returns 0 on success or -1 on error.\n\n"
+        | RInt _ ->
+            pr "On error this function returns -1.\n\n"
+        | RInt64 _ ->
+            pr "On error this function returns -1.\n\n"
+        | RBool _ ->
+            pr "This function returns a C truth value on success or -1 on error.\n\n"
+        | RConstString _ ->
+            pr "This function returns a string, or NULL on error.
 The string is owned by the guest handle and must I<not> be freed.\n\n"
-       | RString _ ->
-          pr "This function returns a string, or NULL on error.
+        | RString _ ->
+            pr "This function returns a string, or NULL on error.
 I<The caller must free the returned string after use>.\n\n"
-       | RStringList _ ->
-          pr "This function returns a NULL-terminated array of strings
+        | RStringList _ ->
+            pr "This function returns a NULL-terminated array of strings
 (like L<environ(3)>), or NULL if there was an error.
 I<The caller must free the strings and the array after use>.\n\n"
-       | RIntBool _ ->
-          pr "This function returns a C<struct guestfs_int_bool *>,
-or NULL if there was an error.
-I<The caller must call C<guestfs_free_int_bool> after use>.\n\n"
-       | RPVList _ ->
-          pr "This function returns a C<struct guestfs_lvm_pv_list *>
-(see E<lt>guestfs-structs.hE<gt>),
+        | RStruct (_, typ) ->
+            pr "This function returns a C<struct guestfs_%s *>,
 or NULL if there was an error.
-I<The caller must call C<guestfs_free_lvm_pv_list> after use>.\n\n"
-       | RVGList _ ->
-          pr "This function returns a C<struct guestfs_lvm_vg_list *>
+I<The caller must call C<guestfs_free_%s> after use>.\n\n" typ typ
+        | RStructList (_, typ) ->
+            pr "This function returns a C<struct guestfs_%s_list *>
 (see E<lt>guestfs-structs.hE<gt>),
 or NULL if there was an error.
-I<The caller must call C<guestfs_free_lvm_vg_list> after use>.\n\n"
-       | RLVList _ ->
-          pr "This function returns a C<struct guestfs_lvm_lv_list *>
-(see E<lt>guestfs-structs.hE<gt>),
-or NULL if there was an error.
-I<The caller must call C<guestfs_free_lvm_lv_list> after use>.\n\n"
-       | RStat _ ->
-          pr "This function returns a C<struct guestfs_stat *>
-(see L<stat(2)> and E<lt>guestfs-structs.hE<gt>),
-or NULL if there was an error.
-I<The caller must call C<free> after use>.\n\n"
-       | RStatVFS _ ->
-          pr "This function returns a C<struct guestfs_statvfs *>
-(see L<statvfs(2)> and E<lt>guestfs-structs.hE<gt>),
-or NULL if there was an error.
-I<The caller must call C<free> after use>.\n\n"
-       | RHashtable _ ->
-          pr "This function returns a NULL-terminated array of
+I<The caller must call C<guestfs_free_%s_list> after use>.\n\n" typ typ
+        | RHashtable _ ->
+            pr "This function returns a NULL-terminated array of
 strings, or NULL if there was an error.
 The array of strings will always have length C<2n+1>, where
 C<n> keys and values alternate, followed by the trailing NULL entry.
 I<The caller must free the strings and the array after use>.\n\n"
-      );
-      if List.mem ProtocolLimitWarning flags then
-       pr "%s\n\n" protocol_limit_warning;
-      if List.mem DangerWillRobinson flags then
-       pr "%s\n\n" danger_will_robinson;
+       );
+       if List.mem ProtocolLimitWarning flags then
+         pr "%s\n\n" protocol_limit_warning;
+       if List.mem DangerWillRobinson flags then
+         pr "%s\n\n" danger_will_robinson
+      )
   ) all_functions_sorted
 
 and generate_structs_pod () =
-  (* LVM structs documentation. *)
+  (* Structs documentation. *)
   List.iter (
     fun (typ, cols) ->
-      pr "=head2 guestfs_lvm_%s\n" typ;
+      pr "=head2 guestfs_%s\n" typ;
       pr "\n";
-      pr " struct guestfs_lvm_%s {\n" typ;
+      pr " struct guestfs_%s {\n" typ;
       List.iter (
        function
-       | name, `String -> pr "  char *%s;\n" name
-       | name, `UUID ->
-           pr "  /* The next field is NOT nul-terminated, be careful when printing it: */\n";
-           pr "  char %s[32];\n" name
-       | name, `Bytes -> pr "  uint64_t %s;\n" name
-       | name, `Int -> pr "  int64_t %s;\n" name
-       | name, `OptPercent ->
-           pr "  /* The next field is [0..100] or -1 meaning 'not present': */\n";
-           pr "  float %s;\n" name
+       | name, FChar -> pr "   char %s;\n" name
+       | name, FUInt32 -> pr "   uint32_t %s;\n" name
+       | name, FInt32 -> pr "   int32_t %s;\n" name
+       | name, (FUInt64|FBytes) -> pr "   uint64_t %s;\n" name
+       | name, FInt64 -> pr "   int64_t %s;\n" name
+       | name, FString -> pr "   char *%s;\n" name
+       | name, FUUID ->
+           pr "   /* The next field is NOT nul-terminated, be careful when printing it: */\n";
+           pr "   char %s[32];\n" name
+       | name, FOptPercent ->
+           pr "   /* The next field is [0..100] or -1 meaning 'not present': */\n";
+           pr "   float %s;\n" name
       ) cols;
+      pr " };\n";
       pr " \n";
-      pr " struct guestfs_lvm_%s_list {\n" typ;
+      pr " struct guestfs_%s_list {\n" typ;
       pr "   uint32_t len; /* Number of elements in list. */\n";
-      pr "   struct guestfs_lvm_%s *val; /* Elements. */\n" typ;
+      pr "   struct guestfs_%s *val; /* Elements. */\n" typ;
       pr " };\n";
       pr " \n";
-      pr " void guestfs_free_lvm_%s_list (struct guestfs_free_lvm_%s_list *);\n"
+      pr " void guestfs_free_%s (struct guestfs_free_%s *);\n" typ typ;
+      pr " void guestfs_free_%s_list (struct guestfs_free_%s_list *);\n"
        typ typ;
       pr "\n"
-  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols]
+  ) structs
 
 (* Generate the protocol (XDR) file, 'guestfs_protocol.x' and
  * indirectly 'guestfs_protocol.h' and 'guestfs_protocol.c'.
@@ -2386,35 +3388,24 @@ and generate_xdr () =
   pr "typedef string str<>;\n";
   pr "\n";
 
-  (* LVM internal structures. *)
+  (* Internal structures. *)
   List.iter (
     function
     | typ, cols ->
-       pr "struct guestfs_lvm_int_%s {\n" typ;
+       pr "struct guestfs_int_%s {\n" typ;
        List.iter (function
-                  | name, `String -> pr "  string %s<>;\n" name
-                  | name, `UUID -> pr "  opaque %s[32];\n" name
-                  | name, `Bytes -> pr "  hyper %s;\n" name
-                  | name, `Int -> pr "  hyper %s;\n" name
-                  | name, `OptPercent -> pr "  float %s;\n" name
+                  | name, FChar -> pr "  char %s;\n" name
+                  | name, FString -> pr "  string %s<>;\n" name
+                  | name, FUUID -> pr "  opaque %s[32];\n" name
+                  | name, (FInt32|FUInt32) -> pr "  int %s;\n" name
+                  | name, (FInt64|FUInt64|FBytes) -> pr "  hyper %s;\n" name
+                  | name, FOptPercent -> pr "  float %s;\n" name
                  ) cols;
        pr "};\n";
        pr "\n";
-       pr "typedef struct guestfs_lvm_int_%s guestfs_lvm_int_%s_list<>;\n" typ typ;
+       pr "typedef struct guestfs_int_%s guestfs_int_%s_list<>;\n" typ typ;
        pr "\n";
-  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
-
-  (* Stat internal structures. *)
-  List.iter (
-    function
-    | typ, cols ->
-       pr "struct guestfs_int_%s {\n" typ;
-       List.iter (function
-                  | name, `Int -> pr "  hyper %s;\n" name
-                 ) cols;
-       pr "};\n";
-       pr "\n";
-  ) ["stat", stat_cols; "statvfs", statvfs_cols];
+  ) structs;
 
   List.iter (
     fun (shortname, style, _, _, _, _, _) ->
@@ -2459,30 +3450,13 @@ and generate_xdr () =
           pr "struct %s_ret {\n" name;
           pr "  str %s<>;\n" n;
           pr "};\n\n"
-       | RIntBool (n,m) ->
-          pr "struct %s_ret {\n" name;
-          pr "  int %s;\n" n;
-          pr "  bool %s;\n" m;
-          pr "};\n\n"
-       | RPVList n ->
-          pr "struct %s_ret {\n" name;
-          pr "  guestfs_lvm_int_pv_list %s;\n" n;
-          pr "};\n\n"
-       | RVGList n ->
-          pr "struct %s_ret {\n" name;
-          pr "  guestfs_lvm_int_vg_list %s;\n" n;
-          pr "};\n\n"
-       | RLVList n ->
+       | RStruct (n, typ) ->
           pr "struct %s_ret {\n" name;
-          pr "  guestfs_lvm_int_lv_list %s;\n" n;
+          pr "  guestfs_int_%s %s;\n" typ n;
           pr "};\n\n"
-       | RStat n ->
+       | RStructList (n, typ) ->
           pr "struct %s_ret {\n" name;
-          pr "  guestfs_int_stat %s;\n" n;
-          pr "};\n\n"
-       | RStatVFS n ->
-          pr "struct %s_ret {\n" name;
-          pr "  guestfs_int_statvfs %s;\n" n;
+          pr "  guestfs_int_%s_list %s;\n" typ n;
           pr "};\n\n"
        | RHashtable n ->
           pr "struct %s_ret {\n" name;
@@ -2575,47 +3549,32 @@ and generate_structs_h () =
    * must be identical to what rpcgen / the RFC defines.
    *)
 
-  (* guestfs_int_bool structure. *)
-  pr "struct guestfs_int_bool {\n";
-  pr "  int32_t i;\n";
-  pr "  int32_t b;\n";
-  pr "};\n";
-  pr "\n";
-
-  (* LVM public structures. *)
-  List.iter (
-    function
-    | typ, cols ->
-       pr "struct guestfs_lvm_%s {\n" typ;
-       List.iter (
-         function
-         | name, `String -> pr "  char *%s;\n" name
-         | name, `UUID -> pr "  char %s[32]; /* this is NOT nul-terminated, be careful when printing */\n" name
-         | name, `Bytes -> pr "  uint64_t %s;\n" name
-         | name, `Int -> pr "  int64_t %s;\n" name
-         | name, `OptPercent -> pr "  float %s; /* [0..100] or -1 */\n" name
-       ) cols;
-       pr "};\n";
-       pr "\n";
-       pr "struct guestfs_lvm_%s_list {\n" typ;
-       pr "  uint32_t len;\n";
-       pr "  struct guestfs_lvm_%s *val;\n" typ;
-       pr "};\n";
-       pr "\n"
-  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
-
-  (* Stat structures. *)
+  (* Public structures. *)
   List.iter (
-    function
-    | typ, cols ->
-       pr "struct guestfs_%s {\n" typ;
-       List.iter (
-         function
-         | name, `Int -> pr "  int64_t %s;\n" name
-       ) cols;
-       pr "};\n";
-       pr "\n"
-  ) ["stat", stat_cols; "statvfs", statvfs_cols]
+    fun (typ, cols) ->
+      pr "struct guestfs_%s {\n" typ;
+      List.iter (
+       function
+       | name, FChar -> pr "  char %s;\n" name
+       | name, FString -> pr "  char *%s;\n" name
+       | name, FUUID -> pr "  char %s[32]; /* this is NOT nul-terminated, be careful when printing */\n" name
+       | name, FUInt32 -> pr "  uint32_t %s;\n" name
+       | name, FInt32 -> pr "  int32_t %s;\n" name
+       | name, (FUInt64|FBytes) -> pr "  uint64_t %s;\n" name
+       | name, FInt64 -> pr "  int64_t %s;\n" name
+       | name, FOptPercent -> pr "  float %s; /* [0..100] or -1 */\n" name
+      ) cols;
+      pr "};\n";
+      pr "\n";
+      pr "struct guestfs_%s_list {\n" typ;
+      pr "  uint32_t len;\n";
+      pr "  struct guestfs_%s *val;\n" typ;
+      pr "};\n";
+      pr "\n";
+      pr "extern void guestfs_free_%s (struct guestfs_%s *);\n" typ typ;
+      pr "extern void guestfs_free_%s_list (struct guestfs_%s_list *);\n" typ typ;
+      pr "\n"
+  ) structs
 
 (* Generate the guestfs-actions.h file. *)
 and generate_actions_h () =
@@ -2720,9 +3679,7 @@ check_state (guestfs_h *g, const char *caller)
           failwithf "RConstString cannot be returned from a daemon function"
        | RInt _ | RInt64 _
        | RBool _ | RString _ | RStringList _
-       | RIntBool _
-       | RPVList _ | RVGList _ | RLVList _
-       | RStat _ | RStatVFS _
+       | RStruct _ | RStructList _
        | RHashtable _ ->
           pr "  struct %s_ret ret;\n" name
       );
@@ -2763,9 +3720,7 @@ check_state (guestfs_h *g, const char *caller)
           failwithf "RConstString cannot be returned from a daemon function"
        | RInt _ | RInt64 _
        | RBool _ | RString _ | RStringList _
-       | RIntBool _
-       | RPVList _ | RVGList _ | RLVList _
-       | RStat _ | RStatVFS _
+       | RStruct _ | RStructList _
        | RHashtable _ ->
            pr "  if (!xdr_%s_ret (xdr, &ctx->ret)) {\n" name;
            pr "    error (g, \"%%s: failed to parse reply\", \"%s\");\n" name;
@@ -2786,9 +3741,8 @@ check_state (guestfs_h *g, const char *caller)
        | RErr | RInt _ | RInt64 _ | RBool _ -> "-1"
        | RConstString _ ->
            failwithf "RConstString cannot be returned from a daemon function"
-       | RString _ | RStringList _ | RIntBool _
-       | RPVList _ | RVGList _ | RLVList _
-       | RStat _ | RStatVFS _
+       | RString _ | RStringList _
+       | RStruct _ | RStructList _
        | RHashtable _ ->
            "NULL" in
 
@@ -2921,17 +3875,43 @@ check_state (guestfs_h *g, const char *caller)
             n n;
           pr "  ctx.ret.%s.%s_val[ctx.ret.%s.%s_len] = NULL;\n" n n n n;
           pr "  return ctx.ret.%s.%s_val;\n" n n
-       | RIntBool _ ->
-          pr "  /* caller with free this */\n";
-          pr "  return safe_memdup (g, &ctx.ret, sizeof (ctx.ret));\n"
-       | RPVList n | RVGList n | RLVList n
-       | RStat n | RStatVFS n ->
+       | RStruct (n, _) ->
+          pr "  /* caller will free this */\n";
+          pr "  return safe_memdup (g, &ctx.ret.%s, sizeof (ctx.ret.%s));\n" n n
+       | RStructList (n, _) ->
           pr "  /* caller will free this */\n";
           pr "  return safe_memdup (g, &ctx.ret.%s, sizeof (ctx.ret.%s));\n" n n
       );
 
       pr "}\n\n"
-  ) daemon_functions
+  ) daemon_functions;
+
+  (* Functions to free structures. *)
+  pr "/* Structure-freeing functions.  These rely on the fact that the\n";
+  pr " * structure format is identical to the XDR format.  See note in\n";
+  pr " * generator.ml.\n";
+  pr " */\n";
+  pr "\n";
+
+  List.iter (
+    fun (typ, _) ->
+      pr "void\n";
+      pr "guestfs_free_%s (struct guestfs_%s *x)\n" typ typ;
+      pr "{\n";
+      pr "  xdr_free ((xdrproc_t) xdr_guestfs_int_%s, (char *) x);\n" typ;
+      pr "  free (x);\n";
+      pr "}\n";
+      pr "\n";
+
+      pr "void\n";
+      pr "guestfs_free_%s_list (struct guestfs_%s_list *x)\n" typ typ;
+      pr "{\n";
+      pr "  xdr_free ((xdrproc_t) xdr_guestfs_int_%s_list, (char *) x);\n" typ;
+      pr "  free (x);\n";
+      pr "}\n";
+      pr "\n";
+
+  ) structs;
 
 (* Generate daemon/actions.h. *)
 and generate_daemon_actions_h () =
@@ -2980,12 +3960,8 @@ and generate_daemon_actions () =
            failwithf "RConstString cannot be returned from a daemon function"
        | RString _ -> pr "  char *r;\n"; "NULL"
        | RStringList _ | RHashtable _ -> pr "  char **r;\n"; "NULL"
-       | RIntBool _ -> pr "  guestfs_%s_ret *r;\n" name; "NULL"
-       | RPVList _ -> pr "  guestfs_lvm_int_pv_list *r;\n"; "NULL"
-       | RVGList _ -> pr "  guestfs_lvm_int_vg_list *r;\n"; "NULL"
-       | RLVList _ -> pr "  guestfs_lvm_int_lv_list *r;\n"; "NULL"
-       | RStat _ -> pr "  guestfs_int_stat *r;\n"; "NULL"
-       | RStatVFS _ -> pr "  guestfs_int_statvfs *r;\n"; "NULL" in
+       | RStruct (_, typ) -> pr "  guestfs_int_%s *r;\n" typ; "NULL"
+       | RStructList (_, typ) -> pr "  guestfs_int_%s_list *r;\n" typ; "NULL" in
 
       (match snd style with
        | [] -> ()
@@ -2993,8 +3969,12 @@ and generate_daemon_actions () =
           pr "  struct guestfs_%s_args args;\n" name;
           List.iter (
             function
+              (* Note we allow the string to be writable, in order to
+               * allow device name translation.  This is safe because
+               * we can modify the string (passed from RPC).
+               *)
             | String n
-            | OptString n -> pr "  const char *%s;\n" n
+            | OptString n -> pr "  char *%s;\n" n
             | StringList n -> pr "  char **%s;\n" n
             | Bool n -> pr "  int %s;\n" n
             | Int n -> pr "  int %s;\n" n
@@ -3077,12 +4057,14 @@ and generate_daemon_actions () =
            pr "  reply ((xdrproc_t) &xdr_guestfs_%s_ret, (char *) &ret);\n"
              name;
            pr "  free_strings (r);\n"
-       | RIntBool _ ->
-           pr "  reply ((xdrproc_t) xdr_guestfs_%s_ret, (char *) r);\n"
+       | RStruct (n, _) ->
+           pr "  struct guestfs_%s_ret ret;\n" name;
+           pr "  ret.%s = *r;\n" n;
+           pr "  reply ((xdrproc_t) xdr_guestfs_%s_ret, (char *) &ret);\n"
              name;
-           pr "  xdr_free ((xdrproc_t) xdr_guestfs_%s_ret, (char *) r);\n" name
-       | RPVList n | RVGList n | RLVList n
-       | RStat n | RStatVFS n ->
+           pr "  xdr_free ((xdrproc_t) xdr_guestfs_%s_ret, (char *) &ret);\n"
+             name
+       | RStructList (n, _) ->
            pr "  struct guestfs_%s_ret ret;\n" name;
            pr "  ret.%s = *r;\n" n;
            pr "  reply ((xdrproc_t) xdr_guestfs_%s_ret, (char *) &ret);\n"
@@ -3117,7 +4099,7 @@ and generate_daemon_actions () =
   ) daemon_functions;
 
   pr "    default:\n";
-  pr "      reply_with_error (\"dispatch_incoming_message: unknown procedure number %%d\", proc_nr);\n";
+  pr "      reply_with_error (\"dispatch_incoming_message: unknown procedure number %%d, set LIBGUESTFS_PATH to point to the matching libguestfs appliance directory\", proc_nr);\n";
   pr "  }\n";
   pr "}\n";
   pr "\n";
@@ -3133,7 +4115,7 @@ and generate_daemon_actions () =
          typ (String.concat "," (List.map fst cols));
        pr "\n";
 
-       pr "static int lvm_tokenize_%s (char *str, struct guestfs_lvm_int_%s *r)\n" typ typ;
+       pr "static int lvm_tokenize_%s (char *str, guestfs_int_lvm_%s *r)\n" typ typ;
        pr "{\n";
        pr "  char *tok, *p, *next;\n";
        pr "  int i, j;\n";
@@ -3161,13 +4143,13 @@ and generate_daemon_actions () =
            pr "  if (*p) next = p+1; else next = NULL;\n";
            pr "  *p = '\\0';\n";
            (match coltype with
-            | `String ->
+            | FString ->
                 pr "  r->%s = strdup (tok);\n" name;
                 pr "  if (r->%s == NULL) {\n" name;
                 pr "    perror (\"strdup\");\n";
                 pr "    return -1;\n";
                 pr "  }\n"
-            | `UUID ->
+            | FUUID ->
                 pr "  for (i = j = 0; i < 32; ++j) {\n";
                 pr "    if (tok[j] == '\\0') {\n";
                 pr "      fprintf (stderr, \"%%s: failed to parse UUID from '%%s'\\n\", __func__, tok);\n";
@@ -3175,23 +4157,25 @@ and generate_daemon_actions () =
                 pr "    } else if (tok[j] != '-')\n";
                 pr "      r->%s[i++] = tok[j];\n" name;
                 pr "  }\n";
-            | `Bytes ->
+            | FBytes ->
                 pr "  if (sscanf (tok, \"%%\"SCNu64, &r->%s) != 1) {\n" name;
                 pr "    fprintf (stderr, \"%%s: failed to parse size '%%s' from token %%s\\n\", __func__, tok, \"%s\");\n" name;
                 pr "    return -1;\n";
                 pr "  }\n";
-            | `Int ->
+            | FInt64 ->
                 pr "  if (sscanf (tok, \"%%\"SCNi64, &r->%s) != 1) {\n" name;
                 pr "    fprintf (stderr, \"%%s: failed to parse int '%%s' from token %%s\\n\", __func__, tok, \"%s\");\n" name;
                 pr "    return -1;\n";
                 pr "  }\n";
-            | `OptPercent ->
+            | FOptPercent ->
                 pr "  if (tok[0] == '\\0')\n";
                 pr "    r->%s = -1;\n" name;
                 pr "  else if (sscanf (tok, \"%%f\", &r->%s) != 1) {\n" name;
                 pr "    fprintf (stderr, \"%%s: failed to parse float '%%s' from token %%s\\n\", __func__, tok, \"%s\");\n" name;
                 pr "    return -1;\n";
                 pr "  }\n";
+            | FInt32 | FUInt32 | FUInt64 | FChar ->
+                assert false (* can never be an LVM column *)
            );
            pr "  tok = next;\n";
        ) cols;
@@ -3204,13 +4188,13 @@ and generate_daemon_actions () =
        pr "}\n";
        pr "\n";
 
-       pr "guestfs_lvm_int_%s_list *\n" typ;
+       pr "guestfs_int_lvm_%s_list *\n" typ;
        pr "parse_command_line_%ss (void)\n" typ;
        pr "{\n";
        pr "  char *out, *err;\n";
        pr "  char *p, *pend;\n";
        pr "  int r, i;\n";
-       pr "  guestfs_lvm_int_%s_list *ret;\n" typ;
+       pr "  guestfs_int_lvm_%s_list *ret;\n" typ;
        pr "  void *newp;\n";
        pr "\n";
        pr "  ret = malloc (sizeof *ret);\n";
@@ -3219,8 +4203,8 @@ and generate_daemon_actions () =
        pr "    return NULL;\n";
        pr "  }\n";
        pr "\n";
-       pr "  ret->guestfs_lvm_int_%s_list_len = 0;\n" typ;
-       pr "  ret->guestfs_lvm_int_%s_list_val = NULL;\n" typ;
+       pr "  ret->guestfs_int_lvm_%s_list_len = 0;\n" typ;
+       pr "  ret->guestfs_int_lvm_%s_list_val = NULL;\n" typ;
        pr "\n";
        pr "  r = command (&out, &err,\n";
        pr "           \"/sbin/lvm\", \"%ss\",\n" typ;
@@ -3255,22 +4239,22 @@ and generate_daemon_actions () =
        pr "    }\n";
        pr "\n";
        pr "    /* Allocate some space to store this next entry. */\n";
-       pr "    newp = realloc (ret->guestfs_lvm_int_%s_list_val,\n" typ;
-       pr "                sizeof (guestfs_lvm_int_%s) * (i+1));\n" typ;
+       pr "    newp = realloc (ret->guestfs_int_lvm_%s_list_val,\n" typ;
+       pr "                sizeof (guestfs_int_lvm_%s) * (i+1));\n" typ;
        pr "    if (newp == NULL) {\n";
        pr "      reply_with_perror (\"realloc\");\n";
-       pr "      free (ret->guestfs_lvm_int_%s_list_val);\n" typ;
+       pr "      free (ret->guestfs_int_lvm_%s_list_val);\n" typ;
        pr "      free (ret);\n";
        pr "      free (out);\n";
        pr "      return NULL;\n";
        pr "    }\n";
-       pr "    ret->guestfs_lvm_int_%s_list_val = newp;\n" typ;
+       pr "    ret->guestfs_int_lvm_%s_list_val = newp;\n" typ;
        pr "\n";
        pr "    /* Tokenize the next entry. */\n";
-       pr "    r = lvm_tokenize_%s (p, &ret->guestfs_lvm_int_%s_list_val[i]);\n" typ typ;
+       pr "    r = lvm_tokenize_%s (p, &ret->guestfs_int_lvm_%s_list_val[i]);\n" typ typ;
        pr "    if (r == -1) {\n";
        pr "      reply_with_error (\"failed to parse output of '%ss' command\");\n" typ;
-        pr "      free (ret->guestfs_lvm_int_%s_list_val);\n" typ;
+        pr "      free (ret->guestfs_int_lvm_%s_list_val);\n" typ;
         pr "      free (ret);\n";
        pr "      free (out);\n";
        pr "      return NULL;\n";
@@ -3280,13 +4264,29 @@ and generate_daemon_actions () =
        pr "    p = pend;\n";
        pr "  }\n";
        pr "\n";
-       pr "  ret->guestfs_lvm_int_%s_list_len = i;\n" typ;
+       pr "  ret->guestfs_int_lvm_%s_list_len = i;\n" typ;
        pr "\n";
        pr "  free (out);\n";
        pr "  return ret;\n";
        pr "}\n"
 
-  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols]
+  ) ["pv", lvm_pv_cols; "vg", lvm_vg_cols; "lv", lvm_lv_cols]
+
+(* Generate a list of function names, for debugging in the daemon.. *)
+and generate_daemon_names () =
+  generate_header CStyle GPLv2;
+
+  pr "#include <config.h>\n";
+  pr "\n";
+  pr "#include \"daemon.h\"\n";
+  pr "\n";
+
+  pr "/* This array is indexed by proc_nr.  See guestfs_protocol.x. */\n";
+  pr "const char *function_names[] = {\n";
+  List.iter (
+    fun (name, _, proc_nr, _, _, _, _) -> pr "  [%d] = \"%s\",\n" proc_nr name
+  ) daemon_functions;
+  pr "};\n";
 
 (* Generate the tests. *)
 and generate_tests () =
@@ -3305,11 +4305,6 @@ and generate_tests () =
 static guestfs_h *g;
 static int suppress_error = 0;
 
-/* This will be 's' or 'h' depending on whether the guest kernel
- * names IDE devices /dev/sd* or /dev/hd*.
- */
-static char devchar = 's';
-
 static void print_error (guestfs_h *g, void *data, const char *msg)
 {
   if (!suppress_error)
@@ -3366,11 +4361,11 @@ int main (int argc, char *argv[])
 {
   char c = 0;
   int failed = 0;
-  const char *srcdir;
   const char *filename;
-  int fd, i;
+  int fd;
   int nr_tests, test_num = 0;
-  char **devs;
+
+  setbuf (stdout, NULL);
 
   no_test_warnings ();
 
@@ -3382,10 +4377,7 @@ int main (int argc, char *argv[])
 
   guestfs_set_error_handler (g, print_error, NULL);
 
-  srcdir = getenv (\"srcdir\");
-  if (!srcdir) srcdir = \".\";
-  chdir (srcdir);
-  guestfs_set_path (g, \".\");
+  guestfs_set_path (g, \"../appliance\");
 
   filename = \"test1.img\";
   fd = open (filename, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
@@ -3471,36 +4463,26 @@ int main (int argc, char *argv[])
     exit (1);
   }
 
+  if (guestfs_add_drive_ro (g, \"../images/test.sqsh\") == -1) {
+    printf (\"guestfs_add_drive_ro ../images/test.sqsh FAILED\\n\");
+    exit (1);
+  }
+
   if (guestfs_launch (g) == -1) {
     printf (\"guestfs_launch FAILED\\n\");
     exit (1);
   }
+
+  /* Set a timeout in case qemu hangs during launch (RHBZ#505329). */
+  alarm (600);
+
   if (guestfs_wait_ready (g) == -1) {
     printf (\"guestfs_wait_ready FAILED\\n\");
     exit (1);
   }
 
-  /* Detect if the appliance uses /dev/sd* or /dev/hd* in device
-   * names.  This changed between RHEL 5 and RHEL 6 so we have to
-   * support both.
-   */
-  devs = guestfs_list_devices (g);
-  if (devs == NULL || devs[0] == NULL) {
-    printf (\"guestfs_list_devices FAILED\\n\");
-    exit (1);
-  }
-  if (strncmp (devs[0], \"/dev/sd\", 7) == 0)
-    devchar = 's';
-  else if (strncmp (devs[0], \"/dev/hd\", 7) == 0)
-    devchar = 'h';
-  else {
-    printf (\"guestfs_list_devices returned unexpected string '%%s'\\n\",
-            devs[0]);
-    exit (1);
-  }
-  for (i = 0; devs[i] != NULL; ++i)
-    free (devs[i]);
-  free (devs);
+  /* Cancel previous alarm. */
+  alarm (0);
 
   nr_tests = %d;
 
@@ -3532,37 +4514,98 @@ int main (int argc, char *argv[])
   pr "  exit (0);\n";
   pr "}\n"
 
-and generate_one_test name i (init, test) =
+and generate_one_test name i (init, prereq, test) =
   let test_name = sprintf "test_%s_%d" name i in
 
-  pr "static int %s (void)\n" test_name;
-  pr "{\n";
+  pr "\
+static int %s_skip (void)
+{
+  const char *str;
+
+  str = getenv (\"TEST_ONLY\");
+  if (str)
+    return strstr (str, \"%s\") == NULL;
+  str = getenv (\"SKIP_%s\");
+  if (str && strcmp (str, \"1\") == 0) return 1;
+  str = getenv (\"SKIP_TEST_%s\");
+  if (str && strcmp (str, \"1\") == 0) return 1;
+  return 0;
+}
+
+" test_name name (String.uppercase test_name) (String.uppercase name);
+
+  (match prereq with
+   | Disabled | Always -> ()
+   | If code | Unless code ->
+       pr "static int %s_prereq (void)\n" test_name;
+       pr "{\n";
+       pr "  %s\n" code;
+       pr "}\n";
+       pr "\n";
+  );
+
+  pr "\
+static int %s (void)
+{
+  if (%s_skip ()) {
+    printf (\"        %%s skipped (reason: environment variable set)\\n\", \"%s\");
+    return 0;
+  }
+
+" test_name test_name test_name;
+
+  (match prereq with
+   | Disabled ->
+       pr "  printf (\"        %%s skipped (reason: test disabled in generator)\\n\", \"%s\");\n" test_name
+   | If _ ->
+       pr "  if (! %s_prereq ()) {\n" test_name;
+       pr "    printf (\"        %%s skipped (reason: test prerequisite)\\n\", \"%s\");\n" test_name;
+       pr "    return 0;\n";
+       pr "  }\n";
+       pr "\n";
+       generate_one_test_body name i test_name init test;
+   | Unless _ ->
+       pr "  if (%s_prereq ()) {\n" test_name;
+       pr "    printf (\"        %%s skipped (reason: test prerequisite)\\n\", \"%s\");\n" test_name;
+       pr "    return 0;\n";
+       pr "  }\n";
+       pr "\n";
+       generate_one_test_body name i test_name init test;
+   | Always ->
+       generate_one_test_body name i test_name init test
+  );
+
+  pr "  return 0;\n";
+  pr "}\n";
+  pr "\n";
+  test_name
 
+and generate_one_test_body name i test_name init test =
   (match init with
-   | InitNone -> ()
+   | InitNone
    | InitEmpty ->
-       pr "  /* InitEmpty for %s (%d) */\n" name i;
+       pr "  /* InitNone|InitEmpty for %s */\n" test_name;
        List.iter (generate_test_command_call test_name)
         [["blockdev_setrw"; "/dev/sda"];
          ["umount_all"];
          ["lvm_remove_all"]]
    | InitBasicFS ->
-       pr "  /* InitBasicFS for %s (%d): create ext2 on /dev/sda1 */\n" name i;
+       pr "  /* InitBasicFS for %s: create ext2 on /dev/sda1 */\n" test_name;
        List.iter (generate_test_command_call test_name)
         [["blockdev_setrw"; "/dev/sda"];
          ["umount_all"];
          ["lvm_remove_all"];
-         ["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+         ["sfdiskM"; "/dev/sda"; ","];
          ["mkfs"; "ext2"; "/dev/sda1"];
          ["mount"; "/dev/sda1"; "/"]]
    | InitBasicFSonLVM ->
-       pr "  /* InitBasicFSonLVM for %s (%d): create ext2 on /dev/VG/LV */\n"
-        name i;
+       pr "  /* InitBasicFSonLVM for %s: create ext2 on /dev/VG/LV */\n"
+        test_name;
        List.iter (generate_test_command_call test_name)
         [["blockdev_setrw"; "/dev/sda"];
          ["umount_all"];
          ["lvm_remove_all"];
-         ["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+         ["sfdiskM"; "/dev/sda"; ","];
          ["pvcreate"; "/dev/sda1"];
          ["vgcreate"; "VG"; "/dev/sda1"];
          ["lvcreate"; "LV"; "VG"; "8"];
@@ -3579,162 +4622,180 @@ and generate_one_test name i (init, test) =
        List.rev (List.tl seq), List.hd seq
   in
 
-  (match test with
-   | TestRun seq ->
-       pr "  /* TestRun for %s (%d) */\n" name i;
-       List.iter (generate_test_command_call test_name) seq
-   | TestOutput (seq, expected) ->
-       pr "  /* TestOutput for %s (%d) */\n" name i;
-       pr "  char expected[] = \"%s\";\n" (c_quote expected);
-       if String.length expected > 7 &&
-          String.sub expected 0 7 = "/dev/sd" then
-        pr "  expected[5] = devchar;\n";
-       let seq, last = get_seq_last seq in
-       let test () =
-        pr "    if (strcmp (r, expected) != 0) {\n";
-        pr "      fprintf (stderr, \"%s: expected \\\"%%s\\\" but got \\\"%%s\\\"\\n\", expected, r);\n" test_name;
-        pr "      return -1;\n";
-        pr "    }\n"
-       in
-       List.iter (generate_test_command_call test_name) seq;
-       generate_test_command_call ~test test_name last
-   | TestOutputList (seq, expected) ->
-       pr "  /* TestOutputList for %s (%d) */\n" name i;
-       let seq, last = get_seq_last seq in
-       let test () =
-        iteri (
-          fun i str ->
-            pr "    if (!r[%d]) {\n" i;
-            pr "      fprintf (stderr, \"%s: short list returned from command\\n\");\n" test_name;
-            pr "      print_strings (r);\n";
-            pr "      return -1;\n";
-            pr "    }\n";
-             pr "    {\n";
-             pr "      char expected[] = \"%s\";\n" (c_quote str);
-             if String.length str > 7 && String.sub str 0 7 = "/dev/sd" then
-              pr "      expected[5] = devchar;\n";
-            pr "      if (strcmp (r[%d], expected) != 0) {\n" i;
-            pr "        fprintf (stderr, \"%s: expected \\\"%%s\\\" but got \\\"%%s\\\"\\n\", expected, r[%d]);\n" test_name i;
-            pr "        return -1;\n";
-            pr "      }\n";
-            pr "    }\n"
-        ) expected;
-        pr "    if (r[%d] != NULL) {\n" (List.length expected);
-        pr "      fprintf (stderr, \"%s: extra elements returned from command\\n\");\n"
-          test_name;
-        pr "      print_strings (r);\n";
-        pr "      return -1;\n";
-        pr "    }\n"
-       in
-       List.iter (generate_test_command_call test_name) seq;
-       generate_test_command_call ~test test_name last
-   | TestOutputInt (seq, expected) ->
-       pr "  /* TestOutputInt for %s (%d) */\n" name i;
-       let seq, last = get_seq_last seq in
-       let test () =
-        pr "    if (r != %d) {\n" expected;
-        pr "      fprintf (stderr, \"%s: expected %d but got %%d\\n\","
-          test_name expected;
-        pr "               (int) r);\n";
-        pr "      return -1;\n";
-        pr "    }\n"
-       in
-       List.iter (generate_test_command_call test_name) seq;
-       generate_test_command_call ~test test_name last
-   | TestOutputTrue seq ->
-       pr "  /* TestOutputTrue for %s (%d) */\n" name i;
-       let seq, last = get_seq_last seq in
-       let test () =
-        pr "    if (!r) {\n";
-        pr "      fprintf (stderr, \"%s: expected true, got false\\n\");\n"
-          test_name;
-        pr "      return -1;\n";
-        pr "    }\n"
-       in
-       List.iter (generate_test_command_call test_name) seq;
-       generate_test_command_call ~test test_name last
-   | TestOutputFalse seq ->
-       pr "  /* TestOutputFalse for %s (%d) */\n" name i;
-       let seq, last = get_seq_last seq in
-       let test () =
-        pr "    if (r) {\n";
-        pr "      fprintf (stderr, \"%s: expected false, got true\\n\");\n"
-          test_name;
-        pr "      return -1;\n";
-        pr "    }\n"
-       in
-       List.iter (generate_test_command_call test_name) seq;
-       generate_test_command_call ~test test_name last
-   | TestOutputLength (seq, expected) ->
-       pr "  /* TestOutputLength for %s (%d) */\n" name i;
-       let seq, last = get_seq_last seq in
-       let test () =
-        pr "    int j;\n";
-        pr "    for (j = 0; j < %d; ++j)\n" expected;
-        pr "      if (r[j] == NULL) {\n";
-        pr "        fprintf (stderr, \"%s: short list returned\\n\");\n"
-          test_name;
-        pr "        print_strings (r);\n";
-        pr "        return -1;\n";
-        pr "      }\n";
-        pr "    if (r[j] != NULL) {\n";
-        pr "      fprintf (stderr, \"%s: long list returned\\n\");\n"
-          test_name;
-        pr "      print_strings (r);\n";
-        pr "      return -1;\n";
-        pr "    }\n"
-       in
-       List.iter (generate_test_command_call test_name) seq;
-       generate_test_command_call ~test test_name last
-   | TestOutputStruct (seq, checks) ->
-       pr "  /* TestOutputStruct for %s (%d) */\n" name i;
-       let seq, last = get_seq_last seq in
-       let test () =
-        List.iter (
-          function
-          | CompareWithInt (field, expected) ->
-              pr "    if (r->%s != %d) {\n" field expected;
-              pr "      fprintf (stderr, \"%s: %s was %%d, expected %d\\n\",\n"
-                test_name field expected;
-              pr "               (int) r->%s);\n" field;
-              pr "      return -1;\n";
-              pr "    }\n"
-          | CompareWithString (field, expected) ->
-              pr "    if (strcmp (r->%s, \"%s\") != 0) {\n" field expected;
-              pr "      fprintf (stderr, \"%s: %s was \"%%s\", expected \"%s\"\\n\",\n"
-                test_name field expected;
-              pr "               r->%s);\n" field;
-              pr "      return -1;\n";
-              pr "    }\n"
-          | CompareFieldsIntEq (field1, field2) ->
-              pr "    if (r->%s != r->%s) {\n" field1 field2;
-              pr "      fprintf (stderr, \"%s: %s (%%d) <> %s (%%d)\\n\",\n"
-                test_name field1 field2;
-              pr "               (int) r->%s, (int) r->%s);\n" field1 field2;
-              pr "      return -1;\n";
-              pr "    }\n"
-          | CompareFieldsStrEq (field1, field2) ->
-              pr "    if (strcmp (r->%s, r->%s) != 0) {\n" field1 field2;
-              pr "      fprintf (stderr, \"%s: %s (\"%%s\") <> %s (\"%%s\")\\n\",\n"
-                test_name field1 field2;
-              pr "               r->%s, r->%s);\n" field1 field2;
-              pr "      return -1;\n";
-              pr "    }\n"
-        ) checks
-       in
-       List.iter (generate_test_command_call test_name) seq;
-       generate_test_command_call ~test test_name last
-   | TestLastFail seq ->
-       pr "  /* TestLastFail for %s (%d) */\n" name i;
-       let seq, last = get_seq_last seq in
-       List.iter (generate_test_command_call test_name) seq;
-       generate_test_command_call test_name ~expect_error:true last
-  );
-
-  pr "  return 0;\n";
-  pr "}\n";
-  pr "\n";
-  test_name
+  match test with
+  | TestRun seq ->
+      pr "  /* TestRun for %s (%d) */\n" name i;
+      List.iter (generate_test_command_call test_name) seq
+  | TestOutput (seq, expected) ->
+      pr "  /* TestOutput for %s (%d) */\n" name i;
+      pr "  const char *expected = \"%s\";\n" (c_quote expected);
+      let seq, last = get_seq_last seq in
+      let test () =
+       pr "    if (strcmp (r, expected) != 0) {\n";
+       pr "      fprintf (stderr, \"%s: expected \\\"%%s\\\" but got \\\"%%s\\\"\\n\", expected, r);\n" test_name;
+       pr "      return -1;\n";
+       pr "    }\n"
+      in
+      List.iter (generate_test_command_call test_name) seq;
+      generate_test_command_call ~test test_name last
+  | TestOutputList (seq, expected) ->
+      pr "  /* TestOutputList for %s (%d) */\n" name i;
+      let seq, last = get_seq_last seq in
+      let test () =
+       iteri (
+         fun i str ->
+           pr "    if (!r[%d]) {\n" i;
+           pr "      fprintf (stderr, \"%s: short list returned from command\\n\");\n" test_name;
+           pr "      print_strings (r);\n";
+           pr "      return -1;\n";
+           pr "    }\n";
+            pr "    {\n";
+            pr "      const char *expected = \"%s\";\n" (c_quote str);
+           pr "      if (strcmp (r[%d], expected) != 0) {\n" i;
+           pr "        fprintf (stderr, \"%s: expected \\\"%%s\\\" but got \\\"%%s\\\"\\n\", expected, r[%d]);\n" test_name i;
+           pr "        return -1;\n";
+           pr "      }\n";
+           pr "    }\n"
+       ) expected;
+       pr "    if (r[%d] != NULL) {\n" (List.length expected);
+       pr "      fprintf (stderr, \"%s: extra elements returned from command\\n\");\n"
+         test_name;
+       pr "      print_strings (r);\n";
+       pr "      return -1;\n";
+       pr "    }\n"
+      in
+      List.iter (generate_test_command_call test_name) seq;
+      generate_test_command_call ~test test_name last
+  | TestOutputListOfDevices (seq, expected) ->
+      pr "  /* TestOutputListOfDevices for %s (%d) */\n" name i;
+      let seq, last = get_seq_last seq in
+      let test () =
+       iteri (
+         fun i str ->
+           pr "    if (!r[%d]) {\n" i;
+           pr "      fprintf (stderr, \"%s: short list returned from command\\n\");\n" test_name;
+           pr "      print_strings (r);\n";
+           pr "      return -1;\n";
+           pr "    }\n";
+            pr "    {\n";
+            pr "      const char *expected = \"%s\";\n" (c_quote str);
+           pr "      r[%d][5] = 's';\n" i;
+           pr "      if (strcmp (r[%d], expected) != 0) {\n" i;
+           pr "        fprintf (stderr, \"%s: expected \\\"%%s\\\" but got \\\"%%s\\\"\\n\", expected, r[%d]);\n" test_name i;
+           pr "        return -1;\n";
+           pr "      }\n";
+           pr "    }\n"
+       ) expected;
+       pr "    if (r[%d] != NULL) {\n" (List.length expected);
+       pr "      fprintf (stderr, \"%s: extra elements returned from command\\n\");\n"
+         test_name;
+       pr "      print_strings (r);\n";
+       pr "      return -1;\n";
+       pr "    }\n"
+      in
+      List.iter (generate_test_command_call test_name) seq;
+      generate_test_command_call ~test test_name last
+  | TestOutputInt (seq, expected) ->
+      pr "  /* TestOutputInt for %s (%d) */\n" name i;
+      let seq, last = get_seq_last seq in
+      let test () =
+       pr "    if (r != %d) {\n" expected;
+       pr "      fprintf (stderr, \"%s: expected %d but got %%d\\n\","
+         test_name expected;
+       pr "               (int) r);\n";
+       pr "      return -1;\n";
+       pr "    }\n"
+      in
+      List.iter (generate_test_command_call test_name) seq;
+      generate_test_command_call ~test test_name last
+  | TestOutputTrue seq ->
+      pr "  /* TestOutputTrue for %s (%d) */\n" name i;
+      let seq, last = get_seq_last seq in
+      let test () =
+       pr "    if (!r) {\n";
+       pr "      fprintf (stderr, \"%s: expected true, got false\\n\");\n"
+         test_name;
+       pr "      return -1;\n";
+       pr "    }\n"
+      in
+      List.iter (generate_test_command_call test_name) seq;
+      generate_test_command_call ~test test_name last
+  | TestOutputFalse seq ->
+      pr "  /* TestOutputFalse for %s (%d) */\n" name i;
+      let seq, last = get_seq_last seq in
+      let test () =
+       pr "    if (r) {\n";
+       pr "      fprintf (stderr, \"%s: expected false, got true\\n\");\n"
+         test_name;
+       pr "      return -1;\n";
+       pr "    }\n"
+      in
+      List.iter (generate_test_command_call test_name) seq;
+      generate_test_command_call ~test test_name last
+  | TestOutputLength (seq, expected) ->
+      pr "  /* TestOutputLength for %s (%d) */\n" name i;
+      let seq, last = get_seq_last seq in
+      let test () =
+       pr "    int j;\n";
+       pr "    for (j = 0; j < %d; ++j)\n" expected;
+       pr "      if (r[j] == NULL) {\n";
+       pr "        fprintf (stderr, \"%s: short list returned\\n\");\n"
+         test_name;
+       pr "        print_strings (r);\n";
+       pr "        return -1;\n";
+       pr "      }\n";
+       pr "    if (r[j] != NULL) {\n";
+       pr "      fprintf (stderr, \"%s: long list returned\\n\");\n"
+         test_name;
+       pr "      print_strings (r);\n";
+       pr "      return -1;\n";
+       pr "    }\n"
+      in
+      List.iter (generate_test_command_call test_name) seq;
+      generate_test_command_call ~test test_name last
+  | TestOutputStruct (seq, checks) ->
+      pr "  /* TestOutputStruct for %s (%d) */\n" name i;
+      let seq, last = get_seq_last seq in
+      let test () =
+       List.iter (
+         function
+         | CompareWithInt (field, expected) ->
+             pr "    if (r->%s != %d) {\n" field expected;
+             pr "      fprintf (stderr, \"%s: %s was %%d, expected %d\\n\",\n"
+               test_name field expected;
+             pr "               (int) r->%s);\n" field;
+             pr "      return -1;\n";
+             pr "    }\n"
+         | CompareWithString (field, expected) ->
+             pr "    if (strcmp (r->%s, \"%s\") != 0) {\n" field expected;
+             pr "      fprintf (stderr, \"%s: %s was \"%%s\", expected \"%s\"\\n\",\n"
+               test_name field expected;
+             pr "               r->%s);\n" field;
+             pr "      return -1;\n";
+             pr "    }\n"
+         | CompareFieldsIntEq (field1, field2) ->
+             pr "    if (r->%s != r->%s) {\n" field1 field2;
+             pr "      fprintf (stderr, \"%s: %s (%%d) <> %s (%%d)\\n\",\n"
+               test_name field1 field2;
+             pr "               (int) r->%s, (int) r->%s);\n" field1 field2;
+             pr "      return -1;\n";
+             pr "    }\n"
+         | CompareFieldsStrEq (field1, field2) ->
+             pr "    if (strcmp (r->%s, r->%s) != 0) {\n" field1 field2;
+             pr "      fprintf (stderr, \"%s: %s (\"%%s\") <> %s (\"%%s\")\\n\",\n"
+               test_name field1 field2;
+             pr "               r->%s, r->%s);\n" field1 field2;
+             pr "      return -1;\n";
+             pr "    }\n"
+       ) checks
+      in
+      List.iter (generate_test_command_call test_name) seq;
+      generate_test_command_call ~test test_name last
+  | TestLastFail seq ->
+      pr "  /* TestLastFail for %s (%d) */\n" name i;
+      let seq, last = get_seq_last seq in
+      List.iter (generate_test_command_call test_name) seq;
+      generate_test_command_call test_name ~expect_error:true last
 
 (* Generate the code to run a command, leaving the result in 'r'.
  * If you expect to get an error then you should set expect_error:true.
@@ -3763,9 +4824,7 @@ and generate_test_command_call ?(expect_error = false) ?test test_name cmd =
        | OptString n, "NULL" -> ()
        | String n, arg
        | OptString n, arg ->
-           pr "    char %s[] = \"%s\";\n" n (c_quote arg);
-           if String.length arg > 7 && String.sub arg 0 7 = "/dev/sd" then
-             pr "    %s[5] = devchar;\n" n
+           pr "    const char *%s = \"%s\";\n" n (c_quote arg);
        | Int _, _
        | Bool _, _
        | FileIn _, _ | FileOut _, _ -> ()
@@ -3773,11 +4832,9 @@ and generate_test_command_call ?(expect_error = false) ?test test_name cmd =
            let strs = string_split " " arg in
            iteri (
              fun i str ->
-                pr "    char %s_%d[] = \"%s\";\n" n i (c_quote str);
-               if String.length str > 7 && String.sub str 0 7 = "/dev/sd" then
-                 pr "    %s_%d[5] = devchar;\n" n i
+                pr "    const char *%s_%d = \"%s\";\n" n i (c_quote str);
            ) strs;
-           pr "    char *%s[] = {\n" n;
+           pr "    const char *%s[] = {\n" n;
            iteri (
              fun i _ -> pr "      %s_%d,\n" n i
            ) strs;
@@ -3795,18 +4852,10 @@ and generate_test_command_call ?(expect_error = false) ?test test_name cmd =
            pr "    char **r;\n";
            pr "    int i;\n";
            "NULL"
-       | RIntBool _ ->
-           pr "    struct guestfs_int_bool *r;\n"; "NULL"
-       | RPVList _ ->
-           pr "    struct guestfs_lvm_pv_list *r;\n"; "NULL"
-       | RVGList _ ->
-           pr "    struct guestfs_lvm_vg_list *r;\n"; "NULL"
-       | RLVList _ ->
-           pr "    struct guestfs_lvm_lv_list *r;\n"; "NULL"
-       | RStat _ ->
-           pr "    struct guestfs_stat *r;\n"; "NULL"
-       | RStatVFS _ ->
-           pr "    struct guestfs_statvfs *r;\n"; "NULL" in
+       | RStruct (_, typ) ->
+           pr "    struct guestfs_%s *r;\n" typ; "NULL"
+       | RStructList (_, typ) ->
+           pr "    struct guestfs_%s_list *r;\n" typ; "NULL" in
 
       pr "    suppress_error = %d;\n" (if expect_error then 1 else 0);
       pr "    r = guestfs_%s (g" name;
@@ -3852,16 +4901,10 @@ and generate_test_command_call ?(expect_error = false) ?test test_name cmd =
           pr "    for (i = 0; r[i] != NULL; ++i)\n";
           pr "      free (r[i]);\n";
           pr "    free (r);\n"
-       | RIntBool _ ->
-          pr "    guestfs_free_int_bool (r);\n"
-       | RPVList _ ->
-          pr "    guestfs_free_lvm_pv_list (r);\n"
-       | RVGList _ ->
-          pr "    guestfs_free_lvm_vg_list (r);\n"
-       | RLVList _ ->
-          pr "    guestfs_free_lvm_lv_list (r);\n"
-       | RStat _ | RStatVFS _ ->
-          pr "    free (r);\n"
+       | RStruct (_, typ) ->
+          pr "    guestfs_free_%s (r);\n" typ
+       | RStructList (_, typ) ->
+          pr "    guestfs_free_%s_list (r);\n" typ
       );
 
       pr "  }\n"
@@ -3963,59 +5006,54 @@ and generate_fish_cmds () =
   pr "}\n";
   pr "\n";
 
-  (* print_{pv,vg,lv}_list functions *)
+  (* print_* functions *)
   List.iter (
-    function
-    | typ, cols ->
-       pr "static void print_%s (struct guestfs_lvm_%s *%s)\n" typ typ typ;
-       pr "{\n";
-       pr "  int i;\n";
-       pr "\n";
-       List.iter (
-         function
-         | name, `String ->
-             pr "  printf (\"%s: %%s\\n\", %s->%s);\n" name typ name
-         | name, `UUID ->
-             pr "  printf (\"%s: \");\n" name;
-             pr "  for (i = 0; i < 32; ++i)\n";
-             pr "    printf (\"%%c\", %s->%s[i]);\n" typ name;
-             pr "  printf (\"\\n\");\n"
-         | name, `Bytes ->
-             pr "  printf (\"%s: %%\" PRIu64 \"\\n\", %s->%s);\n" name typ name
-         | name, `Int ->
-             pr "  printf (\"%s: %%\" PRIi64 \"\\n\", %s->%s);\n" name typ name
-         | name, `OptPercent ->
-             pr "  if (%s->%s >= 0) printf (\"%s: %%g %%%%\\n\", %s->%s);\n"
-               typ name name typ name;
-             pr "  else printf (\"%s: \\n\");\n" name
-       ) cols;
-       pr "}\n";
-       pr "\n";
-       pr "static void print_%s_list (struct guestfs_lvm_%s_list *%ss)\n"
-         typ typ typ;
-       pr "{\n";
-       pr "  int i;\n";
-       pr "\n";
-       pr "  for (i = 0; i < %ss->len; ++i)\n" typ;
-       pr "    print_%s (&%ss->val[i]);\n" typ typ;
-       pr "}\n";
-       pr "\n";
-  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
+    fun (typ, cols) ->
+      let needs_i =
+        List.exists (function (_, FUUID) -> true | _ -> false) cols in
 
-  (* print_{stat,statvfs} functions *)
-  List.iter (
-    function
-    | typ, cols ->
-       pr "static void print_%s (struct guestfs_%s *%s)\n" typ typ typ;
-       pr "{\n";
-       List.iter (
-         function
-         | name, `Int ->
-             pr "  printf (\"%s: %%\" PRIi64 \"\\n\", %s->%s);\n" name typ name
-       ) cols;
-       pr "}\n";
-       pr "\n";
-  ) ["stat", stat_cols; "statvfs", statvfs_cols];
+      pr "static void print_%s (struct guestfs_%s *%s)\n" typ typ typ;
+      pr "{\n";
+      if needs_i then (
+        pr "  int i;\n";
+        pr "\n"
+      );
+      List.iter (
+       function
+       | name, FString ->
+           pr "  printf (\"%s: %%s\\n\", %s->%s);\n" name typ name
+       | name, FUUID ->
+           pr "  printf (\"%s: \");\n" name;
+           pr "  for (i = 0; i < 32; ++i)\n";
+           pr "    printf (\"%%c\", %s->%s[i]);\n" typ name;
+           pr "  printf (\"\\n\");\n"
+       | name, (FUInt64|FBytes) ->
+           pr "  printf (\"%s: %%\" PRIu64 \"\\n\", %s->%s);\n" name typ name
+       | name, FInt64 ->
+           pr "  printf (\"%s: %%\" PRIi64 \"\\n\", %s->%s);\n" name typ name
+       | name, FUInt32 ->
+           pr "  printf (\"%s: %%\" PRIu32 \"\\n\", %s->%s);\n" name typ name
+       | name, FInt32 ->
+           pr "  printf (\"%s: %%\" PRIi32 \"\\n\", %s->%s);\n" name typ name
+       | name, FChar ->
+           pr "  printf (\"%s: %%c\\n\", %s->%s);\n" name typ name
+       | name, FOptPercent ->
+           pr "  if (%s->%s >= 0) printf (\"%s: %%g %%%%\\n\", %s->%s);\n"
+             typ name name typ name;
+           pr "  else printf (\"%s: \\n\");\n" name
+      ) cols;
+      pr "}\n";
+      pr "\n";
+      pr "static void print_%s_list (struct guestfs_%s_list *%ss)\n"
+       typ typ typ;
+      pr "{\n";
+      pr "  int i;\n";
+      pr "\n";
+      pr "  for (i = 0; i < %ss->len; ++i)\n" typ;
+      pr "    print_%s (&%ss->val[i]);\n" typ typ;
+      pr "}\n";
+      pr "\n";
+  ) structs;
 
   (* run_<action> actions *)
   List.iter (
@@ -4030,12 +5068,8 @@ and generate_fish_cmds () =
        | RConstString _ -> pr "  const char *r;\n"
        | RString _ -> pr "  char *r;\n"
        | RStringList _ | RHashtable _ -> pr "  char **r;\n"
-       | RIntBool _ -> pr "  struct guestfs_int_bool *r;\n"
-       | RPVList _ -> pr "  struct guestfs_lvm_pv_list *r;\n"
-       | RVGList _ -> pr "  struct guestfs_lvm_vg_list *r;\n"
-       | RLVList _ -> pr "  struct guestfs_lvm_lv_list *r;\n"
-       | RStat _ -> pr "  struct guestfs_stat *r;\n"
-       | RStatVFS _ -> pr "  struct guestfs_statvfs *r;\n"
+       | RStruct (_, typ) -> pr "  struct guestfs_%s *r;\n" typ
+       | RStructList (_, typ) -> pr "  struct guestfs_%s_list *r;\n" typ
       );
       List.iter (
        function
@@ -4114,36 +5148,15 @@ and generate_fish_cmds () =
           pr "  print_strings (r);\n";
           pr "  free_strings (r);\n";
           pr "  return 0;\n"
-       | RIntBool _ ->
-          pr "  if (r == NULL) return -1;\n";
-          pr "  printf (\"%%d, %%s\\n\", r->i,\n";
-          pr "    r->b ? \"true\" : \"false\");\n";
-          pr "  guestfs_free_int_bool (r);\n";
-          pr "  return 0;\n"
-       | RPVList _ ->
-          pr "  if (r == NULL) return -1;\n";
-          pr "  print_pv_list (r);\n";
-          pr "  guestfs_free_lvm_pv_list (r);\n";
-          pr "  return 0;\n"
-       | RVGList _ ->
+       | RStruct (_, typ) ->
           pr "  if (r == NULL) return -1;\n";
-          pr "  print_vg_list (r);\n";
-          pr "  guestfs_free_lvm_vg_list (r);\n";
+          pr "  print_%s (r);\n" typ;
+          pr "  guestfs_free_%s (r);\n" typ;
           pr "  return 0;\n"
-       | RLVList _ ->
+       | RStructList (_, typ) ->
           pr "  if (r == NULL) return -1;\n";
-          pr "  print_lv_list (r);\n";
-          pr "  guestfs_free_lvm_lv_list (r);\n";
-          pr "  return 0;\n"
-       | RStat _ ->
-          pr "  if (r == NULL) return -1;\n";
-          pr "  print_stat (r);\n";
-          pr "  free (r);\n";
-          pr "  return 0;\n"
-       | RStatVFS _ ->
-          pr "  if (r == NULL) return -1;\n";
-          pr "  print_statvfs (r);\n";
-          pr "  free (r);\n";
+          pr "  print_%s_list (r);\n" typ;
+          pr "  guestfs_free_%s_list (r);\n" typ;
           pr "  return 0;\n"
        | RHashtable _ ->
           pr "  if (r == NULL) return -1;\n";
@@ -4207,9 +5220,12 @@ and generate_fish_completion () =
 #ifdef HAVE_LIBREADLINE
 
 static const char *const commands[] = {
+  BUILTIN_COMMANDS_FOR_COMPLETION,
 ";
 
-  (* Get the commands and sort them, including the aliases. *)
+  (* Get the commands, including the aliases.  They don't need to be
+   * sorted - the generator() function just does a dumb linear search.
+   *)
   let commands =
     List.map (
       fun (name, _, _, flags, _, _, _) ->
@@ -4221,7 +5237,6 @@ static const char *const commands[] = {
        if name <> alias then [name2; alias] else [name2]
     ) all_functions in
   let commands = List.flatten commands in
-  let commands = List.sort compare commands in
 
   List.iter (pr "  \"%s\",\n") commands;
 
@@ -4239,6 +5254,8 @@ generator (const char *text, int state)
     len = strlen (text);
   }
 
+  rl_attempted_completion_over = 1;
+
   while ((name = commands[index]) != NULL) {
     index++;
     if (strncasecmp (name, text, len) == 0)
@@ -4255,8 +5272,12 @@ char **do_completion (const char *text, int start, int end)
   char **matches = NULL;
 
 #ifdef HAVE_LIBREADLINE
+  rl_completion_append_character = ' ';
+
   if (start == 0)
     matches = rl_completion_matches (text, generator);
+  else if (complete_dest_paths)
+    matches = rl_completion_matches (text, complete_dest_paths_generator);
 #endif
 
   return matches;
@@ -4267,7 +5288,8 @@ char **do_completion (const char *text, int start, int end)
 and generate_fish_actions_pod () =
   let all_functions_sorted =
     List.filter (
-      fun (_, _, _, flags, _, _, _) -> not (List.mem NotInFish flags)
+      fun (_, _, _, flags, _, _, _) ->
+       not (List.mem NotInFish flags || List.mem NotInDocs flags)
     ) all_functions_sorted in
 
   let rex = Str.regexp "C<guestfs_\\([^>]+\\)>" in
@@ -4333,24 +5355,12 @@ and generate_prototype ?(extern = true) ?(static = false) ?(semicolon = true)
    | RConstString _ -> pr "const char *"
    | RString _ -> pr "char *"
    | RStringList _ | RHashtable _ -> pr "char **"
-   | RIntBool _ ->
-       if not in_daemon then pr "struct guestfs_int_bool *"
-       else pr "guestfs_%s_ret *" name
-   | RPVList _ ->
-       if not in_daemon then pr "struct guestfs_lvm_pv_list *"
-       else pr "guestfs_lvm_int_pv_list *"
-   | RVGList _ ->
-       if not in_daemon then pr "struct guestfs_lvm_vg_list *"
-       else pr "guestfs_lvm_int_vg_list *"
-   | RLVList _ ->
-       if not in_daemon then pr "struct guestfs_lvm_lv_list *"
-       else pr "guestfs_lvm_int_lv_list *"
-   | RStat _ ->
-       if not in_daemon then pr "struct guestfs_stat *"
-       else pr "guestfs_int_stat *"
-   | RStatVFS _ ->
-       if not in_daemon then pr "struct guestfs_statvfs *"
-       else pr "guestfs_int_statvfs *"
+   | RStruct (_, typ) ->
+       if not in_daemon then pr "struct guestfs_%s *" typ
+       else pr "guestfs_int_%s *" typ
+   | RStructList (_, typ) ->
+       if not in_daemon then pr "struct guestfs_%s_list *" typ
+       else pr "guestfs_int_%s_list *" typ
   );
   pr "%s%s (" prefix name;
   if handle = None && List.length (snd style) = 0 then
@@ -4370,8 +5380,14 @@ and generate_prototype ?(extern = true) ?(static = false) ?(semicolon = true)
     List.iter (
       function
       | String n
-      | OptString n -> next (); pr "const char *%s" n
-      | StringList n -> next (); pr "char * const* const %s" n
+      | OptString n ->
+         next ();
+         if not in_daemon then pr "const char *%s" n
+         else pr "char *%s" n
+      | StringList n ->
+         next ();
+         if not in_daemon then pr "char * const* const %s" n
+         else pr "char **%s" n
       | Bool n -> next (); pr "int %s" n
       | Int n -> next (); pr "int %s" n
       | FileIn n
@@ -4422,9 +5438,7 @@ val close : t -> unit
     provide predictable cleanup. *)
 
 ";
-  generate_ocaml_lvm_structure_decls ();
-
-  generate_ocaml_stat_structure_decls ();
+  generate_ocaml_structure_decls ();
 
   (* The actions. *)
   List.iter (
@@ -4449,9 +5463,7 @@ let () =
 
 ";
 
-  generate_ocaml_lvm_structure_decls ();
-
-  generate_ocaml_stat_structure_decls ();
+  generate_ocaml_structure_decls ();
 
   (* The actions. *)
   List.iter (
@@ -4509,14 +5521,14 @@ copy_table (char * const * argv)
 
 ";
 
-  (* LVM struct copy functions. *)
+  (* Struct copy functions. *)
   List.iter (
     fun (typ, cols) ->
       let has_optpercent_col =
-       List.exists (function (_, `OptPercent) -> true | _ -> false) cols in
+       List.exists (function (_, FOptPercent) -> true | _ -> false) cols in
 
       pr "static CAMLprim value\n";
-      pr "copy_lvm_%s (const struct guestfs_lvm_%s *%s)\n" typ typ typ;
+      pr "copy_%s (const struct guestfs_%s *%s)\n" typ typ typ;
       pr "{\n";
       pr "  CAMLparam0 ();\n";
       if has_optpercent_col then
@@ -4528,21 +5540,24 @@ copy_table (char * const * argv)
       iteri (
        fun i col ->
          (match col with
-          | name, `String ->
+          | name, FString ->
               pr "  v = caml_copy_string (%s->%s);\n" typ name
-          | name, `UUID ->
+          | name, FUUID ->
               pr "  v = caml_alloc_string (32);\n";
               pr "  memcpy (String_val (v), %s->%s, 32);\n" typ name
-          | name, `Bytes
-          | name, `Int ->
+          | name, (FBytes|FInt64|FUInt64) ->
               pr "  v = caml_copy_int64 (%s->%s);\n" typ name
-          | name, `OptPercent ->
+          | name, (FInt32|FUInt32) ->
+              pr "  v = caml_copy_int32 (%s->%s);\n" typ name
+          | name, FOptPercent ->
               pr "  if (%s->%s >= 0) { /* Some %s */\n" typ name name;
               pr "    v2 = caml_copy_double (%s->%s);\n" typ name;
               pr "    v = caml_alloc (1, 0);\n";
               pr "    Store_field (v, 0, v2);\n";
               pr "  } else /* None */\n";
               pr "    v = Val_int (0);\n";
+          | name, FChar ->
+              pr "  v = Val_int (%s->%s);\n" typ name
          );
          pr "  Store_field (rv, %d, v);\n" i
       ) cols;
@@ -4551,7 +5566,7 @@ copy_table (char * const * argv)
       pr "\n";
 
       pr "static CAMLprim value\n";
-      pr "copy_lvm_%s_list (const struct guestfs_lvm_%s_list *%ss)\n"
+      pr "copy_%s_list (const struct guestfs_%s_list *%ss)\n"
        typ typ typ;
       pr "{\n";
       pr "  CAMLparam0 ();\n";
@@ -4563,37 +5578,14 @@ copy_table (char * const * argv)
       pr "  else {\n";
       pr "    rv = caml_alloc (%ss->len, 0);\n" typ;
       pr "    for (i = 0; i < %ss->len; ++i) {\n" typ;
-      pr "      v = copy_lvm_%s (&%ss->val[i]);\n" typ typ;
+      pr "      v = copy_%s (&%ss->val[i]);\n" typ typ;
       pr "      caml_modify (&Field (rv, i), v);\n";
       pr "    }\n";
       pr "    CAMLreturn (rv);\n";
       pr "  }\n";
       pr "}\n";
       pr "\n";
-  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
-
-  (* Stat copy functions. *)
-  List.iter (
-    fun (typ, cols) ->
-      pr "static CAMLprim value\n";
-      pr "copy_%s (const struct guestfs_%s *%s)\n" typ typ typ;
-      pr "{\n";
-      pr "  CAMLparam0 ();\n";
-      pr "  CAMLlocal2 (rv, v);\n";
-      pr "\n";
-      pr "  rv = caml_alloc (%d, 0);\n" (List.length cols);
-      iteri (
-       fun i col ->
-         (match col with
-          | name, `Int ->
-              pr "  v = caml_copy_int64 (%s->%s);\n" typ name
-         );
-         pr "  Store_field (rv, %d, v);\n" i
-      ) cols;
-      pr "  CAMLreturn (rv);\n";
-      pr "}\n";
-      pr "\n";
-  ) ["stat", stat_cols; "statvfs", statvfs_cols];
+  ) structs;
 
   (* The wrappers. *)
   List.iter (
@@ -4654,18 +5646,10 @@ copy_table (char * const * argv)
            pr "  int i;\n";
            pr "  char **r;\n";
            "NULL"
-       | RIntBool _ ->
-           pr "  struct guestfs_int_bool *r;\n"; "NULL"
-       | RPVList _ ->
-           pr "  struct guestfs_lvm_pv_list *r;\n"; "NULL"
-       | RVGList _ ->
-           pr "  struct guestfs_lvm_vg_list *r;\n"; "NULL"
-       | RLVList _ ->
-           pr "  struct guestfs_lvm_lv_list *r;\n"; "NULL"
-       | RStat _ ->
-           pr "  struct guestfs_stat *r;\n"; "NULL"
-       | RStatVFS _ ->
-           pr "  struct guestfs_statvfs *r;\n"; "NULL"
+       | RStruct (_, typ) ->
+           pr "  struct guestfs_%s *r;\n" typ; "NULL"
+       | RStructList (_, typ) ->
+           pr "  struct guestfs_%s_list *r;\n" typ; "NULL"
        | RHashtable _ ->
            pr "  int i;\n";
            pr "  char **r;\n";
@@ -4703,26 +5687,12 @@ copy_table (char * const * argv)
           pr "  rv = caml_copy_string_array ((const char **) r);\n";
           pr "  for (i = 0; r[i] != NULL; ++i) free (r[i]);\n";
           pr "  free (r);\n"
-       | RIntBool _ ->
-          pr "  rv = caml_alloc (2, 0);\n";
-          pr "  Store_field (rv, 0, Val_int (r->i));\n";
-          pr "  Store_field (rv, 1, Val_bool (r->b));\n";
-          pr "  guestfs_free_int_bool (r);\n";
-       | RPVList _ ->
-          pr "  rv = copy_lvm_pv_list (r);\n";
-          pr "  guestfs_free_lvm_pv_list (r);\n";
-       | RVGList _ ->
-          pr "  rv = copy_lvm_vg_list (r);\n";
-          pr "  guestfs_free_lvm_vg_list (r);\n";
-       | RLVList _ ->
-          pr "  rv = copy_lvm_lv_list (r);\n";
-          pr "  guestfs_free_lvm_lv_list (r);\n";
-       | RStat _ ->
-          pr "  rv = copy_stat (r);\n";
-          pr "  free (r);\n";
-       | RStatVFS _ ->
-          pr "  rv = copy_statvfs (r);\n";
-          pr "  free (r);\n";
+       | RStruct (_, typ) ->
+          pr "  rv = copy_%s (r);\n" typ;
+          pr "  guestfs_free_%s (r);\n" typ;
+       | RStructList (_, typ) ->
+          pr "  rv = copy_%s_list (r);\n" typ;
+          pr "  guestfs_free_%s_list (r);\n" typ;
        | RHashtable _ ->
           pr "  rv = copy_table (r);\n";
           pr "  for (i = 0; r[i] != NULL; ++i) free (r[i]);\n";
@@ -4745,33 +5715,22 @@ copy_table (char * const * argv)
       )
   ) all_functions
 
-and generate_ocaml_lvm_structure_decls () =
-  List.iter (
-    fun (typ, cols) ->
-      pr "type lvm_%s = {\n" typ;
-      List.iter (
-       function
-       | name, `String -> pr "  %s : string;\n" name
-       | name, `UUID -> pr "  %s : string;\n" name
-       | name, `Bytes -> pr "  %s : int64;\n" name
-       | name, `Int -> pr "  %s : int64;\n" name
-       | name, `OptPercent -> pr "  %s : float option;\n" name
-      ) cols;
-      pr "}\n";
-      pr "\n"
-  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols]
-
-and generate_ocaml_stat_structure_decls () =
+and generate_ocaml_structure_decls () =
   List.iter (
     fun (typ, cols) ->
       pr "type %s = {\n" typ;
       List.iter (
        function
-       | name, `Int -> pr "  %s : int64;\n" name
+       | name, FString -> pr "  %s : string;\n" name
+       | name, FUUID -> pr "  %s : string;\n" name
+       | name, (FBytes|FInt64|FUInt64) -> pr "  %s : int64;\n" name
+       | name, (FInt32|FUInt32) -> pr "  %s : int32;\n" name
+       | name, FChar -> pr "  %s : char;\n" name
+       | name, FOptPercent -> pr "  %s : float option;\n" name
       ) cols;
       pr "}\n";
       pr "\n"
-  ) ["stat", stat_cols; "statvfs", statvfs_cols]
+  ) structs
 
 and generate_ocaml_prototype ?(is_external = false) name style =
   if is_external then pr "external " else pr "val ";
@@ -4792,12 +5751,8 @@ and generate_ocaml_prototype ?(is_external = false) name style =
    | RConstString _ -> pr "string"
    | RString _ -> pr "string"
    | RStringList _ -> pr "string array"
-   | RIntBool _ -> pr "int * bool"
-   | RPVList _ -> pr "lvm_pv array"
-   | RVGList _ -> pr "lvm_vg array"
-   | RLVList _ -> pr "lvm_lv array"
-   | RStat _ -> pr "stat"
-   | RStatVFS _ -> pr "statvfs"
+   | RStruct (_, typ) -> pr "%s" typ
+   | RStructList (_, typ) -> pr "%s array" typ
    | RHashtable _ -> pr "(string * string) list"
   );
   if is_external then (
@@ -4862,7 +5817,7 @@ XS_unpack_charPtrPtr (SV *arg) {
     croak (\"array reference expected\");
 
   av = (AV *)SvRV (arg);
-  ret = malloc (av_len (av) + 1 + 1);
+  ret = malloc ((av_len (av) + 1 + 1) * sizeof (char *));
   if (!ret)
     croak (\"malloc failed\");
 
@@ -4912,9 +5867,7 @@ DESTROY (g)
        | RConstString _ -> pr "SV *\n"
        | RString _ -> pr "SV *\n"
        | RStringList _
-       | RIntBool _
-       | RPVList _ | RVGList _ | RLVList _
-       | RStat _ | RStatVFS _
+       | RStruct _ | RStructList _
        | RHashtable _ ->
           pr "void\n" (* all lists returned implictly on the stack *)
       );
@@ -4923,13 +5876,19 @@ DESTROY (g)
       generate_call_args ~handle:"g" (snd style);
       pr "\n";
       pr "      guestfs_h *g;\n";
-      List.iter (
-       function
-       | String n | FileIn n | FileOut n -> pr "      char *%s;\n" n
-       | OptString n -> pr "      char *%s;\n" n
-       | StringList n -> pr "      char **%s;\n" n
-       | Bool n -> pr "      int %s;\n" n
-       | Int n -> pr "      int %s;\n" n
+      iteri (
+       fun i ->
+         function
+         | String n | FileIn n | FileOut n -> pr "      char *%s;\n" n
+         | OptString n ->
+             (* http://www.perlmonks.org/?node_id=554277
+              * Note that the implicit handle argument means we have
+              * to add 1 to the ST(x) operator.
+              *)
+             pr "      char *%s = SvOK(ST(%d)) ? SvPV_nolen(ST(%d)) : NULL;\n" n (i+1) (i+1)
+         | StringList n -> pr "      char **%s;\n" n
+         | Bool n -> pr "      int %s;\n" n
+         | Int n -> pr "      int %s;\n" n
       ) (snd style);
 
       let do_cleanups () =
@@ -5025,39 +5984,20 @@ DESTROY (g)
           pr "        free (%s[i]);\n" n;
           pr "      }\n";
           pr "      free (%s);\n" n;
-       | RIntBool _ ->
-          pr "PREINIT:\n";
-          pr "      struct guestfs_int_bool *r;\n";
-          pr " PPCODE:\n";
-          pr "      r = guestfs_%s " name;
-          generate_call_args ~handle:"g" (snd style);
-          pr ";\n";
-          do_cleanups ();
-          pr "      if (r == NULL)\n";
-          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
-          pr "      EXTEND (SP, 2);\n";
-          pr "      PUSHs (sv_2mortal (newSViv (r->i)));\n";
-          pr "      PUSHs (sv_2mortal (newSViv (r->b)));\n";
-          pr "      guestfs_free_int_bool (r);\n";
-       | RPVList n ->
-          generate_perl_lvm_code "pv" pv_cols name style n do_cleanups
-       | RVGList n ->
-          generate_perl_lvm_code "vg" vg_cols name style n do_cleanups
-       | RLVList n ->
-          generate_perl_lvm_code "lv" lv_cols name style n do_cleanups
-       | RStat n ->
-          generate_perl_stat_code "stat" stat_cols name style n do_cleanups
-       | RStatVFS n ->
-          generate_perl_stat_code
-            "statvfs" statvfs_cols name style n do_cleanups
+       | RStruct (n, typ) ->
+          let cols = cols_of_struct typ in
+          generate_perl_struct_code typ cols name style n do_cleanups
+       | RStructList (n, typ) ->
+          let cols = cols_of_struct typ in
+          generate_perl_struct_list_code typ cols name style n do_cleanups
       );
 
       pr "\n"
   ) all_functions
 
-and generate_perl_lvm_code typ cols name style n do_cleanups =
+and generate_perl_struct_list_code typ cols name style n do_cleanups =
   pr "PREINIT:\n";
-  pr "      struct guestfs_lvm_%s_list *%s;\n" typ n;
+  pr "      struct guestfs_%s_list *%s;\n" typ n;
   pr "      int i;\n";
   pr "      HV *hv;\n";
   pr " PPCODE:\n";
@@ -5072,27 +6012,33 @@ and generate_perl_lvm_code typ cols name style n do_cleanups =
   pr "        hv = newHV ();\n";
   List.iter (
     function
-    | name, `String ->
+    | name, FString ->
        pr "        (void) hv_store (hv, \"%s\", %d, newSVpv (%s->val[i].%s, 0), 0);\n"
          name (String.length name) n name
-    | name, `UUID ->
+    | name, FUUID ->
        pr "        (void) hv_store (hv, \"%s\", %d, newSVpv (%s->val[i].%s, 32), 0);\n"
          name (String.length name) n name
-    | name, `Bytes ->
+    | name, (FBytes|FUInt64) ->
        pr "        (void) hv_store (hv, \"%s\", %d, my_newSVull (%s->val[i].%s), 0);\n"
          name (String.length name) n name
-    | name, `Int ->
+    | name, FInt64 ->
        pr "        (void) hv_store (hv, \"%s\", %d, my_newSVll (%s->val[i].%s), 0);\n"
          name (String.length name) n name
-    | name, `OptPercent ->
+    | name, (FInt32|FUInt32) ->
+       pr "        (void) hv_store (hv, \"%s\", %d, newSVnv (%s->val[i].%s), 0);\n"
+         name (String.length name) n name
+    | name, FChar ->
+       pr "        (void) hv_store (hv, \"%s\", %d, newSVpv (&%s->val[i].%s, 1), 0);\n"
+         name (String.length name) n name
+    | name, FOptPercent ->
        pr "        (void) hv_store (hv, \"%s\", %d, newSVnv (%s->val[i].%s), 0);\n"
          name (String.length name) n name
   ) cols;
-  pr "        PUSHs (sv_2mortal ((SV *) hv));\n";
+  pr "        PUSHs (sv_2mortal (newRV ((SV *) hv)));\n";
   pr "      }\n";
-  pr "      guestfs_free_lvm_%s_list (%s);\n" typ n
+  pr "      guestfs_free_%s_list (%s);\n" typ n
 
-and generate_perl_stat_code typ cols name style n do_cleanups =
+and generate_perl_struct_code typ cols name style n do_cleanups =
   pr "PREINIT:\n";
   pr "      struct guestfs_%s *%s;\n" typ n;
   pr " PPCODE:\n";
@@ -5105,8 +6051,27 @@ and generate_perl_stat_code typ cols name style n do_cleanups =
   pr "      EXTEND (SP, %d);\n" (List.length cols);
   List.iter (
     function
-    | name, `Int ->
-       pr "      PUSHs (sv_2mortal (my_newSVll (%s->%s)));\n" n name
+    | name, FString ->
+       pr "      PUSHs (sv_2mortal (newSVpv (%s->%s, 0)));\n"
+         n name
+    | name, FUUID ->
+       pr "      PUSHs (sv_2mortal (newSVpv (%s->%s, 32)));\n"
+         n name
+    | name, (FBytes|FUInt64) ->
+       pr "      PUSHs (sv_2mortal (my_newSVull (%s->%s)));\n"
+         n name
+    | name, FInt64 ->
+       pr "      PUSHs (sv_2mortal (my_newSVll (%s->%s)));\n"
+         n name
+    | name, (FInt32|FUInt32) ->
+       pr "      PUSHs (sv_2mortal (newSVnv (%s->%s)));\n"
+         n name
+    | name, FChar ->
+       pr "      PUSHs (sv_2mortal (newSVpv (&%s->%s, 1)));\n"
+         n name
+    | name, FOptPercent ->
+       pr "      PUSHs (sv_2mortal (newSVnv (%s->%s)));\n"
+         n name
   ) cols;
   pr "      free (%s);\n" n
 
@@ -5124,7 +6089,7 @@ Sys::Guestfs - Perl bindings for libguestfs
 =head1 SYNOPSIS
 
  use Sys::Guestfs;
+
  my $h = Sys::Guestfs->new ();
  $h->add_drive ('guest.img');
  $h->launch ();
@@ -5195,15 +6160,17 @@ sub new {
    *)
   List.iter (
     fun (name, style, _, flags, _, _, longdesc) ->
-      let longdesc = replace_str longdesc "C<guestfs_" "C<$h-E<gt>" in
-      pr "=item ";
-      generate_perl_prototype name style;
-      pr "\n\n";
-      pr "%s\n\n" longdesc;
-      if List.mem ProtocolLimitWarning flags then
-       pr "%s\n\n" protocol_limit_warning;
-      if List.mem DangerWillRobinson flags then
-       pr "%s\n\n" danger_will_robinson
+      if not (List.mem NotInDocs flags) then (
+       let longdesc = replace_str longdesc "C<guestfs_" "C<$h-E<gt>" in
+       pr "=item ";
+       generate_perl_prototype name style;
+       pr "\n\n";
+       pr "%s\n\n" longdesc;
+       if List.mem ProtocolLimitWarning flags then
+         pr "%s\n\n" protocol_limit_warning;
+       if List.mem DangerWillRobinson flags then
+         pr "%s\n\n" danger_will_robinson
+      )
   ) all_functions_sorted;
 
   (* End of file. *)
@@ -5237,14 +6204,10 @@ and generate_perl_prototype name style =
    | RInt64 n
    | RConstString n
    | RString n -> pr "$%s = " n
-   | RIntBool (n, m) -> pr "($%s, $%s) = " n m
-   | RStringList n
-   | RPVList n
-   | RVGList n
-   | RLVList n -> pr "@%s = " n
-   | RStat n
-   | RStatVFS n
+   | RStruct (n,_)
    | RHashtable n -> pr "%%%s = " n
+   | RStringList n
+   | RStructList (n,_) -> pr "@%s = " n
   );
   pr "$h->%s (" name;
   let comma = ref false in
@@ -5401,34 +6364,42 @@ py_guestfs_close (PyObject *self, PyObject *args)
 
 ";
 
-  (* LVM structures, turned into Python dictionaries. *)
+  (* Structures, turned into Python dictionaries. *)
   List.iter (
     fun (typ, cols) ->
       pr "static PyObject *\n";
-      pr "put_lvm_%s (struct guestfs_lvm_%s *%s)\n" typ typ typ;
+      pr "put_%s (struct guestfs_%s *%s)\n" typ typ typ;
       pr "{\n";
       pr "  PyObject *dict;\n";
       pr "\n";
       pr "  dict = PyDict_New ();\n";
       List.iter (
        function
-       | name, `String ->
+       | name, FString ->
            pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
            pr "                        PyString_FromString (%s->%s));\n"
              typ name
-       | name, `UUID ->
+       | name, FUUID ->
            pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
            pr "                        PyString_FromStringAndSize (%s->%s, 32));\n"
              typ name
-       | name, `Bytes ->
+       | name, (FBytes|FUInt64) ->
            pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
            pr "                        PyLong_FromUnsignedLongLong (%s->%s));\n"
              typ name
-       | name, `Int ->
+       | name, FInt64 ->
            pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
            pr "                        PyLong_FromLongLong (%s->%s));\n"
              typ name
-       | name, `OptPercent ->
+       | name, FUInt32 ->
+           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                        PyLong_FromUnsignedLong (%s->%s));\n"
+             typ name
+       | name, FInt32 ->
+           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                        PyLong_FromLong (%s->%s));\n"
+             typ name
+       | name, FOptPercent ->
            pr "  if (%s->%s >= 0)\n" typ name;
            pr "    PyDict_SetItemString (dict, \"%s\",\n" name;
            pr "                          PyFloat_FromDouble ((double) %s->%s));\n"
@@ -5437,45 +6408,27 @@ py_guestfs_close (PyObject *self, PyObject *args)
            pr "    Py_INCREF (Py_None);\n";
            pr "    PyDict_SetItemString (dict, \"%s\", Py_None);" name;
            pr "  }\n"
+       | name, FChar ->
+           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                        PyString_FromStringAndSize (&dirent->%s, 1));\n" name
       ) cols;
       pr "  return dict;\n";
       pr "};\n";
       pr "\n";
 
       pr "static PyObject *\n";
-      pr "put_lvm_%s_list (struct guestfs_lvm_%s_list *%ss)\n" typ typ typ;
+      pr "put_%s_list (struct guestfs_%s_list *%ss)\n" typ typ typ;
       pr "{\n";
       pr "  PyObject *list;\n";
       pr "  int i;\n";
       pr "\n";
       pr "  list = PyList_New (%ss->len);\n" typ;
       pr "  for (i = 0; i < %ss->len; ++i)\n" typ;
-      pr "    PyList_SetItem (list, i, put_lvm_%s (&%ss->val[i]));\n" typ typ;
+      pr "    PyList_SetItem (list, i, put_%s (&%ss->val[i]));\n" typ typ;
       pr "  return list;\n";
       pr "};\n";
       pr "\n"
-  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
-
-  (* Stat structures, turned into Python dictionaries. *)
-  List.iter (
-    fun (typ, cols) ->
-      pr "static PyObject *\n";
-      pr "put_%s (struct guestfs_%s *%s)\n" typ typ typ;
-      pr "{\n";
-      pr "  PyObject *dict;\n";
-      pr "\n";
-      pr "  dict = PyDict_New ();\n";
-      List.iter (
-       function
-       | name, `Int ->
-           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
-           pr "                        PyLong_FromLongLong (%s->%s));\n"
-             typ name
-      ) cols;
-      pr "  return dict;\n";
-      pr "};\n";
-      pr "\n";
-  ) ["stat", stat_cols; "statvfs", statvfs_cols];
+  ) structs;
 
   (* Python wrapper functions. *)
   List.iter (
@@ -5495,12 +6448,9 @@ py_guestfs_close (PyObject *self, PyObject *args)
        | RConstString _ -> pr "  const char *r;\n"; "NULL"
        | RString _ -> pr "  char *r;\n"; "NULL"
        | RStringList _ | RHashtable _ -> pr "  char **r;\n"; "NULL"
-       | RIntBool _ -> pr "  struct guestfs_int_bool *r;\n"; "NULL"
-       | RPVList n -> pr "  struct guestfs_lvm_pv_list *r;\n"; "NULL"
-       | RVGList n -> pr "  struct guestfs_lvm_vg_list *r;\n"; "NULL"
-       | RLVList n -> pr "  struct guestfs_lvm_lv_list *r;\n"; "NULL"
-       | RStat n -> pr "  struct guestfs_stat *r;\n"; "NULL"
-       | RStatVFS n -> pr "  struct guestfs_statvfs *r;\n"; "NULL" in
+       | RStruct (_, typ) -> pr "  struct guestfs_%s *r;\n" typ; "NULL"
+       | RStructList (_, typ) ->
+           pr "  struct guestfs_%s_list *r;\n" typ; "NULL" in
 
       List.iter (
        function
@@ -5581,26 +6531,12 @@ py_guestfs_close (PyObject *self, PyObject *args)
        | RStringList _ ->
           pr "  py_r = put_string_list (r);\n";
           pr "  free_strings (r);\n"
-       | RIntBool _ ->
-          pr "  py_r = PyTuple_New (2);\n";
-          pr "  PyTuple_SetItem (py_r, 0, PyInt_FromLong ((long) r->i));\n";
-          pr "  PyTuple_SetItem (py_r, 1, PyInt_FromLong ((long) r->b));\n";
-          pr "  guestfs_free_int_bool (r);\n"
-       | RPVList n ->
-          pr "  py_r = put_lvm_pv_list (r);\n";
-          pr "  guestfs_free_lvm_pv_list (r);\n"
-       | RVGList n ->
-          pr "  py_r = put_lvm_vg_list (r);\n";
-          pr "  guestfs_free_lvm_vg_list (r);\n"
-       | RLVList n ->
-          pr "  py_r = put_lvm_lv_list (r);\n";
-          pr "  guestfs_free_lvm_lv_list (r);\n"
-       | RStat n ->
-          pr "  py_r = put_stat (r);\n";
-          pr "  free (r);\n"
-       | RStatVFS n ->
-          pr "  py_r = put_statvfs (r);\n";
-          pr "  free (r);\n"
+       | RStruct (_, typ) ->
+          pr "  py_r = put_%s (r);\n" typ;
+          pr "  guestfs_free_%s (r);\n" typ
+       | RStructList (_, typ) ->
+          pr "  py_r = put_%s_list (r);\n" typ;
+          pr "  guestfs_free_%s_list (r);\n" typ
        | RHashtable n ->
           pr "  py_r = put_table (r);\n";
           pr "  free_strings (r);\n"
@@ -5706,43 +6642,37 @@ class GuestFS:
 
   List.iter (
     fun (name, style, _, flags, _, _, longdesc) ->
-      let doc = replace_str longdesc "C<guestfs_" "C<g." in
-      let doc =
-        match fst style with
-       | RErr | RInt _ | RInt64 _ | RBool _ | RConstString _
-       | RString _ -> doc
-       | RStringList _ ->
-           doc ^ "\n\nThis function returns a list of strings."
-       | RIntBool _ ->
-           doc ^ "\n\nThis function returns a tuple (int, bool).\n"
-       | RPVList _ ->
-           doc ^ "\n\nThis function returns a list of PVs.  Each PV is represented as a dictionary."
-       | RVGList _ ->
-           doc ^ "\n\nThis function returns a list of VGs.  Each VG is represented as a dictionary."
-       | RLVList _ ->
-           doc ^ "\n\nThis function returns a list of LVs.  Each LV is represented as a dictionary."
-       | RStat _ ->
-           doc ^ "\n\nThis function returns a dictionary, with keys matching the various fields in the stat structure."
-       | RStatVFS _ ->
-           doc ^ "\n\nThis function returns a dictionary, with keys matching the various fields in the statvfs structure."
-       | RHashtable _ ->
-           doc ^ "\n\nThis function returns a dictionary." in
-      let doc =
-       if List.mem ProtocolLimitWarning flags then
-         doc ^ "\n\n" ^ protocol_limit_warning
-       else doc in
-      let doc =
-       if List.mem DangerWillRobinson flags then
-         doc ^ "\n\n" ^ danger_will_robinson
-       else doc in
-      let doc = pod2text ~width:60 name doc in
-      let doc = List.map (fun line -> replace_str line "\\" "\\\\") doc in
-      let doc = String.concat "\n        " doc in
-
       pr "    def %s " name;
       generate_call_args ~handle:"self" (snd style);
       pr ":\n";
-      pr "        u\"\"\"%s\"\"\"\n" doc;
+
+      if not (List.mem NotInDocs flags) then (
+       let doc = replace_str longdesc "C<guestfs_" "C<g." in
+       let doc =
+          match fst style with
+         | RErr | RInt _ | RInt64 _ | RBool _ | RConstString _
+         | RString _ -> doc
+         | RStringList _ ->
+             doc ^ "\n\nThis function returns a list of strings."
+         | RStruct (_, typ) ->
+             doc ^ sprintf "\n\nThis function returns a dictionary, with keys matching the various fields in the guestfs_%s structure." typ
+         | RStructList (_, typ) ->
+             doc ^ sprintf "\n\nThis function returns a list of %ss.  Each %s is represented as a dictionary." typ typ
+         | RHashtable _ ->
+             doc ^ "\n\nThis function returns a dictionary." in
+       let doc =
+         if List.mem ProtocolLimitWarning flags then
+           doc ^ "\n\n" ^ protocol_limit_warning
+         else doc in
+       let doc =
+         if List.mem DangerWillRobinson flags then
+           doc ^ "\n\n" ^ danger_will_robinson
+         else doc in
+       let doc = pod2text ~width:60 name doc in
+       let doc = List.map (fun line -> replace_str line "\\" "\\\\") doc in
+       let doc = String.concat "\n        " doc in
+       pr "        u\"\"\"%s\"\"\"\n" doc;
+      );
       pr "        return libguestfsmod.%s " name;
       generate_call_args ~handle:"self._o" (snd style);
       pr "\n";
@@ -5752,32 +6682,42 @@ class GuestFS:
 (* Useful if you need the longdesc POD text as plain text.  Returns a
  * list of lines.
  *
- * This is the slowest thing about autogeneration.
+ * Because this is very slow (the slowest part of autogeneration),
+ * we memoize the results.
  *)
 and pod2text ~width name longdesc =
-  let filename, chan = Filename.open_temp_file "gen" ".tmp" in
-  fprintf chan "=head1 %s\n\n%s\n" name longdesc;
-  close_out chan;
-  let cmd = sprintf "pod2text -w %d %s" width (Filename.quote filename) in
-  let chan = Unix.open_process_in cmd in
-  let lines = ref [] in
-  let rec loop i =
-    let line = input_line chan in
-    if i = 1 then              (* discard the first line of output *)
-      loop (i+1)
-    else (
-      let line = triml line in
-      lines := line :: !lines;
-      loop (i+1)
-    ) in
-  let lines = try loop 1 with End_of_file -> List.rev !lines in
-  Unix.unlink filename;
-  match Unix.close_process_in chan with
-  | Unix.WEXITED 0 -> lines
-  | Unix.WEXITED i ->
-      failwithf "pod2text: process exited with non-zero status (%d)" i
-  | Unix.WSIGNALED i | Unix.WSTOPPED i ->
-      failwithf "pod2text: process signalled or stopped by signal %d" i
+  let key = width, name, longdesc in
+  try Hashtbl.find pod2text_memo key
+  with Not_found ->
+    let filename, chan = Filename.open_temp_file "gen" ".tmp" in
+    fprintf chan "=head1 %s\n\n%s\n" name longdesc;
+    close_out chan;
+    let cmd = sprintf "pod2text -w %d %s" width (Filename.quote filename) in
+    let chan = Unix.open_process_in cmd in
+    let lines = ref [] in
+    let rec loop i =
+      let line = input_line chan in
+      if i = 1 then            (* discard the first line of output *)
+       loop (i+1)
+      else (
+       let line = triml line in
+       lines := line :: !lines;
+       loop (i+1)
+      ) in
+    let lines = try loop 1 with End_of_file -> List.rev !lines in
+    Unix.unlink filename;
+    (match Unix.close_process_in chan with
+     | Unix.WEXITED 0 -> ()
+     | Unix.WEXITED i ->
+        failwithf "pod2text: process exited with non-zero status (%d)" i
+     | Unix.WSIGNALED i | Unix.WSTOPPED i ->
+        failwithf "pod2text: process signalled or stopped by signal %d" i
+    );
+    Hashtbl.add pod2text_memo key lines;
+    let chan = open_out pod2text_memo_filename in
+    output_value chan pod2text_memo;
+    close_out chan;
+    lines
 
 (* Generate ruby bindings. *)
 and generate_ruby_c () =
@@ -5854,14 +6794,16 @@ static VALUE ruby_guestfs_close (VALUE gv)
       List.iter (
        function
        | String n | FileIn n | FileOut n ->
+           pr "  Check_Type (%sv, T_STRING);\n" n;
            pr "  const char *%s = StringValueCStr (%sv);\n" n n;
            pr "  if (!%s)\n" n;
            pr "    rb_raise (rb_eTypeError, \"expected string for parameter %%s of %%s\",\n";
            pr "              \"%s\", \"%s\");\n" n name
        | OptString n ->
-           pr "  const char *%s = StringValueCStr (%sv);\n" n n
+           pr "  const char *%s = !NIL_P (%sv) ? StringValueCStr (%sv) : NULL;\n" n n n
        | StringList n ->
-           pr "  char **%s;" n;
+           pr "  char **%s;\n" n;
+           pr "  Check_Type (%sv, T_ARRAY);\n" n;
            pr "  {\n";
            pr "    int i, len;\n";
            pr "    len = RARRAY_LEN (%sv);\n" n;
@@ -5873,7 +6815,8 @@ static VALUE ruby_guestfs_close (VALUE gv)
            pr "    }\n";
            pr "    %s[len] = NULL;\n" n;
            pr "  }\n";
-       | Bool n
+       | Bool n ->
+           pr "  int %s = RTEST (%sv);\n" n n
        | Int n ->
            pr "  int %s = NUM2INT (%sv);\n" n n
       ) (snd style);
@@ -5886,12 +6829,9 @@ static VALUE ruby_guestfs_close (VALUE gv)
        | RConstString _ -> pr "  const char *r;\n"; "NULL"
        | RString _ -> pr "  char *r;\n"; "NULL"
        | RStringList _ | RHashtable _ -> pr "  char **r;\n"; "NULL"
-       | RIntBool _ -> pr "  struct guestfs_int_bool *r;\n"; "NULL"
-       | RPVList n -> pr "  struct guestfs_lvm_pv_list *r;\n"; "NULL"
-       | RVGList n -> pr "  struct guestfs_lvm_vg_list *r;\n"; "NULL"
-       | RLVList n -> pr "  struct guestfs_lvm_lv_list *r;\n"; "NULL"
-       | RStat n -> pr "  struct guestfs_stat *r;\n"; "NULL"
-       | RStatVFS n -> pr "  struct guestfs_statvfs *r;\n"; "NULL" in
+       | RStruct (_, typ) -> pr "  struct guestfs_%s *r;\n" typ; "NULL"
+       | RStructList (_, typ) ->
+           pr "  struct guestfs_%s_list *r;\n" typ; "NULL" in
       pr "\n";
 
       pr "  r = guestfs_%s " name;
@@ -5932,36 +6872,12 @@ static VALUE ruby_guestfs_close (VALUE gv)
           pr "  }\n";
           pr "  free (r);\n";
           pr "  return rv;\n"
-       | RIntBool _ ->
-          pr "  VALUE rv = rb_ary_new2 (2);\n";
-          pr "  rb_ary_push (rv, INT2NUM (r->i));\n";
-          pr "  rb_ary_push (rv, INT2NUM (r->b));\n";
-          pr "  guestfs_free_int_bool (r);\n";
-          pr "  return rv;\n"
-       | RPVList n ->
-          generate_ruby_lvm_code "pv" pv_cols
-       | RVGList n ->
-          generate_ruby_lvm_code "vg" vg_cols
-       | RLVList n ->
-          generate_ruby_lvm_code "lv" lv_cols
-       | RStat n ->
-          pr "  VALUE rv = rb_hash_new ();\n";
-          List.iter (
-            function
-            | name, `Int ->
-                pr "  rb_hash_aset (rv, rb_str_new2 (\"%s\"), ULL2NUM (r->%s));\n" name name
-          ) stat_cols;
-          pr "  free (r);\n";
-          pr "  return rv;\n"
-       | RStatVFS n ->
-          pr "  VALUE rv = rb_hash_new ();\n";
-          List.iter (
-            function
-            | name, `Int ->
-                pr "  rb_hash_aset (rv, rb_str_new2 (\"%s\"), ULL2NUM (r->%s));\n" name name
-          ) statvfs_cols;
-          pr "  free (r);\n";
-          pr "  return rv;\n"
+       | RStruct (_, typ) ->
+          let cols = cols_of_struct typ in
+          generate_ruby_struct_code typ cols
+       | RStructList (_, typ) ->
+          let cols = cols_of_struct typ in
+          generate_ruby_struct_list_code typ cols
        | RHashtable _ ->
           pr "  VALUE rv = rb_hash_new ();\n";
           pr "  int i;\n";
@@ -5999,27 +6915,59 @@ void Init__guestfs ()
 
   pr "}\n"
 
-(* Ruby code to return an LVM struct list. *)
-and generate_ruby_lvm_code typ cols =
+(* Ruby code to return a struct. *)
+and generate_ruby_struct_code typ cols =
+  pr "  VALUE rv = rb_hash_new ();\n";
+  List.iter (
+    function
+    | name, FString ->
+       pr "  rb_hash_aset (rv, rb_str_new2 (\"%s\"), rb_str_new2 (r->%s));\n" name name
+    | name, FUUID ->
+       pr "  rb_hash_aset (rv, rb_str_new2 (\"%s\"), rb_str_new (r->%s, 32));\n" name name
+    | name, (FBytes|FUInt64) ->
+       pr "  rb_hash_aset (rv, rb_str_new2 (\"%s\"), ULL2NUM (r->%s));\n" name name
+    | name, FInt64 ->
+       pr "  rb_hash_aset (rv, rb_str_new2 (\"%s\"), LL2NUM (r->%s));\n" name name
+    | name, FUInt32 ->
+       pr "  rb_hash_aset (rv, rb_str_new2 (\"%s\"), UINT2NUM (r->%s));\n" name name
+    | name, FInt32 ->
+       pr "  rb_hash_aset (rv, rb_str_new2 (\"%s\"), INT2NUM (r->%s));\n" name name
+    | name, FOptPercent ->
+       pr "  rb_hash_aset (rv, rb_str_new2 (\"%s\"), rb_dbl2big (r->%s));\n" name name
+    | name, FChar -> (* XXX wrong? *)
+       pr "  rb_hash_aset (rv, rb_str_new2 (\"%s\"), ULL2NUM (r->%s));\n" name name
+  ) cols;
+  pr "  guestfs_free_%s (r);\n" typ;
+  pr "  return rv;\n"
+
+(* Ruby code to return a struct list. *)
+and generate_ruby_struct_list_code typ cols =
   pr "  VALUE rv = rb_ary_new2 (r->len);\n";
   pr "  int i;\n";
   pr "  for (i = 0; i < r->len; ++i) {\n";
   pr "    VALUE hv = rb_hash_new ();\n";
   List.iter (
     function
-    | name, `String ->
-       pr "    rb_hash_aset (rv, rb_str_new2 (\"%s\"), rb_str_new2 (r->val[i].%s));\n" name name
-    | name, `UUID ->
-       pr "    rb_hash_aset (rv, rb_str_new2 (\"%s\"), rb_str_new (r->val[i].%s, 32));\n" name name
-    | name, `Bytes
-    | name, `Int ->
-       pr "    rb_hash_aset (rv, rb_str_new2 (\"%s\"), ULL2NUM (r->val[i].%s));\n" name name
-    | name, `OptPercent ->
-       pr "    rb_hash_aset (rv, rb_str_new2 (\"%s\"), rb_dbl2big (r->val[i].%s));\n" name name
+    | name, FString ->
+       pr "    rb_hash_aset (hv, rb_str_new2 (\"%s\"), rb_str_new2 (r->val[i].%s));\n" name name
+    | name, FUUID ->
+       pr "    rb_hash_aset (hv, rb_str_new2 (\"%s\"), rb_str_new (r->val[i].%s, 32));\n" name name
+    | name, (FBytes|FUInt64) ->
+       pr "    rb_hash_aset (hv, rb_str_new2 (\"%s\"), ULL2NUM (r->val[i].%s));\n" name name
+    | name, FInt64 ->
+       pr "    rb_hash_aset (hv, rb_str_new2 (\"%s\"), LL2NUM (r->val[i].%s));\n" name name
+    | name, FUInt32 ->
+       pr "    rb_hash_aset (hv, rb_str_new2 (\"%s\"), UINT2NUM (r->val[i].%s));\n" name name
+    | name, FInt32 ->
+       pr "    rb_hash_aset (hv, rb_str_new2 (\"%s\"), INT2NUM (r->val[i].%s));\n" name name
+    | name, FOptPercent ->
+       pr "    rb_hash_aset (hv, rb_str_new2 (\"%s\"), rb_dbl2big (r->val[i].%s));\n" name name
+    | name, FChar -> (* XXX wrong? *)
+       pr "    rb_hash_aset (hv, rb_str_new2 (\"%s\"), ULL2NUM (r->val[i].%s));\n" name name
   ) cols;
   pr "    rb_ary_push (rv, hv);\n";
   pr "  }\n";
-  pr "  guestfs_free_lvm_%s_list (r);\n" typ;
+  pr "  guestfs_free_%s_list (r);\n" typ;
   pr "  return rv;\n"
 
 (* Generate Java bindings GuestFS.java file. *)
@@ -6037,6 +6985,7 @@ import com.redhat.et.libguestfs.LV;
 import com.redhat.et.libguestfs.Stat;
 import com.redhat.et.libguestfs.StatVFS;
 import com.redhat.et.libguestfs.IntBool;
+import com.redhat.et.libguestfs.Dirent;
 
 /**
  * The GuestFS object is a libguestfs handle.
@@ -6093,25 +7042,32 @@ public class GuestFS {
 
   List.iter (
     fun (name, style, _, flags, _, shortdesc, longdesc) ->
-      let doc = replace_str longdesc "C<guestfs_" "C<g." in
-      let doc =
-       if List.mem ProtocolLimitWarning flags then
-         doc ^ "\n\n" ^ protocol_limit_warning
-       else doc in
-      let doc =
-       if List.mem DangerWillRobinson flags then
-         doc ^ "\n\n" ^ danger_will_robinson
-       else doc in
-      let doc = pod2text ~width:60 name doc in
-      let doc = String.concat "\n   * " doc in
-
-      pr "  /**\n";
-      pr "   * %s\n" shortdesc;
-      pr "   *\n";
-      pr "   * %s\n" doc;
-      pr "   * @throws LibGuestFSException\n";
-      pr "   */\n";
-      pr "  ";
+      if not (List.mem NotInDocs flags); then (
+       let doc = replace_str longdesc "C<guestfs_" "C<g." in
+       let doc =
+         if List.mem ProtocolLimitWarning flags then
+           doc ^ "\n\n" ^ protocol_limit_warning
+         else doc in
+       let doc =
+         if List.mem DangerWillRobinson flags then
+           doc ^ "\n\n" ^ danger_will_robinson
+         else doc in
+       let doc = pod2text ~width:60 name doc in
+       let doc = List.map (            (* RHBZ#501883 *)
+         function
+         | "" -> "<p>"
+         | nonempty -> nonempty
+       ) doc in
+       let doc = String.concat "\n   * " doc in
+
+       pr "  /**\n";
+       pr "   * %s\n" shortdesc;
+       pr "   * <p>\n";
+       pr "   * %s\n" doc;
+       pr "   * @throws LibGuestFSException\n";
+       pr "   */\n";
+       pr "  ";
+      );
       generate_java_prototype ~public:true ~semicolon:false name style;
       pr "\n";
       pr "  {\n";
@@ -6146,12 +7102,12 @@ and generate_java_prototype ?(public=false) ?(privat=false) ?(native=false)
    | RBool _ -> pr "boolean ";
    | RConstString _ | RString _ -> pr "String ";
    | RStringList _ -> pr "String[] ";
-   | RIntBool _ -> pr "IntBool ";
-   | RPVList _ -> pr "PV[] ";
-   | RVGList _ -> pr "VG[] ";
-   | RLVList _ -> pr "LV[] ";
-   | RStat _ -> pr "Stat ";
-   | RStatVFS _ -> pr "StatVFS ";
+   | RStruct (_, typ) ->
+       let name = java_name_of_struct typ in
+       pr "%s " name;
+   | RStructList (_, typ) ->
+       let name = java_name_of_struct typ in
+       pr "%s[] " name;
    | RHashtable _ -> pr "HashMap<String,String> ";
   );
 
@@ -6187,7 +7143,7 @@ and generate_java_prototype ?(public=false) ?(privat=false) ?(native=false)
   pr "    throws LibGuestFSException";
   if semicolon then pr ";"
 
-and generate_java_struct typ cols =
+and generate_java_struct jtyp cols =
   generate_header CStyle LGPLv2;
 
   pr "\
@@ -6200,15 +7156,16 @@ package com.redhat.et.libguestfs;
  * @see GuestFS
  */
 public class %s {
-" typ typ;
+" jtyp jtyp;
 
   List.iter (
     function
-    | name, `String
-    | name, `UUID -> pr "  public String %s;\n" name
-    | name, `Bytes
-    | name, `Int -> pr "  public long %s;\n" name
-    | name, `OptPercent ->
+    | name, FString
+    | name, FUUID -> pr "  public String %s;\n" name
+    | name, (FBytes|FUInt64|FInt64) -> pr "  public long %s;\n" name
+    | name, (FUInt32|FInt32) -> pr "  public int %s;\n" name
+    | name, FChar -> pr "  public char %s;\n" name
+    | name, FOptPercent ->
        pr "  /* The next field is [0..100] or -1 meaning 'not present': */\n";
        pr "  public float %s;\n" name
   ) cols;
@@ -6272,9 +7229,9 @@ Java_com_redhat_et_libguestfs_GuestFS__1close
        | RInt64 _ -> pr "jlong ";
        | RBool _ -> pr "jboolean ";
        | RConstString _ | RString _ -> pr "jstring ";
-       | RIntBool _ | RStat _ | RStatVFS _ | RHashtable _ ->
+       | RStruct _ | RHashtable _ ->
           pr "jobject ";
-       | RStringList _ | RPVList _ | RVGList _ | RLVList _ ->
+       | RStringList _ | RStructList _ ->
           pr "jobjectArray ";
       );
       pr "JNICALL\n";
@@ -6315,39 +7272,17 @@ Java_com_redhat_et_libguestfs_GuestFS__1close
            pr "  jclass cl;\n";
            pr "  jstring jstr;\n";
            pr "  char **r;\n"; "NULL", "NULL"
-       | RIntBool _ ->
-           pr "  jobject jr;\n";
-           pr "  jclass cl;\n";
-           pr "  jfieldID fl;\n";
-           pr "  struct guestfs_int_bool *r;\n"; "NULL", "NULL"
-       | RStat _ ->
-           pr "  jobject jr;\n";
-           pr "  jclass cl;\n";
-           pr "  jfieldID fl;\n";
-           pr "  struct guestfs_stat *r;\n"; "NULL", "NULL"
-       | RStatVFS _ ->
+       | RStruct (_, typ) ->
            pr "  jobject jr;\n";
            pr "  jclass cl;\n";
            pr "  jfieldID fl;\n";
-           pr "  struct guestfs_statvfs *r;\n"; "NULL", "NULL"
-       | RPVList _ ->
-           pr "  jobjectArray jr;\n";
-           pr "  jclass cl;\n";
-           pr "  jfieldID fl;\n";
-           pr "  jobject jfl;\n";
-           pr "  struct guestfs_lvm_pv_list *r;\n"; "NULL", "NULL"
-       | RVGList _ ->
-           pr "  jobjectArray jr;\n";
-           pr "  jclass cl;\n";
-           pr "  jfieldID fl;\n";
-           pr "  jobject jfl;\n";
-           pr "  struct guestfs_lvm_vg_list *r;\n"; "NULL", "NULL"
-       | RLVList _ ->
+           pr "  struct guestfs_%s *r;\n" typ; "NULL", "NULL"
+       | RStructList (_, typ) ->
            pr "  jobjectArray jr;\n";
            pr "  jclass cl;\n";
            pr "  jfieldID fl;\n";
            pr "  jobject jfl;\n";
-           pr "  struct guestfs_lvm_lv_list *r;\n"; "NULL", "NULL"
+           pr "  struct guestfs_%s_list *r;\n" typ; "NULL", "NULL"
        | RHashtable _ -> pr "  char **r;\n"; "NULL", "NULL" in
       List.iter (
        function
@@ -6366,10 +7301,9 @@ Java_com_redhat_et_libguestfs_GuestFS__1close
 
       let needs_i =
        (match fst style with
-        | RStringList _ | RPVList _ | RVGList _ | RLVList _ -> true
+        | RStringList _ | RStructList _ -> true
         | RErr | RBool _ | RInt _ | RInt64 _ | RConstString _
-        | RString _ | RIntBool _ | RStat _ | RStatVFS _
-        | RHashtable _ -> false) ||
+        | RString _ | RStruct _ | RHashtable _ -> false) ||
        List.exists (function StringList _ -> true | _ -> false) (snd style) in
       if needs_i then
        pr "  int i;\n";
@@ -6380,10 +7314,14 @@ Java_com_redhat_et_libguestfs_GuestFS__1close
       List.iter (
        function
        | String n
-       | OptString n
        | FileIn n
        | FileOut n ->
            pr "  %s = (*env)->GetStringUTFChars (env, j%s, NULL);\n" n n
+       | OptString n ->
+           (* This is completely undocumented, but Java null becomes
+            * a NULL parameter.
+            *)
+           pr "  %s = j%s ? (*env)->GetStringUTFChars (env, j%s, NULL) : NULL;\n" n n n
        | StringList n ->
            pr "  %s_len = (*env)->GetArrayLength (env, j%s);\n" n n;
            pr "  %s = guestfs_safe_malloc (g, sizeof (char *) * (%s_len+1));\n" n n;
@@ -6407,10 +7345,12 @@ Java_com_redhat_et_libguestfs_GuestFS__1close
       List.iter (
        function
        | String n
-       | OptString n
        | FileIn n
        | FileOut n ->
            pr "  (*env)->ReleaseStringUTFChars (env, j%s, %s);\n" n n
+       | OptString n ->
+           pr "  if (j%s)\n" n;
+           pr "    (*env)->ReleaseStringUTFChars (env, j%s, %s);\n" n n
        | StringList n ->
            pr "  for (i = 0; i < %s_len; ++i) {\n" n;
            pr "    jobject o = (*env)->GetObjectArrayElement (env, j%s, i);\n"
@@ -6451,45 +7391,14 @@ Java_com_redhat_et_libguestfs_GuestFS__1close
           pr "  }\n";
           pr "  free (r);\n";
           pr "  return jr;\n"
-       | RIntBool _ ->
-          pr "  cl = (*env)->FindClass (env, \"com/redhat/et/libguestfs/IntBool\");\n";
-          pr "  jr = (*env)->AllocObject (env, cl);\n";
-          pr "  fl = (*env)->GetFieldID (env, cl, \"i\", \"I\");\n";
-          pr "  (*env)->SetIntField (env, jr, fl, r->i);\n";
-          pr "  fl = (*env)->GetFieldID (env, cl, \"i\", \"Z\");\n";
-          pr "  (*env)->SetBooleanField (env, jr, fl, r->b);\n";
-          pr "  guestfs_free_int_bool (r);\n";
-          pr "  return jr;\n"
-       | RStat _ ->
-          pr "  cl = (*env)->FindClass (env, \"com/redhat/et/libguestfs/Stat\");\n";
-          pr "  jr = (*env)->AllocObject (env, cl);\n";
-          List.iter (
-            function
-            | name, `Int ->
-                pr "  fl = (*env)->GetFieldID (env, cl, \"%s\", \"J\");\n"
-                  name;
-                pr "  (*env)->SetLongField (env, jr, fl, r->%s);\n" name;
-          ) stat_cols;
-          pr "  free (r);\n";
-          pr "  return jr;\n"
-       | RStatVFS _ ->
-          pr "  cl = (*env)->FindClass (env, \"com/redhat/et/libguestfs/StatVFS\");\n";
-          pr "  jr = (*env)->AllocObject (env, cl);\n";
-          List.iter (
-            function
-            | name, `Int ->
-                pr "  fl = (*env)->GetFieldID (env, cl, \"%s\", \"J\");\n"
-                  name;
-                pr "  (*env)->SetLongField (env, jr, fl, r->%s);\n" name;
-          ) statvfs_cols;
-          pr "  free (r);\n";
-          pr "  return jr;\n"
-       | RPVList _ ->
-          generate_java_lvm_return "pv" "PV" pv_cols
-       | RVGList _ ->
-          generate_java_lvm_return "vg" "VG" vg_cols
-       | RLVList _ ->
-          generate_java_lvm_return "lv" "LV" lv_cols
+       | RStruct (_, typ) ->
+          let jtyp = java_name_of_struct typ in
+          let cols = cols_of_struct typ in
+          generate_java_struct_return typ jtyp cols
+       | RStructList (_, typ) ->
+          let jtyp = java_name_of_struct typ in
+          let cols = cols_of_struct typ in
+          generate_java_struct_list_return typ jtyp cols
        | RHashtable _ ->
           (* XXX *)
           pr "  throw_exception (env, \"%s: internal error: please let us know how to make a Java HashMap from JNI bindings!\");\n" name;
@@ -6500,17 +7409,49 @@ Java_com_redhat_et_libguestfs_GuestFS__1close
       pr "\n"
   ) all_functions
 
-and generate_java_lvm_return typ jtyp cols =
+and generate_java_struct_return typ jtyp cols =
+  pr "  cl = (*env)->FindClass (env, \"com/redhat/et/libguestfs/%s\");\n" jtyp;
+  pr "  jr = (*env)->AllocObject (env, cl);\n";
+  List.iter (
+    function
+    | name, FString ->
+       pr "  fl = (*env)->GetFieldID (env, cl, \"%s\", \"Ljava/lang/String;\");\n" name;
+       pr "  (*env)->SetObjectField (env, jr, fl, (*env)->NewStringUTF (env, r->%s));\n" name;
+    | name, FUUID ->
+       pr "  {\n";
+       pr "    char s[33];\n";
+       pr "    memcpy (s, r->%s, 32);\n" name;
+       pr "    s[32] = 0;\n";
+       pr "    fl = (*env)->GetFieldID (env, cl, \"%s\", \"Ljava/lang/String;\");\n" name;
+       pr "    (*env)->SetObjectField (env, jr, fl, (*env)->NewStringUTF (env, s));\n";
+       pr "  }\n";
+    | name, (FBytes|FUInt64|FInt64) ->
+       pr "  fl = (*env)->GetFieldID (env, cl, \"%s\", \"J\");\n" name;
+       pr "  (*env)->SetLongField (env, jr, fl, r->%s);\n" name;
+    | name, (FUInt32|FInt32) ->
+       pr "  fl = (*env)->GetFieldID (env, cl, \"%s\", \"I\");\n" name;
+       pr "  (*env)->SetLongField (env, jr, fl, r->%s);\n" name;
+    | name, FOptPercent ->
+       pr "  fl = (*env)->GetFieldID (env, cl, \"%s\", \"F\");\n" name;
+       pr "  (*env)->SetFloatField (env, jr, fl, r->%s);\n" name;
+    | name, FChar ->
+       pr "  fl = (*env)->GetFieldID (env, cl, \"%s\", \"C\");\n" name;
+       pr "  (*env)->SetLongField (env, jr, fl, r->%s);\n" name;
+  ) cols;
+  pr "  free (r);\n";
+  pr "  return jr;\n"
+
+and generate_java_struct_list_return typ jtyp cols =
   pr "  cl = (*env)->FindClass (env, \"com/redhat/et/libguestfs/%s\");\n" jtyp;
   pr "  jr = (*env)->NewObjectArray (env, r->len, cl, NULL);\n";
   pr "  for (i = 0; i < r->len; ++i) {\n";
   pr "    jfl = (*env)->AllocObject (env, cl);\n";
   List.iter (
     function
-    | name, `String ->
+    | name, FString ->
        pr "    fl = (*env)->GetFieldID (env, cl, \"%s\", \"Ljava/lang/String;\");\n" name;
        pr "    (*env)->SetObjectField (env, jfl, fl, (*env)->NewStringUTF (env, r->val[i].%s));\n" name;
-    | name, `UUID ->
+    | name, FUUID ->
        pr "    {\n";
        pr "      char s[33];\n";
        pr "      memcpy (s, r->val[i].%s, 32);\n" name;
@@ -6518,16 +7459,22 @@ and generate_java_lvm_return typ jtyp cols =
        pr "      fl = (*env)->GetFieldID (env, cl, \"%s\", \"Ljava/lang/String;\");\n" name;
        pr "      (*env)->SetObjectField (env, jfl, fl, (*env)->NewStringUTF (env, s));\n";
        pr "    }\n";
-    | name, (`Bytes|`Int) ->
+    | name, (FBytes|FUInt64|FInt64) ->
        pr "    fl = (*env)->GetFieldID (env, cl, \"%s\", \"J\");\n" name;
        pr "    (*env)->SetLongField (env, jfl, fl, r->val[i].%s);\n" name;
-    | name, `OptPercent ->
+    | name, (FUInt32|FInt32) ->
+       pr "    fl = (*env)->GetFieldID (env, cl, \"%s\", \"I\");\n" name;
+       pr "    (*env)->SetLongField (env, jfl, fl, r->val[i].%s);\n" name;
+    | name, FOptPercent ->
        pr "    fl = (*env)->GetFieldID (env, cl, \"%s\", \"F\");\n" name;
        pr "    (*env)->SetFloatField (env, jfl, fl, r->val[i].%s);\n" name;
+    | name, FChar ->
+       pr "    fl = (*env)->GetFieldID (env, cl, \"%s\", \"C\");\n" name;
+       pr "    (*env)->SetLongField (env, jfl, fl, r->val[i].%s);\n" name;
   ) cols;
   pr "    (*env)->SetObjectArrayElement (env, jfl, i, jfl);\n";
   pr "  }\n";
-  pr "  guestfs_free_lvm_%s_list (r);\n" typ;
+  pr "  guestfs_free_%s_list (r);\n" typ;
   pr "  return jr;\n"
 
 and generate_haskell_hs () =
@@ -6537,23 +7484,16 @@ and generate_haskell_hs () =
    * at the moment.  Please help out!
    *)
   let can_generate style =
-    let check_no_bad_args =
-      List.for_all (function Bool _ | Int _ -> false | _ -> true)
-    in
     match style with
-    | RErr, args -> check_no_bad_args args
-    | RBool _, _
+    | RErr, _
     | RInt _, _
-    | RInt64 _, _
+    | RInt64 _, _ -> true
+    | RBool _, _
     | RConstString _, _
     | RString _, _
     | RStringList _, _
-    | RIntBool _, _
-    | RPVList _, _
-    | RVGList _, _
-    | RLVList _, _
-    | RStat _, _
-    | RStatVFS _, _
+    | RStruct _, _
+    | RStructList _, _
     | RHashtable _, _ -> false in
 
   pr "\
@@ -6573,6 +7513,7 @@ module Guestfs (
   ) where
 import Foreign
 import Foreign.C
+import Foreign.C.Types
 import IO
 import Control.Exception
 import Data.Typeable
@@ -6636,6 +7577,7 @@ last_error h = do
        pr "%s %s = do\n" name
          (String.concat " " ("h" :: List.map name_of_argt (snd style)));
        pr "  r <- ";
+       (* Convert pointer arguments using with* functions. *)
        List.iter (
          function
          | FileIn n
@@ -6643,26 +7585,26 @@ last_error h = do
          | String n -> pr "withCString %s $ \\%s -> " n n
          | OptString n -> pr "maybeWith withCString %s $ \\%s -> " n n
          | StringList n -> pr "withMany withCString %s $ \\%s -> withArray0 nullPtr %s $ \\%s -> " n n n n
-         | Bool n ->
-             (* XXX this doesn't work *)
-             pr "      let\n";
-             pr "        %s = case %s of\n" n n;
-             pr "          False -> 0\n";
-             pr "          True -> 1\n";
-             pr "      in fromIntegral %s $ \\%s ->\n" n n
-         | Int n -> pr "fromIntegral %s $ \\%s -> " n n
+         | Bool _ | Int _ -> ()
        ) (snd style);
+       (* Convert integer arguments. *)
+       let args =
+         List.map (
+           function
+           | Bool n -> sprintf "(fromBool %s)" n
+           | Int n -> sprintf "(fromIntegral %s)" n
+           | FileIn n | FileOut n | String n | OptString n | StringList n -> n
+         ) (snd style) in
        pr "withForeignPtr h (\\p -> c_%s %s)\n" name
-         (String.concat " " ("p" :: List.map name_of_argt (snd style)));
+         (String.concat " " ("p" :: args));
        (match fst style with
         | RErr | RInt _ | RInt64 _ | RBool _ ->
             pr "  if (r == -1)\n";
             pr "    then do\n";
             pr "      err <- last_error h\n";
             pr "      fail err\n";
-        | RConstString _ | RString _ | RStringList _ | RIntBool _
-        | RPVList _ | RVGList _ | RLVList _ | RStat _ | RStatVFS _
-        | RHashtable _ ->
+        | RConstString _ | RString _ | RStringList _ | RStruct _
+        | RStructList _ | RHashtable _ ->
             pr "  if (r == nullPtr)\n";
             pr "    then do\n";
             pr "      err <- last_error h\n";
@@ -6680,12 +7622,8 @@ last_error h = do
         | RConstString _
         | RString _
         | RStringList _
-        | RIntBool _
-        | RPVList _
-        | RVGList _
-        | RLVList _
-        | RStat _
-        | RStatVFS _
+        | RStruct _
+        | RStructList _
         | RHashtable _ ->
             pr "    else return ()\n" (* XXXXXXXXXXXXXXXXXXXX *)
        );
@@ -6721,16 +7659,419 @@ and generate_haskell_prototype ~handle ?(hs = false) style =
    | RConstString _ -> pr "%s" string
    | RString _ -> pr "%s" string
    | RStringList _ -> pr "[%s]" string
-   | RIntBool _ -> pr "IntBool"
-   | RPVList _ -> pr "[PV]"
-   | RVGList _ -> pr "[VG]"
-   | RLVList _ -> pr "[LV]"
-   | RStat _ -> pr "Stat"
-   | RStatVFS _ -> pr "StatVFS"
+   | RStruct (_, typ) ->
+       let name = java_name_of_struct typ in
+       pr "%s" name
+   | RStructList (_, typ) ->
+       let name = java_name_of_struct typ in
+       pr "[%s]" name
    | RHashtable _ -> pr "Hashtable"
   );
   pr ")"
 
+and generate_bindtests () =
+  generate_header CStyle LGPLv2;
+
+  pr "\
+#include <stdio.h>
+#include <stdlib.h>
+#include <inttypes.h>
+#include <string.h>
+
+#include \"guestfs.h\"
+#include \"guestfs_protocol.h\"
+
+#define error guestfs_error
+#define safe_calloc guestfs_safe_calloc
+#define safe_malloc guestfs_safe_malloc
+
+static void
+print_strings (char * const* const argv)
+{
+  int argc;
+
+  printf (\"[\");
+  for (argc = 0; argv[argc] != NULL; ++argc) {
+    if (argc > 0) printf (\", \");
+    printf (\"\\\"%%s\\\"\", argv[argc]);
+  }
+  printf (\"]\\n\");
+}
+
+/* The test0 function prints its parameters to stdout. */
+";
+
+  let test0, tests =
+    match test_functions with
+    | [] -> assert false
+    | test0 :: tests -> test0, tests in
+
+  let () =
+    let (name, style, _, _, _, _, _) = test0 in
+    generate_prototype ~extern:false ~semicolon:false ~newline:true
+      ~handle:"g" ~prefix:"guestfs_" name style;
+    pr "{\n";
+    List.iter (
+      function
+      | String n
+      | FileIn n
+      | FileOut n -> pr "  printf (\"%%s\\n\", %s);\n" n
+      | OptString n -> pr "  printf (\"%%s\\n\", %s ? %s : \"null\");\n" n n
+      | StringList n -> pr "  print_strings (%s);\n" n
+      | Bool n -> pr "  printf (\"%%s\\n\", %s ? \"true\" : \"false\");\n" n
+      | Int n -> pr "  printf (\"%%d\\n\", %s);\n" n
+    ) (snd style);
+    pr "  /* Java changes stdout line buffering so we need this: */\n";
+    pr "  fflush (stdout);\n";
+    pr "  return 0;\n";
+    pr "}\n";
+    pr "\n" in
+
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      if String.sub name (String.length name - 3) 3 <> "err" then (
+       pr "/* Test normal return. */\n";
+       generate_prototype ~extern:false ~semicolon:false ~newline:true
+         ~handle:"g" ~prefix:"guestfs_" name style;
+       pr "{\n";
+       (match fst style with
+        | RErr ->
+            pr "  return 0;\n"
+        | RInt _ ->
+            pr "  int r;\n";
+            pr "  sscanf (val, \"%%d\", &r);\n";
+            pr "  return r;\n"
+        | RInt64 _ ->
+            pr "  int64_t r;\n";
+            pr "  sscanf (val, \"%%\" SCNi64, &r);\n";
+            pr "  return r;\n"
+        | RBool _ ->
+            pr "  return strcmp (val, \"true\") == 0;\n"
+        | RConstString _ ->
+            (* Can't return the input string here.  Return a static
+             * string so we ensure we get a segfault if the caller
+             * tries to free it.
+             *)
+            pr "  return \"static string\";\n"
+        | RString _ ->
+            pr "  return strdup (val);\n"
+        | RStringList _ ->
+            pr "  char **strs;\n";
+            pr "  int n, i;\n";
+            pr "  sscanf (val, \"%%d\", &n);\n";
+            pr "  strs = safe_malloc (g, (n+1) * sizeof (char *));\n";
+            pr "  for (i = 0; i < n; ++i) {\n";
+            pr "    strs[i] = safe_malloc (g, 16);\n";
+            pr "    snprintf (strs[i], 16, \"%%d\", i);\n";
+            pr "  }\n";
+            pr "  strs[n] = NULL;\n";
+            pr "  return strs;\n"
+        | RStruct (_, typ) ->
+            pr "  struct guestfs_%s *r;\n" typ;
+            pr "  r = safe_calloc (g, sizeof *r, 1);\n";
+            pr "  return r;\n"
+        | RStructList (_, typ) ->
+            pr "  struct guestfs_%s_list *r;\n" typ;
+            pr "  r = safe_calloc (g, sizeof *r, 1);\n";
+            pr "  sscanf (val, \"%%d\", &r->len);\n";
+            pr "  r->val = safe_calloc (g, r->len, sizeof *r->val);\n";
+            pr "  return r;\n"
+        | RHashtable _ ->
+            pr "  char **strs;\n";
+            pr "  int n, i;\n";
+            pr "  sscanf (val, \"%%d\", &n);\n";
+            pr "  strs = safe_malloc (g, (n*2+1) * sizeof (*strs));\n";
+            pr "  for (i = 0; i < n; ++i) {\n";
+            pr "    strs[i*2] = safe_malloc (g, 16);\n";
+            pr "    strs[i*2+1] = safe_malloc (g, 16);\n";
+            pr "    snprintf (strs[i*2], 16, \"%%d\", i);\n";
+            pr "    snprintf (strs[i*2+1], 16, \"%%d\", i);\n";
+            pr "  }\n";
+            pr "  strs[n*2] = NULL;\n";
+            pr "  return strs;\n"
+       );
+       pr "}\n";
+       pr "\n"
+      ) else (
+       pr "/* Test error return. */\n";
+       generate_prototype ~extern:false ~semicolon:false ~newline:true
+         ~handle:"g" ~prefix:"guestfs_" name style;
+       pr "{\n";
+       pr "  error (g, \"error\");\n";
+       (match fst style with
+        | RErr | RInt _ | RInt64 _ | RBool _ ->
+            pr "  return -1;\n"
+        | RConstString _
+        | RString _ | RStringList _ | RStruct _
+        | RStructList _
+        | RHashtable _ ->
+            pr "  return NULL;\n"
+       );
+       pr "}\n";
+       pr "\n"
+      )
+  ) tests
+
+and generate_ocaml_bindtests () =
+  generate_header OCamlStyle GPLv2;
+
+  pr "\
+let () =
+  let g = Guestfs.create () in
+";
+
+  let mkargs args =
+    String.concat " " (
+      List.map (
+       function
+       | CallString s -> "\"" ^ s ^ "\""
+       | CallOptString None -> "None"
+       | CallOptString (Some s) -> sprintf "(Some \"%s\")" s
+       | CallStringList xs ->
+           "[|" ^ String.concat ";" (List.map (sprintf "\"%s\"") xs) ^ "|]"
+       | CallInt i when i >= 0 -> string_of_int i
+       | CallInt i (* when i < 0 *) -> "(" ^ string_of_int i ^ ")"
+       | CallBool b -> string_of_bool b
+      ) args
+    )
+  in
+
+  generate_lang_bindtests (
+    fun f args -> pr "  Guestfs.%s g %s;\n" f (mkargs args)
+  );
+
+  pr "print_endline \"EOF\"\n"
+
+and generate_perl_bindtests () =
+  pr "#!/usr/bin/perl -w\n";
+  generate_header HashStyle GPLv2;
+
+  pr "\
+use strict;
+
+use Sys::Guestfs;
+
+my $g = Sys::Guestfs->new ();
+";
+
+  let mkargs args =
+    String.concat ", " (
+      List.map (
+       function
+       | CallString s -> "\"" ^ s ^ "\""
+       | CallOptString None -> "undef"
+       | CallOptString (Some s) -> sprintf "\"%s\"" s
+       | CallStringList xs ->
+           "[" ^ String.concat "," (List.map (sprintf "\"%s\"") xs) ^ "]"
+       | CallInt i -> string_of_int i
+       | CallBool b -> if b then "1" else "0"
+      ) args
+    )
+  in
+
+  generate_lang_bindtests (
+    fun f args -> pr "$g->%s (%s);\n" f (mkargs args)
+  );
+
+  pr "print \"EOF\\n\"\n"
+
+and generate_python_bindtests () =
+  generate_header HashStyle GPLv2;
+
+  pr "\
+import guestfs
+
+g = guestfs.GuestFS ()
+";
+
+  let mkargs args =
+    String.concat ", " (
+      List.map (
+       function
+       | CallString s -> "\"" ^ s ^ "\""
+       | CallOptString None -> "None"
+       | CallOptString (Some s) -> sprintf "\"%s\"" s
+       | CallStringList xs ->
+           "[" ^ String.concat "," (List.map (sprintf "\"%s\"") xs) ^ "]"
+       | CallInt i -> string_of_int i
+       | CallBool b -> if b then "1" else "0"
+      ) args
+    )
+  in
+
+  generate_lang_bindtests (
+    fun f args -> pr "g.%s (%s)\n" f (mkargs args)
+  );
+
+  pr "print \"EOF\"\n"
+
+and generate_ruby_bindtests () =
+  generate_header HashStyle GPLv2;
+
+  pr "\
+require 'guestfs'
+
+g = Guestfs::create()
+";
+
+  let mkargs args =
+    String.concat ", " (
+      List.map (
+       function
+       | CallString s -> "\"" ^ s ^ "\""
+       | CallOptString None -> "nil"
+       | CallOptString (Some s) -> sprintf "\"%s\"" s
+       | CallStringList xs ->
+           "[" ^ String.concat "," (List.map (sprintf "\"%s\"") xs) ^ "]"
+       | CallInt i -> string_of_int i
+       | CallBool b -> string_of_bool b
+      ) args
+    )
+  in
+
+  generate_lang_bindtests (
+    fun f args -> pr "g.%s(%s)\n" f (mkargs args)
+  );
+
+  pr "print \"EOF\\n\"\n"
+
+and generate_java_bindtests () =
+  generate_header CStyle GPLv2;
+
+  pr "\
+import com.redhat.et.libguestfs.*;
+
+public class Bindtests {
+    public static void main (String[] argv)
+    {
+        try {
+            GuestFS g = new GuestFS ();
+";
+
+  let mkargs args =
+    String.concat ", " (
+      List.map (
+       function
+       | CallString s -> "\"" ^ s ^ "\""
+       | CallOptString None -> "null"
+       | CallOptString (Some s) -> sprintf "\"%s\"" s
+       | CallStringList xs ->
+           "new String[]{" ^
+             String.concat "," (List.map (sprintf "\"%s\"") xs) ^ "}"
+       | CallInt i -> string_of_int i
+       | CallBool b -> string_of_bool b
+      ) args
+    )
+  in
+
+  generate_lang_bindtests (
+    fun f args -> pr "            g.%s (%s);\n" f (mkargs args)
+  );
+
+  pr "
+            System.out.println (\"EOF\");
+        }
+        catch (Exception exn) {
+            System.err.println (exn);
+            System.exit (1);
+        }
+    }
+}
+"
+
+and generate_haskell_bindtests () =
+  generate_header HaskellStyle GPLv2;
+
+  pr "\
+module Bindtests where
+import qualified Guestfs
+
+main = do
+  g <- Guestfs.create
+";
+
+  let mkargs args =
+    String.concat " " (
+      List.map (
+       function
+       | CallString s -> "\"" ^ s ^ "\""
+       | CallOptString None -> "Nothing"
+       | CallOptString (Some s) -> sprintf "(Just \"%s\")" s
+       | CallStringList xs ->
+           "[" ^ String.concat "," (List.map (sprintf "\"%s\"") xs) ^ "]"
+       | CallInt i when i < 0 -> "(" ^ string_of_int i ^ ")"
+       | CallInt i -> string_of_int i
+       | CallBool true -> "True"
+       | CallBool false -> "False"
+      ) args
+    )
+  in
+
+  generate_lang_bindtests (
+    fun f args -> pr "  Guestfs.%s g %s\n" f (mkargs args)
+  );
+
+  pr "  putStrLn \"EOF\"\n"
+
+(* Language-independent bindings tests - we do it this way to
+ * ensure there is parity in testing bindings across all languages.
+ *)
+and generate_lang_bindtests call =
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList []; CallBool false;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString None;
+               CallStringList []; CallBool false;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString ""; CallOptString (Some "def");
+               CallStringList []; CallBool false;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString ""; CallOptString (Some "");
+               CallStringList []; CallBool false;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"; "2"]; CallBool false;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool true;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt (-1); CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt (-2); CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt 1; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt 2; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt 4095; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt 0; CallString ""; CallString ""]
+
+  (* XXX Add here tests of the return and error functions. *)
+
+(* This is used to generate the src/MAX_PROC_NR file which
+ * contains the maximum procedure number, a surrogate for the
+ * ABI version number.  See src/Makefile.am for the details.
+ *)
+and generate_max_proc_nr () =
+  let proc_nrs = List.map (
+    fun (_, _, proc_nr, _, _, _, _) -> proc_nr
+  ) daemon_functions in
+
+  let max_proc_nr = List.fold_left max 0 proc_nrs in
+
+  pr "%d\n" max_proc_nr
+
 let output_to filename =
   let filename_new = filename ^ ".new" in
   chan := open_out filename_new;
@@ -6755,7 +8096,7 @@ let output_to filename =
 let () =
   check_functions ();
 
-  if not (Sys.file_exists "configure.ac") then (
+  if not (Sys.file_exists "HACKING") then (
     eprintf "\
 You are probably running this from the wrong directory.
 Run it from the top source directory using the command
@@ -6788,10 +8129,18 @@ Run it from the top source directory using the command
   generate_daemon_actions ();
   close ();
 
-  let close = output_to "tests.c" in
+  let close = output_to "daemon/names.c" in
+  generate_daemon_names ();
+  close ();
+
+  let close = output_to "capitests/tests.c" in
   generate_tests ();
   close ();
 
+  let close = output_to "src/guestfs-bindtests.c" in
+  generate_bindtests ();
+  close ();
+
   let close = output_to "fish/cmds.c" in
   generate_fish_cmds ();
   close ();
@@ -6824,6 +8173,10 @@ Run it from the top source directory using the command
   generate_ocaml_c ();
   close ();
 
+  let close = output_to "ocaml/bindtests.ml" in
+  generate_ocaml_bindtests ();
+  close ();
+
   let close = output_to "perl/Guestfs.xs" in
   generate_perl_xs ();
   close ();
@@ -6832,6 +8185,10 @@ Run it from the top source directory using the command
   generate_perl_pm ();
   close ();
 
+  let close = output_to "perl/bindtests.pl" in
+  generate_perl_bindtests ();
+  close ();
+
   let close = output_to "python/guestfs-py.c" in
   generate_python_c ();
   close ();
@@ -6840,38 +8197,54 @@ Run it from the top source directory using the command
   generate_python_py ();
   close ();
 
+  let close = output_to "python/bindtests.py" in
+  generate_python_bindtests ();
+  close ();
+
   let close = output_to "ruby/ext/guestfs/_guestfs.c" in
   generate_ruby_c ();
   close ();
 
+  let close = output_to "ruby/bindtests.rb" in
+  generate_ruby_bindtests ();
+  close ();
+
   let close = output_to "java/com/redhat/et/libguestfs/GuestFS.java" in
   generate_java_java ();
   close ();
 
-  let close = output_to "java/com/redhat/et/libguestfs/PV.java" in
-  generate_java_struct "PV" pv_cols;
-  close ();
+  List.iter (
+    fun (typ, jtyp) ->
+      let cols = cols_of_struct typ in
+      let filename = sprintf "java/com/redhat/et/libguestfs/%s.java" jtyp in
+      let close = output_to filename in
+      generate_java_struct jtyp cols;
+      close ();
+  ) java_structs;
 
-  let close = output_to "java/com/redhat/et/libguestfs/VG.java" in
-  generate_java_struct "VG" vg_cols;
+  let close = output_to "java/com_redhat_et_libguestfs_GuestFS.c" in
+  generate_java_c ();
   close ();
 
-  let close = output_to "java/com/redhat/et/libguestfs/LV.java" in
-  generate_java_struct "LV" lv_cols;
+  let close = output_to "java/Bindtests.java" in
+  generate_java_bindtests ();
   close ();
 
-  let close = output_to "java/com/redhat/et/libguestfs/Stat.java" in
-  generate_java_struct "Stat" stat_cols;
+  let close = output_to "haskell/Guestfs.hs" in
+  generate_haskell_hs ();
   close ();
 
-  let close = output_to "java/com/redhat/et/libguestfs/StatVFS.java" in
-  generate_java_struct "StatVFS" statvfs_cols;
+  let close = output_to "haskell/Bindtests.hs" in
+  generate_haskell_bindtests ();
   close ();
 
-  let close = output_to "java/com_redhat_et_libguestfs_GuestFS.c" in
-  generate_java_c ();
+  let close = output_to "src/MAX_PROC_NR" in
+  generate_max_proc_nr ();
   close ();
 
-  let close = output_to "haskell/Guestfs.hs" in
-  generate_haskell_hs ();
-  close ();
+  (* Always generate this file last, and unconditionally.  It's used
+   * by the Makefile to know when we must re-run the generator.
+   *)
+  let chan = open_out "src/stamp-generator" in
+  fprintf chan "1\n";
+  close_out chan