prefer sizeof *VAR over sizeof TYPE (no semantic change)
[libguestfs.git] / src / generator.ml
index a01eeb7..4b7efa0 100755 (executable)
@@ -83,6 +83,8 @@ and ret =
      * inefficient.  Keys should be unique.  NULLs are not permitted.
      *)
   | RHashtable of string
      * inefficient.  Keys should be unique.  NULLs are not permitted.
      *)
   | RHashtable of string
+    (* List of directory entries (the result of readdir(3)). *)
+  | RDirentList of string
 
 and args = argt list   (* Function parameters, guestfs handle is implicit. *)
 
 
 and args = argt list   (* Function parameters, guestfs handle is implicit. *)
 
@@ -116,6 +118,7 @@ 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 *)
   | 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 
 
 let protocol_limit_warning =
   "Because of the message protocol, there is a transfer limit 
@@ -129,19 +132,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,
 (* 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
  *
  *
  * 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 umount-all and lvm-remove-all (except InitNone).
+ * 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.
  *
  * 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
 and test =
     (* Run the command sequence and just expect nothing to fail. *)
   | TestRun of seq
@@ -154,6 +174,12 @@ and test =
      *)
   | TestOutputList of seq * string list
     (* Run the command sequence and expect the output of the final
      *)
   | 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
      * command to be the integer.
      *)
   | TestOutputInt of seq * int
@@ -185,6 +211,21 @@ and test_field_compare =
   | CompareFieldsIntEq of string * string
   | CompareFieldsStrEq of string * string
 
   | 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
 (* Some initial scenarios for testing. *)
 and test_init =
     (* Do nothing, block devices could contain random stuff including
@@ -220,7 +261,81 @@ and cmd = string list
  * Apart from that, long descriptions are just perldoc paragraphs.
  *)
 
  * 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";
+  "test0rintbool",     RIntBool ("valout", "valout");
+  "test0rpvlist",      RPVList "valout";
+  "test0rvglist",      RVGList "valout";
+  "test0rlvlist",      RLVList "valout";
+  "test0rstat",        RStat "valout";
+  "test0rstatvfs",     RStatVFS "valout";
+  "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",
   ("launch", (RErr, []), -1, [FishAlias "run"; FishAction "launch"],
    [],
    "launch the qemu subprocess",
@@ -262,7 +377,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).
 
 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"],
    [],
 
   ("add_cdrom", (RErr, [String "filename"]), -1, [FishAlias "cdrom"],
    [],
@@ -270,7 +391,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 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, [],
    [],
 
   ("config", (RErr, [String "qemuparam"; OptString "qemuvalue"]), -1, [],
    [],
@@ -297,9 +444,6 @@ configure script.
 You can also override this by setting the C<LIBGUESTFS_QEMU>
 environment variable.
 
 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, [],
 Setting C<qemu> to C<NULL> restores the default qemu binary.");
 
   ("get_qemu", (RConstString "qemu", []), -1, [],
@@ -320,9 +464,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 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, [],
 Setting C<path> to C<NULL> restores the default path.");
 
   ("get_path", (RConstString "path", []), -1, [],
@@ -334,13 +475,39 @@ Return the current search path.
 This is always non-NULL.  If it wasn't set already, then this will
 return the default 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",
    "\
 If C<autosync> is true, this enables autosync.  Libguestfs will make a
   ("set_autosync", (RErr, [Bool "autosync"]), -1, [FishAlias "autosync"],
    [],
    "set autosync mode",
    "\
 If C<autosync> is true, this enables autosync.  Libguestfs will make a
-best effort attempt to run C<guestfs_sync> when the handle is closed
-(also if the program exits without closing handles).");
+best effort attempt to run C<guestfs_umount_all> followed by
+C<guestfs_sync> when the handle is closed
+(also if the program exits without closing handles).
+
+This is disabled by default (except in guestfish where it is
+enabled by default).");
 
   ("get_autosync", (RBool "autosync", []), -1, [],
    [],
 
   ("get_autosync", (RBool "autosync", []), -1, [],
    [],
@@ -426,12 +593,55 @@ actions using the low-level API.
 
 For more information on states, see L<guestfs(3)>.");
 
 
 For more information on states, see L<guestfs(3)>.");
 
+  ("end_busy", (RErr, []), -1, [NotInFish],
+   [],
+   "leave the busy state",
+   "\
+This sets the state to C<READY>, or if in C<CONFIG> then it leaves the
+state as is.  This is only used when implementing
+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)>.");
+
 ]
 
 ]
 
+(* 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, [],
 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"];
        ["mkfs"; "ext2"; "/dev/sda1"];
        ["mount"; "/dev/sda1"; "/"];
        ["write_file"; "/new"; "new file contents"; "0"];
@@ -456,7 +666,7 @@ The filesystem options C<sync> and C<noatime> are set with this
 call, in order to improve reliability.");
 
   ("sync", (RErr, []), 2, [],
 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
    "sync disks, writes are flushed through to the disk image",
    "\
 This syncs the disk, so that any writes are flushed through to the
@@ -466,7 +676,7 @@ You should always call this if you have modified a disk image, before
 closing the handle.");
 
   ("touch", (RErr, [String "path"]), 3, [],
 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",
       [["touch"; "/new"];
        ["exists"; "/new"]])],
    "update file timestamps or create a new file",
@@ -476,7 +686,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],
 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",
       [["write_file"; "/new"; "new file contents"; "0"];
        ["cat"; "/new"]], "new file contents")],
    "list the contents of a file",
@@ -501,7 +711,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, [],
 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"];
       [["touch"; "/new"];
        ["touch"; "/newer"];
        ["touch"; "/newest"];
@@ -516,8 +726,8 @@ This command is mostly useful for interactive sessions.  Programs
 should probably use C<guestfs_readdir> instead.");
 
   ("list_devices", (RStringList "devices", []), 7, [],
 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.
    "list the block devices",
    "\
 List all the block devices.
@@ -525,10 +735,10 @@ List all the block devices.
 The full block device names are returned, eg. C</dev/sda>");
 
   ("list_partitions", (RStringList "partitions", []), 8, [],
 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"]);
       [["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",
    "\
        ["list_partitions"]], ["/dev/sda1"; "/dev/sda2"; "/dev/sda3"])],
    "list the partitions",
    "\
@@ -540,10 +750,10 @@ This does not return logical volumes.  For that you will need to
 call C<guestfs_lvs>.");
 
   ("pvs", (RStringList "physvols", []), 9, [],
 call C<guestfs_lvs>.");
 
   ("pvs", (RStringList "physvols", []), 9, [],
-   [InitBasicFSonLVM, TestOutputList (
+   [InitBasicFSonLVM, Always, TestOutputListOfDevices (
       [["pvs"]], ["/dev/sda1"]);
       [["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"];
        ["pvcreate"; "/dev/sda1"];
        ["pvcreate"; "/dev/sda2"];
        ["pvcreate"; "/dev/sda3"];
@@ -559,10 +769,10 @@ PVs (eg. C</dev/sda2>).
 See also C<guestfs_pvs_full>.");
 
   ("vgs", (RStringList "volgroups", []), 10, [],
 See also C<guestfs_pvs_full>.");
 
   ("vgs", (RStringList "volgroups", []), 10, [],
-   [InitBasicFSonLVM, TestOutputList (
+   [InitBasicFSonLVM, Always, TestOutputList (
       [["vgs"]], ["VG"]);
       [["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"];
        ["pvcreate"; "/dev/sda1"];
        ["pvcreate"; "/dev/sda2"];
        ["pvcreate"; "/dev/sda3"];
@@ -580,10 +790,10 @@ detected (eg. C<VolGroup00>).
 See also C<guestfs_vgs_full>.");
 
   ("lvs", (RStringList "logvols", []), 11, [],
 See also C<guestfs_vgs_full>.");
 
   ("lvs", (RStringList "logvols", []), 11, [],
-   [InitBasicFSonLVM, TestOutputList (
+   [InitBasicFSonLVM, Always, TestOutputList (
       [["lvs"]], ["/dev/VG/LV"]);
       [["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"];
        ["pvcreate"; "/dev/sda1"];
        ["pvcreate"; "/dev/sda2"];
        ["pvcreate"; "/dev/sda3"];
@@ -625,10 +835,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, [],
 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"]);
       [["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",
       [["write_file"; "/new"; ""; "0"];
        ["read_lines"; "/new"]], [])],
    "read file as lines",
@@ -803,12 +1013,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, [],
 C<path/*> and sorting the resulting nodes into alphabetical order.");
 
   ("rm", (RErr, [String "path"]), 29, [],
-   [InitBasicFS, TestRun
+   [InitBasicFS, Always, TestRun
       [["touch"; "/new"];
        ["rm"; "/new"]];
       [["touch"; "/new"];
        ["rm"; "/new"]];
-    InitBasicFS, TestLastFail
+    InitBasicFS, Always, TestLastFail
       [["rm"; "/new"]];
       [["rm"; "/new"]];
-    InitBasicFS, TestLastFail
+    InitBasicFS, Always, TestLastFail
       [["mkdir"; "/new"];
        ["rm"; "/new"]]],
    "remove a file",
       [["mkdir"; "/new"];
        ["rm"; "/new"]]],
    "remove a file",
@@ -816,12 +1026,12 @@ C<path/*> and sorting the resulting nodes into alphabetical order.");
 Remove the single file C<path>.");
 
   ("rmdir", (RErr, [String "path"]), 30, [],
 Remove the single file C<path>.");
 
   ("rmdir", (RErr, [String "path"]), 30, [],
-   [InitBasicFS, TestRun
+   [InitBasicFS, Always, TestRun
       [["mkdir"; "/new"];
        ["rmdir"; "/new"]];
       [["mkdir"; "/new"];
        ["rmdir"; "/new"]];
-    InitBasicFS, TestLastFail
+    InitBasicFS, Always, TestLastFail
       [["rmdir"; "/new"]];
       [["rmdir"; "/new"]];
-    InitBasicFS, TestLastFail
+    InitBasicFS, Always, TestLastFail
       [["touch"; "/new"];
        ["rmdir"; "/new"]]],
    "remove a directory",
       [["touch"; "/new"];
        ["rmdir"; "/new"]]],
    "remove a directory",
@@ -829,7 +1039,7 @@ Remove the single file C<path>.");
 Remove the single directory C<path>.");
 
   ("rm_rf", (RErr, [String "path"]), 31, [],
 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"];
       [["mkdir"; "/new"];
        ["mkdir"; "/new/foo"];
        ["touch"; "/new/foo/bar"];
@@ -842,25 +1052,32 @@ contents if its a directory.  This is like the C<rm -rf> shell
 command.");
 
   ("mkdir", (RErr, [String "path"]), 32, [],
 command.");
 
   ("mkdir", (RErr, [String "path"]), 32, [],
-   [InitBasicFS, TestOutputTrue
+   [InitBasicFS, Always, TestOutputTrue
       [["mkdir"; "/new"];
        ["is_dir"; "/new"]];
       [["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, [],
       [["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"]];
       [["mkdir_p"; "/new/foo/bar"];
        ["is_dir"; "/new/foo/bar"]];
-    InitBasicFS, TestOutputTrue
+    InitBasicFS, Always, TestOutputTrue
       [["mkdir_p"; "/new/foo/bar"];
        ["is_dir"; "/new/foo"]];
       [["mkdir_p"; "/new/foo/bar"];
        ["is_dir"; "/new/foo"]];
-    InitBasicFS, TestOutputTrue
+    InitBasicFS, Always, TestOutputTrue
       [["mkdir_p"; "/new/foo/bar"];
       [["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
    "create a directory and parents",
    "\
 Create a directory named C<path>, creating any parent directories
@@ -884,10 +1101,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, [],
 yourself (Augeas support makes this relatively easy).");
 
   ("exists", (RBool "existsflag", [String "path"]), 36, [],
-   [InitBasicFS, TestOutputTrue (
+   [InitBasicFS, Always, TestOutputTrue (
       [["touch"; "/new"];
        ["exists"; "/new"]]);
       [["touch"; "/new"];
        ["exists"; "/new"]]);
-    InitBasicFS, TestOutputTrue (
+    InitBasicFS, Always, TestOutputTrue (
       [["mkdir"; "/new"];
        ["exists"; "/new"]])],
    "test if file or directory exists",
       [["mkdir"; "/new"];
        ["exists"; "/new"]])],
    "test if file or directory exists",
@@ -898,10 +1115,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, [],
 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"]]);
       [["touch"; "/new"];
        ["is_file"; "/new"]]);
-    InitBasicFS, TestOutputFalse (
+    InitBasicFS, Always, TestOutputFalse (
       [["mkdir"; "/new"];
        ["is_file"; "/new"]])],
    "test if file exists",
       [["mkdir"; "/new"];
        ["is_file"; "/new"]])],
    "test if file exists",
@@ -913,10 +1130,10 @@ other objects like directories.
 See also C<guestfs_stat>.");
 
   ("is_dir", (RBool "dirflag", [String "path"]), 38, [],
 See also C<guestfs_stat>.");
 
   ("is_dir", (RBool "dirflag", [String "path"]), 38, [],
-   [InitBasicFS, TestOutputFalse (
+   [InitBasicFS, Always, TestOutputFalse (
       [["touch"; "/new"];
        ["is_dir"; "/new"]]);
       [["touch"; "/new"];
        ["is_dir"; "/new"]]);
-    InitBasicFS, TestOutputTrue (
+    InitBasicFS, Always, TestOutputTrue (
       [["mkdir"; "/new"];
        ["is_dir"; "/new"]])],
    "test if file exists",
       [["mkdir"; "/new"];
        ["is_dir"; "/new"]])],
    "test if file exists",
@@ -928,8 +1145,8 @@ other objects like files.
 See also C<guestfs_stat>.");
 
   ("pvcreate", (RErr, [String "device"]), 39, [],
 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"];
        ["pvcreate"; "/dev/sda1"];
        ["pvcreate"; "/dev/sda2"];
        ["pvcreate"; "/dev/sda3"];
@@ -941,8 +1158,8 @@ where C<device> should usually be a partition name such
 as C</dev/sda1>.");
 
   ("vgcreate", (RErr, [String "volgroup"; StringList "physvols"]), 40, [],
 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"];
        ["pvcreate"; "/dev/sda1"];
        ["pvcreate"; "/dev/sda2"];
        ["pvcreate"; "/dev/sda3"];
@@ -955,8 +1172,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, [],
 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"];
        ["pvcreate"; "/dev/sda1"];
        ["pvcreate"; "/dev/sda2"];
        ["pvcreate"; "/dev/sda3"];
@@ -976,8 +1193,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, [],
 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"];
        ["mkfs"; "ext2"; "/dev/sda1"];
        ["mount"; "/dev/sda1"; "/"];
        ["write_file"; "/new"; "new file contents"; "0"];
@@ -985,7 +1202,7 @@ on the volume group C<volgroup>, with C<size> megabytes.");
    "make a filesystem",
    "\
 This creates a filesystem on C<device> (usually a partition
    "make a filesystem",
    "\
 This creates a filesystem on C<device> (usually a partition
-of LVM logical volume).  The filesystem type is C<fstype>, for
+or LVM logical volume).  The filesystem type is C<fstype>, for
 example C<ext3>.");
 
   ("sfdisk", (RErr, [String "device";
 example C<ext3>.");
 
   ("sfdisk", (RErr, [String "device";
@@ -1012,25 +1229,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
 
 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],
 
   ("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");
       [["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");
       [["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");
       [["write_file"; "/new"; "\n\n"; "0"];
        ["cat"; "/new"]], "\n\n");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; ""; "0"];
        ["cat"; "/new"]], "");
       [["write_file"; "/new"; ""; "0"];
        ["cat"; "/new"]], "");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "\n\n\n"; "0"];
        ["cat"; "/new"]], "\n\n\n");
       [["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",
       [["write_file"; "/new"; "\n"; "0"];
        ["cat"; "/new"]], "\n")],
    "create a file",
@@ -1041,16 +1260,21 @@ with length C<size>.
 
 As a special case, if C<size> is C<0>
 then the length is calculated using C<strlen> (so in this case
 
 As a special case, if C<size> is C<0>
 then the length is calculated using C<strlen> (so in this case
-the content cannot contain embedded ASCII NULs).");
+the content cannot contain embedded ASCII NULs).
+
+I<NB.> Owing to a bug, writing content containing ASCII NUL
+characters does I<not> work, even if the length is specified.
+We hope to resolve this bug in a future version.  In the meantime
+use C<guestfs_upload>.");
 
   ("umount", (RErr, [String "pathordevice"]), 45, [FishAlias "unmount"],
 
   ("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"]);
        ["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"; "/"];
        ["mkfs"; "ext2"; "/dev/sda1"];
        ["mount"; "/dev/sda1"; "/"];
        ["umount"; "/"];
@@ -1062,7 +1286,7 @@ specified either by its mountpoint (path) or the device which
 contains the filesystem.");
 
   ("mounts", (RStringList "devices", []), 46, [],
 contains the filesystem.");
 
   ("mounts", (RStringList "devices", []), 46, [],
-   [InitBasicFS, TestOutputList (
+   [InitBasicFS, Always, TestOutputListOfDevices (
       [["mounts"]], ["/dev/sda1"])],
    "show mounted filesystems",
    "\
       [["mounts"]], ["/dev/sda1"])],
    "show mounted filesystems",
    "\
@@ -1072,8 +1296,22 @@ 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"],
 Some internal mounts are not shown.");
 
   ("umount_all", (RErr, []), 47, [FishAlias "unmount-all"],
-   [InitBasicFS, TestOutputList (
+   [InitBasicFS, Always, TestOutputList (
       [["umount_all"];
       [["umount_all"];
+       ["mounts"]], []);
+    (* check that umount_all can unmount nested mounts correctly: *)
+    InitEmpty, Always, TestOutputList (
+      [["sfdiskM"; "/dev/sda"; ",100 ,200 ,"];
+       ["mkfs"; "ext2"; "/dev/sda1"];
+       ["mkfs"; "ext2"; "/dev/sda2"];
+       ["mkfs"; "ext2"; "/dev/sda3"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["mkdir"; "/mp1"];
+       ["mount"; "/dev/sda2"; "/mp1"];
+       ["mkdir"; "/mp1/mp2"];
+       ["mount"; "/dev/sda3"; "/mp1/mp2"];
+       ["mkdir"; "/mp1/mp2/mp3"];
+       ["umount_all"];
        ["mounts"]], [])],
    "unmount all filesystems",
    "\
        ["mounts"]], [])],
    "unmount all filesystems",
    "\
@@ -1089,13 +1327,13 @@ This command removes all LVM logical volumes, volume groups
 and physical volumes.");
 
   ("file", (RString "description", [String "path"]), 49, [],
 and physical volumes.");
 
   ("file", (RString "description", [String "path"]), 49, [],
-   [InitBasicFS, TestOutput (
+   [InitBasicFS, Always, TestOutput (
       [["touch"; "/new"];
        ["file"; "/new"]], "empty");
       [["touch"; "/new"];
        ["file"; "/new"]], "empty");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "some content\n"; "0"];
        ["file"; "/new"]], "ASCII text");
       [["write_file"; "/new"; "some content\n"; "0"];
        ["file"; "/new"]], "ASCII text");
-    InitBasicFS, TestLastFail (
+    InitBasicFS, Always, TestLastFail (
       [["file"; "/nofile"]])],
    "determine file type",
    "\
       [["file"; "/nofile"]])],
    "determine file type",
    "\
@@ -1107,8 +1345,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).");
 
 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
    "run a command from the guest filesystem",
    "\
 This call runs a command from the guest filesystem.  The
@@ -1119,7 +1404,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
 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
 
 The C<$PATH> environment variable will contain at least
 C</usr/bin> and C</bin>.  If you require a program from
@@ -1132,15 +1426,60 @@ correct places.  It is the caller's responsibility to ensure
 all filesystems that are needed are mounted at the right
 locations.");
 
 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
    "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, [],
 
   ("stat", (RStat "statbuf", [String "path"]), 52, [],
-   [InitBasicFS, TestOutputStruct (
+   [InitBasicFS, Always, TestOutputStruct (
       [["touch"; "/new"];
        ["stat"; "/new"]], [CompareWithInt ("size", 0)])],
    "get file information",
       [["touch"; "/new"];
        ["stat"; "/new"]], [CompareWithInt ("size", 0)])],
    "get file information",
@@ -1150,7 +1489,7 @@ 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, [],
 This is the same as the C<stat(2)> system call.");
 
   ("lstat", (RStat "statbuf", [String "path"]), 53, [],
-   [InitBasicFS, TestOutputStruct (
+   [InitBasicFS, Always, TestOutputStruct (
       [["touch"; "/new"];
        ["lstat"; "/new"]], [CompareWithInt ("size", 0)])],
    "get file information for a symbolic link",
       [["touch"; "/new"];
        ["lstat"; "/new"]], [CompareWithInt ("size", 0)])],
    "get file information for a symbolic link",
@@ -1164,9 +1503,8 @@ refers to.
 This is the same as the C<lstat(2)> system call.");
 
   ("statvfs", (RStatVFS "statbuf", [String "path"]), 54, [],
 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);
+   [InitBasicFS, Always, TestOutputStruct (
+      [["statvfs"; "/"]], [CompareWithInt ("namemax", 255);
                           CompareWithInt ("bsize", 1024)])],
    "get file system statistics",
    "\
                           CompareWithInt ("bsize", 1024)])],
    "get file system statistics",
    "\
@@ -1189,7 +1527,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, [],
 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",
       [["blockdev_setro"; "/dev/sda"];
        ["blockdev_getro"; "/dev/sda"]])],
    "set block device to read-only",
@@ -1199,7 +1537,7 @@ Sets the block device named C<device> to read-only.
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_setrw", (RErr, [String "device"]), 57, [],
 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",
       [["blockdev_setrw"; "/dev/sda"];
        ["blockdev_getro"; "/dev/sda"]])],
    "set block device to read-write",
@@ -1209,7 +1547,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, [],
 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",
       [["blockdev_setro"; "/dev/sda"];
        ["blockdev_getro"; "/dev/sda"]])],
    "is block device set to read-only",
@@ -1220,7 +1558,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, [],
 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",
    "\
       [["blockdev_getss"; "/dev/sda"]], 512)],
    "get sectorsize of block device",
    "\
@@ -1233,7 +1571,7 @@ for that).
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_getbsz", (RInt "blocksize", [String "device"]), 60, [],
 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",
    "\
       [["blockdev_getbsz"; "/dev/sda"]], 4096)],
    "get blocksize of block device",
    "\
@@ -1256,7 +1594,7 @@ I<filesystem block size>).
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_getsz", (RInt64 "sizeinsectors", [String "device"]), 62, [],
 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",
    "\
       [["blockdev_getsz"; "/dev/sda"]], 1024000)],
    "get total size of device in 512-byte sectors",
    "\
@@ -1270,7 +1608,7 @@ useful I<size in bytes>.
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_getsize64", (RInt64 "sizeinbytes", [String "device"]), 63, [],
 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",
    "\
       [["blockdev_getsize64"; "/dev/sda"]], 524288000)],
    "get total size of device in bytes",
    "\
@@ -1281,7 +1619,7 @@ See also C<guestfs_blockdev_getsz>.
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_flushbufs", (RErr, [String "device"]), 64, [],
 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",
    "\
       [["blockdev_flushbufs"; "/dev/sda"]]],
    "flush device buffers",
    "\
@@ -1291,7 +1629,7 @@ with C<device>.
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_rereadpt", (RErr, [String "device"]), 65, [],
 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",
    "\
       [["blockdev_rereadpt"; "/dev/sda"]]],
    "reread partition table",
    "\
@@ -1300,9 +1638,9 @@ Reread the partition table on C<device>.
 This uses the L<blockdev(8)> command.");
 
   ("upload", (RErr, [FileIn "filename"; String "remotefilename"]), 66, [],
 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. *)
       (* 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",
    "\
      ["checksum"; "md5"; "/COPYING.LIB"]], "e3eda01d9815f8d24aae2dbd89b68b06")],
    "upload a file from the local machine",
    "\
@@ -1314,9 +1652,9 @@ C<filename> can also be a named pipe.
 See also C<guestfs_download>.");
 
   ("download", (RErr, [String "remotefilename"; FileOut "filename"]), 67, [],
 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. *)
       (* 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")],
      ["download"; "/COPYING.LIB"; "testdownload.tmp"];
      ["upload"; "testdownload.tmp"; "/upload"];
      ["checksum"; "md5"; "/upload"]], "e3eda01d9815f8d24aae2dbd89b68b06")],
@@ -1330,29 +1668,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, [],
 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");
       [["write_file"; "/new"; "test\n"; "0"];
        ["checksum"; "crc"; "/new"]], "935282863");
-    InitBasicFS, TestLastFail (
+    InitBasicFS, Always, TestLastFail (
       [["checksum"; "crc"; "/new"]]);
       [["checksum"; "crc"; "/new"]]);
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "test\n"; "0"];
        ["checksum"; "md5"; "/new"]], "d8e8fca2dc0f896fd7cb4cb0031ba249");
       [["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");
       [["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");
       [["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");
       [["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");
       [["write_file"; "/new"; "test\n"; "0"];
        ["checksum"; "sha384"; "/new"]], "109bb6b5b6d5547c1ce03c7a8bd7d8f80c1cb0957f50c4f7fda04692079917e4f9cad52b878f3d8234e1a170b154b72d");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "test\n"; "0"];
       [["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
    "compute MD5, SHAx or CRC checksum of file",
    "\
 This call computes the MD5, SHAx or CRC checksum of the
@@ -1397,8 +1741,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, [],
 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",
    "\
        ["cat"; "/hello"]], "hello\n")],
    "unpack tarfile to directory",
    "\
@@ -1417,8 +1761,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, [],
 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",
    "\
        ["cat"; "/hello"]], "hello\n")],
    "unpack compressed tarball to directory",
    "\
@@ -1437,11 +1781,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, [],
 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"]]);
       [["umount"; "/"];
        ["mount_ro"; "/dev/sda1"; "/"];
        ["touch"; "/new"]]);
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "data"; "0"];
        ["umount"; "/"];
        ["mount_ro"; "/dev/sda1"; "/"];
       [["write_file"; "/new"; "data"; "0"];
        ["umount"; "/"];
        ["mount_ro"; "/dev/sda1"; "/"];
@@ -1480,23 +1824,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, [],
 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"]);
        ["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"]], []);
        ["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"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["lvremove"; "/dev/VG"];
@@ -1510,16 +1857,18 @@ You can also remove all LVs in a volume group by specifying
 the VG name, C</dev/VG>.");
 
   ("vgremove", (RErr, [String "vgname"]), 78, [],
 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"]], []);
        ["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"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["vgremove"; "VG"];
@@ -1532,29 +1881,32 @@ This also forcibly removes all logical volumes in the volume
 group (if any).");
 
   ("pvremove", (RErr, [String "device"]), 79, [],
 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"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["vgremove"; "VG"];
-       ["pvremove"; "/dev/sda"];
+       ["pvremove"; "/dev/sda1"];
        ["lvs"]], []);
        ["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"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["vgremove"; "VG"];
-       ["pvremove"; "/dev/sda"];
+       ["pvremove"; "/dev/sda1"];
        ["vgs"]], []);
        ["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"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["vgremove"; "VG"];
-       ["pvremove"; "/dev/sda"];
+       ["pvremove"; "/dev/sda1"];
        ["pvs"]], [])],
    "remove an LVM physical volume",
    "\
        ["pvs"]], [])],
    "remove an LVM physical volume",
    "\
@@ -1566,7 +1918,7 @@ wipe physical volumes that contain any volume groups, so you have
 to remove those first.");
 
   ("set_e2label", (RErr, [String "device"; String "label"]), 80, [],
 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",
       [["set_e2label"; "/dev/sda1"; "testlabel"];
        ["get_e2label"; "/dev/sda1"]], "testlabel")],
    "set the ext2/3/4 filesystem label",
@@ -1586,16 +1938,16 @@ This returns the ext2/3/4 filesystem label of the filesystem on
 C<device>.");
 
   ("set_e2uuid", (RErr, [String "device"; String "uuid"]), 82, [],
 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");
       [["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. *)
       [["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"]]);
       [["set_e2uuid"; "/dev/sda1"; "random"]]);
-    InitBasicFS, TestRun (
+    InitBasicFS, Always, TestRun (
       [["set_e2uuid"; "/dev/sda1"; "time"]])],
    "set the ext2/3/4 filesystem UUID",
    "\
       [["set_e2uuid"; "/dev/sda1"; "time"]])],
    "set the ext2/3/4 filesystem UUID",
    "\
@@ -1614,6 +1966,819 @@ to return the existing UUID of a filesystem.");
 This returns the ext2/3/4 filesystem UUID of the filesystem on
 C<device>.");
 
 This returns the ext2/3/4 filesystem UUID of the filesystem on
 C<device>.");
 
+  ("fsck", (RInt "status", [String "fstype"; String "device"]), 84, [],
+   [InitBasicFS, Always, TestOutputInt (
+      [["umount"; "/dev/sda1"];
+       ["fsck"; "ext2"; "/dev/sda1"]], 0);
+    InitBasicFS, Always, TestOutputInt (
+      [["umount"; "/dev/sda1"];
+       ["zero"; "/dev/sda1"];
+       ["fsck"; "ext2"; "/dev/sda1"]], 8)],
+   "run the filesystem checker",
+   "\
+This runs the filesystem checker (fsck) on C<device> which
+should have filesystem type C<fstype>.
+
+The returned integer is the status.  See L<fsck(8)> for the
+list of status codes from C<fsck>.
+
+Notes:
+
+=over 4
+
+=item *
+
+Multiple status codes can be summed together.
+
+=item *
+
+A non-zero return code can mean \"success\", for example if
+errors have been corrected on the filesystem.
+
+=item *
+
+Checking or repairing NTFS volumes is not supported
+(by linux-ntfs).
+
+=back
+
+This command is entirely equivalent to running C<fsck -a -t fstype device>.");
+
+  ("zero", (RErr, [String "device"]), 85, [],
+   [InitBasicFS, Always, TestOutput (
+      [["umount"; "/dev/sda1"];
+       ["zero"; "/dev/sda1"];
+       ["file"; "/dev/sda1"]], "data")],
+   "write zeroes to the device",
+   "\
+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.
+
+See also: C<guestfs_scrub_device>.");
+
+  ("grub_install", (RErr, [String "root"; String "device"]), 86, [],
+   (* 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",
+   "\
+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, Always, TestOutput (
+      [["write_file"; "/old"; "file content"; "0"];
+       ["cp"; "/old"; "/new"];
+       ["cat"; "/new"]], "file content");
+    InitBasicFS, Always, TestOutputTrue (
+      [["write_file"; "/old"; "file content"; "0"];
+       ["cp"; "/old"; "/new"];
+       ["is_file"; "/old"]]);
+    InitBasicFS, Always, TestOutput (
+      [["write_file"; "/old"; "file content"; "0"];
+       ["mkdir"; "/dir"];
+       ["cp"; "/old"; "/dir/new"];
+       ["cat"; "/dir/new"]], "file content")],
+   "copy a file",
+   "\
+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, Always, TestOutput (
+      [["mkdir"; "/olddir"];
+       ["mkdir"; "/newdir"];
+       ["write_file"; "/olddir/file"; "file content"; "0"];
+       ["cp_a"; "/olddir"; "/newdir"];
+       ["cat"; "/newdir/olddir/file"]], "file content")],
+   "copy a file or directory recursively",
+   "\
+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, Always, TestOutput (
+      [["write_file"; "/old"; "file content"; "0"];
+       ["mv"; "/old"; "/new"];
+       ["cat"; "/new"]], "file content");
+    InitBasicFS, Always, TestOutputFalse (
+      [["write_file"; "/old"; "file content"; "0"];
+       ["mv"; "/old"; "/new"];
+       ["is_file"; "/old"]])],
+   "move a file",
+   "\
+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, Always, TestRun (
+      [["drop_caches"; "3"]])],
+   "drop kernel page cache, dentries and inodes",
+   "\
+This instructs the guest kernel to drop its page cache,
+and/or dentries and inode caches.  The parameter C<whattodrop>
+tells the kernel what precisely to drop, see
+L<http://linux-mm.org/Drop_Caches>
+
+Setting C<whattodrop> to 3 should drop everything.
+
+This automatically calls L<sync(2)> before the operation,
+so that the maximum guest memory is freed.");
+
+  ("dmesg", (RString "kmsgs", []), 91, [],
+   [InitEmpty, Always, TestRun (
+      [["dmesg"]])],
+   "return kernel messages",
+   "\
+This returns the kernel messages (C<dmesg> output) from
+the guest kernel.  This is sometimes useful for extended
+debugging of problems.
+
+Another way to get the same information is to enable
+verbose messages with C<guestfs_set_verbose> or by setting
+the environment variable C<LIBGUESTFS_DEBUG=1> before
+running the program.");
+
+  ("ping_daemon", (RErr, []), 92, [],
+   [InitEmpty, Always, TestRun (
+      [["ping_daemon"]])],
+   "ping the guest daemon",
+   "\
+This is a test probe into the guestfs daemon running inside
+the qemu subprocess.  Calling this function checks that the
+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, Always, TestOutputTrue (
+      [["write_file"; "/file1"; "contents of a file"; "0"];
+       ["cp"; "/file1"; "/file2"];
+       ["equal"; "/file1"; "/file2"]]);
+    InitBasicFS, Always, TestOutputFalse (
+      [["write_file"; "/file1"; "contents of a file"; "0"];
+       ["write_file"; "/file2"; "contents of another file"; "0"];
+       ["equal"; "/file1"; "/file2"]]);
+    InitBasicFS, Always, TestLastFail (
+      [["equal"; "/file1"; "/file2"]])],
+   "test if two files have equal contents",
+   "\
+This compares the two files C<file1> and C<file2> and returns
+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, Always, TestOutputList (
+      [["write_file"; "/new"; "hello\nworld\n"; "0"];
+       ["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, Always, TestOutputList (
+      [["write_file"; "/new"; "hello\nworld\n"; "0"];
+       ["strings_e"; "b"; "/new"]], []);
+    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"])],
+   "print the printable strings in a file",
+   "\
+This is like the C<guestfs_strings> command, but allows you to
+specify the encoding.
+
+See the L<strings(1)> manpage for the full list of encodings.
+
+Commonly useful encodings are C<l> (lower case L) which will
+show strings inside Windows/x86 files.
+
+The returned strings are transcoded to UTF-8.");
+
+  ("hexdump", (RString "dump", [String "path"]), 96, [ProtocolLimitWarning],
+   [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");
+    (* 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", (RDirentList "entries", [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
 ]
 
 let all_functions = non_daemon_functions @ daemon_functions
@@ -1721,7 +2886,33 @@ let statvfs_cols = [
   "namemax", `Int;
 ]
 
   "namemax", `Int;
 ]
 
-(* Useful functions.
+(* Column names in dirent structure. *)
+let dirent_cols = [
+  "ino", `Int;
+  "ftyp", `Char; (* 'b' 'c' 'd' 'f' (FIFO) 'l' 'r' (regular file) 's' 'u' '?' *)
+  "name", `String;
+]
+
+(* 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.
  *)
  * Note we don't want to use any external OCaml libraries which
  * makes this a bit harder than it should be.
  *)
@@ -1804,6 +2995,13 @@ let rec string_split sep str =
     s' :: string_split sep s''
   )
 
     s' :: string_split sep s''
   )
 
+let files_equal n1 n2 =
+  let cmd = sprintf "cmp -s %s %s" (Filename.quote n1) (Filename.quote n2) in
+  match Sys.command cmd with
+  | 0 -> true
+  | 1 -> false
+  | i -> failwithf "%s: failed with error code %d" cmd i
+
 let rec find_map f = function
   | [] -> raise Not_found
   | x :: xs ->
 let rec find_map f = function
   | [] -> raise Not_found
   | x :: xs ->
@@ -1831,6 +3029,7 @@ let name_of_argt = function
 
 let seq_of_test = function
   | TestRun s | TestOutput (s, _) | TestOutputList (s, _)
 
 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
   | TestOutputInt (s, _) | TestOutputTrue s | TestOutputFalse s
   | TestOutputLength (s, _) | TestOutputStruct (s, _)
   | TestLastFail s -> s
@@ -1855,8 +3054,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;
     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
       if String.contains name '-' then
        failwithf "function name %s should not contain '-', use '_' instead."
          name
@@ -1873,9 +3074,13 @@ let check_functions () =
          failwithf "%s param/ret %s should not contain '-' or '_'"
            name n;
        if n = "value" then
          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
        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
       in
 
       (match fst style with
@@ -1883,7 +3088,8 @@ let check_functions () =
        | RInt n | RInt64 n | RBool n | RConstString n | RString n
        | RStringList n | RPVList n | RVGList n | RLVList n
        | RStat n | RStatVFS n
        | RInt n | RInt64 n | RBool n | RConstString n | RString n
        | RStringList n | RPVList n | RVGList n | RLVList n
        | RStat n | RStatVFS n
-       | RHashtable n ->
+       | RHashtable n
+       | RDirentList n ->
           check_arg_ret_name n
        | RIntBool (n,m) ->
           check_arg_ret_name n;
           check_arg_ret_name n
        | RIntBool (n,m) ->
           check_arg_ret_name n;
@@ -1948,7 +3154,7 @@ let check_functions () =
     | name, _, _, _, tests, _, _ ->
        let funcs =
          List.map (
     | 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
              match seq_of_test test with
              | [] ->
                  failwithf "%s has a test containing an empty sequence" name
@@ -1967,14 +3173,15 @@ let chan = ref stdout
 let pr fs = ksprintf (output_string !chan) fs
 
 (* Generate a header block in a number of standard styles. *)
 let pr fs = ksprintf (output_string !chan) fs
 
 (* Generate a header block in a number of standard styles. *)
-type comment_style = CStyle | HashStyle | OCamlStyle
+type comment_style = CStyle | HashStyle | OCamlStyle | HaskellStyle
 type license = GPLv2 | LGPLv2
 
 let generate_header comment license =
   let c = match comment with
     | CStyle ->     pr "/* "; " *"
     | HashStyle ->  pr "# ";  "#"
 type license = GPLv2 | LGPLv2
 
 let generate_header comment license =
   let c = match comment with
     | CStyle ->     pr "/* "; " *"
     | HashStyle ->  pr "# ";  "#"
-    | OCamlStyle -> pr "(* "; " *" in
+    | OCamlStyle -> pr "(* "; " *"
+    | HaskellStyle -> pr "{- "; "  " in
   pr "libguestfs generated file\n";
   pr "%s WARNING: THIS FILE IS GENERATED BY 'src/generator.ml'.\n" c;
   pr "%s ANY CHANGES YOU MAKE TO THIS FILE WILL BE LOST.\n" c;
   pr "libguestfs generated file\n";
   pr "%s WARNING: THIS FILE IS GENERATED BY 'src/generator.ml'.\n" c;
   pr "%s ANY CHANGES YOU MAKE TO THIS FILE WILL BE LOST.\n" c;
@@ -2016,6 +3223,7 @@ let generate_header comment license =
    | CStyle -> pr " */\n"
    | HashStyle -> ()
    | OCamlStyle -> pr " *)\n"
    | CStyle -> pr " */\n"
    | HashStyle -> ()
    | OCamlStyle -> pr " *)\n"
+   | HaskellStyle -> pr "-}\n"
   );
   pr "\n"
 
   );
   pr "\n"
 
@@ -2025,71 +3233,78 @@ let generate_header comment license =
 let rec generate_actions_pod () =
   List.iter (
     fun (shortname, style, _, flags, _, _, longdesc) ->
 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"
 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"
 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"
 (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 *>,
+        | 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"
 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 *>
+        | RPVList _ ->
+            pr "This function returns a C<struct guestfs_lvm_pv_list *>
 (see E<lt>guestfs-structs.hE<gt>),
 or NULL if there was an error.
 I<The caller must call C<guestfs_free_lvm_pv_list> after use>.\n\n"
 (see E<lt>guestfs-structs.hE<gt>),
 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 *>
+        | RVGList _ ->
+            pr "This function returns a C<struct guestfs_lvm_vg_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"
 (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 *>
+        | 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"
 (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 *>
+        | 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"
 (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 *>
+        | 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"
 (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
+        | 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"
 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;
+        | RDirentList _ ->
+            pr "This function returns a C<struct guestfs_dirent_list *>
+(see E<lt>guestfs-structs.hE<gt>),
+or NULL if there was an error.
+I<The caller must call C<guestfs_free_dirent_list> 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
+      )
   ) all_functions_sorted
 
 and generate_structs_pod () =
   ) all_functions_sorted
 
 and generate_structs_pod () =
@@ -2120,7 +3335,41 @@ and generate_structs_pod () =
       pr " void guestfs_free_lvm_%s_list (struct guestfs_free_lvm_%s_list *);\n"
        typ typ;
       pr "\n"
       pr " void guestfs_free_lvm_%s_list (struct guestfs_free_lvm_%s_list *);\n"
        typ typ;
       pr "\n"
-  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols]
+  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
+
+  (* Stat *)
+  List.iter (
+    fun (typ, cols) ->
+      pr "=head2 guestfs_%s\n" typ;
+      pr "\n";
+      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 ];
+
+  (* DirentList *)
+  pr "=head2 guestfs_dirent\n";
+  pr "\n";
+  pr " struct guestfs_dirent {\n";
+  List.iter (
+    function
+    | name, `String -> pr "   char *%s;\n" name
+    | name, `Int -> pr "   int64_t %s;\n" name
+    | name, `Char -> pr "   char %s;\n" name
+  ) dirent_cols;
+  pr " };\n";
+  pr "\n";
+  pr " struct guestfs_dirent_list {\n";
+  pr "   uint32_t len; /* Number of elements in list. */\n";
+  pr "   struct guestfs_dirent *val; /* Elements. */\n";
+  pr " };\n";
+  pr " \n";
+  pr " void guestfs_free_dirent_list (struct guestfs_free_dirent_list *);\n";
+  pr "\n"
 
 (* Generate the protocol (XDR) file, 'guestfs_protocol.x' and
  * indirectly 'guestfs_protocol.h' and 'guestfs_protocol.c'.
 
 (* Generate the protocol (XDR) file, 'guestfs_protocol.x' and
  * indirectly 'guestfs_protocol.h' and 'guestfs_protocol.c'.
@@ -2167,6 +3416,18 @@ and generate_xdr () =
        pr "\n";
   ) ["stat", stat_cols; "statvfs", statvfs_cols];
 
        pr "\n";
   ) ["stat", stat_cols; "statvfs", statvfs_cols];
 
+  (* Dirent structures. *)
+  pr "struct guestfs_int_dirent {\n";
+  List.iter (function
+            | name, `Int -> pr "  hyper %s;\n" name
+            | name, `Char -> pr "  char %s;\n" name
+            | name, `String -> pr "  string %s<>;\n" name
+           ) dirent_cols;
+  pr "};\n";
+  pr "\n";
+  pr "typedef struct guestfs_int_dirent guestfs_int_dirent_list<>;\n";
+  pr "\n";
+
   List.iter (
     fun (shortname, style, _, _, _, _, _) ->
       let name = "guestfs_" ^ shortname in
   List.iter (
     fun (shortname, style, _, _, _, _, _) ->
       let name = "guestfs_" ^ shortname in
@@ -2239,6 +3500,10 @@ and generate_xdr () =
           pr "struct %s_ret {\n" name;
           pr "  str %s<>;\n" n;
           pr "};\n\n"
           pr "struct %s_ret {\n" name;
           pr "  str %s<>;\n" n;
           pr "};\n\n"
+       | RDirentList n ->
+          pr "struct %s_ret {\n" name;
+          pr "  guestfs_int_dirent_list %s;\n" n;
+          pr "};\n\n"
       );
   ) daemon_functions;
 
       );
   ) daemon_functions;
 
@@ -2366,7 +3631,23 @@ and generate_structs_h () =
        ) cols;
        pr "};\n";
        pr "\n"
        ) cols;
        pr "};\n";
        pr "\n"
-  ) ["stat", stat_cols; "statvfs", statvfs_cols]
+  ) ["stat", stat_cols; "statvfs", statvfs_cols];
+
+  (* Dirent structures. *)
+  pr "struct guestfs_dirent {\n";
+  List.iter (
+    function
+    | name, `Int -> pr "  int64_t %s;\n" name
+    | name, `Char -> pr "  char %s;\n" name
+    | name, `String -> pr "  char *%s;\n" name
+  ) dirent_cols;
+  pr "};\n";
+  pr "\n";
+  pr "struct guestfs_dirent_list {\n";
+  pr "  uint32_t len;\n";
+  pr "  struct guestfs_dirent *val;\n";
+  pr "};\n";
+  pr "\n"
 
 (* Generate the guestfs-actions.h file. *)
 and generate_actions_h () =
 
 (* Generate the guestfs-actions.h file. *)
 and generate_actions_h () =
@@ -2474,7 +3755,8 @@ check_state (guestfs_h *g, const char *caller)
        | RIntBool _
        | RPVList _ | RVGList _ | RLVList _
        | RStat _ | RStatVFS _
        | RIntBool _
        | RPVList _ | RVGList _ | RLVList _
        | RStat _ | RStatVFS _
-       | RHashtable _ ->
+       | RHashtable _
+       | RDirentList _ ->
           pr "  struct %s_ret ret;\n" name
       );
       pr "};\n";
           pr "  struct %s_ret ret;\n" name
       );
       pr "};\n";
@@ -2517,7 +3799,8 @@ check_state (guestfs_h *g, const char *caller)
        | RIntBool _
        | RPVList _ | RVGList _ | RLVList _
        | RStat _ | RStatVFS _
        | RIntBool _
        | RPVList _ | RVGList _ | RLVList _
        | RStat _ | RStatVFS _
-       | RHashtable _ ->
+       | RHashtable _
+       | RDirentList _ ->
            pr "  if (!xdr_%s_ret (xdr, &ctx->ret)) {\n" name;
            pr "    error (g, \"%%s: failed to parse reply\", \"%s\");\n" name;
            pr "    return;\n";
            pr "  if (!xdr_%s_ret (xdr, &ctx->ret)) {\n" name;
            pr "    error (g, \"%%s: failed to parse reply\", \"%s\");\n" name;
            pr "    return;\n";
@@ -2540,7 +3823,8 @@ check_state (guestfs_h *g, const char *caller)
        | RString _ | RStringList _ | RIntBool _
        | RPVList _ | RVGList _ | RLVList _
        | RStat _ | RStatVFS _
        | RString _ | RStringList _ | RIntBool _
        | RPVList _ | RVGList _ | RLVList _
        | RStat _ | RStatVFS _
-       | RHashtable _ ->
+       | RHashtable _
+       | RDirentList _ ->
            "NULL" in
 
       pr "{\n";
            "NULL" in
 
       pr "{\n";
@@ -2587,7 +3871,7 @@ check_state (guestfs_h *g, const char *caller)
             name;
       );
       pr "  if (serial == -1) {\n";
             name;
       );
       pr "  if (serial == -1) {\n";
-      pr "    guestfs_set_ready (g);\n";
+      pr "    guestfs_end_busy (g);\n";
       pr "    return %s;\n" error_code;
       pr "  }\n";
       pr "\n";
       pr "    return %s;\n" error_code;
       pr "  }\n";
       pr "\n";
@@ -2602,7 +3886,7 @@ check_state (guestfs_h *g, const char *caller)
            pr "\n";
            pr "    r = guestfs__send_file_sync (g, %s);\n" n;
            pr "    if (r == -1) {\n";
            pr "\n";
            pr "    r = guestfs__send_file_sync (g, %s);\n" n;
            pr "    if (r == -1) {\n";
-           pr "      guestfs_set_ready (g);\n";
+           pr "      guestfs_end_busy (g);\n";
            pr "      return %s;\n" error_code;
            pr "    }\n";
            pr "    if (r == -2) /* daemon cancelled */\n";
            pr "      return %s;\n" error_code;
            pr "    }\n";
            pr "    if (r == -2) /* daemon cancelled */\n";
@@ -2622,21 +3906,22 @@ check_state (guestfs_h *g, const char *caller)
       pr "  guestfs_set_reply_callback (g, NULL, NULL);\n";
       pr "  if (ctx.cb_sequence != 1) {\n";
       pr "    error (g, \"%%s reply failed, see earlier error messages\", \"%s\");\n" name;
       pr "  guestfs_set_reply_callback (g, NULL, NULL);\n";
       pr "  if (ctx.cb_sequence != 1) {\n";
       pr "    error (g, \"%%s reply failed, see earlier error messages\", \"%s\");\n" name;
-      pr "    guestfs_set_ready (g);\n";
+      pr "    guestfs_end_busy (g);\n";
       pr "    return %s;\n" error_code;
       pr "  }\n";
       pr "\n";
 
       pr "  if (check_reply_header (g, &ctx.hdr, GUESTFS_PROC_%s, serial) == -1) {\n"
        (String.uppercase shortname);
       pr "    return %s;\n" error_code;
       pr "  }\n";
       pr "\n";
 
       pr "  if (check_reply_header (g, &ctx.hdr, GUESTFS_PROC_%s, serial) == -1) {\n"
        (String.uppercase shortname);
-      pr "    guestfs_set_ready (g);\n";
+      pr "    guestfs_end_busy (g);\n";
       pr "    return %s;\n" error_code;
       pr "  }\n";
       pr "\n";
 
       pr "  if (ctx.hdr.status == GUESTFS_STATUS_ERROR) {\n";
       pr "    error (g, \"%%s\", ctx.err.error_message);\n";
       pr "    return %s;\n" error_code;
       pr "  }\n";
       pr "\n";
 
       pr "  if (ctx.hdr.status == GUESTFS_STATUS_ERROR) {\n";
       pr "    error (g, \"%%s\", ctx.err.error_message);\n";
-      pr "    guestfs_set_ready (g);\n";
+      pr "    free (ctx.err.error_message);\n";
+      pr "    guestfs_end_busy (g);\n";
       pr "    return %s;\n" error_code;
       pr "  }\n";
       pr "\n";
       pr "    return %s;\n" error_code;
       pr "  }\n";
       pr "\n";
@@ -2646,14 +3931,14 @@ check_state (guestfs_h *g, const char *caller)
        function
        | FileOut n ->
            pr "  if (guestfs__receive_file_sync (g, %s) == -1) {\n" n;
        function
        | FileOut n ->
            pr "  if (guestfs__receive_file_sync (g, %s) == -1) {\n" n;
-           pr "    guestfs_set_ready (g);\n";
+           pr "    guestfs_end_busy (g);\n";
            pr "    return %s;\n" error_code;
            pr "  }\n";
            pr "\n";
        | _ -> ()
       ) (snd style);
 
            pr "    return %s;\n" error_code;
            pr "  }\n";
            pr "\n";
        | _ -> ()
       ) (snd style);
 
-      pr "  guestfs_set_ready (g);\n";
+      pr "  guestfs_end_busy (g);\n";
 
       (match fst style with
        | RErr -> pr "  return 0;\n"
 
       (match fst style with
        | RErr -> pr "  return 0;\n"
@@ -2675,7 +3960,8 @@ check_state (guestfs_h *g, const char *caller)
           pr "  /* caller with free this */\n";
           pr "  return safe_memdup (g, &ctx.ret, sizeof (ctx.ret));\n"
        | RPVList n | RVGList n | RLVList n
           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 ->
+       | RStat n | RStatVFS n
+       | RDirentList n ->
           pr "  /* caller will free this */\n";
           pr "  return safe_memdup (g, &ctx.ret.%s, sizeof (ctx.ret.%s));\n" n n
       );
           pr "  /* caller will free this */\n";
           pr "  return safe_memdup (g, &ctx.ret.%s, sizeof (ctx.ret.%s));\n" n n
       );
@@ -2735,7 +4021,8 @@ and generate_daemon_actions () =
        | 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"
        | 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
+       | RStatVFS _ -> pr "  guestfs_int_statvfs *r;\n"; "NULL"
+       | RDirentList _ -> pr "  guestfs_int_dirent_list *r;\n"; "NULL" in
 
       (match snd style with
        | [] -> ()
 
       (match snd style with
        | [] -> ()
@@ -2743,8 +4030,12 @@ and generate_daemon_actions () =
           pr "  struct guestfs_%s_args args;\n" name;
           List.iter (
             function
           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
             | 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
             | StringList n -> pr "  char **%s;\n" n
             | Bool n -> pr "  int %s;\n" n
             | Int n -> pr "  int %s;\n" n
@@ -2832,7 +4123,8 @@ and generate_daemon_actions () =
              name;
            pr "  xdr_free ((xdrproc_t) xdr_guestfs_%s_ret, (char *) r);\n" name
        | RPVList n | RVGList n | RLVList 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 ->
+       | RStat n | RStatVFS n
+       | RDirentList 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"
            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"
@@ -2867,7 +4159,7 @@ and generate_daemon_actions () =
   ) daemon_functions;
 
   pr "    default:\n";
   ) 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";
   pr "  }\n";
   pr "}\n";
   pr "\n";
@@ -3038,6 +4330,22 @@ and generate_daemon_actions () =
 
   ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols]
 
 
   ) ["pv", pv_cols; "vg", vg_cols; "lv", 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 () =
   generate_header CStyle GPLv2;
 (* Generate the tests. *)
 and generate_tests () =
   generate_header CStyle GPLv2;
@@ -3111,11 +4419,12 @@ int main (int argc, char *argv[])
 {
   char c = 0;
   int failed = 0;
 {
   char c = 0;
   int failed = 0;
-  const char *srcdir;
   const char *filename;
   int fd;
   int nr_tests, test_num = 0;
 
   const char *filename;
   int fd;
   int nr_tests, test_num = 0;
 
+  setbuf (stdout, NULL);
+
   no_test_warnings ();
 
   g = guestfs_create ();
   no_test_warnings ();
 
   g = guestfs_create ();
@@ -3126,10 +4435,7 @@ int main (int argc, char *argv[])
 
   guestfs_set_error_handler (g, print_error, NULL);
 
 
   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);
 
   filename = \"test1.img\";
   fd = open (filename, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
@@ -3215,15 +4521,27 @@ int main (int argc, char *argv[])
     exit (1);
   }
 
     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);
   }
   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);
   }
 
   if (guestfs_wait_ready (g) == -1) {
     printf (\"guestfs_wait_ready FAILED\\n\");
     exit (1);
   }
 
+  /* Cancel previous alarm. */
+  alarm (0);
+
   nr_tests = %d;
 
 " (500 * 1024 * 1024) (50 * 1024 * 1024) (10 * 1024 * 1024) nr_tests;
   nr_tests = %d;
 
 " (500 * 1024 * 1024) (50 * 1024 * 1024) (10 * 1024 * 1024) nr_tests;
@@ -3254,34 +4572,98 @@ int main (int argc, char *argv[])
   pr "  exit (0);\n";
   pr "}\n"
 
   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
 
   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
   (match init with
-   | InitNone -> ()
+   | InitNone
    | InitEmpty ->
    | 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)
        List.iter (generate_test_command_call test_name)
-        [["umount_all"];
+        [["blockdev_setrw"; "/dev/sda"];
+         ["umount_all"];
          ["lvm_remove_all"]]
    | InitBasicFS ->
          ["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)
        List.iter (generate_test_command_call test_name)
-        [["umount_all"];
+        [["blockdev_setrw"; "/dev/sda"];
+         ["umount_all"];
          ["lvm_remove_all"];
          ["lvm_remove_all"];
-         ["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+         ["sfdiskM"; "/dev/sda"; ","];
          ["mkfs"; "ext2"; "/dev/sda1"];
          ["mount"; "/dev/sda1"; "/"]]
    | InitBasicFSonLVM ->
          ["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)
        List.iter (generate_test_command_call test_name)
-        [["umount_all"];
+        [["blockdev_setrw"; "/dev/sda"];
+         ["umount_all"];
          ["lvm_remove_all"];
          ["lvm_remove_all"];
-         ["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+         ["sfdiskM"; "/dev/sda"; ","];
          ["pvcreate"; "/dev/sda1"];
          ["vgcreate"; "VG"; "/dev/sda1"];
          ["lvcreate"; "LV"; "VG"; "8"];
          ["pvcreate"; "/dev/sda1"];
          ["vgcreate"; "VG"; "/dev/sda1"];
          ["lvcreate"; "LV"; "VG"; "8"];
@@ -3298,153 +4680,180 @@ and generate_one_test name i (init, test) =
        List.rev (List.tl seq), List.hd seq
   in
 
        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;
-       let seq, last = get_seq_last seq in
-       let test () =
-        pr "    if (strcmp (r, \"%s\") != 0) {\n" (c_quote expected);
-        pr "      fprintf (stderr, \"%s: expected \\\"%s\\\" but got \\\"%%s\\\"\\n\", r);\n" test_name (c_quote expected);
-        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 "    if (strcmp (r[%d], \"%s\") != 0) {\n" i (c_quote str);
-            pr "      fprintf (stderr, \"%s: expected \\\"%s\\\" but got \\\"%%s\\\"\\n\", r[%d]);\n" test_name (c_quote str) i;
-            pr "      return -1;\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 "  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 "      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 "      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.
 
 (* 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.
@@ -3470,16 +4879,22 @@ and generate_test_command_call ?(expect_error = false) ?test test_name cmd =
 
       List.iter (
        function
 
       List.iter (
        function
-       | String _, _
-       | OptString _, _
+       | OptString n, "NULL" -> ()
+       | String n, arg
+       | OptString n, arg ->
+           pr "    char %s[] = \"%s\";\n" n (c_quote arg);
        | Int _, _
        | Int _, _
-       | Bool _, _ -> ()
+       | Bool _, _
        | FileIn _, _ | FileOut _, _ -> ()
        | StringList n, arg ->
        | FileIn _, _ | FileOut _, _ -> ()
        | StringList n, arg ->
-           pr "    char *%s[] = {\n" n;
            let strs = string_split " " arg in
            let strs = string_split " " arg in
-           List.iter (
-             fun str -> pr "      \"%s\",\n" (c_quote str)
+           iteri (
+             fun i str ->
+                pr "    char %s_%d[] = \"%s\";\n" n i (c_quote str);
+           ) strs;
+           pr "    char *%s[] = {\n" n;
+           iteri (
+             fun i _ -> pr "      %s_%d,\n" n i
            ) strs;
            pr "      NULL\n";
            pr "    };\n";
            ) strs;
            pr "      NULL\n";
            pr "    };\n";
@@ -3506,7 +4921,9 @@ and generate_test_command_call ?(expect_error = false) ?test test_name cmd =
        | RStat _ ->
            pr "    struct guestfs_stat *r;\n"; "NULL"
        | RStatVFS _ ->
        | RStat _ ->
            pr "    struct guestfs_stat *r;\n"; "NULL"
        | RStatVFS _ ->
-           pr "    struct guestfs_statvfs *r;\n"; "NULL" in
+           pr "    struct guestfs_statvfs *r;\n"; "NULL"
+       | RDirentList _ ->
+           pr "    struct guestfs_dirent_list *r;\n"; "NULL" in
 
       pr "    suppress_error = %d;\n" (if expect_error then 1 else 0);
       pr "    r = guestfs_%s (g" name;
 
       pr "    suppress_error = %d;\n" (if expect_error then 1 else 0);
       pr "    r = guestfs_%s (g" name;
@@ -3514,11 +4931,12 @@ and generate_test_command_call ?(expect_error = false) ?test test_name cmd =
       (* Generate the parameters. *)
       List.iter (
        function
       (* Generate the parameters. *)
       List.iter (
        function
-       | String _, arg
+       | OptString _, "NULL" -> pr ", NULL"
+       | String n, _
+       | OptString n, _ ->
+            pr ", %s" n
        | FileIn _, arg | FileOut _, arg ->
            pr ", \"%s\"" (c_quote arg)
        | FileIn _, arg | FileOut _, arg ->
            pr ", \"%s\"" (c_quote arg)
-       | OptString _, arg ->
-           if arg = "NULL" then pr ", NULL" else pr ", \"%s\"" (c_quote arg)
        | StringList n, _ ->
            pr ", %s" n
        | Int _, arg ->
        | StringList n, _ ->
            pr ", %s" n
        | Int _, arg ->
@@ -3561,6 +4979,8 @@ and generate_test_command_call ?(expect_error = false) ?test test_name cmd =
           pr "    guestfs_free_lvm_lv_list (r);\n"
        | RStat _ | RStatVFS _ ->
           pr "    free (r);\n"
           pr "    guestfs_free_lvm_lv_list (r);\n"
        | RStat _ | RStatVFS _ ->
           pr "    free (r);\n"
+       | RDirentList _ ->
+          pr "    guestfs_free_dirent_list (r);\n"
       );
 
       pr "  }\n"
       );
 
       pr "  }\n"
@@ -3569,6 +4989,7 @@ and c_quote str =
   let str = replace_str str "\r" "\\r" in
   let str = replace_str str "\n" "\\n" in
   let str = replace_str str "\t" "\\t" in
   let str = replace_str str "\r" "\\r" in
   let str = replace_str str "\n" "\\n" in
   let str = replace_str str "\t" "\\t" in
+  let str = replace_str str "\000" "\\0" in
   str
 
 (* Generate a lot of different functions for guestfish. *)
   str
 
 (* Generate a lot of different functions for guestfish. *)
@@ -3715,6 +5136,29 @@ and generate_fish_cmds () =
        pr "\n";
   ) ["stat", stat_cols; "statvfs", statvfs_cols];
 
        pr "\n";
   ) ["stat", stat_cols; "statvfs", statvfs_cols];
 
+  (* print_dirent_list function *)
+  pr "static void print_dirent (struct guestfs_dirent *dirent)\n";
+  pr "{\n";
+  List.iter (
+    function
+    | name, `String ->
+       pr "  printf (\"%s: %%s\\n\", dirent->%s);\n" name name
+    | name, `Int ->
+       pr "  printf (\"%s: %%\" PRIi64 \"\\n\", dirent->%s);\n" name name
+    | name, `Char ->
+       pr "  printf (\"%s: %%c\\n\", dirent->%s);\n" name name
+  ) dirent_cols;
+  pr "}\n";
+  pr "\n";
+  pr "static void print_dirent_list (struct guestfs_dirent_list *dirents)\n";
+  pr "{\n";
+  pr "  int i;\n";
+  pr "\n";
+  pr "  for (i = 0; i < dirents->len; ++i)\n";
+  pr "    print_dirent (&dirents->val[i]);\n";
+  pr "}\n";
+  pr "\n";
+
   (* run_<action> actions *)
   List.iter (
     fun (name, style, _, flags, _, _, _) ->
   (* run_<action> actions *)
   List.iter (
     fun (name, style, _, flags, _, _, _) ->
@@ -3734,6 +5178,7 @@ and generate_fish_cmds () =
        | RLVList _ -> pr "  struct guestfs_lvm_lv_list *r;\n"
        | RStat _ -> pr "  struct guestfs_stat *r;\n"
        | RStatVFS _ -> pr "  struct guestfs_statvfs *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"
+       | RDirentList _ -> pr "  struct guestfs_dirent_list *r;\n"
       );
       List.iter (
        function
       );
       List.iter (
        function
@@ -3848,6 +5293,11 @@ and generate_fish_cmds () =
           pr "  print_table (r);\n";
           pr "  free_strings (r);\n";
           pr "  return 0;\n"
           pr "  print_table (r);\n";
           pr "  free_strings (r);\n";
           pr "  return 0;\n"
+       | RDirentList _ ->
+          pr "  if (r == NULL) return -1;\n";
+          pr "  print_dirent_list (r);\n";
+          pr "  guestfs_free_dirent_list (r);\n";
+          pr "  return 0;\n"
       );
       pr "}\n";
       pr "\n"
       );
       pr "}\n";
       pr "\n"
@@ -3904,10 +5354,13 @@ and generate_fish_completion () =
 
 #ifdef HAVE_LIBREADLINE
 
 
 #ifdef HAVE_LIBREADLINE
 
-static const char *commands[] = {
+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, _, _, _) ->
   let commands =
     List.map (
       fun (name, _, _, flags, _, _, _) ->
@@ -3919,7 +5372,6 @@ static const char *commands[] = {
        if name <> alias then [name2; alias] else [name2]
     ) all_functions in
   let commands = List.flatten commands in
        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;
 
 
   List.iter (pr "  \"%s\",\n") commands;
 
@@ -3937,6 +5389,8 @@ generator (const char *text, int state)
     len = strlen (text);
   }
 
     len = strlen (text);
   }
 
+  rl_attempted_completion_over = 1;
+
   while ((name = commands[index]) != NULL) {
     index++;
     if (strncasecmp (name, text, len) == 0)
   while ((name = commands[index]) != NULL) {
     index++;
     if (strncasecmp (name, text, len) == 0)
@@ -3953,8 +5407,12 @@ char **do_completion (const char *text, int start, int end)
   char **matches = NULL;
 
 #ifdef HAVE_LIBREADLINE
   char **matches = NULL;
 
 #ifdef HAVE_LIBREADLINE
+  rl_completion_append_character = ' ';
+
   if (start == 0)
     matches = rl_completion_matches (text, generator);
   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;
 #endif
 
   return matches;
@@ -3965,7 +5423,8 @@ char **do_completion (const char *text, int start, int end)
 and generate_fish_actions_pod () =
   let all_functions_sorted =
     List.filter (
 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
     ) all_functions_sorted in
 
   let rex = Str.regexp "C<guestfs_\\([^>]+\\)>" in
@@ -4049,6 +5508,9 @@ and generate_prototype ?(extern = true) ?(static = false) ?(semicolon = true)
    | RStatVFS _ ->
        if not in_daemon then pr "struct guestfs_statvfs *"
        else pr "guestfs_int_statvfs *"
    | RStatVFS _ ->
        if not in_daemon then pr "struct guestfs_statvfs *"
        else pr "guestfs_int_statvfs *"
+   | RDirentList _ ->
+       if not in_daemon then pr "struct guestfs_dirent_list *"
+       else pr "guestfs_int_dirent_list *"
   );
   pr "%s%s (" prefix name;
   if handle = None && List.length (snd style) = 0 then
   );
   pr "%s%s (" prefix name;
   if handle = None && List.length (snd style) = 0 then
@@ -4068,8 +5530,14 @@ and generate_prototype ?(extern = true) ?(static = false) ?(semicolon = true)
     List.iter (
       function
       | String n
     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
       | Bool n -> next (); pr "int %s" n
       | Int n -> next (); pr "int %s" n
       | FileIn n
@@ -4124,6 +5592,8 @@ val close : t -> unit
 
   generate_ocaml_stat_structure_decls ();
 
 
   generate_ocaml_stat_structure_decls ();
 
+  generate_ocaml_dirent_structure_decls ();
+
   (* The actions. *)
   List.iter (
     fun (name, style, _, _, _, shortdesc, _) ->
   (* The actions. *)
   List.iter (
     fun (name, style, _, _, _, shortdesc, _) ->
@@ -4151,6 +5621,8 @@ let () =
 
   generate_ocaml_stat_structure_decls ();
 
 
   generate_ocaml_stat_structure_decls ();
 
+  generate_ocaml_dirent_structure_decls ();
+
   (* The actions. *)
   List.iter (
     fun (name, style, _, _, _, shortdesc, _) ->
   (* The actions. *)
   List.iter (
     fun (name, style, _, _, _, shortdesc, _) ->
@@ -4293,6 +5765,50 @@ copy_table (char * const * argv)
       pr "\n";
   ) ["stat", stat_cols; "statvfs", statvfs_cols];
 
       pr "\n";
   ) ["stat", stat_cols; "statvfs", statvfs_cols];
 
+  (* Dirent copy functions. *)
+  pr "static CAMLprim value\n";
+  pr "copy_dirent (const struct guestfs_dirent *dirent)\n";
+  pr "{\n";
+  pr "  CAMLparam0 ();\n";
+  pr "  CAMLlocal2 (rv, v);\n";
+  pr "\n";
+  pr "  rv = caml_alloc (%d, 0);\n" (List.length dirent_cols);
+  iteri (
+    fun i col ->
+      (match col with
+       | name, `String ->
+          pr "  v = caml_copy_string (dirent->%s);\n" name
+       | name, `Int ->
+          pr "  v = caml_copy_int64 (dirent->%s);\n" name
+       | name, `Char ->
+          pr "  v = Val_int (dirent->%s);\n" name
+      );
+      pr "  Store_field (rv, %d, v);\n" i
+  ) dirent_cols;
+  pr "  CAMLreturn (rv);\n";
+  pr "}\n";
+  pr "\n";
+
+  pr "static CAMLprim value\n";
+  pr "copy_dirent_list (const struct guestfs_dirent_list *dirents)\n";
+  pr "{\n";
+  pr "  CAMLparam0 ();\n";
+  pr "  CAMLlocal2 (rv, v);\n";
+  pr "  int i;\n";
+  pr "\n";
+  pr "  if (dirents->len == 0)\n";
+  pr "    CAMLreturn (Atom (0));\n";
+  pr "  else {\n";
+  pr "    rv = caml_alloc (dirents->len, 0);\n";
+  pr "    for (i = 0; i < dirents->len; ++i) {\n";
+  pr "      v = copy_dirent (&dirents->val[i]);\n";
+  pr "      caml_modify (&Field (rv, i), v);\n";
+  pr "    }\n";
+  pr "    CAMLreturn (rv);\n";
+  pr "  }\n";
+  pr "}\n";
+  pr "\n";
+
   (* The wrappers. *)
   List.iter (
     fun (name, style, _, _, _, _, _) ->
   (* The wrappers. *)
   List.iter (
     fun (name, style, _, _, _, _, _) ->
@@ -4334,7 +5850,7 @@ copy_table (char * const * argv)
            pr "    %sv != Val_int (0) ? String_val (Field (%sv, 0)) : NULL;\n"
              n n
        | StringList n ->
            pr "    %sv != Val_int (0) ? String_val (Field (%sv, 0)) : NULL;\n"
              n n
        | StringList n ->
-           pr "  char **%s = ocaml_guestfs_strings_val (%sv);\n" n n
+           pr "  char **%s = ocaml_guestfs_strings_val (g, %sv);\n" n n
        | Bool n ->
            pr "  int %s = Bool_val (%sv);\n" n n
        | Int n ->
        | Bool n ->
            pr "  int %s = Bool_val (%sv);\n" n n
        | Int n ->
@@ -4367,7 +5883,9 @@ copy_table (char * const * argv)
        | RHashtable _ ->
            pr "  int i;\n";
            pr "  char **r;\n";
        | RHashtable _ ->
            pr "  int i;\n";
            pr "  char **r;\n";
-           "NULL" in
+           "NULL"
+       | RDirentList _ ->
+           pr "  struct guestfs_dirent_list *r;\n"; "NULL" in
       pr "\n";
 
       pr "  caml_enter_blocking_section ();\n";
       pr "\n";
 
       pr "  caml_enter_blocking_section ();\n";
@@ -4425,6 +5943,9 @@ copy_table (char * const * argv)
           pr "  rv = copy_table (r);\n";
           pr "  for (i = 0; r[i] != NULL; ++i) free (r[i]);\n";
           pr "  free (r);\n";
           pr "  rv = copy_table (r);\n";
           pr "  for (i = 0; r[i] != NULL; ++i) free (r[i]);\n";
           pr "  free (r);\n";
+       | RDirentList _ ->
+          pr "  rv = copy_dirent_list (r);\n";
+          pr "  guestfs_free_dirent_list (r);\n";
       );
 
       pr "  CAMLreturn (rv);\n";
       );
 
       pr "  CAMLreturn (rv);\n";
@@ -4471,6 +5992,17 @@ and generate_ocaml_stat_structure_decls () =
       pr "\n"
   ) ["stat", stat_cols; "statvfs", statvfs_cols]
 
       pr "\n"
   ) ["stat", stat_cols; "statvfs", statvfs_cols]
 
+and generate_ocaml_dirent_structure_decls () =
+  pr "type dirent = {\n";
+  List.iter (
+    function
+    | name, `Int -> pr "  %s : int64;\n" name
+    | name, `Char -> pr "  %s : char;\n" name
+    | name, `String -> pr "  %s : string;\n" name
+  ) dirent_cols;
+  pr "}\n";
+  pr "\n"
+
 and generate_ocaml_prototype ?(is_external = false) name style =
   if is_external then pr "external " else pr "val ";
   pr "%s : t -> " name;
 and generate_ocaml_prototype ?(is_external = false) name style =
   if is_external then pr "external " else pr "val ";
   pr "%s : t -> " name;
@@ -4497,6 +6029,7 @@ and generate_ocaml_prototype ?(is_external = false) name style =
    | RStat _ -> pr "stat"
    | RStatVFS _ -> pr "statvfs"
    | RHashtable _ -> pr "(string * string) list"
    | RStat _ -> pr "stat"
    | RStatVFS _ -> pr "statvfs"
    | RHashtable _ -> pr "(string * string) list"
+   | RDirentList _ -> pr "dirent array"
   );
   if is_external then (
     pr " = ";
   );
   if is_external then (
     pr " = ";
@@ -4556,12 +6089,13 @@ XS_unpack_charPtrPtr (SV *arg) {
   AV *av;
   I32 i;
 
   AV *av;
   I32 i;
 
-  if (!arg || !SvOK (arg) || !SvROK (arg) || SvTYPE (SvRV (arg)) != SVt_PVAV) {
+  if (!arg || !SvOK (arg) || !SvROK (arg) || SvTYPE (SvRV (arg)) != SVt_PVAV)
     croak (\"array reference expected\");
     croak (\"array reference expected\");
-  }
 
   av = (AV *)SvRV (arg);
 
   av = (AV *)SvRV (arg);
-  ret = (char **)malloc (av_len (av) + 1 + 1);
+  ret = malloc ((av_len (av) + 1 + 1) * sizeof (char *));
+  if (!ret)
+    croak (\"malloc failed\");
 
   for (i = 0; i <= av_len (av); i++) {
     SV **elem = av_fetch (av, i, 0);
 
   for (i = 0; i <= av_len (av); i++) {
     SV **elem = av_fetch (av, i, 0);
@@ -4579,6 +6113,8 @@ XS_unpack_charPtrPtr (SV *arg) {
 
 MODULE = Sys::Guestfs  PACKAGE = Sys::Guestfs
 
 
 MODULE = Sys::Guestfs  PACKAGE = Sys::Guestfs
 
+PROTOTYPES: ENABLE
+
 guestfs_h *
 _create ()
    CODE:
 guestfs_h *
 _create ()
    CODE:
@@ -4610,7 +6146,8 @@ DESTROY (g)
        | RIntBool _
        | RPVList _ | RVGList _ | RLVList _
        | RStat _ | RStatVFS _
        | RIntBool _
        | RPVList _ | RVGList _ | RLVList _
        | RStat _ | RStatVFS _
-       | RHashtable _ ->
+       | RHashtable _
+       | RDirentList _ ->
           pr "void\n" (* all lists returned implictly on the stack *)
       );
       (* Call and arguments. *)
           pr "void\n" (* all lists returned implictly on the stack *)
       );
       (* Call and arguments. *)
@@ -4618,13 +6155,19 @@ DESTROY (g)
       generate_call_args ~handle:"g" (snd style);
       pr "\n";
       pr "      guestfs_h *g;\n";
       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 () =
       ) (snd style);
 
       let do_cleanups () =
@@ -4745,6 +6288,9 @@ DESTROY (g)
        | RStatVFS n ->
           generate_perl_stat_code
             "statvfs" statvfs_cols name style n do_cleanups
        | RStatVFS n ->
           generate_perl_stat_code
             "statvfs" statvfs_cols name style n do_cleanups
+       | RDirentList n ->
+          generate_perl_dirent_code
+            "dirent" dirent_cols name style n do_cleanups
       );
 
       pr "\n"
       );
 
       pr "\n"
@@ -4783,7 +6329,7 @@ and generate_perl_lvm_code typ cols name style n do_cleanups =
        pr "        (void) hv_store (hv, \"%s\", %d, newSVnv (%s->val[i].%s), 0);\n"
          name (String.length name) n name
   ) cols;
        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 "      }\n";
   pr "      guestfs_free_lvm_%s_list (%s);\n" typ n
 
@@ -4805,6 +6351,37 @@ and generate_perl_stat_code typ cols name style n do_cleanups =
   ) cols;
   pr "      free (%s);\n" n
 
   ) cols;
   pr "      free (%s);\n" n
 
+and generate_perl_dirent_code typ cols name style n do_cleanups =
+  pr "PREINIT:\n";
+  pr "      struct guestfs_%s_list *%s;\n" typ n;
+  pr "      int i;\n";
+  pr "      HV *hv;\n";
+  pr " PPCODE:\n";
+  pr "      %s = guestfs_%s " n name;
+  generate_call_args ~handle:"g" (snd style);
+  pr ";\n";
+  do_cleanups ();
+  pr "      if (%s == NULL)\n" n;
+  pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
+  pr "      EXTEND (SP, %s->len);\n" n;
+  pr "      for (i = 0; i < %s->len; ++i) {\n" n;
+  pr "        hv = newHV ();\n";
+  List.iter (
+    function
+    | name, `String ->
+       pr "        (void) hv_store (hv, \"%s\", %d, newSVpv (%s->val[i].%s, 0), 0);\n"
+         name (String.length name) n name
+    | name, `Int ->
+       pr "        (void) hv_store (hv, \"%s\", %d, my_newSVull (%s->val[i].%s), 0);\n"
+         name (String.length name) n name
+    | name, `Char ->
+       pr "        (void) hv_store (hv, \"%s\", %d, newSVpv (&%s->val[i].%s, 1), 0);\n"
+         name (String.length name) n name
+  ) cols;
+  pr "        PUSHs (newRV (sv_2mortal ((SV *) hv)));\n";
+  pr "      }\n";
+  pr "      guestfs_free_%s_list (%s);\n" typ n
+
 (* Generate Sys/Guestfs.pm. *)
 and generate_perl_pm () =
   generate_header HashStyle LGPLv2;
 (* Generate Sys/Guestfs.pm. *)
 and generate_perl_pm () =
   generate_header HashStyle LGPLv2;
@@ -4890,15 +6467,17 @@ sub new {
    *)
   List.iter (
     fun (name, style, _, flags, _, _, longdesc) ->
    *)
   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. *)
   ) all_functions_sorted;
 
   (* End of file. *)
@@ -4936,7 +6515,8 @@ and generate_perl_prototype name style =
    | RStringList n
    | RPVList n
    | RVGList n
    | RStringList n
    | RPVList n
    | RVGList n
-   | RLVList n -> pr "@%s = " n
+   | RLVList n
+   | RDirentList n -> pr "@%s = " n
    | RStat n
    | RStatVFS n
    | RHashtable n -> pr "%%%s = " n
    | RStat n
    | RStatVFS n
    | RHashtable n -> pr "%%%s = " n
@@ -5172,6 +6752,42 @@ py_guestfs_close (PyObject *self, PyObject *args)
       pr "\n";
   ) ["stat", stat_cols; "statvfs", statvfs_cols];
 
       pr "\n";
   ) ["stat", stat_cols; "statvfs", statvfs_cols];
 
+  (* Dirent structures, turned into Python dictionaries. *)
+  pr "static PyObject *\n";
+  pr "put_dirent (struct guestfs_dirent *dirent)\n";
+  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 (dirent->%s));\n" name
+    | name, `Char ->
+       pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+       pr "                        PyString_FromStringAndSize (&dirent->%s, 1));\n" name
+    | name, `String ->
+       pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+       pr "                        PyString_FromString (dirent->%s));\n" name
+  ) dirent_cols;
+  pr "  return dict;\n";
+  pr "};\n";
+  pr "\n";
+
+  pr "static PyObject *\n";
+  pr "put_dirent_list (struct guestfs_dirent_list *dirents)\n";
+  pr "{\n";
+  pr "  PyObject *list;\n";
+  pr "  int i;\n";
+  pr "\n";
+  pr "  list = PyList_New (dirents->len);\n";
+  pr "  for (i = 0; i < dirents->len; ++i)\n";
+  pr "    PyList_SetItem (list, i, put_dirent (&dirents->val[i]));\n";
+  pr "  return list;\n";
+  pr "};\n";
+  pr "\n";
+
   (* Python wrapper functions. *)
   List.iter (
     fun (name, style, _, _, _, _, _) ->
   (* Python wrapper functions. *)
   List.iter (
     fun (name, style, _, _, _, _, _) ->
@@ -5195,7 +6811,8 @@ py_guestfs_close (PyObject *self, PyObject *args)
        | 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"
        | 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
+       | RStatVFS n -> pr "  struct guestfs_statvfs *r;\n"; "NULL"
+       | RDirentList n -> pr "  struct guestfs_dirent_list *r;\n"; "NULL" in
 
       List.iter (
        function
 
       List.iter (
        function
@@ -5299,6 +6916,9 @@ py_guestfs_close (PyObject *self, PyObject *args)
        | RHashtable n ->
           pr "  py_r = put_table (r);\n";
           pr "  free_strings (r);\n"
        | RHashtable n ->
           pr "  py_r = put_table (r);\n";
           pr "  free_strings (r);\n"
+       | RDirentList n ->
+          pr "  py_r = put_dirent_list (r);\n";
+          pr "  guestfs_free_dirent_list (r);\n"
       );
 
       pr "  return py_r;\n";
       );
 
       pr "  return py_r;\n";
@@ -5401,43 +7021,47 @@ class GuestFS:
 
   List.iter (
     fun (name, style, _, flags, _, _, longdesc) ->
 
   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 "    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."
+         | 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."
+         | RDirentList _ ->
+             doc ^ "\n\nThis function returns a list of directory entries.  Each directory entry is represented as 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";
       pr "        return libguestfsmod.%s " name;
       generate_call_args ~handle:"self._o" (snd style);
       pr "\n";
@@ -5447,32 +7071,42 @@ class GuestFS:
 (* Useful if you need the longdesc POD text as plain text.  Returns a
  * list of lines.
  *
 (* 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 =
  *)
 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 () =
 
 (* Generate ruby bindings. *)
 and generate_ruby_c () =
@@ -5488,6 +7122,11 @@ and generate_ruby_c () =
 
 #include \"extconf.h\"
 
 
 #include \"extconf.h\"
 
+/* For Ruby < 1.9 */
+#ifndef RARRAY_LEN
+#define RARRAY_LEN(r) (RARRAY((r))->len)
+#endif
+
 static VALUE m_guestfs;                        /* guestfs module */
 static VALUE c_guestfs;                        /* guestfs_h handle */
 static VALUE e_Error;                  /* used for all errors */
 static VALUE m_guestfs;                        /* guestfs module */
 static VALUE c_guestfs;                        /* guestfs_h handle */
 static VALUE e_Error;                  /* used for all errors */
@@ -5544,25 +7183,29 @@ static VALUE ruby_guestfs_close (VALUE gv)
       List.iter (
        function
        | String n | FileIn n | FileOut n ->
       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 "  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 ->
        | 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;
            pr "  {\n";
            pr "    int i, len;\n";
            pr "    len = RARRAY_LEN (%sv);\n" n;
-           pr "    %s = malloc (sizeof (char *) * (len+1));\n" n;
+           pr "    %s = guestfs_safe_malloc (g, sizeof (char *) * (len+1));\n"
+             n;
            pr "    for (i = 0; i < len; ++i) {\n";
            pr "      VALUE v = rb_ary_entry (%sv, i);\n" n;
            pr "      %s[i] = StringValueCStr (v);\n" n;
            pr "    }\n";
            pr "    %s[len] = NULL;\n" n;
            pr "  }\n";
            pr "    for (i = 0; i < len; ++i) {\n";
            pr "      VALUE v = rb_ary_entry (%sv, i);\n" n;
            pr "      %s[i] = StringValueCStr (v);\n" n;
            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);
        | Int n ->
            pr "  int %s = NUM2INT (%sv);\n" n n
       ) (snd style);
@@ -5580,7 +7223,8 @@ static VALUE ruby_guestfs_close (VALUE gv)
        | 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"
        | 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
+       | RStatVFS n -> pr "  struct guestfs_statvfs *r;\n"; "NULL"
+       | RDirentList n -> pr "  struct guestfs_dirent_list *r;\n"; "NULL" in
       pr "\n";
 
       pr "  r = guestfs_%s " name;
       pr "\n";
 
       pr "  r = guestfs_%s " name;
@@ -5661,6 +7305,8 @@ static VALUE ruby_guestfs_close (VALUE gv)
           pr "  }\n";
           pr "  free (r);\n";
           pr "  return rv;\n"
           pr "  }\n";
           pr "  free (r);\n";
           pr "  return rv;\n"
+       | RDirentList n ->
+          generate_ruby_dirent_code "dirent" dirent_cols
       );
 
       pr "}\n";
       );
 
       pr "}\n";
@@ -5711,6 +7357,24 @@ and generate_ruby_lvm_code typ cols =
   pr "  guestfs_free_lvm_%s_list (r);\n" typ;
   pr "  return rv;\n"
 
   pr "  guestfs_free_lvm_%s_list (r);\n" typ;
   pr "  return rv;\n"
 
+(* Ruby code to return a dirent struct list. *)
+and generate_ruby_dirent_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, (`Char|`Int) ->
+       pr "    rb_hash_aset (rv, 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_%s_list (r);\n" typ;
+  pr "  return rv;\n"
+
 (* Generate Java bindings GuestFS.java file. *)
 and generate_java_java () =
   generate_header CStyle LGPLv2;
 (* Generate Java bindings GuestFS.java file. *)
 and generate_java_java () =
   generate_header CStyle LGPLv2;
@@ -5726,6 +7390,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.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.
 
 /**
  * The GuestFS object is a libguestfs handle.
@@ -5782,25 +7447,32 @@ public class GuestFS {
 
   List.iter (
     fun (name, style, _, flags, _, shortdesc, longdesc) ->
 
   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";
       generate_java_prototype ~public:true ~semicolon:false name style;
       pr "\n";
       pr "  {\n";
@@ -5842,6 +7514,7 @@ and generate_java_prototype ?(public=false) ?(privat=false) ?(native=false)
    | RStat _ -> pr "Stat ";
    | RStatVFS _ -> pr "StatVFS ";
    | RHashtable _ -> pr "HashMap<String,String> ";
    | RStat _ -> pr "Stat ";
    | RStatVFS _ -> pr "StatVFS ";
    | RHashtable _ -> pr "HashMap<String,String> ";
+   | RDirentList _ -> pr "Dirent[] ";
   );
 
   if native then pr "_%s " name else pr "%s " name;
   );
 
   if native then pr "_%s " name else pr "%s " name;
@@ -5897,6 +7570,7 @@ public class %s {
     | name, `UUID -> pr "  public String %s;\n" name
     | name, `Bytes
     | name, `Int -> pr "  public long %s;\n" name
     | name, `UUID -> pr "  public String %s;\n" name
     | name, `Bytes
     | name, `Int -> pr "  public long %s;\n" name
+    | name, `Char -> pr "  public char %s;\n" name
     | name, `OptPercent ->
        pr "  /* The next field is [0..100] or -1 meaning 'not present': */\n";
        pr "  public float %s;\n" name
     | name, `OptPercent ->
        pr "  /* The next field is [0..100] or -1 meaning 'not present': */\n";
        pr "  public float %s;\n" name
@@ -5963,7 +7637,7 @@ Java_com_redhat_et_libguestfs_GuestFS__1close
        | RConstString _ | RString _ -> pr "jstring ";
        | RIntBool _ | RStat _ | RStatVFS _ | RHashtable _ ->
           pr "jobject ";
        | RConstString _ | RString _ -> pr "jstring ";
        | RIntBool _ | RStat _ | RStatVFS _ | RHashtable _ ->
           pr "jobject ";
-       | RStringList _ | RPVList _ | RVGList _ | RLVList _ ->
+       | RStringList _ | RPVList _ | RVGList _ | RLVList _ | RDirentList _ ->
           pr "jobjectArray ";
       );
       pr "JNICALL\n";
           pr "jobjectArray ";
       );
       pr "JNICALL\n";
@@ -6037,7 +7711,13 @@ Java_com_redhat_et_libguestfs_GuestFS__1close
            pr "  jfieldID fl;\n";
            pr "  jobject jfl;\n";
            pr "  struct guestfs_lvm_lv_list *r;\n"; "NULL", "NULL"
            pr "  jfieldID fl;\n";
            pr "  jobject jfl;\n";
            pr "  struct guestfs_lvm_lv_list *r;\n"; "NULL", "NULL"
-       | RHashtable _ -> pr "  char **r;\n"; "NULL", "NULL" in
+       | RHashtable _ -> pr "  char **r;\n"; "NULL", "NULL"
+       | RDirentList _ ->
+           pr "  jobjectArray jr;\n";
+           pr "  jclass cl;\n";
+           pr "  jfieldID fl;\n";
+           pr "  jobject jfl;\n";
+           pr "  struct guestfs_dirent_list *r;\n"; "NULL", "NULL" in
       List.iter (
        function
        | String n
       List.iter (
        function
        | String n
@@ -6055,8 +7735,9 @@ Java_com_redhat_et_libguestfs_GuestFS__1close
 
       let needs_i =
        (match fst style with
 
       let needs_i =
        (match fst style with
-        | RStringList _ | RPVList _ | RVGList _ | RLVList _ -> true
-        | RErr _ | RBool _ | RInt _ | RInt64 _ | RConstString _
+        | RStringList _ | RPVList _ | RVGList _ | RLVList _
+        | RDirentList _ -> true
+        | RErr | RBool _ | RInt _ | RInt64 _ | RConstString _
         | RString _ | RIntBool _ | RStat _ | RStatVFS _
         | RHashtable _ -> false) ||
        List.exists (function StringList _ -> true | _ -> false) (snd style) in
         | RString _ | RIntBool _ | RStat _ | RStatVFS _
         | RHashtable _ -> false) ||
        List.exists (function StringList _ -> true | _ -> false) (snd style) in
@@ -6069,13 +7750,17 @@ Java_com_redhat_et_libguestfs_GuestFS__1close
       List.iter (
        function
        | String n
       List.iter (
        function
        | String n
-       | OptString n
        | FileIn n
        | FileOut n ->
            pr "  %s = (*env)->GetStringUTFChars (env, j%s, NULL);\n" n 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;
        | StringList n ->
            pr "  %s_len = (*env)->GetArrayLength (env, j%s);\n" n n;
-           pr "  %s = malloc (sizeof (char *) * (%s_len+1));\n" n n;
+           pr "  %s = guestfs_safe_malloc (g, sizeof (char *) * (%s_len+1));\n" n n;
            pr "  for (i = 0; i < %s_len; ++i) {\n" n;
            pr "    jobject o = (*env)->GetObjectArrayElement (env, j%s, i);\n"
              n;
            pr "  for (i = 0; i < %s_len; ++i) {\n" n;
            pr "    jobject o = (*env)->GetObjectArrayElement (env, j%s, i);\n"
              n;
@@ -6096,10 +7781,12 @@ Java_com_redhat_et_libguestfs_GuestFS__1close
       List.iter (
        function
        | String n
       List.iter (
        function
        | String n
-       | OptString n
        | FileIn n
        | FileOut n ->
            pr "  (*env)->ReleaseStringUTFChars (env, j%s, %s);\n" n 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"
        | StringList n ->
            pr "  for (i = 0; i < %s_len; ++i) {\n" n;
            pr "    jobject o = (*env)->GetObjectArrayElement (env, j%s, i);\n"
@@ -6183,6 +7870,8 @@ Java_com_redhat_et_libguestfs_GuestFS__1close
           (* XXX *)
           pr "  throw_exception (env, \"%s: internal error: please let us know how to make a Java HashMap from JNI bindings!\");\n" name;
           pr "  return NULL;\n"
           (* XXX *)
           pr "  throw_exception (env, \"%s: internal error: please let us know how to make a Java HashMap from JNI bindings!\");\n" name;
           pr "  return NULL;\n"
+       | RDirentList _ ->
+          generate_java_dirent_return "dirent" "Dirent" dirent_cols
       );
 
       pr "}\n";
       );
 
       pr "}\n";
@@ -6219,14 +7908,696 @@ and generate_java_lvm_return typ jtyp cols =
   pr "  guestfs_free_lvm_%s_list (r);\n" typ;
   pr "  return jr;\n"
 
   pr "  guestfs_free_lvm_%s_list (r);\n" typ;
   pr "  return jr;\n"
 
+and generate_java_dirent_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 ->
+       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, (`Char|`Int) ->
+       pr "    fl = (*env)->GetFieldID (env, cl, \"%s\", \"J\");\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_%s_list (r);\n" typ;
+  pr "  return jr;\n"
+
+and generate_haskell_hs () =
+  generate_header HaskellStyle LGPLv2;
+
+  (* XXX We only know how to generate partial FFI for Haskell
+   * at the moment.  Please help out!
+   *)
+  let can_generate style =
+    match style with
+    | RErr, _
+    | RInt _, _
+    | RInt64 _, _ -> true
+    | RBool _, _
+    | RConstString _, _
+    | RString _, _
+    | RStringList _, _
+    | RIntBool _, _
+    | RPVList _, _
+    | RVGList _, _
+    | RLVList _, _
+    | RStat _, _
+    | RStatVFS _, _
+    | RHashtable _, _
+    | RDirentList _, _ -> false in
+
+  pr "\
+{-# INCLUDE <guestfs.h> #-}
+{-# LANGUAGE ForeignFunctionInterface #-}
+
+module Guestfs (
+  create";
+
+  (* List out the names of the actions we want to export. *)
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      if can_generate style then pr ",\n  %s" name
+  ) all_functions;
+
+  pr "
+  ) where
+import Foreign
+import Foreign.C
+import Foreign.C.Types
+import IO
+import Control.Exception
+import Data.Typeable
+
+data GuestfsS = GuestfsS            -- represents the opaque C struct
+type GuestfsP = Ptr GuestfsS        -- guestfs_h *
+type GuestfsH = ForeignPtr GuestfsS -- guestfs_h * with attached finalizer
+
+-- XXX define properly later XXX
+data PV = PV
+data VG = VG
+data LV = LV
+data IntBool = IntBool
+data Stat = Stat
+data StatVFS = StatVFS
+data Hashtable = Hashtable
+
+foreign import ccall unsafe \"guestfs_create\" c_create
+  :: IO GuestfsP
+foreign import ccall unsafe \"&guestfs_close\" c_close
+  :: FunPtr (GuestfsP -> IO ())
+foreign import ccall unsafe \"guestfs_set_error_handler\" c_set_error_handler
+  :: GuestfsP -> Ptr CInt -> Ptr CInt -> IO ()
+
+create :: IO GuestfsH
+create = do
+  p <- c_create
+  c_set_error_handler p nullPtr nullPtr
+  h <- newForeignPtr c_close p
+  return h
+
+foreign import ccall unsafe \"guestfs_last_error\" c_last_error
+  :: GuestfsP -> IO CString
+
+-- last_error :: GuestfsH -> IO (Maybe String)
+-- last_error h = do
+--   str <- withForeignPtr h (\\p -> c_last_error p)
+--   maybePeek peekCString str
+
+last_error :: GuestfsH -> IO (String)
+last_error h = do
+  str <- withForeignPtr h (\\p -> c_last_error p)
+  if (str == nullPtr)
+    then return \"no error\"
+    else peekCString str
+
+";
+
+  (* Generate wrappers for each foreign function. *)
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      if can_generate style then (
+       pr "foreign import ccall unsafe \"guestfs_%s\" c_%s\n" name name;
+       pr "  :: ";
+       generate_haskell_prototype ~handle:"GuestfsP" style;
+       pr "\n";
+       pr "\n";
+       pr "%s :: " name;
+       generate_haskell_prototype ~handle:"GuestfsH" ~hs:true style;
+       pr "\n";
+       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
+         | FileOut n
+         | 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 _ | 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" :: 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 _ | RDirentList _ ->
+            pr "  if (r == nullPtr)\n";
+            pr "    then do\n";
+            pr "      err <- last_error h\n";
+            pr "      fail err\n";
+       );
+       (match fst style with
+        | RErr ->
+            pr "    else return ()\n"
+        | RInt _ ->
+            pr "    else return (fromIntegral r)\n"
+        | RInt64 _ ->
+            pr "    else return (fromIntegral r)\n"
+        | RBool _ ->
+            pr "    else return (toBool r)\n"
+        | RConstString _
+        | RString _
+        | RStringList _
+        | RIntBool _
+        | RPVList _
+        | RVGList _
+        | RLVList _
+        | RStat _
+        | RStatVFS _
+        | RHashtable _
+        | RDirentList _ ->
+            pr "    else return ()\n" (* XXXXXXXXXXXXXXXXXXXX *)
+       );
+       pr "\n";
+      )
+  ) all_functions
+
+and generate_haskell_prototype ~handle ?(hs = false) style =
+  pr "%s -> " handle;
+  let string = if hs then "String" else "CString" in
+  let int = if hs then "Int" else "CInt" in
+  let bool = if hs then "Bool" else "CInt" in
+  let int64 = if hs then "Integer" else "Int64" in
+  List.iter (
+    fun arg ->
+      (match arg with
+       | String _ -> pr "%s" string
+       | OptString _ -> if hs then pr "Maybe String" else pr "CString"
+       | StringList _ -> if hs then pr "[String]" else pr "Ptr CString"
+       | Bool _ -> pr "%s" bool
+       | Int _ -> pr "%s" int
+       | FileIn _ -> pr "%s" string
+       | FileOut _ -> pr "%s" string
+      );
+      pr " -> ";
+  ) (snd style);
+  pr "IO (";
+  (match fst style with
+   | RErr -> if not hs then pr "CInt"
+   | RInt _ -> pr "%s" int
+   | RInt64 _ -> pr "%s" int64
+   | RBool _ -> pr "%s" bool
+   | 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"
+   | RHashtable _ -> pr "Hashtable"
+   | RDirentList _ -> pr "[Dirent]"
+  );
+  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
+
+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 = malloc ((n+1) * sizeof (char *));\n";
+            pr "  for (i = 0; i < n; ++i) {\n";
+            pr "    strs[i] = malloc (16);\n";
+            pr "    snprintf (strs[i], 16, \"%%d\", i);\n";
+            pr "  }\n";
+            pr "  strs[n] = NULL;\n";
+            pr "  return strs;\n"
+        | RIntBool _ ->
+            pr "  struct guestfs_int_bool *r;\n";
+            pr "  r = malloc (sizeof *r);\n";
+            pr "  sscanf (val, \"%%\" SCNi32, &r->i);\n";
+            pr "  r->b = 0;\n";
+            pr "  return r;\n"
+        | RPVList _ ->
+            pr "  struct guestfs_lvm_pv_list *r;\n";
+            pr "  int i;\n";
+            pr "  r = malloc (sizeof *r);\n";
+            pr "  sscanf (val, \"%%d\", &r->len);\n";
+            pr "  r->val = calloc (r->len, sizeof *r->val);\n";
+            pr "  for (i = 0; i < r->len; ++i) {\n";
+            pr "    r->val[i].pv_name = malloc (16);\n";
+            pr "    snprintf (r->val[i].pv_name, 16, \"%%d\", i);\n";
+            pr "  }\n";
+            pr "  return r;\n"
+        | RVGList _ ->
+            pr "  struct guestfs_lvm_vg_list *r;\n";
+            pr "  int i;\n";
+            pr "  r = malloc (sizeof *r);\n";
+            pr "  sscanf (val, \"%%d\", &r->len);\n";
+            pr "  r->val = calloc (r->len, sizeof *r->val);\n";
+            pr "  for (i = 0; i < r->len; ++i) {\n";
+            pr "    r->val[i].vg_name = malloc (16);\n";
+            pr "    snprintf (r->val[i].vg_name, 16, \"%%d\", i);\n";
+            pr "  }\n";
+            pr "  return r;\n"
+        | RLVList _ ->
+            pr "  struct guestfs_lvm_lv_list *r;\n";
+            pr "  int i;\n";
+            pr "  r = malloc (sizeof *r);\n";
+            pr "  sscanf (val, \"%%d\", &r->len);\n";
+            pr "  r->val = calloc (r->len, sizeof *r->val);\n";
+            pr "  for (i = 0; i < r->len; ++i) {\n";
+            pr "    r->val[i].lv_name = malloc (16);\n";
+            pr "    snprintf (r->val[i].lv_name, 16, \"%%d\", i);\n";
+            pr "  }\n";
+            pr "  return r;\n"
+        | RStat _ ->
+            pr "  struct guestfs_stat *r;\n";
+            pr "  r = calloc (1, sizeof (*r));\n";
+            pr "  sscanf (val, \"%%\" SCNi64, &r->dev);\n";
+            pr "  return r;\n"
+        | RStatVFS _ ->
+            pr "  struct guestfs_statvfs *r;\n";
+            pr "  r = calloc (1, sizeof (*r));\n";
+            pr "  sscanf (val, \"%%\" SCNi64, &r->bsize);\n";
+            pr "  return r;\n"
+        | RHashtable _ ->
+            pr "  char **strs;\n";
+            pr "  int n, i;\n";
+            pr "  sscanf (val, \"%%d\", &n);\n";
+            pr "  strs = malloc ((n*2+1) * sizeof (*strs));\n";
+            pr "  for (i = 0; i < n; ++i) {\n";
+            pr "    strs[i*2] = malloc (16);\n";
+            pr "    strs[i*2+1] = malloc (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"
+        | RDirentList _ ->
+            pr "  struct guestfs_dirent_list *r;\n";
+            pr "  int i;\n";
+            pr "  r = malloc (sizeof *r);\n";
+            pr "  sscanf (val, \"%%d\", &r->len);\n";
+            pr "  r->val = calloc (r->len, sizeof *r->val);\n";
+            pr "  for (i = 0; i < r->len; ++i)\n";
+            pr "    r->val[i].ino = i;\n";
+            pr "  return r;\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 _ | RIntBool _
+        | RPVList _ | RVGList _ | RLVList _ | RStat _ | RStatVFS _
+        | RHashtable _
+        | RDirentList _ ->
+            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;
   let close () =
     close_out !chan;
     chan := stdout;
 let output_to filename =
   let filename_new = filename ^ ".new" in
   chan := open_out filename_new;
   let close () =
     close_out !chan;
     chan := stdout;
-    Unix.rename filename_new filename;
-    printf "written %s\n%!" filename;
+
+    (* Is the new file different from the current file? *)
+    if Sys.file_exists filename && files_equal filename filename_new then
+      Unix.unlink filename_new         (* same, so skip it *)
+    else (
+      (* different, overwrite old one *)
+      (try Unix.chmod filename 0o644 with Unix.Unix_error _ -> ());
+      Unix.rename filename_new filename;
+      Unix.chmod filename 0o444;
+      printf "written %s\n%!" filename;
+    )
   in
   close
 
   in
   close
 
@@ -6267,10 +8638,18 @@ Run it from the top source directory using the command
   generate_daemon_actions ();
   close ();
 
   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 ();
 
   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 ();
   let close = output_to "fish/cmds.c" in
   generate_fish_cmds ();
   close ();
@@ -6303,6 +8682,10 @@ Run it from the top source directory using the command
   generate_ocaml_c ();
   close ();
 
   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 ();
   let close = output_to "perl/Guestfs.xs" in
   generate_perl_xs ();
   close ();
@@ -6311,6 +8694,10 @@ Run it from the top source directory using the command
   generate_perl_pm ();
   close ();
 
   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 ();
   let close = output_to "python/guestfs-py.c" in
   generate_python_c ();
   close ();
@@ -6319,10 +8706,18 @@ Run it from the top source directory using the command
   generate_python_py ();
   close ();
 
   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/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/GuestFS.java" in
   generate_java_java ();
   close ();
@@ -6347,6 +8742,33 @@ Run it from the top source directory using the command
   generate_java_struct "StatVFS" statvfs_cols;
   close ();
 
   generate_java_struct "StatVFS" statvfs_cols;
   close ();
 
+  let close = output_to "java/com/redhat/et/libguestfs/Dirent.java" in
+  generate_java_struct "Dirent" dirent_cols;
+  close ();
+
   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_GuestFS.c" in
   generate_java_c ();
   close ();
+
+  let close = output_to "java/Bindtests.java" in
+  generate_java_bindtests ();
+  close ();
+
+  let close = output_to "haskell/Guestfs.hs" in
+  generate_haskell_hs ();
+  close ();
+
+  let close = output_to "haskell/Bindtests.hs" in
+  generate_haskell_bindtests ();
+  close ();
+
+  let close = output_to "src/MAX_PROC_NR" in
+  generate_max_proc_nr ();
+  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