Added stat, lstat, statvfs and associated stat structures.
[libguestfs.git] / src / generator.ml
index 067ac90..c32b0ed 100755 (executable)
  *)
 
 (* This script generates a large amount of code and documentation for
  *)
 
 (* This script generates a large amount of code and documentation for
- * all the daemon actions.  To add a new action there are only two
- * files you need to change, this one to describe the interface, and
+ * all the daemon actions.
+ *
+ * To add a new action there are only two files you need to change,
+ * this one to describe the interface (see the big table below), and
  * daemon/<somefile>.c to write the implementation.
  *
  * After editing this file, run it (./src/generator.ml) to regenerate
  * daemon/<somefile>.c to write the implementation.
  *
  * After editing this file, run it (./src/generator.ml) to regenerate
@@ -63,19 +65,127 @@ and ret =
   | RPVList of string
   | RVGList of string
   | RLVList of string
   | RPVList of string
   | RVGList of string
   | RLVList of string
+    (* Stat buffers. *)
+  | RStat of string
+  | RStatVFS of string
 and args = argt list   (* Function parameters, guestfs handle is implicit. *)
 and args = argt list   (* Function parameters, guestfs handle is implicit. *)
+
+    (* Note in future we should allow a "variable args" parameter as
+     * the final parameter, to allow commands like
+     *   chmod mode file [file(s)...]
+     * This is not implemented yet, but many commands (such as chmod)
+     * are currently defined with the argument order keeping this future
+     * possibility in mind.
+     *)
 and argt =
   | String of string   (* const char *name, cannot be NULL *)
   | OptString of string        (* const char *name, may be NULL *)
 and argt =
   | String of string   (* const char *name, cannot be NULL *)
   | OptString of string        (* const char *name, may be NULL *)
+  | StringList of string(* list of strings (each string cannot be NULL) *)
   | Bool of string     (* boolean *)
   | Int of string      (* int (smallish ints, signed, <= 31 bits) *)
 
 type flags =
   | ProtocolLimitWarning  (* display warning about protocol size limits *)
   | Bool of string     (* boolean *)
   | Int of string      (* int (smallish ints, signed, <= 31 bits) *)
 
 type flags =
   | ProtocolLimitWarning  (* display warning about protocol size limits *)
+  | DangerWillRobinson   (* flags particularly dangerous commands *)
   | 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 *)
 
+let protocol_limit_warning =
+  "Because of the message protocol, there is a transfer limit 
+of somewhere between 2MB and 4MB.  To transfer large files you should use
+FTP."
+
+let danger_will_robinson =
+  "B<This command is dangerous.  Without careful use you
+can easily destroy all your data>."
+
+(* You can supply zero or as many tests as you want per API call.
+ *
+ * Note that the test environment has 3 block devices, of size 500MB,
+ * 50MB and 10MB (respectively /dev/sda, /dev/sdb, /dev/sdc).
+ * Note for partitioning purposes, the 500MB device has 63 cylinders.
+ *
+ * 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).
+ *
+ * Don't assume anything about the previous contents of the block
+ * devices.  Use 'Init*' to create some initial scenarios.
+ *)
+type tests = (test_init * test) list
+and test =
+    (* Run the command sequence and just expect nothing to fail. *)
+  | TestRun of seq
+    (* Run the command sequence and expect the output of the final
+     * command to be the string.
+     *)
+  | TestOutput of seq * string
+    (* Run the command sequence and expect the output of the final
+     * command to be the list of strings.
+     *)
+  | TestOutputList of seq * string list
+    (* Run the command sequence and expect the output of the final
+     * command to be the integer.
+     *)
+  | TestOutputInt of seq * int
+    (* Run the command sequence and expect the output of the final
+     * command to be a true value (!= 0 or != NULL).
+     *)
+  | TestOutputTrue of seq
+    (* Run the command sequence and expect the output of the final
+     * command to be a false value (== 0 or == NULL, but not an error).
+     *)
+  | TestOutputFalse of seq
+    (* Run the command sequence and expect the output of the final
+     * command to be a list of the given length (but don't care about
+     * content).
+     *)
+  | TestOutputLength of seq * int
+    (* Run the command sequence and expect the output of the final
+     * command to be a structure.
+     *)
+  | TestOutputStruct of seq * test_field_compare list
+    (* Run the command sequence and expect the final command (only)
+     * to fail.
+     *)
+  | TestLastFail of seq
+
+and test_field_compare =
+  | CompareWithInt of string * int
+  | CompareWithString of string * string
+  | CompareFieldsIntEq of string * string
+  | CompareFieldsStrEq of string * string
+
+(* Some initial scenarios for testing. *)
+and test_init =
+    (* Do nothing, block devices could contain random stuff including
+     * LVM PVs, and some filesystems might be mounted.  This is usually
+     * a bad idea.
+     *)
+  | InitNone
+    (* Block devices are empty and no filesystems are mounted. *)
+  | InitEmpty
+    (* /dev/sda contains a single partition /dev/sda1, which is formatted
+     * as ext2, empty [except for lost+found] and mounted on /.
+     * /dev/sdb and /dev/sdc may have random content.
+     * No LVM.
+     *)
+  | InitBasicFS
+    (* /dev/sda:
+     *   /dev/sda1 (is a PV):
+     *     /dev/VG/LV (size 8MB):
+     *       formatted as ext2, empty [except for lost+found], mounted on /
+     * /dev/sdb and /dev/sdc may have random content.
+     *)
+  | InitBasicFSonLVM
+
+(* Sequence of commands for testing. *)
+and seq = cmd list
+and cmd = string list
+
 (* Note about long descriptions: When referring to another
  * action, use the format C<guestfs_other> (ie. the full name of
  * the C function).  This will be replaced as appropriate in other
 (* Note about long descriptions: When referring to another
  * action, use the format C<guestfs_other> (ie. the full name of
  * the C function).  This will be replaced as appropriate in other
@@ -86,6 +196,7 @@ type flags =
 
 let non_daemon_functions = [
   ("launch", (RErr, []), -1, [FishAlias "run"; FishAction "launch"],
 
 let non_daemon_functions = [
   ("launch", (RErr, []), -1, [FishAlias "run"; FishAction "launch"],
+   [],
    "launch the qemu subprocess",
    "\
 Internally libguestfs is implemented by running a virtual machine
    "launch the qemu subprocess",
    "\
 Internally libguestfs is implemented by running a virtual machine
@@ -95,6 +206,7 @@ You should call this after configuring the handle
 (eg. adding drives) but before performing any actions.");
 
   ("wait_ready", (RErr, []), -1, [NotInFish],
 (eg. adding drives) but before performing any actions.");
 
   ("wait_ready", (RErr, []), -1, [NotInFish],
+   [],
    "wait until the qemu subprocess launches",
    "\
 Internally libguestfs is implemented by running a virtual machine
    "wait until the qemu subprocess launches",
    "\
 Internally libguestfs is implemented by running a virtual machine
@@ -104,11 +216,13 @@ You should call this after C<guestfs_launch> to wait for the launch
 to complete.");
 
   ("kill_subprocess", (RErr, []), -1, [],
 to complete.");
 
   ("kill_subprocess", (RErr, []), -1, [],
+   [],
    "kill the qemu subprocess",
    "\
 This kills the qemu subprocess.  You should never need to call this.");
 
   ("add_drive", (RErr, [String "filename"]), -1, [FishAlias "add"],
    "kill the qemu subprocess",
    "\
 This kills the qemu subprocess.  You should never need to call this.");
 
   ("add_drive", (RErr, [String "filename"]), -1, [FishAlias "add"],
+   [],
    "add an image to examine or modify",
    "\
 This function adds a virtual machine disk image C<filename> to the
    "add an image to examine or modify",
    "\
 This function adds a virtual machine disk image C<filename> to the
@@ -125,6 +239,7 @@ image).
 This is equivalent to the qemu parameter C<-drive file=filename>.");
 
   ("add_cdrom", (RErr, [String "filename"]), -1, [FishAlias "cdrom"],
 This is equivalent to the qemu parameter C<-drive file=filename>.");
 
   ("add_cdrom", (RErr, [String "filename"]), -1, [FishAlias "cdrom"],
+   [],
    "add a CD-ROM disk image to examine",
    "\
 This function adds a virtual CD-ROM disk image to the guest.
    "add a CD-ROM disk image to examine",
    "\
 This function adds a virtual CD-ROM disk image to the guest.
@@ -132,6 +247,7 @@ This function adds a virtual CD-ROM disk image to the guest.
 This is equivalent to the qemu parameter C<-cdrom filename>.");
 
   ("config", (RErr, [String "qemuparam"; OptString "qemuvalue"]), -1, [],
 This is equivalent to the qemu parameter C<-cdrom filename>.");
 
   ("config", (RErr, [String "qemuparam"; OptString "qemuvalue"]), -1, [],
+   [],
    "add qemu parameters",
    "\
 This can be used to add arbitrary qemu command line parameters
    "add qemu parameters",
    "\
 This can be used to add arbitrary qemu command line parameters
@@ -144,6 +260,7 @@ The first character of C<param> string must be a C<-> (dash).
 C<value> can be NULL.");
 
   ("set_path", (RErr, [String "path"]), -1, [FishAlias "path"],
 C<value> can be NULL.");
 
   ("set_path", (RErr, [String "path"]), -1, [FishAlias "path"],
+   [],
    "set the search path",
    "\
 Set the path that libguestfs searches for kernel and initrd.img.
    "set the search path",
    "\
 Set the path that libguestfs searches for kernel and initrd.img.
@@ -157,6 +274,7 @@ 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, [],
+   [],
    "get the search path",
    "\
 Return the current search path.
    "get the search path",
    "\
 Return the current search path.
@@ -165,6 +283,7 @@ This is always non-NULL.  If it wasn't set already, then this will
 return the default path.");
 
   ("set_autosync", (RErr, [Bool "autosync"]), -1, [FishAlias "autosync"],
 return the default path.");
 
   ("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 mode",
    "\
 If C<autosync> is true, this enables autosync.  Libguestfs will make a
@@ -172,11 +291,13 @@ best effort attempt to run C<guestfs_sync> when the handle is closed
 (also if the program exits without closing handles).");
 
   ("get_autosync", (RBool "autosync", []), -1, [],
 (also if the program exits without closing handles).");
 
   ("get_autosync", (RBool "autosync", []), -1, [],
+   [],
    "get autosync mode",
    "\
 Get the autosync flag.");
 
   ("set_verbose", (RErr, [Bool "verbose"]), -1, [FishAlias "verbose"],
    "get autosync mode",
    "\
 Get the autosync flag.");
 
   ("set_verbose", (RErr, [Bool "verbose"]), -1, [FishAlias "verbose"],
+   [],
    "set verbose mode",
    "\
 If C<verbose> is true, this turns on verbose messages (to C<stderr>).
    "set verbose mode",
    "\
 If C<verbose> is true, this turns on verbose messages (to C<stderr>).
@@ -185,6 +306,7 @@ Verbose messages are disabled unless the environment variable
 C<LIBGUESTFS_DEBUG> is defined and set to C<1>.");
 
   ("get_verbose", (RBool "verbose", []), -1, [],
 C<LIBGUESTFS_DEBUG> is defined and set to C<1>.");
 
   ("get_verbose", (RBool "verbose", []), -1, [],
+   [],
    "get verbose mode",
    "\
 This returns the verbose messages flag.")
    "get verbose mode",
    "\
 This returns the verbose messages flag.")
@@ -192,6 +314,12 @@ This returns the verbose messages flag.")
 
 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"; ","];
+       ["mkfs"; "ext2"; "/dev/sda1"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["write_file"; "/new"; "new file contents"; "0"];
+       ["cat"; "/new"]], "new file contents")],
    "mount a guest disk at a position in the filesystem",
    "\
 Mount a guest disk at a position in the filesystem.  Block devices
    "mount a guest disk at a position in the filesystem",
    "\
 Mount a guest disk at a position in the filesystem.  Block devices
@@ -212,6 +340,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"]]],
    "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
@@ -221,6 +350,9 @@ 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 (
+      [["touch"; "/new"];
+       ["exists"; "/new"]])],
    "update file timestamps or create a new file",
    "\
 Touch acts like the L<touch(1)> command.  It can be used to
    "update file timestamps or create a new file",
    "\
 Touch acts like the L<touch(1)> command.  It can be used to
@@ -228,6 +360,9 @@ 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 (
+      [["write_file"; "/new"; "new file contents"; "0"];
+       ["cat"; "/new"]], "new file contents")],
    "list the contents of a file",
    "\
 Return the contents of the file named C<path>.
    "list the contents of a file",
    "\
 Return the contents of the file named C<path>.
@@ -238,6 +373,9 @@ as end of string).  For those you need to use the C<guestfs_read_file>
 function which has a more complex interface.");
 
   ("ll", (RString "listing", [String "directory"]), 5, [],
 function which has a more complex interface.");
 
   ("ll", (RString "listing", [String "directory"]), 5, [],
+   [], (* XXX Tricky to test because it depends on the exact format
+       * of the 'ls -l' command, which changes between F10 and F11.
+       *)
    "list the files in a directory (long format)",
    "\
 List the files in C<directory> (relative to the root directory,
    "list the files in a directory (long format)",
    "\
 List the files in C<directory> (relative to the root directory,
@@ -247,6 +385,11 @@ 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 (
+      [["touch"; "/new"];
+       ["touch"; "/newer"];
+       ["touch"; "/newest"];
+       ["ls"; "/"]], ["lost+found"; "new"; "newer"; "newest"])],
    "list the files in a directory",
    "\
 List the files in C<directory> (relative to the root directory,
    "list the files in a directory",
    "\
 List the files in C<directory> (relative to the root directory,
@@ -257,6 +400,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"])],
    "list the block devices",
    "\
 List all the block devices.
    "list the block devices",
    "\
 List all the block devices.
@@ -264,6 +409,11 @@ 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 (
+      [["list_partitions"]], ["/dev/sda1"]);
+    InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+       ["list_partitions"]], ["/dev/sda1"; "/dev/sda2"; "/dev/sda3"])],
    "list the partitions",
    "\
 List all the partitions detected on all block devices.
    "list the partitions",
    "\
 List all the partitions detected on all block devices.
@@ -274,6 +424,14 @@ 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 (
+      [["pvs"]], ["/dev/sda1"]);
+    InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+       ["pvcreate"; "/dev/sda1"];
+       ["pvcreate"; "/dev/sda2"];
+       ["pvcreate"; "/dev/sda3"];
+       ["pvs"]], ["/dev/sda1"; "/dev/sda2"; "/dev/sda3"])],
    "list the LVM physical volumes (PVs)",
    "\
 List all the physical volumes detected.  This is the equivalent
    "list the LVM physical volumes (PVs)",
    "\
 List all the physical volumes detected.  This is the equivalent
@@ -285,6 +443,16 @@ 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 (
+      [["vgs"]], ["VG"]);
+    InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+       ["pvcreate"; "/dev/sda1"];
+       ["pvcreate"; "/dev/sda2"];
+       ["pvcreate"; "/dev/sda3"];
+       ["vgcreate"; "VG1"; "/dev/sda1 /dev/sda2"];
+       ["vgcreate"; "VG2"; "/dev/sda3"];
+       ["vgs"]], ["VG1"; "VG2"])],
    "list the LVM volume groups (VGs)",
    "\
 List all the volumes groups detected.  This is the equivalent
    "list the LVM volume groups (VGs)",
    "\
 List all the volumes groups detected.  This is the equivalent
@@ -296,6 +464,19 @@ 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 (
+      [["lvs"]], ["/dev/VG/LV"]);
+    InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+       ["pvcreate"; "/dev/sda1"];
+       ["pvcreate"; "/dev/sda2"];
+       ["pvcreate"; "/dev/sda3"];
+       ["vgcreate"; "VG1"; "/dev/sda1 /dev/sda2"];
+       ["vgcreate"; "VG2"; "/dev/sda3"];
+       ["lvcreate"; "LV1"; "VG1"; "50"];
+       ["lvcreate"; "LV2"; "VG1"; "50"];
+       ["lvcreate"; "LV3"; "VG2"; "50"];
+       ["lvs"]], ["/dev/VG1/LV1"; "/dev/VG1/LV2"; "/dev/VG2/LV3"])],
    "list the LVM logical volumes (LVs)",
    "\
 List all the logical volumes detected.  This is the equivalent
    "list the LVM logical volumes (LVs)",
    "\
 List all the logical volumes detected.  This is the equivalent
@@ -307,24 +488,33 @@ This returns a list of the logical volume device names
 See also C<guestfs_lvs_full>.");
 
   ("pvs_full", (RPVList "physvols", []), 12, [],
 See also C<guestfs_lvs_full>.");
 
   ("pvs_full", (RPVList "physvols", []), 12, [],
+   [], (* XXX how to test? *)
    "list the LVM physical volumes (PVs)",
    "\
 List all the physical volumes detected.  This is the equivalent
 of the L<pvs(8)> command.  The \"full\" version includes all fields.");
 
   ("vgs_full", (RVGList "volgroups", []), 13, [],
    "list the LVM physical volumes (PVs)",
    "\
 List all the physical volumes detected.  This is the equivalent
 of the L<pvs(8)> command.  The \"full\" version includes all fields.");
 
   ("vgs_full", (RVGList "volgroups", []), 13, [],
+   [], (* XXX how to test? *)
    "list the LVM volume groups (VGs)",
    "\
 List all the volumes groups detected.  This is the equivalent
 of the L<vgs(8)> command.  The \"full\" version includes all fields.");
 
   ("lvs_full", (RLVList "logvols", []), 14, [],
    "list the LVM volume groups (VGs)",
    "\
 List all the volumes groups detected.  This is the equivalent
 of the L<vgs(8)> command.  The \"full\" version includes all fields.");
 
   ("lvs_full", (RLVList "logvols", []), 14, [],
+   [], (* XXX how to test? *)
    "list the LVM logical volumes (LVs)",
    "\
 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, [],
    "list the LVM logical volumes (LVs)",
    "\
 List all the logical volumes detected.  This is the equivalent
 of the L<lvs(8)> command.  The \"full\" version includes all fields.");
 
   ("read_lines", (RStringList "lines", [String "path"]), 15, [],
+   [InitBasicFS, TestOutputList (
+      [["write_file"; "/new"; "line1\r\nline2\nline3"; "0"];
+       ["read_lines"; "/new"]], ["line1"; "line2"; "line3"]);
+    InitBasicFS, TestOutputList (
+      [["write_file"; "/new"; ""; "0"];
+       ["read_lines"; "/new"]], [])],
    "read file as lines",
    "\
 Return the contents of the file named C<path>.
    "read file as lines",
    "\
 Return the contents of the file named C<path>.
@@ -338,6 +528,7 @@ as end of line).  For those you need to use the C<guestfs_read_file>
 function which has a more complex interface.");
 
   ("aug_init", (RErr, [String "root"; Int "flags"]), 16, [],
 function which has a more complex interface.");
 
   ("aug_init", (RErr, [String "root"; Int "flags"]), 16, [],
+   [], (* XXX Augeas code needs tests. *)
    "create a new Augeas handle",
    "\
 Create a new Augeas handle for editing configuration files.
    "create a new Augeas handle",
    "\
 Create a new Augeas handle for editing configuration files.
@@ -388,6 +579,7 @@ To close the handle, you can call C<guestfs_aug_close>.
 To find out more about Augeas, see L<http://augeas.net/>.");
 
   ("aug_close", (RErr, []), 26, [],
 To find out more about Augeas, see L<http://augeas.net/>.");
 
   ("aug_close", (RErr, []), 26, [],
+   [], (* XXX Augeas code needs tests. *)
    "close the current Augeas handle",
    "\
 Close the current Augeas handle and free up any resources
    "close the current Augeas handle",
    "\
 Close the current Augeas handle and free up any resources
@@ -396,6 +588,7 @@ C<guestfs_aug_init> again before you can use any other
 Augeas functions.");
 
   ("aug_defvar", (RInt "nrnodes", [String "name"; OptString "expr"]), 17, [],
 Augeas functions.");
 
   ("aug_defvar", (RInt "nrnodes", [String "name"; OptString "expr"]), 17, [],
+   [], (* XXX Augeas code needs tests. *)
    "define an Augeas variable",
    "\
 Defines an Augeas variable C<name> whose value is the result
    "define an Augeas variable",
    "\
 Defines an Augeas variable C<name> whose value is the result
@@ -406,6 +599,7 @@ On success this returns the number of nodes in C<expr>, or
 C<0> if C<expr> evaluates to something which is not a nodeset.");
 
   ("aug_defnode", (RIntBool ("nrnodes", "created"), [String "name"; String "expr"; String "val"]), 18, [],
 C<0> if C<expr> evaluates to something which is not a nodeset.");
 
   ("aug_defnode", (RIntBool ("nrnodes", "created"), [String "name"; String "expr"; String "val"]), 18, [],
+   [], (* XXX Augeas code needs tests. *)
    "define an Augeas node",
    "\
 Defines a variable C<name> whose value is the result of
    "define an Augeas node",
    "\
 Defines a variable C<name> whose value is the result of
@@ -420,17 +614,20 @@ number of nodes in the nodeset, and a boolean flag
 if a node was created.");
 
   ("aug_get", (RString "val", [String "path"]), 19, [],
 if a node was created.");
 
   ("aug_get", (RString "val", [String "path"]), 19, [],
+   [], (* XXX Augeas code needs tests. *)
    "look up the value of an Augeas path",
    "\
 Look up the value associated with C<path>.  If C<path>
 matches exactly one node, the C<value> is returned.");
 
   ("aug_set", (RErr, [String "path"; String "val"]), 20, [],
    "look up the value of an Augeas path",
    "\
 Look up the value associated with C<path>.  If C<path>
 matches exactly one node, the C<value> is returned.");
 
   ("aug_set", (RErr, [String "path"; String "val"]), 20, [],
+   [], (* XXX Augeas code needs tests. *)
    "set Augeas path to value",
    "\
 Set the value associated with C<path> to C<value>.");
 
   ("aug_insert", (RErr, [String "path"; String "label"; Bool "before"]), 21, [],
    "set Augeas path to value",
    "\
 Set the value associated with C<path> to C<value>.");
 
   ("aug_insert", (RErr, [String "path"; String "label"; Bool "before"]), 21, [],
+   [], (* XXX Augeas code needs tests. *)
    "insert a sibling Augeas node",
    "\
 Create a new sibling C<label> for C<path>, inserting it into
    "insert a sibling Augeas node",
    "\
 Create a new sibling C<label> for C<path>, inserting it into
@@ -442,6 +639,7 @@ C<label> must be a label, ie. not contain C</>, C<*> or end
 with a bracketed index C<[N]>.");
 
   ("aug_rm", (RInt "nrnodes", [String "path"]), 22, [],
 with a bracketed index C<[N]>.");
 
   ("aug_rm", (RInt "nrnodes", [String "path"]), 22, [],
+   [], (* XXX Augeas code needs tests. *)
    "remove an Augeas path",
    "\
 Remove C<path> and all of its children.
    "remove an Augeas path",
    "\
 Remove C<path> and all of its children.
@@ -449,12 +647,14 @@ Remove C<path> and all of its children.
 On success this returns the number of entries which were removed.");
 
   ("aug_mv", (RErr, [String "src"; String "dest"]), 23, [],
 On success this returns the number of entries which were removed.");
 
   ("aug_mv", (RErr, [String "src"; String "dest"]), 23, [],
+   [], (* XXX Augeas code needs tests. *)
    "move Augeas node",
    "\
 Move the node C<src> to C<dest>.  C<src> must match exactly
 one node.  C<dest> is overwritten if it exists.");
 
   ("aug_match", (RStringList "matches", [String "path"]), 24, [],
    "move Augeas node",
    "\
 Move the node C<src> to C<dest>.  C<src> must match exactly
 one node.  C<dest> is overwritten if it exists.");
 
   ("aug_match", (RStringList "matches", [String "path"]), 24, [],
+   [], (* XXX Augeas code needs tests. *)
    "return Augeas nodes which match path",
    "\
 Returns a list of paths which match the path expression C<path>.
    "return Augeas nodes which match path",
    "\
 Returns a list of paths which match the path expression C<path>.
@@ -462,6 +662,7 @@ The returned paths are sufficiently qualified so that they match
 exactly one node in the current tree.");
 
   ("aug_save", (RErr, []), 25, [],
 exactly one node in the current tree.");
 
   ("aug_save", (RErr, []), 25, [],
+   [], (* XXX Augeas code needs tests. *)
    "write all pending Augeas changes to disk",
    "\
 This writes all pending changes to disk.
    "write all pending Augeas changes to disk",
    "\
 This writes all pending changes to disk.
@@ -470,6 +671,7 @@ The flags which were passed to C<guestfs_aug_init> affect exactly
 how files are saved.");
 
   ("aug_load", (RErr, []), 27, [],
 how files are saved.");
 
   ("aug_load", (RErr, []), 27, [],
+   [], (* XXX Augeas code needs tests. *)
    "load files into the tree",
    "\
 Load files into the tree.
    "load files into the tree",
    "\
 Load files into the tree.
@@ -478,11 +680,374 @@ See C<aug_load> in the Augeas documentation for the full gory
 details.");
 
   ("aug_ls", (RStringList "matches", [String "path"]), 28, [],
 details.");
 
   ("aug_ls", (RStringList "matches", [String "path"]), 28, [],
+   [], (* XXX Augeas code needs tests. *)
    "list Augeas nodes under a path",
    "\
 This is just a shortcut for listing C<guestfs_aug_match>
 C<path/*> and sorting the resulting nodes into alphabetical order.");
 
    "list Augeas nodes under a path",
    "\
 This is just a shortcut for listing C<guestfs_aug_match>
 C<path/*> and sorting the resulting nodes into alphabetical order.");
 
+  ("rm", (RErr, [String "path"]), 29, [],
+   [InitBasicFS, TestRun
+      [["touch"; "/new"];
+       ["rm"; "/new"]];
+    InitBasicFS, TestLastFail
+      [["rm"; "/new"]];
+    InitBasicFS, TestLastFail
+      [["mkdir"; "/new"];
+       ["rm"; "/new"]]],
+   "remove a file",
+   "\
+Remove the single file C<path>.");
+
+  ("rmdir", (RErr, [String "path"]), 30, [],
+   [InitBasicFS, TestRun
+      [["mkdir"; "/new"];
+       ["rmdir"; "/new"]];
+    InitBasicFS, TestLastFail
+      [["rmdir"; "/new"]];
+    InitBasicFS, TestLastFail
+      [["touch"; "/new"];
+       ["rmdir"; "/new"]]],
+   "remove a directory",
+   "\
+Remove the single directory C<path>.");
+
+  ("rm_rf", (RErr, [String "path"]), 31, [],
+   [InitBasicFS, TestOutputFalse
+      [["mkdir"; "/new"];
+       ["mkdir"; "/new/foo"];
+       ["touch"; "/new/foo/bar"];
+       ["rm_rf"; "/new"];
+       ["exists"; "/new"]]],
+   "remove a file or directory recursively",
+   "\
+Remove the file or directory C<path>, recursively removing the
+contents if its a directory.  This is like the C<rm -rf> shell
+command.");
+
+  ("mkdir", (RErr, [String "path"]), 32, [],
+   [InitBasicFS, TestOutputTrue
+      [["mkdir"; "/new"];
+       ["is_dir"; "/new"]];
+    InitBasicFS, TestLastFail
+      [["mkdir"; "/new/foo/bar"]]],
+   "create a directory",
+   "\
+Create a directory named C<path>.");
+
+  ("mkdir_p", (RErr, [String "path"]), 33, [],
+   [InitBasicFS, TestOutputTrue
+      [["mkdir_p"; "/new/foo/bar"];
+       ["is_dir"; "/new/foo/bar"]];
+    InitBasicFS, TestOutputTrue
+      [["mkdir_p"; "/new/foo/bar"];
+       ["is_dir"; "/new/foo"]];
+    InitBasicFS, TestOutputTrue
+      [["mkdir_p"; "/new/foo/bar"];
+       ["is_dir"; "/new"]]],
+   "create a directory and parents",
+   "\
+Create a directory named C<path>, creating any parent directories
+as necessary.  This is like the C<mkdir -p> shell command.");
+
+  ("chmod", (RErr, [Int "mode"; String "path"]), 34, [],
+   [], (* XXX Need stat command to test *)
+   "change file mode",
+   "\
+Change the mode (permissions) of C<path> to C<mode>.  Only
+numeric modes are supported.");
+
+  ("chown", (RErr, [Int "owner"; Int "group"; String "path"]), 35, [],
+   [], (* XXX Need stat command to test *)
+   "change file owner and group",
+   "\
+Change the file owner to C<owner> and group to C<group>.
+
+Only numeric uid and gid are supported.  If you want to use
+names, you will need to locate and parse the password file
+yourself (Augeas support makes this relatively easy).");
+
+  ("exists", (RBool "existsflag", [String "path"]), 36, [],
+   [InitBasicFS, TestOutputTrue (
+      [["touch"; "/new"];
+       ["exists"; "/new"]]);
+    InitBasicFS, TestOutputTrue (
+      [["mkdir"; "/new"];
+       ["exists"; "/new"]])],
+   "test if file or directory exists",
+   "\
+This returns C<true> if and only if there is a file, directory
+(or anything) with the given C<path> name.
+
+See also C<guestfs_is_file>, C<guestfs_is_dir>, C<guestfs_stat>.");
+
+  ("is_file", (RBool "fileflag", [String "path"]), 37, [],
+   [InitBasicFS, TestOutputTrue (
+      [["touch"; "/new"];
+       ["is_file"; "/new"]]);
+    InitBasicFS, TestOutputFalse (
+      [["mkdir"; "/new"];
+       ["is_file"; "/new"]])],
+   "test if file exists",
+   "\
+This returns C<true> if and only if there is a file
+with the given C<path> name.  Note that it returns false for
+other objects like directories.
+
+See also C<guestfs_stat>.");
+
+  ("is_dir", (RBool "dirflag", [String "path"]), 38, [],
+   [InitBasicFS, TestOutputFalse (
+      [["touch"; "/new"];
+       ["is_dir"; "/new"]]);
+    InitBasicFS, TestOutputTrue (
+      [["mkdir"; "/new"];
+       ["is_dir"; "/new"]])],
+   "test if file exists",
+   "\
+This returns C<true> if and only if there is a directory
+with the given C<path> name.  Note that it returns false for
+other objects like files.
+
+See also C<guestfs_stat>.");
+
+  ("pvcreate", (RErr, [String "device"]), 39, [],
+   [InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+       ["pvcreate"; "/dev/sda1"];
+       ["pvcreate"; "/dev/sda2"];
+       ["pvcreate"; "/dev/sda3"];
+       ["pvs"]], ["/dev/sda1"; "/dev/sda2"; "/dev/sda3"])],
+   "create an LVM physical volume",
+   "\
+This creates an LVM physical volume on the named C<device>,
+where C<device> should usually be a partition name such
+as C</dev/sda1>.");
+
+  ("vgcreate", (RErr, [String "volgroup"; StringList "physvols"]), 40, [],
+   [InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+       ["pvcreate"; "/dev/sda1"];
+       ["pvcreate"; "/dev/sda2"];
+       ["pvcreate"; "/dev/sda3"];
+       ["vgcreate"; "VG1"; "/dev/sda1 /dev/sda2"];
+       ["vgcreate"; "VG2"; "/dev/sda3"];
+       ["vgs"]], ["VG1"; "VG2"])],
+   "create an LVM volume group",
+   "\
+This creates an LVM volume group called C<volgroup>
+from the non-empty list of physical volumes C<physvols>.");
+
+  ("lvcreate", (RErr, [String "logvol"; String "volgroup"; Int "mbytes"]), 41, [],
+   [InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+       ["pvcreate"; "/dev/sda1"];
+       ["pvcreate"; "/dev/sda2"];
+       ["pvcreate"; "/dev/sda3"];
+       ["vgcreate"; "VG1"; "/dev/sda1 /dev/sda2"];
+       ["vgcreate"; "VG2"; "/dev/sda3"];
+       ["lvcreate"; "LV1"; "VG1"; "50"];
+       ["lvcreate"; "LV2"; "VG1"; "50"];
+       ["lvcreate"; "LV3"; "VG2"; "50"];
+       ["lvcreate"; "LV4"; "VG2"; "50"];
+       ["lvcreate"; "LV5"; "VG2"; "50"];
+       ["lvs"]],
+      ["/dev/VG1/LV1"; "/dev/VG1/LV2";
+       "/dev/VG2/LV3"; "/dev/VG2/LV4"; "/dev/VG2/LV5"])],
+   "create an LVM volume group",
+   "\
+This creates an LVM volume group called C<logvol>
+on the volume group C<volgroup>, with C<size> megabytes.");
+
+  ("mkfs", (RErr, [String "fstype"; String "device"]), 42, [],
+   [InitEmpty, TestOutput (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["mkfs"; "ext2"; "/dev/sda1"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["write_file"; "/new"; "new file contents"; "0"];
+       ["cat"; "/new"]], "new file contents")],
+   "make a filesystem",
+   "\
+This creates a filesystem on C<device> (usually a partition
+of LVM logical volume).  The filesystem type is C<fstype>, for
+example C<ext3>.");
+
+  ("sfdisk", (RErr, [String "device";
+                    Int "cyls"; Int "heads"; Int "sectors";
+                    StringList "lines"]), 43, [DangerWillRobinson],
+   [],
+   "create partitions on a block device",
+   "\
+This is a direct interface to the L<sfdisk(8)> program for creating
+partitions on block devices.
+
+C<device> should be a block device, for example C</dev/sda>.
+
+C<cyls>, C<heads> and C<sectors> are the number of cylinders, heads
+and sectors on the device, which are passed directly to sfdisk as
+the I<-C>, I<-H> and I<-S> parameters.  If you pass C<0> for any
+of these, then the corresponding parameter is omitted.  Usually for
+'large' disks, you can just pass C<0> for these, but for small
+(floppy-sized) disks, sfdisk (or rather, the kernel) cannot work
+out the right geometry and you will need to tell it.
+
+C<lines> is a list of lines that we feed to C<sfdisk>.  For more
+information refer to the L<sfdisk(8)> manpage.
+
+To create a single partition occupying the whole disk, you would
+pass C<lines> as a single element list, when the single element being
+the string C<,> (comma).");
+
+  ("write_file", (RErr, [String "path"; String "content"; Int "size"]), 44, [ProtocolLimitWarning],
+   [InitEmpty, TestOutput (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["mkfs"; "ext2"; "/dev/sda1"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["write_file"; "/new"; "new file contents"; "0"];
+       ["cat"; "/new"]], "new file contents")],
+   "create a file",
+   "\
+This call creates a file called C<path>.  The contents of the
+file is the string C<content> (which can contain any 8 bit data),
+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
+the content cannot contain embedded ASCII NULs).");
+
+  ("umount", (RErr, [String "pathordevice"]), 45, [FishAlias "unmount"],
+   [InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["mkfs"; "ext2"; "/dev/sda1"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["mounts"]], ["/dev/sda1"]);
+    InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["mkfs"; "ext2"; "/dev/sda1"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["umount"; "/"];
+       ["mounts"]], [])],
+   "unmount a filesystem",
+   "\
+This unmounts the given filesystem.  The filesystem may be
+specified either by its mountpoint (path) or the device which
+contains the filesystem.");
+
+  ("mounts", (RStringList "devices", []), 46, [],
+   [InitBasicFS, TestOutputList (
+      [["mounts"]], ["/dev/sda1"])],
+   "show mounted filesystems",
+   "\
+This returns the list of currently mounted filesystems.  It returns
+the list of devices (eg. C</dev/sda1>, C</dev/VG/LV>).
+
+Some internal mounts are not shown.");
+
+  ("umount_all", (RErr, []), 47, [FishAlias "unmount-all"],
+   [InitBasicFS, TestOutputList (
+      [["umount_all"];
+       ["mounts"]], [])],
+   "unmount all filesystems",
+   "\
+This unmounts all mounted filesystems.
+
+Some internal mounts are not unmounted by this call.");
+
+  ("lvm_remove_all", (RErr, []), 48, [DangerWillRobinson],
+   [],
+   "remove all LVM LVs, VGs and PVs",
+   "\
+This command removes all LVM logical volumes, volume groups
+and physical volumes.");
+
+  ("file", (RString "description", [String "path"]), 49, [],
+   [InitBasicFS, TestOutput (
+      [["touch"; "/new"];
+       ["file"; "/new"]], "empty");
+    InitBasicFS, TestOutput (
+      [["write_file"; "/new"; "some content\n"; "0"];
+       ["file"; "/new"]], "ASCII text");
+    InitBasicFS, TestLastFail (
+      [["file"; "/nofile"]])],
+   "determine file type",
+   "\
+This call uses the standard L<file(1)> command to determine
+the type or contents of the file.  This also works on devices,
+for example to find out whether a partition contains a filesystem.
+
+The exact command which runs is C<file -bsL path>.  Note in
+particular that the filename is not prepended to the output
+(the C<-b> option).");
+
+  ("command", (RString "output", [StringList "arguments"]), 50, [],
+   [], (* XXX how to test? *)
+   "run a command from the guest filesystem",
+   "\
+This call runs a command from the guest filesystem.  The
+filesystem must be mounted, and must contain a compatible
+operating system (ie. something Linux, with the same
+or compatible processor architecture).
+
+The single parameter is an argv-style list of arguments.
+The first element is the name of the program to run.
+Subsequent elements are parameters.  The list must be
+non-empty (ie. must contain a program name).
+
+The C<$PATH> environment variable will contain at least
+C</usr/bin> and C</bin>.  If you require a program from
+another location, you should provide the full path in the
+first parameter.
+
+Shared libraries and data files required by the program
+must be available on filesystems which are mounted in the
+correct places.  It is the caller's responsibility to ensure
+all filesystems that are needed are mounted at the right
+locations.");
+
+  ("command_lines", (RStringList "lines", [StringList "arguments"]), 51, [],
+   [], (* XXX how to test? *)
+   "run a command, returning lines",
+   "\
+This is the same as C<guestfs_command>, but splits the
+result into a list of lines.");
+
+  ("stat", (RStat "statbuf", [String "path"]), 52, [],
+   [InitBasicFS, TestOutputStruct (
+      [["touch"; "/new"];
+       ["stat"; "/new"]], [CompareWithInt ("size", 0)])],
+   "get file information",
+   "\
+Returns file information for the given C<path>.
+
+This is the same as the C<stat(2)> system call.");
+
+  ("lstat", (RStat "statbuf", [String "path"]), 53, [],
+   [InitBasicFS, TestOutputStruct (
+      [["touch"; "/new"];
+       ["lstat"; "/new"]], [CompareWithInt ("size", 0)])],
+   "get file information for a symbolic link",
+   "\
+Returns file information for the given C<path>.
+
+This is the same as C<guestfs_stat> except that if C<path>
+is a symbolic link, then the link is stat-ed, not the file it
+refers to.
+
+This is the same as the C<lstat(2)> system call.");
+
+  ("statvfs", (RStatVFS "statbuf", [String "path"]), 54, [],
+   [InitBasicFS, TestOutputStruct (
+      [["statvfs"; "/"]], [CompareWithInt ("bfree", 487702);
+                          CompareWithInt ("blocks", 490020);
+                          CompareWithInt ("bsize", 1024)])],
+   "get file system statistics",
+   "\
+Returns file system statistics for any mounted file system.
+C<path> should be a file or directory in the mounted file system
+(typically it is the mount point itself, but it doesn't need to be).
+
+This is the same as the C<statvfs(2)> system call.");
+
 ]
 
 let all_functions = non_daemon_functions @ daemon_functions
 ]
 
 let all_functions = non_daemon_functions @ daemon_functions
@@ -491,7 +1056,8 @@ let all_functions = non_daemon_functions @ daemon_functions
  * alphabetically, so this is useful:
  *)
 let all_functions_sorted =
  * alphabetically, so this is useful:
  *)
 let all_functions_sorted =
-  List.sort (fun (n1,_,_,_,_,_) (n2,_,_,_,_,_) -> compare n1 n2) all_functions
+  List.sort (fun (n1,_,_,_,_,_,_) (n2,_,_,_,_,_,_) ->
+              compare n1 n2) all_functions
 
 (* Column names and types from LVM PVs/VGs/LVs. *)
 let pv_cols = [
 
 (* Column names and types from LVM PVs/VGs/LVs. *)
 let pv_cols = [
@@ -556,6 +1122,39 @@ let lv_cols = [
   "modules", `String;
 ]
 
   "modules", `String;
 ]
 
+(* Column names and types from stat structures.
+ * NB. Can't use things like 'st_atime' because glibc header files
+ * define some of these as macros.  Ugh.
+ *)
+let stat_cols = [
+  "dev", `Int;
+  "ino", `Int;
+  "mode", `Int;
+  "nlink", `Int;
+  "uid", `Int;
+  "gid", `Int;
+  "rdev", `Int;
+  "size", `Int;
+  "blksize", `Int;
+  "blocks", `Int;
+  "atime", `Int;
+  "mtime", `Int;
+  "ctime", `Int;
+]
+let statvfs_cols = [
+  "bsize", `Int;
+  "frsize", `Int;
+  "blocks", `Int;
+  "bfree", `Int;
+  "bavail", `Int;
+  "files", `Int;
+  "ffree", `Int;
+  "favail", `Int;
+  "fsid", `Int;
+  "flag", `Int;
+  "namemax", `Int;
+]
+
 (* Useful functions.
  * Note we don't want to use any external OCaml libraries which
  * makes this a bit harder than it should be.
 (* Useful functions.
  * Note we don't want to use any external OCaml libraries which
  * makes this a bit harder than it should be.
@@ -603,6 +1202,17 @@ let rec replace_str s s1 s2 =
     s' ^ s2 ^ replace_str s'' s1 s2
   )
 
     s' ^ s2 ^ replace_str s'' s1 s2
   )
 
+let rec string_split sep str =
+  let len = String.length str in
+  let seplen = String.length sep in
+  let i = find str sep in
+  if i = -1 then [str]
+  else (
+    let s' = String.sub str 0 i in
+    let s'' = String.sub str (i+seplen) (len-i-seplen) in
+    s' :: string_split sep s''
+  )
+
 let rec find_map f = function
   | [] -> raise Not_found
   | x :: xs ->
 let rec find_map f = function
   | [] -> raise Not_found
   | x :: xs ->
@@ -617,7 +1227,15 @@ let iteri f xs =
   in
   loop 0 xs
 
   in
   loop 0 xs
 
-let name_of_argt = function String n | OptString n | Bool n | Int n -> n
+let mapi f xs =
+  let rec loop i = function
+    | [] -> []
+    | x :: xs -> let r = f i x in r :: loop (i+1) xs
+  in
+  loop 0 xs
+
+let name_of_argt = function
+  | String n | OptString n | StringList n | Bool n | Int n -> n
 
 (* Check function names etc. for consistency. *)
 let check_functions () =
 
 (* Check function names etc. for consistency. *)
 let check_functions () =
@@ -636,7 +1254,7 @@ let check_functions () =
 
   (* Check function names. *)
   List.iter (
 
   (* Check function names. *)
   List.iter (
-    fun (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
       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
@@ -648,7 +1266,7 @@ let check_functions () =
 
   (* Check function parameter/return names. *)
   List.iter (
 
   (* Check function parameter/return names. *)
   List.iter (
-    fun (name, style, _, _, _, _) ->
+    fun (name, style, _, _, _, _, _) ->
       let check_arg_ret_name n =
        if contains_uppercase n then
          failwithf "%s param/ret %s should not contain uppercase chars"
       let check_arg_ret_name n =
        if contains_uppercase n then
          failwithf "%s param/ret %s should not contain uppercase chars"
@@ -657,13 +1275,16 @@ 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" n;
+       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
       in
 
       (match fst style with
        | RErr -> ()
        | RInt n | RBool n | RConstString n | RString n
       in
 
       (match fst style with
        | RErr -> ()
        | RInt n | RBool n | RConstString n | RString n
-       | RStringList n | RPVList n | RVGList n | RLVList n ->
+       | RStringList n | RPVList n | RVGList n | RLVList n
+       | RStat n | RStatVFS 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;
@@ -672,28 +1293,38 @@ let check_functions () =
       List.iter (fun arg -> check_arg_ret_name (name_of_argt arg)) (snd style)
   ) all_functions;
 
       List.iter (fun arg -> check_arg_ret_name (name_of_argt arg)) (snd style)
   ) all_functions;
 
+  (* Check short descriptions. *)
+  List.iter (
+    fun (name, _, _, _, _, shortdesc, _) ->
+      if shortdesc.[0] <> Char.lowercase shortdesc.[0] then
+       failwithf "short description of %s should begin with lowercase." name;
+      let c = shortdesc.[String.length shortdesc-1] in
+      if c = '\n' || c = '.' then
+       failwithf "short description of %s should not end with . or \\n." name
+  ) all_functions;
+
   (* Check long dscriptions. *)
   List.iter (
   (* Check long dscriptions. *)
   List.iter (
-    fun (name, _, _, _, _, longdesc) ->
+    fun (name, _, _, _, _, _, longdesc) ->
       if longdesc.[String.length longdesc-1] = '\n' then
        failwithf "long description of %s should not end with \\n." name
   ) all_functions;
 
   (* Check proc_nrs. *)
   List.iter (
       if longdesc.[String.length longdesc-1] = '\n' then
        failwithf "long description of %s should not end with \\n." name
   ) all_functions;
 
   (* Check proc_nrs. *)
   List.iter (
-    fun (name, _, proc_nr, _, _, _) ->
+    fun (name, _, proc_nr, _, _, _, _) ->
       if proc_nr <= 0 then
        failwithf "daemon function %s should have proc_nr > 0" name
   ) daemon_functions;
 
   List.iter (
       if proc_nr <= 0 then
        failwithf "daemon function %s should have proc_nr > 0" name
   ) daemon_functions;
 
   List.iter (
-    fun (name, _, proc_nr, _, _, _) ->
+    fun (name, _, proc_nr, _, _, _, _) ->
       if proc_nr <> -1 then
        failwithf "non-daemon function %s should have proc_nr -1" name
   ) non_daemon_functions;
 
   let proc_nrs =
       if proc_nr <> -1 then
        failwithf "non-daemon function %s should have proc_nr -1" name
   ) non_daemon_functions;
 
   let proc_nrs =
-    List.map (fun (name, _, proc_nr, _, _, _) -> name, proc_nr)
+    List.map (fun (name, _, proc_nr, _, _, _, _) -> name, proc_nr)
       daemon_functions in
   let proc_nrs =
     List.sort (fun (_,nr1) (_,nr2) -> compare nr1 nr2) proc_nrs in
       daemon_functions in
   let proc_nrs =
     List.sort (fun (_,nr1) (_,nr2) -> compare nr1 nr2) proc_nrs in
@@ -770,7 +1401,7 @@ let generate_header comment license =
 (* Generate the pod documentation for the C API. *)
 let rec generate_actions_pod () =
   List.iter (
 (* Generate the pod documentation for the C API. *)
 let rec generate_actions_pod () =
   List.iter (
-    fun (shortname, style, _, flags, _, longdesc) ->
+    fun (shortname, style, _, flags, _, _, longdesc) ->
       let name = "guestfs_" ^ shortname in
       pr "=head2 %s\n\n" name;
       pr " ";
       let name = "guestfs_" ^ shortname in
       pr "=head2 %s\n\n" name;
       pr " ";
@@ -785,32 +1416,49 @@ let rec generate_actions_pod () =
        | RBool _ ->
           pr "This function returns a C truth value on success or -1 on error.\n\n"
        | RConstString _ ->
        | 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.
+          pr "This function returns a string, or NULL on error.
 The string is owned by the guest handle and must I<not> be freed.\n\n"
        | RString _ ->
 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.
+          pr "This function returns a string, or NULL on error.
 I<The caller must free the returned string after use>.\n\n"
        | RStringList _ ->
           pr "This function returns a NULL-terminated array of strings
 (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 _ ->
 I<The caller must free the returned string after use>.\n\n"
        | RStringList _ ->
           pr "This function returns a NULL-terminated array of strings
 (like L<environ(3)>), or NULL if there was an error.
 I<The caller must free the strings and the array after use>.\n\n"
        | RIntBool _ ->
-          pr "This function returns a C<struct guestfs_int_bool *>.
+          pr "This function returns a C<struct guestfs_int_bool *>,
+or NULL if there was an error.
 I<The caller must call C<guestfs_free_int_bool> after use>.\n\n"
        | RPVList _ ->
 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 *>.
+          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"
        | RVGList _ ->
 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 *>.
+          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"
        | RLVList _ ->
 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 *>.
+          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"
 I<The caller must call C<guestfs_free_lvm_lv_list> after use>.\n\n"
+       | RStat _ ->
+          pr "This function returns a C<struct guestfs_stat *>
+(see L<stat(2)> and E<lt>guestfs-structs.hE<gt>),
+or NULL if there was an error.
+I<The caller must call C<free> after use>.\n\n"
+       | RStatVFS _ ->
+          pr "This function returns a C<struct guestfs_statvfs *>
+(see L<statvfs(2)> and E<lt>guestfs-structs.hE<gt>),
+or NULL if there was an error.
+I<The caller must call C<free> after use>.\n\n"
       );
       if List.mem ProtocolLimitWarning flags then
       );
       if List.mem ProtocolLimitWarning flags then
-       pr "Because of the message protocol, there is a transfer limit 
-of somewhere between 2MB and 4MB.  To transfer large files you should use
-FTP.\n\n";
+       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 () =
@@ -876,8 +1524,20 @@ and generate_xdr () =
        pr "\n";
   ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
 
        pr "\n";
   ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
 
+  (* Stat internal structures. *)
+  List.iter (
+    function
+    | typ, cols ->
+       pr "struct guestfs_int_%s {\n" typ;
+       List.iter (function
+                  | name, `Int -> pr "  hyper %s;\n" name
+                 ) cols;
+       pr "};\n";
+       pr "\n";
+  ) ["stat", stat_cols; "statvfs", statvfs_cols];
+
   List.iter (
   List.iter (
-    fun(shortname, style, _, _, _, _) ->
+    fun (shortname, style, _, _, _, _, _) ->
       let name = "guestfs_" ^ shortname in
 
       (match snd style with
       let name = "guestfs_" ^ shortname in
 
       (match snd style with
@@ -888,6 +1548,7 @@ and generate_xdr () =
             function
             | String n -> pr "  string %s<>;\n" n
             | OptString n -> pr "  str *%s;\n" n
             function
             | String n -> pr "  string %s<>;\n" n
             | OptString n -> pr "  str *%s;\n" n
+            | StringList n -> pr "  str %s<>;\n" n
             | Bool n -> pr "  bool %s;\n" n
             | Int n -> pr "  int %s;\n" n
           ) args;
             | Bool n -> pr "  bool %s;\n" n
             | Int n -> pr "  int %s;\n" n
           ) args;
@@ -930,13 +1591,21 @@ and generate_xdr () =
           pr "struct %s_ret {\n" name;
           pr "  guestfs_lvm_int_lv_list %s;\n" n;
           pr "};\n\n"
           pr "struct %s_ret {\n" name;
           pr "  guestfs_lvm_int_lv_list %s;\n" n;
           pr "};\n\n"
+       | RStat n ->
+          pr "struct %s_ret {\n" name;
+          pr "  guestfs_int_stat %s;\n" n;
+          pr "};\n\n";
+       | RStatVFS n ->
+          pr "struct %s_ret {\n" name;
+          pr "  guestfs_int_statvfs %s;\n" n;
+          pr "};\n\n";
       );
   ) daemon_functions;
 
   (* Table of procedure numbers. *)
   pr "enum guestfs_procedure {\n";
   List.iter (
       );
   ) daemon_functions;
 
   (* Table of procedure numbers. *)
   pr "enum guestfs_procedure {\n";
   List.iter (
-    fun (shortname, _, proc_nr, _, _, _) ->
+    fun (shortname, _, proc_nr, _, _, _, _) ->
       pr "  GUESTFS_PROC_%s = %d,\n" (String.uppercase shortname) proc_nr
   ) daemon_functions;
   pr "  GUESTFS_PROC_dummy\n"; (* so we don't have a "hanging comma" *)
       pr "  GUESTFS_PROC_%s = %d,\n" (String.uppercase shortname) proc_nr
   ) daemon_functions;
   pr "  GUESTFS_PROC_dummy\n"; (* so we don't have a "hanging comma" *)
@@ -1028,13 +1697,26 @@ and generate_structs_h () =
        pr "  struct guestfs_lvm_%s *val;\n" typ;
        pr "};\n";
        pr "\n"
        pr "  struct guestfs_lvm_%s *val;\n" typ;
        pr "};\n";
        pr "\n"
-  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols]
+  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
+
+  (* Stat structures. *)
+  List.iter (
+    function
+    | typ, cols ->
+       pr "struct guestfs_%s {\n" typ;
+       List.iter (
+         function
+         | name, `Int -> pr "  int64_t %s;\n" name
+       ) cols;
+       pr "};\n";
+       pr "\n"
+  ) ["stat", stat_cols; "statvfs", statvfs_cols]
 
 (* Generate the guestfs-actions.h file. *)
 and generate_actions_h () =
   generate_header CStyle LGPLv2;
   List.iter (
 
 (* Generate the guestfs-actions.h file. *)
 and generate_actions_h () =
   generate_header CStyle LGPLv2;
   List.iter (
-    fun (shortname, style, _, _, _, _) ->
+    fun (shortname, style, _, _, _, _, _) ->
       let name = "guestfs_" ^ shortname in
       generate_prototype ~single_line:true ~newline:true ~handle:"handle"
        name style
       let name = "guestfs_" ^ shortname in
       generate_prototype ~single_line:true ~newline:true ~handle:"handle"
        name style
@@ -1046,7 +1728,7 @@ and generate_client_actions () =
 
   (* Client-side stubs for each function. *)
   List.iter (
 
   (* Client-side stubs for each function. *)
   List.iter (
-    fun (shortname, style, _, _, _, _) ->
+    fun (shortname, style, _, _, _, _, _) ->
       let name = "guestfs_" ^ shortname in
 
       (* Generate the return value struct. *)
       let name = "guestfs_" ^ shortname in
 
       (* Generate the return value struct. *)
@@ -1061,7 +1743,8 @@ and generate_client_actions () =
        | RInt _
        | RBool _ | RString _ | RStringList _
        | RIntBool _
        | RInt _
        | RBool _ | RString _ | RStringList _
        | RIntBool _
-       | RPVList _ | RVGList _ | RLVList _ ->
+       | RPVList _ | RVGList _ | RLVList _
+       | RStat _ | RStatVFS _ ->
           pr "  struct %s_ret ret;\n" name
       );
       pr "};\n\n";
           pr "  struct %s_ret ret;\n" name
       );
       pr "};\n\n";
@@ -1090,7 +1773,8 @@ and generate_client_actions () =
        | RInt _
        | RBool _ | RString _ | RStringList _
        | RIntBool _
        | RInt _
        | RBool _ | RString _ | RStringList _
        | RIntBool _
-       | RPVList _ | RVGList _ | RLVList _ ->
+       | RPVList _ | RVGList _ | RLVList _
+       | RStat _ | RStatVFS _ ->
            pr "  if (!xdr_%s_ret (xdr, &rv->ret)) {\n" name;
            pr "    error (g, \"%s: failed to parse reply\");\n" name;
            pr "    return;\n";
            pr "  if (!xdr_%s_ret (xdr, &rv->ret)) {\n" name;
            pr "    error (g, \"%s: failed to parse reply\");\n" name;
            pr "    return;\n";
@@ -1112,7 +1796,8 @@ and generate_client_actions () =
        | RConstString _ ->
            failwithf "RConstString cannot be returned from a daemon function"
        | RString _ | RStringList _ | RIntBool _
        | RConstString _ ->
            failwithf "RConstString cannot be returned from a daemon function"
        | RString _ | RStringList _ | RIntBool _
-       | RPVList _ | RVGList _ | RLVList _ ->
+       | RPVList _ | RVGList _ | RLVList _
+       | RStat _ | RStatVFS _ ->
            "NULL" in
 
       pr "{\n";
            "NULL" in
 
       pr "{\n";
@@ -1146,6 +1831,9 @@ and generate_client_actions () =
                 pr "  args.%s = (char *) %s;\n" n n
             | OptString n ->
                 pr "  args.%s = %s ? (char **) &%s : NULL;\n" n n n
                 pr "  args.%s = (char *) %s;\n" n n
             | OptString n ->
                 pr "  args.%s = %s ? (char **) &%s : NULL;\n" n n n
+            | StringList n ->
+                pr "  args.%s.%s_val = (char **) %s;\n" n n n;
+                pr "  for (args.%s.%s_len = 0; %s[args.%s.%s_len]; args.%s.%s_len++) ;\n" n n n n n n n;
             | Bool n ->
                 pr "  args.%s = %s;\n" n n
             | Int n ->
             | Bool n ->
                 pr "  args.%s = %s;\n" n n
             | Int n ->
@@ -1211,6 +1899,12 @@ and generate_client_actions () =
        | RLVList n ->
           pr "  /* caller will free this */\n";
           pr "  return safe_memdup (g, &rv.ret.%s, sizeof (rv.ret.%s));\n" n n
        | RLVList n ->
           pr "  /* caller will free this */\n";
           pr "  return safe_memdup (g, &rv.ret.%s, sizeof (rv.ret.%s));\n" n n
+       | RStat n ->
+          pr "  /* caller will free this */\n";
+          pr "  return safe_memdup (g, &rv.ret.%s, sizeof (rv.ret.%s));\n" n n
+       | RStatVFS n ->
+          pr "  /* caller will free this */\n";
+          pr "  return safe_memdup (g, &rv.ret.%s, sizeof (rv.ret.%s));\n" n n
       );
 
       pr "}\n\n"
       );
 
       pr "}\n\n"
@@ -1224,7 +1918,7 @@ and generate_daemon_actions_h () =
   pr "\n";
 
   List.iter (
   pr "\n";
 
   List.iter (
-    fun (name, style, _, _, _, _) ->
+    fun (name, style, _, _, _, _, _) ->
        generate_prototype
          ~single_line:true ~newline:true ~in_daemon:true ~prefix:"do_"
          name style;
        generate_prototype
          ~single_line:true ~newline:true ~in_daemon:true ~prefix:"do_"
          name style;
@@ -1250,7 +1944,7 @@ and generate_daemon_actions () =
   pr "\n";
 
   List.iter (
   pr "\n";
 
   List.iter (
-    fun (name, style, _, _, _, _) ->
+    fun (name, style, _, _, _, _, _) ->
       (* Generate server-side stubs. *)
       pr "static void %s_stub (XDR *xdr_in)\n" name;
       pr "{\n";
       (* Generate server-side stubs. *)
       pr "static void %s_stub (XDR *xdr_in)\n" name;
       pr "{\n";
@@ -1265,7 +1959,9 @@ and generate_daemon_actions () =
        | RIntBool _ -> pr "  guestfs_%s_ret *r;\n" name; "NULL"
        | RPVList _ -> pr "  guestfs_lvm_int_pv_list *r;\n"; "NULL"
        | RVGList _ -> pr "  guestfs_lvm_int_vg_list *r;\n"; "NULL"
        | RIntBool _ -> pr "  guestfs_%s_ret *r;\n" name; "NULL"
        | RPVList _ -> pr "  guestfs_lvm_int_pv_list *r;\n"; "NULL"
        | RVGList _ -> pr "  guestfs_lvm_int_vg_list *r;\n"; "NULL"
-       | RLVList _ -> pr "  guestfs_lvm_int_lv_list *r;\n"; "NULL" in
+       | 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
 
       (match snd style with
        | [] -> ()
 
       (match snd style with
        | [] -> ()
@@ -1275,6 +1971,7 @@ and generate_daemon_actions () =
             function
             | String n
             | OptString n -> pr "  const char *%s;\n" n
             function
             | String n
             | OptString n -> pr "  const 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
           ) args
             | Bool n -> pr "  int %s;\n" n
             | Int n -> pr "  int %s;\n" n
           ) args
@@ -1294,6 +1991,10 @@ and generate_daemon_actions () =
             function
             | String n -> pr "  %s = args.%s;\n" n n
             | OptString n -> pr "  %s = args.%s ? *args.%s : NULL;\n" n n n
             function
             | String n -> pr "  %s = args.%s;\n" n n
             | OptString n -> pr "  %s = args.%s ? *args.%s : NULL;\n" n n n
+            | StringList n ->
+                pr "  args.%s.%s_val = realloc (args.%s.%s_val, sizeof (char *) * (args.%s.%s_len+1));\n" n n n n n n;
+                pr "  args.%s.%s_val[args.%s.%s_len] = NULL;\n" n n n n;
+                pr "  %s = args.%s.%s_val;\n" n n n
             | Bool n -> pr "  %s = args.%s;\n" n n
             | Int n -> pr "  %s = args.%s;\n" n n
           ) args;
             | Bool n -> pr "  %s = args.%s;\n" n n
             | Int n -> pr "  %s = args.%s;\n" n n
           ) args;
@@ -1305,8 +2006,8 @@ and generate_daemon_actions () =
       pr ";\n";
 
       pr "  if (r == %s)\n" error_code;
       pr ";\n";
 
       pr "  if (r == %s)\n" error_code;
-      pr "    /* do_%s has already called reply_with_error, so just return */\n" name;
-      pr "    return;\n";
+      pr "    /* do_%s has already called reply_with_error */\n" name;
+      pr "    goto done;\n";
       pr "\n";
 
       (match fst style with
       pr "\n";
 
       (match fst style with
@@ -1335,23 +2036,23 @@ and generate_daemon_actions () =
        | RIntBool _ ->
           pr "  reply ((xdrproc_t) xdr_guestfs_%s_ret, (char *) r);\n" name;
           pr "  xdr_free ((xdrproc_t) xdr_guestfs_%s_ret, (char *) r);\n" name
        | RIntBool _ ->
           pr "  reply ((xdrproc_t) xdr_guestfs_%s_ret, (char *) r);\n" name;
           pr "  xdr_free ((xdrproc_t) xdr_guestfs_%s_ret, (char *) r);\n" name
-       | RPVList n ->
-          pr "  struct guestfs_%s_ret ret;\n" name;
-          pr "  ret.%s = *r;\n" n;
-          pr "  reply ((xdrproc_t) xdr_guestfs_%s_ret, (char *) &ret);\n" name;
-          pr "  xdr_free ((xdrproc_t) xdr_guestfs_%s_ret, (char *) &ret);\n" name
-       | RVGList n ->
-          pr "  struct guestfs_%s_ret ret;\n" name;
-          pr "  ret.%s = *r;\n" n;
-          pr "  reply ((xdrproc_t) xdr_guestfs_%s_ret, (char *) &ret);\n" name;
-          pr "  xdr_free ((xdrproc_t) xdr_guestfs_%s_ret, (char *) &ret);\n" name
-       | RLVList n ->
+       | RPVList n | RVGList n | RLVList n | RStat n | RStatVFS n ->
           pr "  struct guestfs_%s_ret ret;\n" name;
           pr "  ret.%s = *r;\n" n;
           pr "  reply ((xdrproc_t) xdr_guestfs_%s_ret, (char *) &ret);\n" name;
           pr "  xdr_free ((xdrproc_t) xdr_guestfs_%s_ret, (char *) &ret);\n" name
       );
 
           pr "  struct guestfs_%s_ret ret;\n" name;
           pr "  ret.%s = *r;\n" n;
           pr "  reply ((xdrproc_t) xdr_guestfs_%s_ret, (char *) &ret);\n" name;
           pr "  xdr_free ((xdrproc_t) xdr_guestfs_%s_ret, (char *) &ret);\n" name
       );
 
+      (* Free the args. *)
+      (match snd style with
+       | [] ->
+          pr "done: ;\n";
+       | _ ->
+          pr "done:\n";
+          pr "  xdr_free ((xdrproc_t) xdr_guestfs_%s_args, (char *) &args);\n"
+            name
+      );
+
       pr "}\n\n";
   ) daemon_functions;
 
       pr "}\n\n";
   ) daemon_functions;
 
@@ -1361,7 +2062,7 @@ and generate_daemon_actions () =
   pr "  switch (proc_nr) {\n";
 
   List.iter (
   pr "  switch (proc_nr) {\n";
 
   List.iter (
-    fun (name, style, _, _, _, _) ->
+    fun (name, style, _, _, _, _, _) ->
        pr "    case GUESTFS_PROC_%s:\n" (String.uppercase name);
        pr "      %s_stub (xdr_in);\n" name;
        pr "      break;\n"
        pr "    case GUESTFS_PROC_%s:\n" (String.uppercase name);
        pr "      %s_stub (xdr_in);\n" name;
        pr "      break;\n"
@@ -1538,17 +2239,516 @@ and generate_daemon_actions () =
 
   ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols]
 
 
   ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols]
 
+(* Generate the tests. *)
+and generate_tests () =
+  generate_header CStyle GPLv2;
+
+  pr "\
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <fcntl.h>
+
+#include \"guestfs.h\"
+
+static guestfs_h *g;
+static int suppress_error = 0;
+
+static void print_error (guestfs_h *g, void *data, const char *msg)
+{
+  if (!suppress_error)
+    fprintf (stderr, \"%%s\\n\", msg);
+}
+
+static void print_strings (char * const * const argv)
+{
+  int argc;
+
+  for (argc = 0; argv[argc] != NULL; ++argc)
+    printf (\"\\t%%s\\n\", argv[argc]);
+}
+
+";
+
+  let test_names =
+    List.map (
+      fun (name, _, _, _, tests, _, _) ->
+       mapi (generate_one_test name) tests
+    ) all_functions in
+  let test_names = List.concat test_names in
+  let nr_tests = List.length test_names in
+
+  pr "\
+int main (int argc, char *argv[])
+{
+  char c = 0;
+  int failed = 0;
+  const char *srcdir;
+  int fd;
+  char buf[256];
+  int nr_tests;
+
+  g = guestfs_create ();
+  if (g == NULL) {
+    printf (\"guestfs_create FAILED\\n\");
+    exit (1);
+  }
+
+  guestfs_set_error_handler (g, print_error, NULL);
+
+  srcdir = getenv (\"srcdir\");
+  if (!srcdir) srcdir = \".\";
+  guestfs_set_path (g, srcdir);
+
+  snprintf (buf, sizeof buf, \"%%s/test1.img\", srcdir);
+  fd = open (buf, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
+  if (fd == -1) {
+    perror (buf);
+    exit (1);
+  }
+  if (lseek (fd, %d, SEEK_SET) == -1) {
+    perror (\"lseek\");
+    close (fd);
+    unlink (buf);
+    exit (1);
+  }
+  if (write (fd, &c, 1) == -1) {
+    perror (\"write\");
+    close (fd);
+    unlink (buf);
+    exit (1);
+  }
+  if (close (fd) == -1) {
+    perror (buf);
+    unlink (buf);
+    exit (1);
+  }
+  if (guestfs_add_drive (g, buf) == -1) {
+    printf (\"guestfs_add_drive %%s FAILED\\n\", buf);
+    exit (1);
+  }
+
+  snprintf (buf, sizeof buf, \"%%s/test2.img\", srcdir);
+  fd = open (buf, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
+  if (fd == -1) {
+    perror (buf);
+    exit (1);
+  }
+  if (lseek (fd, %d, SEEK_SET) == -1) {
+    perror (\"lseek\");
+    close (fd);
+    unlink (buf);
+    exit (1);
+  }
+  if (write (fd, &c, 1) == -1) {
+    perror (\"write\");
+    close (fd);
+    unlink (buf);
+    exit (1);
+  }
+  if (close (fd) == -1) {
+    perror (buf);
+    unlink (buf);
+    exit (1);
+  }
+  if (guestfs_add_drive (g, buf) == -1) {
+    printf (\"guestfs_add_drive %%s FAILED\\n\", buf);
+    exit (1);
+  }
+
+  snprintf (buf, sizeof buf, \"%%s/test3.img\", srcdir);
+  fd = open (buf, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
+  if (fd == -1) {
+    perror (buf);
+    exit (1);
+  }
+  if (lseek (fd, %d, SEEK_SET) == -1) {
+    perror (\"lseek\");
+    close (fd);
+    unlink (buf);
+    exit (1);
+  }
+  if (write (fd, &c, 1) == -1) {
+    perror (\"write\");
+    close (fd);
+    unlink (buf);
+    exit (1);
+  }
+  if (close (fd) == -1) {
+    perror (buf);
+    unlink (buf);
+    exit (1);
+  }
+  if (guestfs_add_drive (g, buf) == -1) {
+    printf (\"guestfs_add_drive %%s FAILED\\n\", buf);
+    exit (1);
+  }
+
+  if (guestfs_launch (g) == -1) {
+    printf (\"guestfs_launch FAILED\\n\");
+    exit (1);
+  }
+  if (guestfs_wait_ready (g) == -1) {
+    printf (\"guestfs_wait_ready FAILED\\n\");
+    exit (1);
+  }
+
+  nr_tests = %d;
+" (500 * 1024 * 1024) (50 * 1024 * 1024) (10 * 1024 * 1024) nr_tests;
+
+  iteri (
+    fun i test_name ->
+      pr "  printf (\"%3d/%%3d %s\\n\", nr_tests);\n" (i+1) test_name;
+      pr "  if (%s () == -1) {\n" test_name;
+      pr "    printf (\"%s FAILED\\n\");\n" test_name;
+      pr "    failed++;\n";
+      pr "  }\n";
+  ) test_names;
+  pr "\n";
+
+  pr "  guestfs_close (g);\n";
+  pr "  snprintf (buf, sizeof buf, \"%%s/test1.img\", srcdir);\n";
+  pr "  unlink (buf);\n";
+  pr "  snprintf (buf, sizeof buf, \"%%s/test2.img\", srcdir);\n";
+  pr "  unlink (buf);\n";
+  pr "  snprintf (buf, sizeof buf, \"%%s/test3.img\", srcdir);\n";
+  pr "  unlink (buf);\n";
+  pr "\n";
+
+  pr "  if (failed > 0) {\n";
+  pr "    printf (\"***** %%d / %%d tests FAILED *****\\n\", failed, nr_tests);\n";
+  pr "    exit (1);\n";
+  pr "  }\n";
+  pr "\n";
+
+  pr "  exit (0);\n";
+  pr "}\n"
+
+and generate_one_test name i (init, test) =
+  let test_name = sprintf "test_%s_%d" name i in
+
+  pr "static int %s (void)\n" test_name;
+  pr "{\n";
+
+  (match init with
+   | InitNone -> ()
+   | InitEmpty ->
+       pr "  /* InitEmpty for %s (%d) */\n" name i;
+       List.iter (generate_test_command_call test_name)
+        [["umount_all"];
+         ["lvm_remove_all"]]
+   | InitBasicFS ->
+       pr "  /* InitBasicFS for %s (%d): create ext2 on /dev/sda1 */\n" name i;
+       List.iter (generate_test_command_call test_name)
+        [["umount_all"];
+         ["lvm_remove_all"];
+         ["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+         ["mkfs"; "ext2"; "/dev/sda1"];
+         ["mount"; "/dev/sda1"; "/"]]
+   | InitBasicFSonLVM ->
+       pr "  /* InitBasicFSonLVM for %s (%d): create ext2 on /dev/VG/LV */\n"
+        name i;
+       List.iter (generate_test_command_call test_name)
+        [["umount_all"];
+         ["lvm_remove_all"];
+         ["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+         ["pvcreate"; "/dev/sda1"];
+         ["vgcreate"; "VG"; "/dev/sda1"];
+         ["lvcreate"; "LV"; "VG"; "8"];
+         ["mkfs"; "ext2"; "/dev/VG/LV"];
+         ["mount"; "/dev/VG/LV"; "/"]]
+  );
+
+  let get_seq_last = function
+    | [] ->
+       failwithf "%s: you cannot use [] (empty list) when expecting a command"
+         test_name
+    | seq ->
+       let seq = List.rev 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\", r);\n"
+          test_name 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
+   | 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
+
+(* 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.
+ *)
+and generate_test_command_call ?(expect_error = false) ?test test_name cmd =
+  match cmd with
+  | [] -> assert false
+  | name :: args ->
+      (* Look up the command to find out what args/ret it has. *)
+      let style =
+       try
+         let _, style, _, _, _, _, _ =
+           List.find (fun (n, _, _, _, _, _, _) -> n = name) all_functions in
+         style
+       with Not_found ->
+         failwithf "%s: in test, command %s was not found" test_name name in
+
+      if List.length (snd style) <> List.length args then
+       failwithf "%s: in test, wrong number of args given to %s"
+         test_name name;
+
+      pr "  {\n";
+
+      List.iter (
+       function
+       | String _, _
+       | OptString _, _
+       | Int _, _
+       | Bool _, _ -> ()
+       | StringList n, arg ->
+           pr "    char *%s[] = {\n" n;
+           let strs = string_split " " arg in
+           List.iter (
+             fun str -> pr "      \"%s\",\n" (c_quote str)
+           ) strs;
+           pr "      NULL\n";
+           pr "    };\n";
+      ) (List.combine (snd style) args);
+
+      let error_code =
+       match fst style with
+       | RErr | RInt _ | RBool _ -> pr "    int r;\n"; "-1"
+       | RConstString _ -> pr "    const char *r;\n"; "NULL"
+       | RString _ -> pr "    char *r;\n"; "NULL"
+       | RStringList _ ->
+           pr "    char **r;\n";
+           pr "    int i;\n";
+           "NULL"
+       | RIntBool _ ->
+           pr "    struct guestfs_int_bool *r;\n"; "NULL"
+       | RPVList _ ->
+           pr "    struct guestfs_lvm_pv_list *r;\n"; "NULL"
+       | RVGList _ ->
+           pr "    struct guestfs_lvm_vg_list *r;\n"; "NULL"
+       | RLVList _ ->
+           pr "    struct guestfs_lvm_lv_list *r;\n"; "NULL"
+       | RStat _ ->
+           pr "    struct guestfs_stat *r;\n"; "NULL"
+       | RStatVFS _ ->
+           pr "    struct guestfs_statvfs *r;\n"; "NULL" in
+
+      pr "    suppress_error = %d;\n" (if expect_error then 1 else 0);
+      pr "    r = guestfs_%s (g" name;
+
+      (* Generate the parameters. *)
+      List.iter (
+       function
+       | String _, 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 ->
+           let i =
+             try int_of_string arg
+             with Failure "int_of_string" ->
+               failwithf "%s: expecting an int, but got '%s'" test_name arg in
+           pr ", %d" i
+       | Bool _, arg ->
+           let b = bool_of_string arg in pr ", %d" (if b then 1 else 0)
+      ) (List.combine (snd style) args);
+
+      pr ");\n";
+      if not expect_error then
+       pr "    if (r == %s)\n" error_code
+      else
+       pr "    if (r != %s)\n" error_code;
+      pr "      return -1;\n";
+
+      (* Insert the test code. *)
+      (match test with
+       | None -> ()
+       | Some f -> f ()
+      );
+
+      (match fst style with
+       | RErr | RInt _ | RBool _ | RConstString _ -> ()
+       | RString _ -> pr "    free (r);\n"
+       | RStringList _ ->
+          pr "    for (i = 0; r[i] != NULL; ++i)\n";
+          pr "      free (r[i]);\n";
+          pr "    free (r);\n"
+       | RIntBool _ ->
+          pr "    guestfs_free_int_bool (r);\n"
+       | RPVList _ ->
+          pr "    guestfs_free_lvm_pv_list (r);\n"
+       | RVGList _ ->
+          pr "    guestfs_free_lvm_vg_list (r);\n"
+       | RLVList _ ->
+          pr "    guestfs_free_lvm_lv_list (r);\n"
+       | RStat _ | RStatVFS _ ->
+          pr "    free (r);\n"
+      );
+
+      pr "  }\n"
+
+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
+  str
+
 (* Generate a lot of different functions for guestfish. *)
 and generate_fish_cmds () =
   generate_header CStyle GPLv2;
 
   let all_functions =
     List.filter (
 (* Generate a lot of different functions for guestfish. *)
 and generate_fish_cmds () =
   generate_header CStyle GPLv2;
 
   let all_functions =
     List.filter (
-      fun (_, _, _, flags, _, _) -> not (List.mem NotInFish flags)
+      fun (_, _, _, flags, _, _, _) -> not (List.mem NotInFish flags)
     ) all_functions in
   let all_functions_sorted =
     List.filter (
     ) all_functions in
   let all_functions_sorted =
     List.filter (
-      fun (_, _, _, flags, _, _) -> not (List.mem NotInFish flags)
+      fun (_, _, _, flags, _, _, _) -> not (List.mem NotInFish flags)
     ) all_functions_sorted in
 
   pr "#include <stdio.h>\n";
     ) all_functions_sorted in
 
   pr "#include <stdio.h>\n";
@@ -1566,7 +2766,7 @@ and generate_fish_cmds () =
   pr "  printf (\"    %%-16s     %%s\\n\", \"Command\", \"Description\");\n";
   pr "  list_builtin_commands ();\n";
   List.iter (
   pr "  printf (\"    %%-16s     %%s\\n\", \"Command\", \"Description\");\n";
   pr "  list_builtin_commands ();\n";
   List.iter (
-    fun (name, _, _, flags, shortdesc, _) ->
+    fun (name, _, _, flags, _, shortdesc, _) ->
       let name = replace_char name '_' '-' in
       pr "  printf (\"%%-20s %%s\\n\", \"%s\", \"%s\");\n"
        name shortdesc
       let name = replace_char name '_' '-' in
       pr "  printf (\"%%-20s %%s\\n\", \"%s\", \"%s\");\n"
        name shortdesc
@@ -1579,7 +2779,7 @@ and generate_fish_cmds () =
   pr "void display_command (const char *cmd)\n";
   pr "{\n";
   List.iter (
   pr "void display_command (const char *cmd)\n";
   pr "{\n";
   List.iter (
-    fun (name, style, _, flags, shortdesc, longdesc) ->
+    fun (name, style, _, flags, _, shortdesc, longdesc) ->
       let name2 = replace_char name '_' '-' in
       let alias =
        try find_map (function FishAlias n -> Some n | _ -> None) flags
       let name2 = replace_char name '_' '-' in
       let alias =
        try find_map (function FishAlias n -> Some n | _ -> None) flags
@@ -1594,11 +2794,19 @@ and generate_fish_cmds () =
 
       let warnings =
        if List.mem ProtocolLimitWarning flags then
 
       let warnings =
        if List.mem ProtocolLimitWarning flags then
-         "\n\nBecause of the message protocol, there is a transfer limit 
-of somewhere between 2MB and 4MB.  To transfer large files you should use
-FTP."
+         ("\n\n" ^ protocol_limit_warning)
        else "" in
 
        else "" in
 
+      (* For DangerWillRobinson commands, we should probably have
+       * guestfish prompt before allowing you to use them (especially
+       * in interactive mode). XXX
+       *)
+      let warnings =
+       warnings ^
+         if List.mem DangerWillRobinson flags then
+           ("\n\n" ^ danger_will_robinson)
+         else "" in
+
       let describe_alias =
        if name <> alias then
          sprintf "\n\nYou can use '%s' as an alias for this command." alias
       let describe_alias =
        if name <> alias then
          sprintf "\n\nYou can use '%s' as an alias for this command." alias
@@ -1659,9 +2867,24 @@ FTP."
        pr "\n";
   ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
 
        pr "\n";
   ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
 
+  (* print_{stat,statvfs} functions *)
+  List.iter (
+    function
+    | typ, cols ->
+       pr "static void print_%s (struct guestfs_%s *%s)\n" typ typ typ;
+       pr "{\n";
+       List.iter (
+         function
+         | name, `Int ->
+             pr "  printf (\"%s: %%\" PRIi64 \"\\n\", %s->%s);\n" name typ name
+       ) cols;
+       pr "}\n";
+       pr "\n";
+  ) ["stat", stat_cols; "statvfs", statvfs_cols];
+
   (* run_<action> actions *)
   List.iter (
   (* run_<action> actions *)
   List.iter (
-    fun (name, style, _, flags, _, _) ->
+    fun (name, style, _, flags, _, _, _) ->
       pr "static int run_%s (const char *cmd, int argc, char *argv[])\n" name;
       pr "{\n";
       (match fst style with
       pr "static int run_%s (const char *cmd, int argc, char *argv[])\n" name;
       pr "{\n";
       (match fst style with
@@ -1675,11 +2898,14 @@ FTP."
        | RPVList _ -> pr "  struct guestfs_lvm_pv_list *r;\n"
        | RVGList _ -> pr "  struct guestfs_lvm_vg_list *r;\n"
        | RLVList _ -> pr "  struct guestfs_lvm_lv_list *r;\n"
        | RPVList _ -> pr "  struct guestfs_lvm_pv_list *r;\n"
        | RVGList _ -> pr "  struct guestfs_lvm_vg_list *r;\n"
        | RLVList _ -> pr "  struct guestfs_lvm_lv_list *r;\n"
+       | RStat _ -> pr "  struct guestfs_stat *r;\n"
+       | RStatVFS _ -> pr "  struct guestfs_statvfs *r;\n"
       );
       List.iter (
        function
       );
       List.iter (
        function
-       | String n -> pr "  const char *%s;\n" n
+       | String n
        | OptString n -> pr "  const char *%s;\n" n
        | OptString n -> pr "  const 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
       ) (snd style);
        | Bool n -> pr "  int %s;\n" n
        | Int n -> pr "  int %s;\n" n
       ) (snd style);
@@ -1699,6 +2925,8 @@ FTP."
          | OptString name ->
              pr "  %s = strcmp (argv[%d], \"\") != 0 ? argv[%d] : NULL;\n"
                name i i
          | OptString name ->
              pr "  %s = strcmp (argv[%d], \"\") != 0 ? argv[%d] : NULL;\n"
                name i i
+         | StringList name ->
+             pr "  %s = parse_string_list (argv[%d]);\n" name i
          | Bool name ->
              pr "  %s = is_true (argv[%d]) ? 1 : 0;\n" name i
          | Int name ->
          | Bool name ->
              pr "  %s = is_true (argv[%d]) ? 1 : 0;\n" name i
          | Int name ->
@@ -1759,6 +2987,16 @@ FTP."
           pr "  print_lv_list (r);\n";
           pr "  guestfs_free_lvm_lv_list (r);\n";
           pr "  return 0;\n"
           pr "  print_lv_list (r);\n";
           pr "  guestfs_free_lvm_lv_list (r);\n";
           pr "  return 0;\n"
+       | RStat _ ->
+          pr "  if (r == NULL) return -1;\n";
+          pr "  print_stat (r);\n";
+          pr "  free (r);\n";
+          pr "  return 0;\n"
+       | RStatVFS _ ->
+          pr "  if (r == NULL) return -1;\n";
+          pr "  print_statvfs (r);\n";
+          pr "  free (r);\n";
+          pr "  return 0;\n"
       );
       pr "}\n";
       pr "\n"
       );
       pr "}\n";
       pr "\n"
@@ -1768,7 +3006,7 @@ FTP."
   pr "int run_action (const char *cmd, int argc, char *argv[])\n";
   pr "{\n";
   List.iter (
   pr "int run_action (const char *cmd, int argc, char *argv[])\n";
   pr "{\n";
   List.iter (
-    fun (name, _, _, flags, _, _) ->
+    fun (name, _, _, flags, _, _, _) ->
       let name2 = replace_char name '_' '-' in
       let alias =
        try find_map (function FishAlias n -> Some n | _ -> None) flags
       let name2 = replace_char name '_' '-' in
       let alias =
        try find_map (function FishAlias n -> Some n | _ -> None) flags
@@ -1791,41 +3029,129 @@ FTP."
   pr "}\n";
   pr "\n"
 
   pr "}\n";
   pr "\n"
 
-(* Generate the POD documentation for guestfish. *)
-and generate_fish_actions_pod () =
-  let all_functions_sorted =
+(* Readline completion for guestfish. *)
+and generate_fish_completion () =
+  generate_header CStyle GPLv2;
+
+  let all_functions =
     List.filter (
     List.filter (
-      fun (_, _, _, flags, _, _) -> not (List.mem NotInFish flags)
-    ) all_functions_sorted in
+      fun (_, _, _, flags, _, _, _) -> not (List.mem NotInFish flags)
+    ) all_functions in
 
 
-  List.iter (
-    fun (name, style, _, flags, _, longdesc) ->
-      let longdesc = replace_str longdesc "C<guestfs_" "C<" in
-      let name = replace_char name '_' '-' in
-      let alias =
-       try find_map (function FishAlias n -> Some n | _ -> None) flags
-       with Not_found -> name in
+  pr "\
+#include <config.h>
 
 
-      pr "=head2 %s" name;
-      if name <> alias then
-       pr " | %s" alias;
-      pr "\n";
-      pr "\n";
-      pr " %s" name;
-      List.iter (
-       function
-       | String n -> pr " %s" n
-       | OptString n -> pr " %s" n
-       | Bool _ -> pr " true|false"
-       | Int n -> pr " %s" n
-      ) (snd style);
-      pr "\n";
-      pr "\n";
-      pr "%s\n\n" longdesc
-  ) all_functions_sorted
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
 
 
-(* Generate a C function prototype. *)
-and generate_prototype ?(extern = true) ?(static = false) ?(semicolon = true)
+#ifdef HAVE_LIBREADLINE
+#include <readline/readline.h>
+#endif
+
+#include \"fish.h\"
+
+#ifdef HAVE_LIBREADLINE
+
+static const char *commands[] = {
+";
+
+  (* Get the commands and sort them, including the aliases. *)
+  let commands =
+    List.map (
+      fun (name, _, _, flags, _, _, _) ->
+       let name2 = replace_char name '_' '-' in
+       let alias =
+         try find_map (function FishAlias n -> Some n | _ -> None) flags
+         with Not_found -> name 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;
+
+  pr "  NULL
+};
+
+static char *
+generator (const char *text, int state)
+{
+  static int index, len;
+  const char *name;
+
+  if (!state) {
+    index = 0;
+    len = strlen (text);
+  }
+
+  while ((name = commands[index]) != NULL) {
+    index++;
+    if (strncasecmp (name, text, len) == 0)
+      return strdup (name);
+  }
+
+  return NULL;
+}
+
+#endif /* HAVE_LIBREADLINE */
+
+char **do_completion (const char *text, int start, int end)
+{
+  char **matches = NULL;
+
+#ifdef HAVE_LIBREADLINE
+  if (start == 0)
+    matches = rl_completion_matches (text, generator);
+#endif
+
+  return matches;
+}
+";
+
+(* Generate the POD documentation for guestfish. *)
+and generate_fish_actions_pod () =
+  let all_functions_sorted =
+    List.filter (
+      fun (_, _, _, flags, _, _, _) -> not (List.mem NotInFish flags)
+    ) all_functions_sorted in
+
+  List.iter (
+    fun (name, style, _, flags, _, _, longdesc) ->
+      let longdesc = replace_str longdesc "C<guestfs_" "C<" in
+      let name = replace_char name '_' '-' in
+      let alias =
+       try find_map (function FishAlias n -> Some n | _ -> None) flags
+       with Not_found -> name in
+
+      pr "=head2 %s" name;
+      if name <> alias then
+       pr " | %s" alias;
+      pr "\n";
+      pr "\n";
+      pr " %s" name;
+      List.iter (
+       function
+       | String n -> pr " %s" n
+       | OptString n -> pr " %s" n
+       | StringList n -> pr " %s,..." n
+       | Bool _ -> pr " true|false"
+       | Int n -> pr " %s" n
+      ) (snd style);
+      pr "\n";
+      pr "\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
+
+(* Generate a C function prototype. *)
+and generate_prototype ?(extern = true) ?(static = false) ?(semicolon = true)
     ?(single_line = false) ?(newline = false) ?(in_daemon = false)
     ?(prefix = "")
     ?handle name style =
     ?(single_line = false) ?(newline = false) ?(in_daemon = false)
     ?(prefix = "")
     ?handle name style =
@@ -1850,6 +3176,12 @@ and generate_prototype ?(extern = true) ?(static = false) ?(semicolon = true)
    | RLVList _ ->
        if not in_daemon then pr "struct guestfs_lvm_lv_list *"
        else pr "guestfs_lvm_int_lv_list *"
    | RLVList _ ->
        if not in_daemon then pr "struct guestfs_lvm_lv_list *"
        else pr "guestfs_lvm_int_lv_list *"
+   | RStat _ ->
+       if not in_daemon then pr "struct guestfs_stat *"
+       else pr "guestfs_int_stat *"
+   | RStatVFS _ ->
+       if not in_daemon then pr "struct guestfs_statvfs *"
+       else pr "guestfs_int_statvfs *"
   );
   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
@@ -1870,6 +3202,7 @@ and generate_prototype ?(extern = true) ?(static = false) ?(semicolon = true)
       function
       | String n -> next (); pr "const char *%s" n
       | OptString n -> next (); pr "const char *%s" n
       function
       | String n -> next (); pr "const char *%s" n
       | OptString n -> next (); pr "const char *%s" n
+      | StringList n -> next (); pr "char * const* const %s" n
       | Bool n -> next (); pr "int %s" n
       | Int n -> next (); pr "int %s" n
     ) (snd style);
       | Bool n -> next (); pr "int %s" n
       | Int n -> next (); pr "int %s" n
     ) (snd style);
@@ -1891,9 +3224,10 @@ and generate_call_args ?handle style =
       if !comma then pr ", ";
       comma := true;
       match arg with
       if !comma then pr ", ";
       comma := true;
       match arg with
-      | String n -> pr "%s" n
-      | OptString n -> pr "%s" n
-      | Bool n -> pr "%s" n
+      | String n
+      | OptString n
+      | StringList n
+      | Bool n
       | Int n -> pr "%s" n
   ) (snd style);
   pr ")"
       | Int n -> pr "%s" n
   ) (snd style);
   pr ")"
@@ -1923,9 +3257,11 @@ val close : t -> unit
 ";
   generate_ocaml_lvm_structure_decls ();
 
 ";
   generate_ocaml_lvm_structure_decls ();
 
+  generate_ocaml_stat_structure_decls ();
+
   (* The actions. *)
   List.iter (
   (* The actions. *)
   List.iter (
-    fun (name, style, _, _, shortdesc, _) ->
+    fun (name, style, _, _, _, shortdesc, _) ->
       generate_ocaml_prototype name style;
       pr "(** %s *)\n" shortdesc;
       pr "\n"
       generate_ocaml_prototype name style;
       pr "(** %s *)\n" shortdesc;
       pr "\n"
@@ -1948,9 +3284,11 @@ let () =
 
   generate_ocaml_lvm_structure_decls ();
 
 
   generate_ocaml_lvm_structure_decls ();
 
+  generate_ocaml_stat_structure_decls ();
+
   (* The actions. *)
   List.iter (
   (* The actions. *)
   List.iter (
-    fun (name, style, _, _, shortdesc, _) ->
+    fun (name, style, _, _, _, shortdesc, _) ->
       generate_ocaml_prototype ~is_external:true name style;
   ) all_functions
 
       generate_ocaml_prototype ~is_external:true name style;
   ) all_functions
 
@@ -2038,20 +3376,49 @@ and generate_ocaml_c () =
       pr "\n";
   ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
 
       pr "\n";
   ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
 
+  (* Stat copy functions. *)
   List.iter (
   List.iter (
-    fun (name, style, _, _, _, _) ->
+    fun (typ, cols) ->
+      pr "static CAMLprim value\n";
+      pr "copy_%s (const struct guestfs_%s *%s)\n" typ typ typ;
+      pr "{\n";
+      pr "  CAMLparam0 ();\n";
+      pr "  CAMLlocal2 (rv, v);\n";
+      pr "\n";
+      pr "  rv = caml_alloc (%d, 0);\n" (List.length cols);
+      iteri (
+       fun i col ->
+         (match col with
+          | name, `Int ->
+              pr "  v = caml_copy_int64 (%s->%s);\n" typ name
+         );
+         pr "  Store_field (rv, %d, v);\n" i
+      ) cols;
+      pr "  CAMLreturn (rv);\n";
+      pr "}\n";
+      pr "\n";
+  ) ["stat", stat_cols; "statvfs", statvfs_cols];
+
+  (* The wrappers. *)
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      let params =
+       "gv" :: List.map (fun arg -> name_of_argt arg ^ "v") (snd style) in
+
       pr "CAMLprim value\n";
       pr "CAMLprim value\n";
-      pr "ocaml_guestfs_%s (value gv" name;
-      List.iter (
-       fun arg -> pr ", value %sv" (name_of_argt arg)
-      ) (snd style);
+      pr "ocaml_guestfs_%s (value %s" name (List.hd params);
+      List.iter (pr ", value %s") (List.tl params);
       pr ")\n";
       pr "{\n";
       pr ")\n";
       pr "{\n";
-      pr "  CAMLparam%d (gv" (1 + (List.length (snd style)));
-      List.iter (
-       fun arg -> pr ", %sv" (name_of_argt arg)
-      ) (snd style);
-      pr ");\n";
+
+      (match params with
+       | p1 :: p2 :: p3 :: p4 :: p5 :: rest ->
+          pr "  CAMLparam5 (%s);\n" (String.concat ", " [p1; p2; p3; p4; p5]);
+          pr "  CAMLxparam%d (%s);\n"
+            (List.length rest) (String.concat ", " rest)
+       | ps ->
+          pr "  CAMLparam%d (%s);\n" (List.length ps) (String.concat ", " ps)
+      );
       pr "  CAMLlocal1 (rv);\n";
       pr "\n";
 
       pr "  CAMLlocal1 (rv);\n";
       pr "\n";
 
@@ -2068,6 +3435,8 @@ and generate_ocaml_c () =
            pr "  const char *%s =\n" n;
            pr "    %sv != Val_int (0) ? String_val (Field (%sv, 0)) : NULL;\n"
              n n
            pr "  const char *%s =\n" 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
        | 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 ->
@@ -2085,17 +3454,17 @@ and generate_ocaml_c () =
            pr "  char **r;\n";
            "NULL"
        | RIntBool _ ->
            pr "  char **r;\n";
            "NULL"
        | RIntBool _ ->
-           pr "  struct guestfs_int_bool *r;\n";
-           "NULL"
+           pr "  struct guestfs_int_bool *r;\n"; "NULL"
        | RPVList _ ->
        | RPVList _ ->
-           pr "  struct guestfs_lvm_pv_list *r;\n";
-           "NULL"
+           pr "  struct guestfs_lvm_pv_list *r;\n"; "NULL"
        | RVGList _ ->
        | RVGList _ ->
-           pr "  struct guestfs_lvm_vg_list *r;\n";
-           "NULL"
+           pr "  struct guestfs_lvm_vg_list *r;\n"; "NULL"
        | RLVList _ ->
        | RLVList _ ->
-           pr "  struct guestfs_lvm_lv_list *r;\n";
-           "NULL" in
+           pr "  struct guestfs_lvm_lv_list *r;\n"; "NULL"
+       | RStat _ ->
+           pr "  struct guestfs_stat *r;\n"; "NULL"
+       | RStatVFS _ ->
+           pr "  struct guestfs_statvfs *r;\n"; "NULL" in
       pr "\n";
 
       pr "  caml_enter_blocking_section ();\n";
       pr "\n";
 
       pr "  caml_enter_blocking_section ();\n";
@@ -2103,6 +3472,14 @@ and generate_ocaml_c () =
       generate_call_args ~handle:"g" style;
       pr ";\n";
       pr "  caml_leave_blocking_section ();\n";
       generate_call_args ~handle:"g" style;
       pr ";\n";
       pr "  caml_leave_blocking_section ();\n";
+
+      List.iter (
+       function
+       | StringList n ->
+           pr "  ocaml_guestfs_free_strings (%s);\n" n;
+       | String _ | OptString _ | Bool _ | Int _ -> ()
+      ) (snd style);
+
       pr "  if (r == %s)\n" error_code;
       pr "    ocaml_guestfs_raise_error (g, \"%s\");\n" name;
       pr "\n";
       pr "  if (r == %s)\n" error_code;
       pr "    ocaml_guestfs_raise_error (g, \"%s\");\n" name;
       pr "\n";
@@ -2133,11 +3510,28 @@ and generate_ocaml_c () =
        | RLVList _ ->
           pr "  rv = copy_lvm_lv_list (r);\n";
           pr "  guestfs_free_lvm_lv_list (r);\n";
        | RLVList _ ->
           pr "  rv = copy_lvm_lv_list (r);\n";
           pr "  guestfs_free_lvm_lv_list (r);\n";
+       | RStat _ ->
+          pr "  rv = copy_stat (r);\n";
+          pr "  free (r);\n";
+       | RStatVFS _ ->
+          pr "  rv = copy_statvfs (r);\n";
+          pr "  free (r);\n";
       );
 
       pr "  CAMLreturn (rv);\n";
       pr "}\n";
       );
 
       pr "  CAMLreturn (rv);\n";
       pr "}\n";
-      pr "\n"
+      pr "\n";
+
+      if List.length params > 5 then (
+       pr "CAMLprim value\n";
+       pr "ocaml_guestfs_%s_byte (value *argv, int argn)\n" name;
+       pr "{\n";
+       pr "  return ocaml_guestfs_%s (argv[0]" name;
+       iteri (fun i _ -> pr ", argv[%d]" i) (List.tl params);
+       pr ");\n";
+       pr "}\n";
+       pr "\n"
+      )
   ) all_functions
 
 and generate_ocaml_lvm_structure_decls () =
   ) all_functions
 
 and generate_ocaml_lvm_structure_decls () =
@@ -2156,6 +3550,18 @@ and generate_ocaml_lvm_structure_decls () =
       pr "\n"
   ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols]
 
       pr "\n"
   ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols]
 
+and generate_ocaml_stat_structure_decls () =
+  List.iter (
+    fun (typ, cols) ->
+      pr "type %s = {\n" typ;
+      List.iter (
+       function
+       | name, `Int -> pr "  %s : int64;\n" name
+      ) cols;
+      pr "}\n";
+      pr "\n"
+  ) ["stat", stat_cols; "statvfs", statvfs_cols]
+
 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;
@@ -2163,6 +3569,7 @@ and generate_ocaml_prototype ?(is_external = false) name style =
     function
     | String _ -> pr "string -> "
     | OptString _ -> pr "string option -> "
     function
     | String _ -> pr "string -> "
     | OptString _ -> pr "string option -> "
+    | StringList _ -> pr "string array -> "
     | Bool _ -> pr "bool -> "
     | Int _ -> pr "int -> "
   ) (snd style);
     | Bool _ -> pr "bool -> "
     | Int _ -> pr "int -> "
   ) (snd style);
@@ -2177,8 +3584,15 @@ and generate_ocaml_prototype ?(is_external = false) name style =
    | RPVList _ -> pr "lvm_pv array"
    | RVGList _ -> pr "lvm_vg array"
    | RLVList _ -> pr "lvm_lv array"
    | RPVList _ -> pr "lvm_pv array"
    | RVGList _ -> pr "lvm_vg array"
    | RLVList _ -> pr "lvm_lv array"
+   | RStat _ -> pr "stat"
+   | RStatVFS _ -> pr "statvfs"
+  );
+  if is_external then (
+    pr " = ";
+    if List.length (snd style) + 1 > 5 then
+      pr "\"ocaml_guestfs_%s_byte\" " name;
+    pr "\"ocaml_guestfs_%s\"" name
   );
   );
-  if is_external then pr " = \"ocaml_guestfs_%s\"" name;
   pr "\n"
 
 (* Generate Perl xs code, a sort of crazy variation of C with macros. *)
   pr "\n"
 
 (* Generate Perl xs code, a sort of crazy variation of C with macros. *)
@@ -2224,20 +3638,32 @@ my_newSVull(unsigned long long val) {
 #endif
 }
 
 #endif
 }
 
-/* XXX Not thread-safe, and in general not safe if the caller is
- * issuing multiple requests in parallel (on different guestfs
- * handles).  We should use the guestfs_h handle passed to the
- * error handle to distinguish these cases.
- */
-static char *last_error = NULL;
+/* http://www.perlmonks.org/?node_id=680842 */
+static char **
+XS_unpack_charPtrPtr (SV *arg) {
+  char **ret;
+  AV *av;
+  I32 i;
 
 
-static void
-error_handler (guestfs_h *g,
-              void *data,
-              const char *msg)
-{
-  if (last_error != NULL) free (last_error);
-  last_error = strdup (msg);
+  if (!arg || !SvOK (arg) || !SvROK (arg) || SvTYPE (SvRV (arg)) != SVt_PVAV) {
+    croak (\"array reference expected\");
+  }
+
+  av = (AV *)SvRV (arg);
+  ret = (char **)malloc (av_len (av) + 1 + 1);
+
+  for (i = 0; i <= av_len (av); i++) {
+    SV **elem = av_fetch (av, i, 0);
+
+    if (!elem || !*elem)
+      croak (\"missing element in list\");
+
+    ret[i] = SvPV_nolen (*elem);
+  }
+
+  ret[i] = NULL;
+
+  return ret;
 }
 
 MODULE = Sys::Guestfs  PACKAGE = Sys::Guestfs
 }
 
 MODULE = Sys::Guestfs  PACKAGE = Sys::Guestfs
@@ -2248,7 +3674,7 @@ _create ()
       RETVAL = guestfs_create ();
       if (!RETVAL)
         croak (\"could not create guestfs handle\");
       RETVAL = guestfs_create ();
       if (!RETVAL)
         croak (\"could not create guestfs handle\");
-      guestfs_set_error_handler (RETVAL, error_handler, NULL);
+      guestfs_set_error_handler (RETVAL, NULL, NULL);
  OUTPUT:
       RETVAL
 
  OUTPUT:
       RETVAL
 
@@ -2261,7 +3687,7 @@ DESTROY (g)
 ";
 
   List.iter (
 ";
 
   List.iter (
-    fun (name, style, _, _, _, _) ->
+    fun (name, style, _, _, _, _, _) ->
       (match fst style with
        | RErr -> pr "void\n"
        | RInt _ -> pr "SV *\n"
       (match fst style with
        | RErr -> pr "void\n"
        | RInt _ -> pr "SV *\n"
@@ -2270,7 +3696,8 @@ DESTROY (g)
        | RString _ -> pr "SV *\n"
        | RStringList _
        | RIntBool _
        | RString _ -> pr "SV *\n"
        | RStringList _
        | RIntBool _
-       | RPVList _ | RVGList _ | RLVList _ ->
+       | RPVList _ | RVGList _ | RLVList _
+       | RStat _ | RStatVFS _ ->
           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. *)
@@ -2282,17 +3709,34 @@ DESTROY (g)
        function
        | String n -> pr "      char *%s;\n" n
        | OptString n -> pr "      char *%s;\n" n
        function
        | String 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
       ) (snd style);
        | Bool n -> pr "      int %s;\n" n
        | Int n -> pr "      int %s;\n" n
       ) (snd style);
+
+      let do_cleanups () =
+       List.iter (
+         function
+         | String _
+         | OptString _
+         | Bool _
+         | Int _ -> ()
+         | StringList n -> pr "      free (%s);\n" n
+       ) (snd style)
+      in
+
       (* Code. *)
       (match fst style with
        | RErr ->
       (* Code. *)
       (match fst style with
        | RErr ->
+          pr "PREINIT:\n";
+          pr "      int r;\n";
           pr " PPCODE:\n";
           pr " PPCODE:\n";
-          pr "      if (guestfs_%s " name;
+          pr "      r = guestfs_%s " name;
           generate_call_args ~handle:"g" style;
           generate_call_args ~handle:"g" style;
-          pr " == -1)\n";
-          pr "        croak (\"%s: %%s\", last_error);\n" name
+          pr ";\n";
+          do_cleanups ();
+          pr "      if (r == -1)\n";
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
        | RInt n
        | RBool n ->
           pr "PREINIT:\n";
        | RInt n
        | RBool n ->
           pr "PREINIT:\n";
@@ -2301,8 +3745,9 @@ DESTROY (g)
           pr "      %s = guestfs_%s " n name;
           generate_call_args ~handle:"g" style;
           pr ";\n";
           pr "      %s = guestfs_%s " n name;
           generate_call_args ~handle:"g" style;
           pr ";\n";
+          do_cleanups ();
           pr "      if (%s == -1)\n" n;
           pr "      if (%s == -1)\n" n;
-          pr "        croak (\"%s: %%s\", last_error);\n" name;
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
           pr "      RETVAL = newSViv (%s);\n" n;
           pr " OUTPUT:\n";
           pr "      RETVAL\n"
           pr "      RETVAL = newSViv (%s);\n" n;
           pr " OUTPUT:\n";
           pr "      RETVAL\n"
@@ -2313,8 +3758,9 @@ DESTROY (g)
           pr "      %s = guestfs_%s " n name;
           generate_call_args ~handle:"g" style;
           pr ";\n";
           pr "      %s = guestfs_%s " n name;
           generate_call_args ~handle:"g" style;
           pr ";\n";
+          do_cleanups ();
           pr "      if (%s == NULL)\n" n;
           pr "      if (%s == NULL)\n" n;
-          pr "        croak (\"%s: %%s\", last_error);\n" name;
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
           pr "      RETVAL = newSVpv (%s, 0);\n" n;
           pr " OUTPUT:\n";
           pr "      RETVAL\n"
           pr "      RETVAL = newSVpv (%s, 0);\n" n;
           pr " OUTPUT:\n";
           pr "      RETVAL\n"
@@ -2325,8 +3771,9 @@ DESTROY (g)
           pr "      %s = guestfs_%s " n name;
           generate_call_args ~handle:"g" style;
           pr ";\n";
           pr "      %s = guestfs_%s " n name;
           generate_call_args ~handle:"g" style;
           pr ";\n";
+          do_cleanups ();
           pr "      if (%s == NULL)\n" n;
           pr "      if (%s == NULL)\n" n;
-          pr "        croak (\"%s: %%s\", last_error);\n" name;
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
           pr "      RETVAL = newSVpv (%s, 0);\n" n;
           pr "      free (%s);\n" n;
           pr " OUTPUT:\n";
           pr "      RETVAL = newSVpv (%s, 0);\n" n;
           pr "      free (%s);\n" n;
           pr " OUTPUT:\n";
@@ -2339,8 +3786,9 @@ DESTROY (g)
           pr "      %s = guestfs_%s " n name;
           generate_call_args ~handle:"g" style;
           pr ";\n";
           pr "      %s = guestfs_%s " n name;
           generate_call_args ~handle:"g" style;
           pr ";\n";
+          do_cleanups ();
           pr "      if (%s == NULL)\n" n;
           pr "      if (%s == NULL)\n" n;
-          pr "        croak (\"%s: %%s\", last_error);\n" name;
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
           pr "      for (n = 0; %s[n] != NULL; ++n) /**/;\n" n;
           pr "      EXTEND (SP, n);\n";
           pr "      for (i = 0; i < n; ++i) {\n";
           pr "      for (n = 0; %s[n] != NULL; ++n) /**/;\n" n;
           pr "      EXTEND (SP, n);\n";
           pr "      for (i = 0; i < n; ++i) {\n";
@@ -2355,23 +3803,30 @@ DESTROY (g)
           pr "      r = guestfs_%s " name;
           generate_call_args ~handle:"g" style;
           pr ";\n";
           pr "      r = guestfs_%s " name;
           generate_call_args ~handle:"g" style;
           pr ";\n";
+          do_cleanups ();
           pr "      if (r == NULL)\n";
           pr "      if (r == NULL)\n";
-          pr "        croak (\"%s: %%s\", last_error);\n" name;
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
           pr "      EXTEND (SP, 2);\n";
           pr "      PUSHs (sv_2mortal (newSViv (r->i)));\n";
           pr "      PUSHs (sv_2mortal (newSViv (r->b)));\n";
           pr "      guestfs_free_int_bool (r);\n";
        | RPVList n ->
           pr "      EXTEND (SP, 2);\n";
           pr "      PUSHs (sv_2mortal (newSViv (r->i)));\n";
           pr "      PUSHs (sv_2mortal (newSViv (r->b)));\n";
           pr "      guestfs_free_int_bool (r);\n";
        | RPVList n ->
-          generate_perl_lvm_code "pv" pv_cols name style n;
+          generate_perl_lvm_code "pv" pv_cols name style n do_cleanups
        | RVGList n ->
        | RVGList n ->
-          generate_perl_lvm_code "vg" vg_cols name style n;
+          generate_perl_lvm_code "vg" vg_cols name style n do_cleanups
        | RLVList n ->
        | RLVList n ->
-          generate_perl_lvm_code "lv" lv_cols name style n;
+          generate_perl_lvm_code "lv" lv_cols name style n do_cleanups
+       | RStat n ->
+          generate_perl_stat_code "stat" stat_cols name style n do_cleanups
+       | RStatVFS n ->
+          generate_perl_stat_code
+            "statvfs" statvfs_cols name style n do_cleanups
       );
       );
+
       pr "\n"
   ) all_functions
 
       pr "\n"
   ) all_functions
 
-and generate_perl_lvm_code typ cols name style n =
+and generate_perl_lvm_code typ cols name style n do_cleanups =
   pr "PREINIT:\n";
   pr "      struct guestfs_lvm_%s_list *%s;\n" typ n;
   pr "      int i;\n";
   pr "PREINIT:\n";
   pr "      struct guestfs_lvm_%s_list *%s;\n" typ n;
   pr "      int i;\n";
@@ -2380,8 +3835,9 @@ and generate_perl_lvm_code typ cols name style n =
   pr "      %s = guestfs_%s " n name;
   generate_call_args ~handle:"g" style;
   pr ";\n";
   pr "      %s = guestfs_%s " n name;
   generate_call_args ~handle:"g" style;
   pr ";\n";
+  do_cleanups ();
   pr "      if (%s == NULL)\n" n;
   pr "      if (%s == NULL)\n" n;
-  pr "        croak (\"%s: %%s\", last_error);\n" name;
+  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";
   pr "      EXTEND (SP, %s->len);\n" n;
   pr "      for (i = 0; i < %s->len; ++i) {\n" n;
   pr "        hv = newHV ();\n";
@@ -2407,6 +3863,24 @@ and generate_perl_lvm_code typ cols name style 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
 
+and generate_perl_stat_code typ cols name style n do_cleanups =
+  pr "PREINIT:\n";
+  pr "      struct guestfs_%s *%s;\n" typ n;
+  pr " PPCODE:\n";
+  pr "      %s = guestfs_%s " n name;
+  generate_call_args ~handle:"g" style;
+  pr ";\n";
+  do_cleanups ();
+  pr "      if (%s == NULL)\n" n;
+  pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
+  pr "      EXTEND (SP, %d);\n" (List.length cols);
+  List.iter (
+    function
+    | name, `Int ->
+       pr "      PUSHs (sv_2mortal (my_newSVll (%s->%s)));\n" n name
+  ) cols;
+  pr "      free (%s);\n" 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;
@@ -2491,16 +3965,16 @@ sub new {
    * they are pulled in from the XS code automatically.
    *)
   List.iter (
    * they are pulled in from the XS code automatically.
    *)
   List.iter (
-    fun (name, style, _, flags, _, longdesc) ->
+    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
       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 "Because of the message protocol, there is a transfer limit 
-of somewhere between 2MB and 4MB.  To transfer large files you should use
-FTP.\n\n";
+       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. *)
@@ -2538,6 +4012,8 @@ and generate_perl_prototype name style =
    | RPVList n
    | RVGList n
    | RLVList n -> pr "@%s = " n
    | RPVList n
    | RVGList n
    | RLVList n -> pr "@%s = " n
+   | RStat n
+   | RStatVFS n -> pr "%%%s = " n
   );
   pr "$h->%s (" name;
   let comma = ref false in
   );
   pr "$h->%s (" name;
   let comma = ref false in
@@ -2545,10 +4021,391 @@ and generate_perl_prototype name style =
     fun arg ->
       if !comma then pr ", ";
       comma := true;
     fun arg ->
       if !comma then pr ", ";
       comma := true;
-      pr "%s" (name_of_argt arg)
+      match arg with
+      | String n | OptString n | Bool n | Int n ->
+         pr "$%s" n
+      | StringList n ->
+         pr "\\@%s" n
   ) (snd style);
   pr ");"
 
   ) (snd style);
   pr ");"
 
+(* Generate Python C module. *)
+and generate_python_c () =
+  generate_header CStyle LGPLv2;
+
+  pr "\
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+
+#include <Python.h>
+
+#include \"guestfs.h\"
+
+typedef struct {
+  PyObject_HEAD
+  guestfs_h *g;
+} Pyguestfs_Object;
+
+static guestfs_h *
+get_handle (PyObject *obj)
+{
+  assert (obj);
+  assert (obj != Py_None);
+  return ((Pyguestfs_Object *) obj)->g;
+}
+
+static PyObject *
+put_handle (guestfs_h *g)
+{
+  assert (g);
+  return
+    PyCObject_FromVoidPtrAndDesc ((void *) g, (char *) \"guestfs_h\", NULL);
+}
+
+/* This list should be freed (but not the strings) after use. */
+static const char **
+get_string_list (PyObject *obj)
+{
+  int i, len;
+  const char **r;
+
+  assert (obj);
+
+  if (!PyList_Check (obj)) {
+    PyErr_SetString (PyExc_RuntimeError, \"expecting a list parameter\");
+    return NULL;
+  }
+
+  len = PyList_Size (obj);
+  r = malloc (sizeof (char *) * (len+1));
+  if (r == NULL) {
+    PyErr_SetString (PyExc_RuntimeError, \"get_string_list: out of memory\");
+    return NULL;
+  }
+
+  for (i = 0; i < len; ++i)
+    r[i] = PyString_AsString (PyList_GetItem (obj, i));
+  r[len] = NULL;
+
+  return r;
+}
+
+static PyObject *
+put_string_list (char * const * const argv)
+{
+  PyObject *list;
+  int argc, i;
+
+  for (argc = 0; argv[argc] != NULL; ++argc)
+    ;
+
+  list = PyList_New (argc);
+  for (i = 0; i < argc; ++i)
+    PyList_SetItem (list, i, PyString_FromString (argv[i]));
+
+  return list;
+}
+
+static void
+free_strings (char **argv)
+{
+  int argc;
+
+  for (argc = 0; argv[argc] != NULL; ++argc)
+    free (argv[argc]);
+  free (argv);
+}
+
+static PyObject *
+py_guestfs_create (PyObject *self, PyObject *args)
+{
+  guestfs_h *g;
+
+  g = guestfs_create ();
+  if (g == NULL) {
+    PyErr_SetString (PyExc_RuntimeError,
+                     \"guestfs.create: failed to allocate handle\");
+    return NULL;
+  }
+  guestfs_set_error_handler (g, NULL, NULL);
+  return put_handle (g);
+}
+
+static PyObject *
+py_guestfs_close (PyObject *self, PyObject *args)
+{
+  PyObject *py_g;
+  guestfs_h *g;
+
+  if (!PyArg_ParseTuple (args, (char *) \"O:guestfs_close\", &py_g))
+    return NULL;
+  g = get_handle (py_g);
+
+  guestfs_close (g);
+
+  Py_INCREF (Py_None);
+  return Py_None;
+}
+
+";
+
+  (* LVM structures, turned into Python dictionaries. *)
+  List.iter (
+    fun (typ, cols) ->
+      pr "static PyObject *\n";
+      pr "put_lvm_%s (struct guestfs_lvm_%s *%s)\n" typ typ typ;
+      pr "{\n";
+      pr "  PyObject *dict;\n";
+      pr "\n";
+      pr "  dict = PyDict_New ();\n";
+      List.iter (
+       function
+       | name, `String ->
+           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                        PyString_FromString (%s->%s));\n"
+             typ name
+       | name, `UUID ->
+           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                        PyString_FromStringAndSize (%s->%s, 32));\n"
+             typ name
+       | name, `Bytes ->
+           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                        PyLong_FromUnsignedLongLong (%s->%s));\n"
+             typ name
+       | name, `Int ->
+           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                        PyLong_FromLongLong (%s->%s));\n"
+             typ name
+       | name, `OptPercent ->
+           pr "  if (%s->%s >= 0)\n" typ name;
+           pr "    PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                          PyFloat_FromDouble ((double) %s->%s));\n"
+             typ name;
+           pr "  else {\n";
+           pr "    Py_INCREF (Py_None);\n";
+           pr "    PyDict_SetItemString (dict, \"%s\", Py_None);" name;
+           pr "  }\n"
+      ) cols;
+      pr "  return dict;\n";
+      pr "};\n";
+      pr "\n";
+
+      pr "static PyObject *\n";
+      pr "put_lvm_%s_list (struct guestfs_lvm_%s_list *%ss)\n" typ typ typ;
+      pr "{\n";
+      pr "  PyObject *list;\n";
+      pr "  int i;\n";
+      pr "\n";
+      pr "  list = PyList_New (%ss->len);\n" typ;
+      pr "  for (i = 0; i < %ss->len; ++i)\n" typ;
+      pr "    PyList_SetItem (list, i, put_lvm_%s (&%ss->val[i]));\n" typ typ;
+      pr "  return list;\n";
+      pr "};\n";
+      pr "\n"
+  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
+
+  (* Stat structures, turned into Python dictionaries. *)
+  List.iter (
+    fun (typ, cols) ->
+      pr "static PyObject *\n";
+      pr "put_%s (struct guestfs_%s *%s)\n" typ typ typ;
+      pr "{\n";
+      pr "  PyObject *dict;\n";
+      pr "\n";
+      pr "  dict = PyDict_New ();\n";
+      List.iter (
+       function
+       | name, `Int ->
+           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                        PyLong_FromLongLong (%s->%s));\n"
+             typ name
+      ) cols;
+      pr "  return dict;\n";
+      pr "};\n";
+      pr "\n";
+  ) ["stat", stat_cols; "statvfs", statvfs_cols];
+
+  (* Python wrapper functions. *)
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      pr "static PyObject *\n";
+      pr "py_guestfs_%s (PyObject *self, PyObject *args)\n" name;
+      pr "{\n";
+
+      pr "  PyObject *py_g;\n";
+      pr "  guestfs_h *g;\n";
+      pr "  PyObject *py_r;\n";
+
+      let error_code =
+       match fst style with
+       | RErr | RInt _ | RBool _ -> pr "  int r;\n"; "-1"
+       | RConstString _ -> pr "  const char *r;\n"; "NULL"
+       | RString _ -> pr "  char *r;\n"; "NULL"
+       | RStringList _ -> pr "  char **r;\n"; "NULL"
+       | RIntBool _ -> pr "  struct guestfs_int_bool *r;\n"; "NULL"
+       | RPVList n -> pr "  struct guestfs_lvm_pv_list *r;\n"; "NULL"
+       | RVGList n -> pr "  struct guestfs_lvm_vg_list *r;\n"; "NULL"
+       | RLVList n -> pr "  struct guestfs_lvm_lv_list *r;\n"; "NULL"
+       | RStat n -> pr "  struct guestfs_stat *r;\n"; "NULL"
+       | RStatVFS n -> pr "  struct guestfs_statvfs *r;\n"; "NULL" in
+
+      List.iter (
+       function
+       | String n -> pr "  const char *%s;\n" n
+       | OptString n -> pr "  const char *%s;\n" n
+       | StringList n ->
+           pr "  PyObject *py_%s;\n" n;
+           pr "  const char **%s;\n" n
+       | Bool n -> pr "  int %s;\n" n
+       | Int n -> pr "  int %s;\n" n
+      ) (snd style);
+
+      pr "\n";
+
+      (* Convert the parameters. *)
+      pr "  if (!PyArg_ParseTuple (args, (char *) \"O";
+      List.iter (
+       function
+       | String _ -> pr "s"
+       | OptString _ -> pr "z"
+       | StringList _ -> pr "O"
+       | Bool _ -> pr "i" (* XXX Python has booleans? *)
+       | Int _ -> pr "i"
+      ) (snd style);
+      pr ":guestfs_%s\",\n" name;
+      pr "                         &py_g";
+      List.iter (
+       function
+       | String n -> pr ", &%s" n
+       | OptString n -> pr ", &%s" n
+       | StringList n -> pr ", &py_%s" n
+       | Bool n -> pr ", &%s" n
+       | Int n -> pr ", &%s" n
+      ) (snd style);
+
+      pr "))\n";
+      pr "    return NULL;\n";
+
+      pr "  g = get_handle (py_g);\n";
+      List.iter (
+       function
+       | String _ | OptString _ | Bool _ | Int _ -> ()
+       | StringList n ->
+           pr "  %s = get_string_list (py_%s);\n" n n;
+           pr "  if (!%s) return NULL;\n" n
+      ) (snd style);
+
+      pr "\n";
+
+      pr "  r = guestfs_%s " name;
+      generate_call_args ~handle:"g" style;
+      pr ";\n";
+
+      List.iter (
+       function
+       | String _ | OptString _ | Bool _ | Int _ -> ()
+       | StringList n ->
+           pr "  free (%s);\n" n
+      ) (snd style);
+
+      pr "  if (r == %s) {\n" error_code;
+      pr "    PyErr_SetString (PyExc_RuntimeError, guestfs_last_error (g));\n";
+      pr "    return NULL;\n";
+      pr "  }\n";
+      pr "\n";
+
+      (match fst style with
+       | RErr ->
+          pr "  Py_INCREF (Py_None);\n";
+          pr "  py_r = Py_None;\n"
+       | RInt _
+       | RBool _ -> pr "  py_r = PyInt_FromLong ((long) r);\n"
+       | RConstString _ -> pr "  py_r = PyString_FromString (r);\n"
+       | RString _ ->
+          pr "  py_r = PyString_FromString (r);\n";
+          pr "  free (r);\n"
+       | RStringList _ ->
+          pr "  py_r = put_string_list (r);\n";
+          pr "  free_strings (r);\n"
+       | RIntBool _ ->
+          pr "  py_r = PyTuple_New (2);\n";
+          pr "  PyTuple_SetItem (py_r, 0, PyInt_FromLong ((long) r->i));\n";
+          pr "  PyTuple_SetItem (py_r, 1, PyInt_FromLong ((long) r->b));\n";
+          pr "  guestfs_free_int_bool (r);\n"
+       | RPVList n ->
+          pr "  py_r = put_lvm_pv_list (r);\n";
+          pr "  guestfs_free_lvm_pv_list (r);\n"
+       | RVGList n ->
+          pr "  py_r = put_lvm_vg_list (r);\n";
+          pr "  guestfs_free_lvm_vg_list (r);\n"
+       | RLVList n ->
+          pr "  py_r = put_lvm_lv_list (r);\n";
+          pr "  guestfs_free_lvm_lv_list (r);\n"
+       | RStat n ->
+          pr "  py_r = put_stat (r);\n";
+          pr "  free (r);\n"
+       | RStatVFS n ->
+          pr "  py_r = put_statvfs (r);\n";
+          pr "  free (r);\n"
+      );
+
+      pr "  return py_r;\n";
+      pr "}\n";
+      pr "\n"
+  ) all_functions;
+
+  (* Table of functions. *)
+  pr "static PyMethodDef methods[] = {\n";
+  pr "  { (char *) \"create\", py_guestfs_create, METH_VARARGS, NULL },\n";
+  pr "  { (char *) \"close\", py_guestfs_close, METH_VARARGS, NULL },\n";
+  List.iter (
+    fun (name, _, _, _, _, _, _) ->
+      pr "  { (char *) \"%s\", py_guestfs_%s, METH_VARARGS, NULL },\n"
+       name name
+  ) all_functions;
+  pr "  { NULL, NULL, 0, NULL }\n";
+  pr "};\n";
+  pr "\n";
+
+  (* Init function. *)
+  pr "\
+void
+initlibguestfsmod (void)
+{
+  static int initialized = 0;
+
+  if (initialized) return;
+  Py_InitModule ((char *) \"libguestfsmod\", methods);
+  initialized = 1;
+}
+"
+
+(* Generate Python module. *)
+and generate_python_py () =
+  generate_header HashStyle LGPLv2;
+
+  pr "import libguestfsmod\n";
+  pr "\n";
+  pr "class GuestFS:\n";
+  pr "    def __init__ (self):\n";
+  pr "        self._o = libguestfsmod.create ()\n";
+  pr "\n";
+  pr "    def __del__ (self):\n";
+  pr "        libguestfsmod.close (self._o)\n";
+  pr "\n";
+
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      pr "    def %s " name;
+      generate_call_args ~handle:"self" style;
+      pr ":\n";
+      pr "        return libguestfsmod.%s " name;
+      generate_call_args ~handle:"self._o" style;
+      pr "\n";
+      pr "\n";
+  ) all_functions
+
 let output_to filename =
   let filename_new = filename ^ ".new" in
   chan := open_out filename_new;
 let output_to filename =
   let filename_new = filename ^ ".new" in
   chan := open_out filename_new;
@@ -2597,10 +4454,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
+  generate_tests ();
+  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 ();
 
+  let close = output_to "fish/completion.c" in
+  generate_fish_completion ();
+  close ();
+
   let close = output_to "guestfs-structs.pod" in
   generate_structs_pod ();
   close ();
   let close = output_to "guestfs-structs.pod" in
   generate_structs_pod ();
   close ();
@@ -2632,3 +4497,11 @@ Run it from the top source directory using the command
   let close = output_to "perl/lib/Sys/Guestfs.pm" in
   generate_perl_pm ();
   close ();
   let close = output_to "perl/lib/Sys/Guestfs.pm" in
   generate_perl_pm ();
   close ();
+
+  let close = output_to "python/guestfs-py.c" in
+  generate_python_c ();
+  close ();
+
+  let close = output_to "python/guestfs.py" in
+  generate_python_py ();
+  close ();