Allow Ruby bindings to build correctly even if libguestfs not installed.
[libguestfs.git] / src / generator.ml
index 06a95a9..c0a4740 100755 (executable)
@@ -43,9 +43,15 @@ and ret =
      *)
   | RErr
     (* "RInt" as a return value means an int which is -1 for error
      *)
   | RErr
     (* "RInt" as a return value means an int which is -1 for error
-     * or any value >= 0 on success.
+     * or any value >= 0 on success.  Only use this for smallish
+     * positive ints (0 <= i < 2^30).
      *)
   | RInt of string
      *)
   | RInt of string
+    (* "RInt64" is the same as RInt, but is guaranteed to be able
+     * to return a full 64 bit value, _except_ that -1 means error
+     * (so -1 cannot be a valid, non-error return value).
+     *)
+  | RInt64 of string
     (* "RBool" is a bool return value which can be true/false or
      * -1 for error.
      *)
     (* "RBool" is a bool return value which can be true/false or
      * -1 for error.
      *)
@@ -65,6 +71,18 @@ 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
+    (* Key-value pairs of untyped strings.  Turns into a hashtable or
+     * dictionary in languages which support it.  DON'T use this as a
+     * general "bucket" for results.  Prefer a stronger typed return
+     * value if one is available, or write a custom struct.  Don't use
+     * this if the list could potentially be very long, since it is
+     * inefficient.  Keys should be unique.  NULLs are not permitted.
+     *)
+  | RHashtable of string
+
 and args = argt list   (* Function parameters, guestfs handle is implicit. *)
 
     (* Note in future we should allow a "variable args" parameter as
 and args = argt list   (* Function parameters, guestfs handle is implicit. *)
 
     (* Note in future we should allow a "variable args" parameter as
@@ -107,7 +125,7 @@ can easily destroy all your data>."
  * the virtual machine and block devices are reused between tests.
  * So don't try testing kill_subprocess :-x
  *
  * 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.
+ * 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.
  *
  * Don't assume anything about the previous contents of the block
  * devices.  Use 'Init*' to create some initial scenarios.
@@ -141,11 +159,21 @@ and test =
      * content).
      *)
   | TestOutputLength of seq * int
      * 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
 
     (* 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
 (* Some initial scenarios for testing. *)
 and test_init =
     (* Do nothing, block devices could contain random stuff including
@@ -475,24 +503,21 @@ 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, [],
-   [InitBasicFSonLVM, TestOutputLength (
-      [["pvs"]], 1)],
+   [], (* 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, [],
-   [InitBasicFSonLVM, TestOutputLength (
-      [["pvs"]], 1)],
+   [], (* 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, [],
-   [InitBasicFSonLVM, TestOutputLength (
-      [["pvs"]], 1)],
+   [], (* XXX how to test? *)
    "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
@@ -889,12 +914,24 @@ 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],
 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")],
+   [InitBasicFS, TestOutput (
+      [["write_file"; "/new"; "new file contents"; "0"];
+       ["cat"; "/new"]], "new file contents");
+    InitBasicFS, TestOutput (
+      [["write_file"; "/new"; "\nnew file contents\n"; "0"];
+       ["cat"; "/new"]], "\nnew file contents\n");
+    InitBasicFS, TestOutput (
+      [["write_file"; "/new"; "\n\n"; "0"];
+       ["cat"; "/new"]], "\n\n");
+    InitBasicFS, TestOutput (
+      [["write_file"; "/new"; ""; "0"];
+       ["cat"; "/new"]], "");
+    InitBasicFS, TestOutput (
+      [["write_file"; "/new"; "\n\n\n"; "0"];
+       ["cat"; "/new"]], "\n\n\n");
+    InitBasicFS, TestOutput (
+      [["write_file"; "/new"; "\n"; "0"];
+       ["cat"; "/new"]], "\n")],
    "create a file",
    "\
 This call creates a file called C<path>.  The contents of the
    "create a file",
    "\
 This call creates a file called C<path>.  The contents of the
@@ -950,6 +987,217 @@ Some internal mounts are not unmounted by this call.");
 This command removes all LVM logical volumes, volume groups
 and physical volumes.");
 
 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.");
+
+  ("tune2fs_l", (RHashtable "superblock", [String "device"]), 55, [],
+   [], (* XXX test *)
+   "get ext2/ext3 superblock details",
+   "\
+This returns the contents of the ext2 or ext3 filesystem superblock
+on C<device>.
+
+It is the same as running C<tune2fs -l device>.  See L<tune2fs(8)>
+manpage for more details.  The list of fields returned isn't
+clearly defined, and depends on both the version of C<tune2fs>
+that libguestfs was built against, and the filesystem itself.");
+
+  ("blockdev_setro", (RErr, [String "device"]), 56, [],
+   [InitEmpty, TestOutputTrue (
+      [["blockdev_setro"; "/dev/sda"];
+       ["blockdev_getro"; "/dev/sda"]])],
+   "set block device to read-only",
+   "\
+Sets the block device named C<device> to read-only.
+
+This uses the L<blockdev(8)> command.");
+
+  ("blockdev_setrw", (RErr, [String "device"]), 57, [],
+   [InitEmpty, TestOutputFalse (
+      [["blockdev_setrw"; "/dev/sda"];
+       ["blockdev_getro"; "/dev/sda"]])],
+   "set block device to read-write",
+   "\
+Sets the block device named C<device> to read-write.
+
+This uses the L<blockdev(8)> command.");
+
+  ("blockdev_getro", (RBool "ro", [String "device"]), 58, [],
+   [InitEmpty, TestOutputTrue (
+      [["blockdev_setro"; "/dev/sda"];
+       ["blockdev_getro"; "/dev/sda"]])],
+   "is block device set to read-only",
+   "\
+Returns a boolean indicating if the block device is read-only
+(true if read-only, false if not).
+
+This uses the L<blockdev(8)> command.");
+
+  ("blockdev_getss", (RInt "sectorsize", [String "device"]), 59, [],
+   [InitEmpty, TestOutputInt (
+      [["blockdev_getss"; "/dev/sda"]], 512)],
+   "get sectorsize of block device",
+   "\
+This returns the size of sectors on a block device.
+Usually 512, but can be larger for modern devices.
+
+(Note, this is not the size in sectors, use C<guestfs_blockdev_getsz>
+for that).
+
+This uses the L<blockdev(8)> command.");
+
+  ("blockdev_getbsz", (RInt "blocksize", [String "device"]), 60, [],
+   [InitEmpty, TestOutputInt (
+      [["blockdev_getbsz"; "/dev/sda"]], 4096)],
+   "get blocksize of block device",
+   "\
+This returns the block size of a device.
+
+(Note this is different from both I<size in blocks> and
+I<filesystem block size>).
+
+This uses the L<blockdev(8)> command.");
+
+  ("blockdev_setbsz", (RErr, [String "device"; Int "blocksize"]), 61, [],
+   [], (* XXX test *)
+   "set blocksize of block device",
+   "\
+This sets the block size of a device.
+
+(Note this is different from both I<size in blocks> and
+I<filesystem block size>).
+
+This uses the L<blockdev(8)> command.");
+
+  ("blockdev_getsz", (RInt64 "sizeinsectors", [String "device"]), 62, [],
+   [InitEmpty, TestOutputInt (
+      [["blockdev_getsz"; "/dev/sda"]], 1024000)],
+   "get total size of device in 512-byte sectors",
+   "\
+This returns the size of the device in units of 512-byte sectors
+(even if the sectorsize isn't 512 bytes ... weird).
+
+See also C<guestfs_blockdev_getss> for the real sector size of
+the device, and C<guestfs_blockdev_getsize64> for the more
+useful I<size in bytes>.
+
+This uses the L<blockdev(8)> command.");
+
+  ("blockdev_getsize64", (RInt64 "sizeinbytes", [String "device"]), 63, [],
+   [InitEmpty, TestOutputInt (
+      [["blockdev_getsize64"; "/dev/sda"]], 524288000)],
+   "get total size of device in bytes",
+   "\
+This returns the size of the device in bytes.
+
+See also C<guestfs_blockdev_getsz>.
+
+This uses the L<blockdev(8)> command.");
+
+  ("blockdev_flushbufs", (RErr, [String "device"]), 64, [],
+   [InitEmpty, TestRun
+      [["blockdev_flushbufs"; "/dev/sda"]]],
+   "flush device buffers",
+   "\
+This tells the kernel to flush internal buffers associated
+with C<device>.
+
+This uses the L<blockdev(8)> command.");
+
+  ("blockdev_rereadpt", (RErr, [String "device"]), 65, [],
+   [InitEmpty, TestRun
+      [["blockdev_rereadpt"; "/dev/sda"]]],
+   "reread partition table",
+   "\
+Reread the partition table on C<device>.
+
+This uses the L<blockdev(8)> command.");
+
 ]
 
 let all_functions = non_daemon_functions @ daemon_functions
 ]
 
 let all_functions = non_daemon_functions @ daemon_functions
@@ -1024,6 +1272,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.
@@ -1041,6 +1322,31 @@ let replace_char s c1 c2 =
   done;
   if not !r then s else s2
 
   done;
   if not !r then s else s2
 
+let isspace c =
+  c = ' '
+  (* || c = '\f' *) || c = '\n' || c = '\r' || c = '\t' (* || c = '\v' *)
+
+let triml ?(test = isspace) str =
+  let i = ref 0 in
+  let n = ref (String.length str) in
+  while !n > 0 && test str.[!i]; do
+    decr n;
+    incr i
+  done;
+  if !i = 0 then str
+  else String.sub str !i !n
+
+let trimr ?(test = isspace) str =
+  let n = ref (String.length str) in
+  while !n > 0 && test str.[!n-1]; do
+    decr n
+  done;
+  if !n = String.length str then str
+  else String.sub str 0 !n
+
+let trim ?(test = isspace) str =
+  trimr ~test (triml ~test str)
+
 let rec find s sub =
   let len = String.length s in
   let sublen = String.length sub in
 let rec find s sub =
   let len = String.length s in
   let sublen = String.length sub in
@@ -1106,6 +1412,12 @@ let mapi f xs =
 let name_of_argt = function
   | String n | OptString n | StringList n | Bool n | Int n -> n
 
 let name_of_argt = function
   | String n | OptString n | StringList n | Bool n | Int n -> n
 
+let seq_of_test = function
+  | TestRun s | TestOutput (s, _) | TestOutputList (s, _)
+  | TestOutputInt (s, _) | TestOutputTrue s | TestOutputFalse s
+  | TestOutputLength (s, _) | TestOutputStruct (s, _)
+  | TestLastFail s -> s
+
 (* Check function names etc. for consistency. *)
 let check_functions () =
   let contains_uppercase str =
 (* Check function names etc. for consistency. *)
 let check_functions () =
   let contains_uppercase str =
@@ -1144,13 +1456,17 @@ 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 -> ()
       in
 
       (match fst style with
        | RErr -> ()
-       | RInt n | RBool n | RConstString n | RString n
-       | RStringList n | RPVList n | RVGList n | RLVList n ->
+       | RInt n | RInt64 n | RBool n | RConstString n | RString n
+       | RStringList n | RPVList n | RVGList n | RLVList n
+       | RStat n | RStatVFS n
+       | RHashtable n ->
           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;
@@ -1203,7 +1519,31 @@ let check_functions () =
        failwithf "%s and %s have conflicting procedure numbers (%d, %d)"
          name1 name2 nr1 nr2
   in
        failwithf "%s and %s have conflicting procedure numbers (%d, %d)"
          name1 name2 nr1 nr2
   in
-  loop proc_nrs
+  loop proc_nrs;
+
+  (* Check tests. *)
+  List.iter (
+    function
+      (* Ignore functions that have no tests.  We generate a
+       * warning when the user does 'make check' instead.
+       *)
+    | name, _, _, _, [], _, _ -> ()
+    | name, _, _, _, tests, _, _ ->
+       let funcs =
+         List.map (
+           fun (_, test) ->
+             match seq_of_test test with
+             | [] ->
+                 failwithf "%s has a test containing an empty sequence" name
+             | cmds -> List.map List.hd cmds
+         ) tests in
+       let funcs = List.flatten funcs in
+
+       let tested = List.mem name funcs in
+
+       if not tested then
+         failwithf "function %s has tests but does not test itself" name
+  ) all_functions
 
 (* 'pr' prints to the current output file. *)
 let chan = ref stdout
 
 (* 'pr' prints to the current output file. *)
 let chan = ref stdout
@@ -1279,30 +1619,55 @@ let rec generate_actions_pod () =
           pr "This function returns 0 on success or -1 on error.\n\n"
        | RInt _ ->
           pr "On error this function returns -1.\n\n"
           pr "This function returns 0 on success or -1 on error.\n\n"
        | RInt _ ->
           pr "On error this function returns -1.\n\n"
+       | RInt64 _ ->
+          pr "On error this function returns -1.\n\n"
        | RBool _ ->
           pr "This function returns a C truth value on success or -1 on error.\n\n"
        | RConstString _ ->
        | 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"
+       | RHashtable _ ->
+          pr "This function returns a NULL-terminated array of
+strings, or NULL if there was an error.
+The array of strings will always have length C<2n+1>, where
+C<n> keys and values alternate, followed by the trailing NULL entry.
+I<The caller must free the strings and the array after use>.\n\n"
       );
       if List.mem ProtocolLimitWarning flags then
        pr "%s\n\n" protocol_limit_warning;
       );
       if List.mem ProtocolLimitWarning flags then
        pr "%s\n\n" protocol_limit_warning;
@@ -1373,6 +1738,18 @@ 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 (
     fun (shortname, style, _, _, _, _, _) ->
       let name = "guestfs_" ^ shortname in
   List.iter (
     fun (shortname, style, _, _, _, _, _) ->
       let name = "guestfs_" ^ shortname in
@@ -1397,6 +1774,10 @@ and generate_xdr () =
           pr "struct %s_ret {\n" name;
           pr "  int %s;\n" n;
           pr "};\n\n"
           pr "struct %s_ret {\n" name;
           pr "  int %s;\n" n;
           pr "};\n\n"
+       | RInt64 n ->
+          pr "struct %s_ret {\n" name;
+          pr "  hyper %s;\n" n;
+          pr "};\n\n"
        | RBool n ->
           pr "struct %s_ret {\n" name;
           pr "  bool %s;\n" n;
        | RBool n ->
           pr "struct %s_ret {\n" name;
           pr "  bool %s;\n" n;
@@ -1428,6 +1809,18 @@ 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"
+       | RHashtable n ->
+          pr "struct %s_ret {\n" name;
+          pr "  str %s<>;\n" n;
+          pr "};\n\n"
       );
   ) daemon_functions;
 
       );
   ) daemon_functions;
 
@@ -1526,7 +1919,20 @@ 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 the guestfs-actions.h file. *)
 and generate_actions_h () =
@@ -1556,10 +1962,12 @@ and generate_client_actions () =
        | RErr -> ()
        | RConstString _ ->
           failwithf "RConstString cannot be returned from a daemon function"
        | RErr -> ()
        | RConstString _ ->
           failwithf "RConstString cannot be returned from a daemon function"
-       | RInt _
+       | RInt _ | RInt64 _
        | RBool _ | RString _ | RStringList _
        | RIntBool _
        | RBool _ | RString _ | RStringList _
        | RIntBool _
-       | RPVList _ | RVGList _ | RLVList _ ->
+       | RPVList _ | RVGList _ | RLVList _
+       | RStat _ | RStatVFS _
+       | RHashtable _ ->
           pr "  struct %s_ret ret;\n" name
       );
       pr "};\n\n";
           pr "  struct %s_ret ret;\n" name
       );
       pr "};\n\n";
@@ -1585,10 +1993,12 @@ and generate_client_actions () =
        | RErr -> ()
        | RConstString _ ->
           failwithf "RConstString cannot be returned from a daemon function"
        | RErr -> ()
        | RConstString _ ->
           failwithf "RConstString cannot be returned from a daemon function"
-       | RInt _
+       | RInt _ | RInt64 _
        | RBool _ | RString _ | RStringList _
        | RIntBool _
        | RBool _ | RString _ | RStringList _
        | RIntBool _
-       | RPVList _ | RVGList _ | RLVList _ ->
+       | RPVList _ | RVGList _ | RLVList _
+       | RStat _ | RStatVFS _
+       | RHashtable _ ->
            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";
@@ -1606,11 +2016,13 @@ and generate_client_actions () =
 
       let error_code =
        match fst style with
 
       let error_code =
        match fst style with
-       | RErr | RInt _ | RBool _ -> "-1"
+       | RErr | RInt _ | RInt64 _ | RBool _ -> "-1"
        | 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 _
+       | RHashtable _ ->
            "NULL" in
 
       pr "{\n";
            "NULL" in
 
       pr "{\n";
@@ -1686,13 +2098,13 @@ and generate_client_actions () =
 
       (match fst style with
        | RErr -> pr "  return 0;\n"
 
       (match fst style with
        | RErr -> pr "  return 0;\n"
-       | RInt n
-       | RBool n -> pr "  return rv.ret.%s;\n" n
+       | RInt n | RInt64 n | RBool n ->
+          pr "  return rv.ret.%s;\n" n
        | RConstString _ ->
           failwithf "RConstString cannot be returned from a daemon function"
        | RString n ->
           pr "  return rv.ret.%s; /* caller will free */\n" n
        | RConstString _ ->
           failwithf "RConstString cannot be returned from a daemon function"
        | RString n ->
           pr "  return rv.ret.%s; /* caller will free */\n" n
-       | RStringList n ->
+       | RStringList n | RHashtable n ->
           pr "  /* caller will free this, but we need to add a NULL entry */\n";
           pr "  rv.ret.%s.%s_val =" n n;
           pr "    safe_realloc (g, rv.ret.%s.%s_val,\n" n n;
           pr "  /* caller will free this, but we need to add a NULL entry */\n";
           pr "  rv.ret.%s.%s_val =" n n;
           pr "    safe_realloc (g, rv.ret.%s.%s_val,\n" n n;
@@ -1703,13 +2115,8 @@ and generate_client_actions () =
        | RIntBool _ ->
           pr "  /* caller with free this */\n";
           pr "  return safe_memdup (g, &rv.ret, sizeof (rv.ret));\n"
        | RIntBool _ ->
           pr "  /* caller with free this */\n";
           pr "  return safe_memdup (g, &rv.ret, sizeof (rv.ret));\n"
-       | RPVList n ->
-          pr "  /* caller will free this */\n";
-          pr "  return safe_memdup (g, &rv.ret.%s, sizeof (rv.ret.%s));\n" n n
-       | RVGList n ->
-          pr "  /* caller will free this */\n";
-          pr "  return safe_memdup (g, &rv.ret.%s, sizeof (rv.ret.%s));\n" n n
-       | RLVList n ->
+       | RPVList n | RVGList n | RLVList n
+       | RStat 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 "  /* caller will free this */\n";
           pr "  return safe_memdup (g, &rv.ret.%s, sizeof (rv.ret.%s));\n" n n
       );
@@ -1758,15 +2165,18 @@ and generate_daemon_actions () =
       let error_code =
        match fst style with
        | RErr | RInt _ -> pr "  int r;\n"; "-1"
       let error_code =
        match fst style with
        | RErr | RInt _ -> pr "  int r;\n"; "-1"
+       | RInt64 _ -> pr "  int64_t r;\n"; "-1"
        | RBool _ -> pr "  int r;\n"; "-1"
        | RConstString _ ->
            failwithf "RConstString cannot be returned from a daemon function"
        | RString _ -> pr "  char *r;\n"; "NULL"
        | RBool _ -> pr "  int r;\n"; "-1"
        | RConstString _ ->
            failwithf "RConstString cannot be returned from a daemon function"
        | RString _ -> pr "  char *r;\n"; "NULL"
-       | RStringList _ -> pr "  char **r;\n"; "NULL"
+       | RStringList _ | RHashtable _ -> pr "  char **r;\n"; "NULL"
        | RIntBool _ -> pr "  guestfs_%s_ret *r;\n" name; "NULL"
        | RPVList _ -> pr "  guestfs_lvm_int_pv_list *r;\n"; "NULL"
        | RVGList _ -> pr "  guestfs_lvm_int_vg_list *r;\n"; "NULL"
        | 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
        | [] -> ()
@@ -1817,11 +2227,7 @@ and generate_daemon_actions () =
 
       (match fst style with
        | RErr -> pr "  reply (NULL, NULL);\n"
 
       (match fst style with
        | RErr -> pr "  reply (NULL, NULL);\n"
-       | RInt 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
-       | RBool n ->
+       | RInt n | RInt64 n | RBool 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 "  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
@@ -1832,7 +2238,7 @@ and generate_daemon_actions () =
           pr "  ret.%s = r;\n" n;
           pr "  reply ((xdrproc_t) &xdr_guestfs_%s_ret, (char *) &ret);\n" name;
           pr "  free (r);\n"
           pr "  ret.%s = r;\n" n;
           pr "  reply ((xdrproc_t) &xdr_guestfs_%s_ret, (char *) &ret);\n" name;
           pr "  free (r);\n"
-       | RStringList n ->
+       | RStringList n | RHashtable n ->
           pr "  struct guestfs_%s_ret ret;\n" name;
           pr "  ret.%s.%s_len = count_strings (r);\n" n n;
           pr "  ret.%s.%s_val = r;\n" n n;
           pr "  struct guestfs_%s_ret ret;\n" name;
           pr "  ret.%s.%s_len = count_strings (r);\n" n n;
           pr "  ret.%s.%s_val = r;\n" n n;
@@ -1841,17 +2247,8 @@ 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 "  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;
@@ -1997,6 +2394,7 @@ and generate_daemon_actions () =
        pr "    reply_with_error (\"%%s\", err);\n";
        pr "    free (out);\n";
        pr "    free (err);\n";
        pr "    reply_with_error (\"%%s\", err);\n";
        pr "    free (out);\n";
        pr "    free (err);\n";
+       pr "    free (ret);\n";
        pr "    return NULL;\n";
        pr "  }\n";
        pr "\n";
        pr "    return NULL;\n";
        pr "  }\n";
        pr "\n";
@@ -2085,13 +2483,40 @@ static void print_strings (char * const * const argv)
     printf (\"\\t%%s\\n\", argv[argc]);
 }
 
     printf (\"\\t%%s\\n\", argv[argc]);
 }
 
+/*
+static void print_table (char * const * const argv)
+{
+  int i;
+
+  for (i = 0; argv[i] != NULL; i += 2)
+    printf (\"%%s: %%s\\n\", argv[i], argv[i+1]);
+}
+*/
+
+static void no_test_warnings (void)
+{
 ";
 
 ";
 
+  List.iter (
+    function
+    | name, _, _, _, [], _, _ ->
+       pr "  fprintf (stderr, \"warning: \\\"guestfs_%s\\\" has no tests\\n\");\n" name
+    | name, _, _, _, tests, _, _ -> ()
+  ) all_functions;
+
+  pr "}\n";
+  pr "\n";
+
+  (* Generate the actual tests.  Note that we generate the tests
+   * in reverse order, deliberately, so that (in general) the
+   * newest tests run first.  This makes it quicker and easier to
+   * debug them.
+   *)
   let test_names =
     List.map (
       fun (name, _, _, _, tests, _, _) ->
        mapi (generate_one_test name) tests
   let test_names =
     List.map (
       fun (name, _, _, _, tests, _, _) ->
        mapi (generate_one_test name) tests
-    ) all_functions in
+    ) (List.rev all_functions) in
   let test_names = List.concat test_names in
   let nr_tests = List.length test_names in
 
   let test_names = List.concat test_names in
   let nr_tests = List.length test_names in
 
@@ -2103,6 +2528,9 @@ int main (int argc, char *argv[])
   const char *srcdir;
   int fd;
   char buf[256];
   const char *srcdir;
   int fd;
   char buf[256];
+  int nr_tests, test_num = 0;
+
+  no_test_warnings ();
 
   g = guestfs_create ();
   if (g == NULL) {
 
   g = guestfs_create ();
   if (g == NULL) {
@@ -2209,11 +2637,14 @@ int main (int argc, char *argv[])
     exit (1);
   }
 
     exit (1);
   }
 
-" (500 * 1024 * 1024) (50 * 1024 * 1024) (10 * 1024 * 1024);
+  nr_tests = %d;
+
+" (500 * 1024 * 1024) (50 * 1024 * 1024) (10 * 1024 * 1024) nr_tests;
 
   iteri (
     fun i test_name ->
 
   iteri (
     fun i test_name ->
-      pr "  printf (\"%3d/%3d %s\\n\");\n" (i+1) nr_tests test_name;
+      pr "  test_num++;\n";
+      pr "  printf (\"%%3d/%%3d %s\\n\", test_num, nr_tests);\n" test_name;
       pr "  if (%s () == -1) {\n" test_name;
       pr "    printf (\"%s FAILED\\n\");\n" test_name;
       pr "    failed++;\n";
       pr "  if (%s () == -1) {\n" test_name;
       pr "    printf (\"%s FAILED\\n\");\n" test_name;
       pr "    failed++;\n";
@@ -2231,8 +2662,7 @@ int main (int argc, char *argv[])
   pr "\n";
 
   pr "  if (failed > 0) {\n";
   pr "\n";
 
   pr "  if (failed > 0) {\n";
-  pr "    printf (\"***** %%d / %d tests FAILED *****\\n\", failed);\n"
-    nr_tests;
+  pr "    printf (\"***** %%d / %%d tests FAILED *****\\n\", failed, nr_tests);\n";
   pr "    exit (1);\n";
   pr "  }\n";
   pr "\n";
   pr "    exit (1);\n";
   pr "  }\n";
   pr "\n";
@@ -2329,8 +2759,9 @@ and generate_one_test name i (init, test) =
        let seq, last = get_seq_last seq in
        let test () =
         pr "    if (r != %d) {\n" expected;
        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"
+        pr "      fprintf (stderr, \"%s: expected %d but got %%d\\n\","
           test_name expected;
           test_name expected;
+        pr "               (int) r);\n";
         pr "      return -1;\n";
         pr "    }\n"
        in
         pr "      return -1;\n";
         pr "    }\n"
        in
@@ -2381,6 +2812,44 @@ and generate_one_test name i (init, test) =
        in
        List.iter (generate_test_command_call test_name) seq;
        generate_test_command_call ~test test_name last
        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
    | TestLastFail seq ->
        pr "  /* TestLastFail for %s (%d) */\n" name i;
        let seq, last = get_seq_last seq in
@@ -2434,24 +2903,25 @@ and generate_test_command_call ?(expect_error = false) ?test test_name cmd =
       let error_code =
        match fst style with
        | RErr | RInt _ | RBool _ -> pr "    int r;\n"; "-1"
       let error_code =
        match fst style with
        | RErr | RInt _ | RBool _ -> pr "    int r;\n"; "-1"
+       | RInt64 _ -> pr "    int64_t r;\n"; "-1"
        | RConstString _ -> pr "    const char *r;\n"; "NULL"
        | RString _ -> pr "    char *r;\n"; "NULL"
        | RConstString _ -> pr "    const char *r;\n"; "NULL"
        | RString _ -> pr "    char *r;\n"; "NULL"
-       | RStringList _ ->
+       | RStringList _ | RHashtable _ ->
            pr "    char **r;\n";
            pr "    int i;\n";
            "NULL"
        | RIntBool _ ->
            pr "    char **r;\n";
            pr "    int i;\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 "    suppress_error = %d;\n" (if expect_error then 1 else 0);
       pr "    r = guestfs_%s (g" name;
 
       pr "    suppress_error = %d;\n" (if expect_error then 1 else 0);
       pr "    r = guestfs_%s (g" name;
@@ -2488,9 +2958,9 @@ and generate_test_command_call ?(expect_error = false) ?test test_name cmd =
       );
 
       (match fst style with
       );
 
       (match fst style with
-       | RErr | RInt _ | RBool _ | RConstString _ -> ()
+       | RErr | RInt _ | RInt64 _ | RBool _ | RConstString _ -> ()
        | RString _ -> pr "    free (r);\n"
        | RString _ -> pr "    free (r);\n"
-       | RStringList _ ->
+       | RStringList _ | RHashtable _ ->
           pr "    for (i = 0; r[i] != NULL; ++i)\n";
           pr "      free (r[i]);\n";
           pr "    free (r);\n"
           pr "    for (i = 0; r[i] != NULL; ++i)\n";
           pr "      free (r[i]);\n";
           pr "    free (r);\n"
@@ -2502,6 +2972,8 @@ and generate_test_command_call ?(expect_error = false) ?test test_name cmd =
           pr "    guestfs_free_lvm_vg_list (r);\n"
        | RLVList _ ->
           pr "    guestfs_free_lvm_lv_list (r);\n"
           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"
       );
 
       pr "  }\n"
@@ -2641,6 +3113,21 @@ and generate_fish_cmds () =
        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 (
     fun (name, style, _, flags, _, _, _) ->
   (* run_<action> actions *)
   List.iter (
     fun (name, style, _, flags, _, _, _) ->
@@ -2650,13 +3137,16 @@ and generate_fish_cmds () =
        | RErr
        | RInt _
        | RBool _ -> pr "  int r;\n"
        | RErr
        | RInt _
        | RBool _ -> pr "  int r;\n"
+       | RInt64 _ -> pr "  int64_t r;\n"
        | RConstString _ -> pr "  const char *r;\n"
        | RString _ -> pr "  char *r;\n"
        | RConstString _ -> pr "  const char *r;\n"
        | RString _ -> pr "  char *r;\n"
-       | RStringList _ -> pr "  char **r;\n"
+       | RStringList _ | RHashtable _ -> pr "  char **r;\n"
        | RIntBool _ -> pr "  struct guestfs_int_bool *r;\n"
        | RPVList _ -> pr "  struct guestfs_lvm_pv_list *r;\n"
        | RVGList _ -> pr "  struct guestfs_lvm_vg_list *r;\n"
        | RLVList _ -> pr "  struct guestfs_lvm_lv_list *r;\n"
        | RIntBool _ -> pr "  struct guestfs_int_bool *r;\n"
        | RPVList _ -> pr "  struct guestfs_lvm_pv_list *r;\n"
        | RVGList _ -> pr "  struct guestfs_lvm_vg_list *r;\n"
        | RLVList _ -> pr "  struct guestfs_lvm_lv_list *r;\n"
+       | RStat _ -> pr "  struct guestfs_stat *r;\n"
+       | RStatVFS _ -> pr "  struct guestfs_statvfs *r;\n"
       );
       List.iter (
        function
       );
       List.iter (
        function
@@ -2703,7 +3193,11 @@ and generate_fish_cmds () =
        | RErr -> pr "  return r;\n"
        | RInt _ ->
           pr "  if (r == -1) return -1;\n";
        | RErr -> pr "  return r;\n"
        | RInt _ ->
           pr "  if (r == -1) return -1;\n";
-          pr "  if (r) printf (\"%%d\\n\", r);\n";
+          pr "  printf (\"%%d\\n\", r);\n";
+          pr "  return 0;\n"
+       | RInt64 _ ->
+          pr "  if (r == -1) return -1;\n";
+          pr "  printf (\"%%\" PRIi64 \"\\n\", r);\n";
           pr "  return 0;\n"
        | RBool _ ->
           pr "  if (r == -1) return -1;\n";
           pr "  return 0;\n"
        | RBool _ ->
           pr "  if (r == -1) return -1;\n";
@@ -2744,6 +3238,21 @@ and generate_fish_cmds () =
           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"
+       | RHashtable _ ->
+          pr "  if (r == NULL) return -1;\n";
+          pr "  print_table (r);\n";
+          pr "  free_strings (r);\n";
+          pr "  return 0;\n"
       );
       pr "}\n";
       pr "\n"
       );
       pr "}\n";
       pr "\n"
@@ -2776,6 +3285,87 @@ and generate_fish_cmds () =
   pr "}\n";
   pr "\n"
 
   pr "}\n";
   pr "\n"
 
+(* Readline completion for guestfish. *)
+and generate_fish_completion () =
+  generate_header CStyle GPLv2;
+
+  let all_functions =
+    List.filter (
+      fun (_, _, _, flags, _, _, _) -> not (List.mem NotInFish flags)
+    ) all_functions in
+
+  pr "\
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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 =
 (* Generate the POD documentation for guestfish. *)
 and generate_fish_actions_pod () =
   let all_functions_sorted =
@@ -2826,10 +3416,11 @@ and generate_prototype ?(extern = true) ?(static = false) ?(semicolon = true)
   (match fst style with
    | RErr -> pr "int "
    | RInt _ -> pr "int "
   (match fst style with
    | RErr -> pr "int "
    | RInt _ -> pr "int "
+   | RInt64 _ -> pr "int64_t "
    | RBool _ -> pr "int "
    | RConstString _ -> pr "const char *"
    | RString _ -> pr "char *"
    | RBool _ -> pr "int "
    | RConstString _ -> pr "const char *"
    | RString _ -> pr "char *"
-   | RStringList _ -> pr "char **"
+   | RStringList _ | RHashtable _ -> pr "char **"
    | RIntBool _ ->
        if not in_daemon then pr "struct guestfs_int_bool *"
        else pr "guestfs_%s_ret *" name
    | RIntBool _ ->
        if not in_daemon then pr "struct guestfs_int_bool *"
        else pr "guestfs_%s_ret *" name
@@ -2842,6 +3433,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
@@ -2917,6 +3514,8 @@ val close : t -> unit
 ";
   generate_ocaml_lvm_structure_decls ();
 
 ";
   generate_ocaml_lvm_structure_decls ();
 
+  generate_ocaml_stat_structure_decls ();
+
   (* The actions. *)
   List.iter (
     fun (name, style, _, _, _, shortdesc, _) ->
   (* The actions. *)
   List.iter (
     fun (name, style, _, _, _, shortdesc, _) ->
@@ -2942,6 +3541,8 @@ let () =
 
   generate_ocaml_lvm_structure_decls ();
 
 
   generate_ocaml_lvm_structure_decls ();
 
+  generate_ocaml_stat_structure_decls ();
+
   (* The actions. *)
   List.iter (
     fun (name, style, _, _, _, shortdesc, _) ->
   (* The actions. *)
   List.iter (
     fun (name, style, _, _, _, shortdesc, _) ->
@@ -2952,25 +3553,54 @@ let () =
 and generate_ocaml_c () =
   generate_header CStyle LGPLv2;
 
 and generate_ocaml_c () =
   generate_header CStyle LGPLv2;
 
-  pr "#include <stdio.h>\n";
-  pr "#include <stdlib.h>\n";
-  pr "#include <string.h>\n";
-  pr "\n";
-  pr "#include <caml/config.h>\n";
-  pr "#include <caml/alloc.h>\n";
-  pr "#include <caml/callback.h>\n";
-  pr "#include <caml/fail.h>\n";
-  pr "#include <caml/memory.h>\n";
-  pr "#include <caml/mlvalues.h>\n";
-  pr "#include <caml/signals.h>\n";
-  pr "\n";
-  pr "#include <guestfs.h>\n";
-  pr "\n";
-  pr "#include \"guestfs_c.h\"\n";
-  pr "\n";
+  pr "\
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
 
 
-  (* LVM struct copy functions. *)
-  List.iter (
+#include <caml/config.h>
+#include <caml/alloc.h>
+#include <caml/callback.h>
+#include <caml/fail.h>
+#include <caml/memory.h>
+#include <caml/mlvalues.h>
+#include <caml/signals.h>
+
+#include <guestfs.h>
+
+#include \"guestfs_c.h\"
+
+/* Copy a hashtable of string pairs into an assoc-list.  We return
+ * the list in reverse order, but hashtables aren't supposed to be
+ * ordered anyway.
+ */
+static CAMLprim value
+copy_table (char * const * argv)
+{
+  CAMLparam0 ();
+  CAMLlocal5 (rv, pairv, kv, vv, cons);
+  int i;
+
+  rv = Val_int (0);
+  for (i = 0; argv[i] != NULL; i += 2) {
+    kv = caml_copy_string (argv[i]);
+    vv = caml_copy_string (argv[i+1]);
+    pairv = caml_alloc (2, 0);
+    Store_field (pairv, 0, kv);
+    Store_field (pairv, 1, vv);
+    cons = caml_alloc (2, 0);
+    Store_field (cons, 1, rv);
+    rv = cons;
+    Store_field (cons, 0, pairv);
+  }
+
+  CAMLreturn (rv);
+}
+
+";
+
+  (* LVM struct copy functions. *)
+  List.iter (
     fun (typ, cols) ->
       let has_optpercent_col =
        List.exists (function (_, `OptPercent) -> true | _ -> false) cols in
     fun (typ, cols) ->
       let has_optpercent_col =
        List.exists (function (_, `OptPercent) -> true | _ -> false) cols in
@@ -3032,6 +3662,30 @@ 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 (
+    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 =
   List.iter (
     fun (name, style, _, _, _, _, _) ->
       let params =
@@ -3078,6 +3732,7 @@ and generate_ocaml_c () =
        match fst style with
        | RErr -> pr "  int r;\n"; "-1"
        | RInt _ -> pr "  int r;\n"; "-1"
        match fst style with
        | RErr -> pr "  int r;\n"; "-1"
        | RInt _ -> pr "  int r;\n"; "-1"
+       | RInt64 _ -> pr "  int64_t r;\n"; "-1"
        | RBool _ -> pr "  int r;\n"; "-1"
        | RConstString _ -> pr "  const char *r;\n"; "NULL"
        | RString _ -> pr "  char *r;\n"; "NULL"
        | RBool _ -> pr "  int r;\n"; "-1"
        | RConstString _ -> pr "  const char *r;\n"; "NULL"
        | RString _ -> pr "  char *r;\n"; "NULL"
@@ -3086,16 +3741,20 @@ 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";
+           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"
+       | RHashtable _ ->
+           pr "  int i;\n";
+           pr "  char **r;\n";
            "NULL" in
       pr "\n";
 
            "NULL" in
       pr "\n";
 
@@ -3119,6 +3778,8 @@ and generate_ocaml_c () =
       (match fst style with
        | RErr -> pr "  rv = Val_unit;\n"
        | RInt _ -> pr "  rv = Val_int (r);\n"
       (match fst style with
        | RErr -> pr "  rv = Val_unit;\n"
        | RInt _ -> pr "  rv = Val_int (r);\n"
+       | RInt64 _ ->
+          pr "  rv = caml_copy_int64 (r);\n"
        | RBool _ -> pr "  rv = Val_bool (r);\n"
        | RConstString _ -> pr "  rv = caml_copy_string (r);\n"
        | RString _ ->
        | RBool _ -> pr "  rv = Val_bool (r);\n"
        | RConstString _ -> pr "  rv = caml_copy_string (r);\n"
        | RString _ ->
@@ -3142,6 +3803,16 @@ 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";
+       | RHashtable _ ->
+          pr "  rv = copy_table (r);\n";
+          pr "  for (i = 0; r[i] != NULL; ++i) free (r[i]);\n";
+          pr "  free (r);\n";
       );
 
       pr "  CAMLreturn (rv);\n";
       );
 
       pr "  CAMLreturn (rv);\n";
@@ -3176,6 +3847,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;
@@ -3190,6 +3873,7 @@ and generate_ocaml_prototype ?(is_external = false) name style =
   (match fst style with
    | RErr -> pr "unit" (* all errors are turned into exceptions *)
    | RInt _ -> pr "int"
   (match fst style with
    | RErr -> pr "unit" (* all errors are turned into exceptions *)
    | RInt _ -> pr "int"
+   | RInt64 _ -> pr "int64"
    | RBool _ -> pr "bool"
    | RConstString _ -> pr "string"
    | RString _ -> pr "string"
    | RBool _ -> pr "bool"
    | RConstString _ -> pr "string"
    | RString _ -> pr "string"
@@ -3198,6 +3882,9 @@ 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"
+   | RHashtable _ -> pr "(string * string) list"
   );
   if is_external then (
     pr " = ";
   );
   if is_external then (
     pr " = ";
@@ -3303,12 +3990,15 @@ DESTROY (g)
       (match fst style with
        | RErr -> pr "void\n"
        | RInt _ -> pr "SV *\n"
       (match fst style with
        | RErr -> pr "void\n"
        | RInt _ -> pr "SV *\n"
+       | RInt64 _ -> pr "SV *\n"
        | RBool _ -> pr "SV *\n"
        | RConstString _ -> pr "SV *\n"
        | RString _ -> pr "SV *\n"
        | RStringList _
        | RIntBool _
        | RBool _ -> pr "SV *\n"
        | RConstString _ -> pr "SV *\n"
        | RString _ -> pr "SV *\n"
        | RStringList _
        | RIntBool _
-       | RPVList _ | RVGList _ | RLVList _ ->
+       | RPVList _ | RVGList _ | RLVList _
+       | RStat _ | RStatVFS _
+       | RHashtable _ ->
           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. *)
@@ -3332,20 +4022,22 @@ DESTROY (g)
          | OptString _
          | Bool _
          | Int _ -> ()
          | OptString _
          | Bool _
          | Int _ -> ()
-         | StringList n -> pr "        free (%s);\n" n
+         | StringList n -> pr "      free (%s);\n" n
        ) (snd style)
       in
 
       (* Code. *)
       (match fst style with
        | RErr ->
        ) (snd style)
       in
 
       (* 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 ";\n";
           do_cleanups ();
           do_cleanups ();
+          pr "      if (r == -1)\n";
           pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
           pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
-          pr "      }\n"
        | RInt n
        | RBool n ->
           pr "PREINIT:\n";
        | RInt n
        | RBool n ->
           pr "PREINIT:\n";
@@ -3354,13 +4046,25 @@ 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";
-          pr "      if (%s == -1) {\n" n;
           do_cleanups ();
           do_cleanups ();
+          pr "      if (%s == -1)\n" n;
           pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
           pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
-          pr "      }\n";
           pr "      RETVAL = newSViv (%s);\n" n;
           pr " OUTPUT:\n";
           pr "      RETVAL\n"
           pr "      RETVAL = newSViv (%s);\n" n;
           pr " OUTPUT:\n";
           pr "      RETVAL\n"
+       | RInt64 n ->
+          pr "PREINIT:\n";
+          pr "      int64_t %s;\n" n;
+          pr "   CODE:\n";
+          pr "      %s = guestfs_%s " n name;
+          generate_call_args ~handle:"g" style;
+          pr ";\n";
+          do_cleanups ();
+          pr "      if (%s == -1)\n" n;
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
+          pr "      RETVAL = my_newSVll (%s);\n" n;
+          pr " OUTPUT:\n";
+          pr "      RETVAL\n"
        | RConstString n ->
           pr "PREINIT:\n";
           pr "      const char *%s;\n" n;
        | RConstString n ->
           pr "PREINIT:\n";
           pr "      const char *%s;\n" n;
@@ -3368,10 +4072,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";
-          pr "      if (%s == NULL) {\n" n;
           do_cleanups ();
           do_cleanups ();
+          pr "      if (%s == NULL)\n" n;
           pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
           pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
-          pr "      }\n";
           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"
@@ -3382,15 +4085,14 @@ 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";
-          pr "      if (%s == NULL) {\n" n;
           do_cleanups ();
           do_cleanups ();
+          pr "      if (%s == NULL)\n" n;
           pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
           pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
-          pr "      }\n";
           pr "      RETVAL = newSVpv (%s, 0);\n" n;
           pr "      free (%s);\n" n;
           pr " OUTPUT:\n";
           pr "      RETVAL\n"
           pr "      RETVAL = newSVpv (%s, 0);\n" n;
           pr "      free (%s);\n" n;
           pr " OUTPUT:\n";
           pr "      RETVAL\n"
-       | RStringList n ->
+       | RStringList n | RHashtable n ->
           pr "PREINIT:\n";
           pr "      char **%s;\n" n;
           pr "      int i, n;\n";
           pr "PREINIT:\n";
           pr "      char **%s;\n" n;
           pr "      int i, n;\n";
@@ -3398,10 +4100,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";
-          pr "      if (%s == NULL) {\n" n;
           do_cleanups ();
           do_cleanups ();
+          pr "      if (%s == NULL)\n" n;
           pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
           pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
-          pr "      }\n";
           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";
@@ -3416,28 +4117,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";
-          pr "      if (r == NULL) {\n";
           do_cleanups ();
           do_cleanups ();
+          pr "      if (r == NULL)\n";
           pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
           pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
-          pr "      }\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 ->
           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
       );
 
       );
 
-      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";
@@ -3446,6 +4149,7 @@ 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 "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
   pr "      EXTEND (SP, %s->len);\n" n;
   pr "      if (%s == NULL)\n" n;
   pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
   pr "      EXTEND (SP, %s->len);\n" n;
@@ -3473,6 +4177,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;
@@ -3597,6 +4319,7 @@ and generate_perl_prototype name style =
    | RErr -> ()
    | RBool n
    | RInt n
    | RErr -> ()
    | RBool n
    | RInt n
+   | RInt64 n
    | RConstString n
    | RString n -> pr "$%s = " n
    | RIntBool (n, m) -> pr "($%s, $%s) = " n m
    | RConstString n
    | RString n -> pr "$%s = " n
    | RIntBool (n, m) -> pr "($%s, $%s) = " n m
@@ -3604,6 +4327,9 @@ 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
+   | RHashtable n -> pr "%%%s = " n
   );
   pr "$h->%s (" name;
   let comma = ref false in
   );
   pr "$h->%s (" name;
   let comma = ref false in
@@ -3697,6 +4423,27 @@ put_string_list (char * const * const argv)
   return list;
 }
 
   return list;
 }
 
+static PyObject *
+put_table (char * const * const argv)
+{
+  PyObject *list, *item;
+  int argc, i;
+
+  for (argc = 0; argv[argc] != NULL; ++argc)
+    ;
+
+  list = PyList_New (argc >> 1);
+  for (i = 0; i < argc; i += 2) {
+    PyObject *item;
+    item = PyTuple_New (2);
+    PyTuple_SetItem (item, 0, PyString_FromString (argv[i]));
+    PyTuple_SetItem (item, 1, PyString_FromString (argv[i+1]));
+    PyList_SetItem (list, i >> 1, item);
+  }
+
+  return list;
+}
+
 static void
 free_strings (char **argv)
 {
 static void
 free_strings (char **argv)
 {
@@ -3795,6 +4542,27 @@ py_guestfs_close (PyObject *self, PyObject *args)
       pr "\n"
   ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
 
       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, _, _, _, _, _) ->
   (* Python wrapper functions. *)
   List.iter (
     fun (name, style, _, _, _, _, _) ->
@@ -3809,13 +4577,16 @@ py_guestfs_close (PyObject *self, PyObject *args)
       let error_code =
        match fst style with
        | RErr | RInt _ | RBool _ -> pr "  int r;\n"; "-1"
       let error_code =
        match fst style with
        | RErr | RInt _ | RBool _ -> pr "  int r;\n"; "-1"
+       | RInt64 _ -> pr "  int64_t r;\n"; "-1"
        | RConstString _ -> pr "  const char *r;\n"; "NULL"
        | RString _ -> pr "  char *r;\n"; "NULL"
        | RConstString _ -> pr "  const char *r;\n"; "NULL"
        | RString _ -> pr "  char *r;\n"; "NULL"
-       | RStringList _ -> pr "  char **r;\n"; "NULL"
+       | RStringList _ | RHashtable _ -> pr "  char **r;\n"; "NULL"
        | RIntBool _ -> pr "  struct guestfs_int_bool *r;\n"; "NULL"
        | RPVList n -> pr "  struct guestfs_lvm_pv_list *r;\n"; "NULL"
        | RVGList n -> pr "  struct guestfs_lvm_vg_list *r;\n"; "NULL"
        | 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" in
+       | 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
 
       List.iter (
        function
@@ -3888,6 +4659,7 @@ py_guestfs_close (PyObject *self, PyObject *args)
           pr "  py_r = Py_None;\n"
        | RInt _
        | RBool _ -> pr "  py_r = PyInt_FromLong ((long) r);\n"
           pr "  py_r = Py_None;\n"
        | RInt _
        | RBool _ -> pr "  py_r = PyInt_FromLong ((long) r);\n"
+       | RInt64 _ -> pr "  py_r = PyLong_FromLongLong (r);\n"
        | RConstString _ -> pr "  py_r = PyString_FromString (r);\n"
        | RString _ ->
           pr "  py_r = PyString_FromString (r);\n";
        | RConstString _ -> pr "  py_r = PyString_FromString (r);\n"
        | RString _ ->
           pr "  py_r = PyString_FromString (r);\n";
@@ -3909,6 +4681,15 @@ py_guestfs_close (PyObject *self, PyObject *args)
        | RLVList n ->
           pr "  py_r = put_lvm_lv_list (r);\n";
           pr "  guestfs_free_lvm_lv_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"
+       | RHashtable n ->
+          pr "  py_r = put_table (r);\n";
+          pr "  free_strings (r);\n"
       );
 
       pr "  return py_r;\n";
       );
 
       pr "  return py_r;\n";
@@ -3946,27 +4727,378 @@ initlibguestfsmod (void)
 and generate_python_py () =
   generate_header HashStyle LGPLv2;
 
 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";
+  pr "\
+u\"\"\"Python bindings for libguestfs
+
+import guestfs
+g = guestfs.GuestFS ()
+g.add_drive (\"guest.img\")
+g.launch ()
+g.wait_ready ()
+parts = g.list_partitions ()
+
+The guestfs module provides a Python binding to the libguestfs API
+for examining and modifying virtual machine disk images.
+
+Amongst the things this is good for: making batch configuration
+changes to guests, getting disk used/free statistics (see also:
+virt-df), migrating between virtualization systems (see also:
+virt-p2v), performing partial backups, performing partial guest
+clones, cloning guests and changing registry/UUID/hostname info, and
+much else besides.
+
+Libguestfs uses Linux kernel and qemu code, and can access any type of
+guest filesystem that Linux and qemu can, including but not limited
+to: ext2/3/4, btrfs, FAT and NTFS, LVM, many different disk partition
+schemes, qcow, qcow2, vmdk.
+
+Libguestfs provides ways to enumerate guest storage (eg. partitions,
+LVs, what filesystem is in each LV, etc.).  It can also run commands
+in the context of the guest.  Also you can access filesystems over FTP.
+
+Errors which happen while using the API are turned into Python
+RuntimeError exceptions.
+
+To create a guestfs handle you usually have to perform the following
+sequence of calls:
+
+# Create the handle, call add_drive at least once, and possibly
+# several times if the guest has multiple block devices:
+g = guestfs.GuestFS ()
+g.add_drive (\"guest.img\")
+
+# Launch the qemu subprocess and wait for it to become ready:
+g.launch ()
+g.wait_ready ()
+
+# Now you can issue commands, for example:
+logvols = g.lvs ()
+
+\"\"\"
+
+import libguestfsmod
+
+class GuestFS:
+    \"\"\"Instances of this class are libguestfs API handles.\"\"\"
+
+    def __init__ (self):
+        \"\"\"Create a new libguestfs handle.\"\"\"
+        self._o = libguestfsmod.create ()
+
+    def __del__ (self):
+        libguestfsmod.close (self._o)
+
+";
 
   List.iter (
 
   List.iter (
-    fun (name, style, _, _, _, _, _) ->
+    fun (name, style, _, flags, _, _, longdesc) ->
+      let doc = replace_str longdesc "C<guestfs_" "C<g." in
+      let doc =
+        match fst style with
+       | RErr | RInt _ | RInt64 _ | RBool _ | RConstString _
+       | RString _ -> doc
+       | RStringList _ ->
+           doc ^ "\n\nThis function returns a list of strings."
+       | RIntBool _ ->
+           doc ^ "\n\nThis function returns a tuple (int, bool).\n"
+       | RPVList _ ->
+           doc ^ "\n\nThis function returns a list of PVs.  Each PV is represented as a dictionary."
+       | RVGList _ ->
+           doc ^ "\n\nThis function returns a list of VGs.  Each VG is represented as a dictionary."
+       | RLVList _ ->
+           doc ^ "\n\nThis function returns a list of LVs.  Each LV is represented as a dictionary."
+       | RStat _ ->
+           doc ^ "\n\nThis function returns a dictionary, with keys matching the various fields in the stat structure."
+       | RStatVFS _ ->
+           doc ^ "\n\nThis function returns a dictionary, with keys matching the various fields in the statvfs structure."
+       | RHashtable _ ->
+           doc ^ "\n\nThis function returns a dictionary." in
+      let doc =
+       if List.mem ProtocolLimitWarning flags then
+         doc ^ "\n\n" ^ protocol_limit_warning
+       else doc in
+      let doc =
+       if List.mem DangerWillRobinson flags then
+         doc ^ "\n\n" ^ danger_will_robinson
+       else doc in
+      let doc = pod2text ~width:60 name doc in
+      let doc = List.map (fun line -> replace_str line "\\" "\\\\") doc in
+      let doc = String.concat "\n        " doc in
+
       pr "    def %s " name;
       generate_call_args ~handle:"self" style;
       pr ":\n";
       pr "    def %s " name;
       generate_call_args ~handle:"self" style;
       pr ":\n";
+      pr "        u\"\"\"%s\"\"\"\n" doc;
       pr "        return libguestfsmod.%s " name;
       generate_call_args ~handle:"self._o" style;
       pr "\n";
       pr "\n";
   ) all_functions
 
       pr "        return libguestfsmod.%s " name;
       generate_call_args ~handle:"self._o" style;
       pr "\n";
       pr "\n";
   ) all_functions
 
+(* Useful if you need the longdesc POD text as plain text.  Returns a
+ * list of lines.
+ *)
+and pod2text ~width name longdesc =
+  let filename, chan = Filename.open_temp_file "gen" ".tmp" in
+  fprintf chan "=head1 %s\n\n%s\n" name longdesc;
+  close_out chan;
+  let cmd = sprintf "pod2text -w %d %s" width (Filename.quote filename) in
+  let chan = Unix.open_process_in cmd in
+  let lines = ref [] in
+  let rec loop i =
+    let line = input_line chan in
+    if i = 1 then              (* discard the first line of output *)
+      loop (i+1)
+    else (
+      let line = triml line in
+      lines := line :: !lines;
+      loop (i+1)
+    ) in
+  let lines = try loop 1 with End_of_file -> List.rev !lines in
+  Unix.unlink filename;
+  match Unix.close_process_in chan with
+  | Unix.WEXITED 0 -> lines
+  | Unix.WEXITED i ->
+      failwithf "pod2text: process exited with non-zero status (%d)" i
+  | Unix.WSIGNALED i | Unix.WSTOPPED i ->
+      failwithf "pod2text: process signalled or stopped by signal %d" i
+
+(* Generate ruby bindings. *)
+and generate_ruby_c () =
+  generate_header CStyle LGPLv2;
+
+  pr "\
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <ruby.h>
+
+#include \"guestfs.h\"
+
+#include \"extconf.h\"
+
+static VALUE m_guestfs;                        /* guestfs module */
+static VALUE c_guestfs;                        /* guestfs_h handle */
+static VALUE e_Error;                  /* used for all errors */
+
+static void ruby_guestfs_free (void *p)
+{
+  if (!p) return;
+  guestfs_close ((guestfs_h *) p);
+}
+
+static VALUE ruby_guestfs_create (VALUE m)
+{
+  guestfs_h *g;
+
+  g = guestfs_create ();
+  if (!g)
+    rb_raise (e_Error, \"failed to create guestfs handle\");
+
+  /* Don't print error messages to stderr by default. */
+  guestfs_set_error_handler (g, NULL, NULL);
+
+  /* Wrap it, and make sure the close function is called when the
+   * handle goes away.
+   */
+  return Data_Wrap_Struct (c_guestfs, NULL, ruby_guestfs_free, g);
+}
+
+static VALUE ruby_guestfs_close (VALUE gv)
+{
+  guestfs_h *g;
+  Data_Get_Struct (gv, guestfs_h, g);
+
+  ruby_guestfs_free (g);
+  DATA_PTR (gv) = NULL;
+
+  return Qnil;
+}
+
+";
+
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      pr "static VALUE ruby_guestfs_%s (VALUE gv" name;
+      List.iter (fun arg -> pr ", VALUE %sv" (name_of_argt arg)) (snd style);
+      pr ")\n";
+      pr "{\n";
+      pr "  guestfs_h *g;\n";
+      pr "  Data_Get_Struct (gv, guestfs_h, g);\n";
+      pr "  if (!g)\n";
+      pr "    rb_raise (rb_eArgError, \"%%s: used handle after closing it\", \"%s\");\n"
+       name;
+      pr "\n";
+
+      List.iter (
+       function
+       | String n ->
+           pr "  const char *%s = StringValueCStr (%sv);\n" n n;
+           pr "  if (!%s)\n" n;
+           pr "    rb_raise (rb_eTypeError, \"expected string for parameter %%s of %%s\",\n";
+           pr "              \"%s\", \"%s\");\n" n name
+       | OptString n ->
+           pr "  const char *%s = StringValueCStr (%sv);\n" n n
+       | StringList n ->
+           pr "  char **%s;" n;
+           pr "  {\n";
+           pr "    int i, len;\n";
+           pr "    len = RARRAY_LEN (%sv);\n" n;
+           pr "    %s = malloc (sizeof (char *) * (len+1));\n" n;
+           pr "    for (i = 0; i < len; ++i) {\n";
+           pr "      VALUE v = rb_ary_entry (%sv, i);\n" n;
+           pr "      %s[i] = StringValueCStr (v);\n" n;
+           pr "    }\n";
+           pr "  }\n";
+       | Bool n
+       | Int n ->
+           pr "  int %s = NUM2INT (%sv);\n" n n
+      ) (snd style);
+      pr "\n";
+
+      let error_code =
+       match fst style with
+       | RErr | RInt _ | RBool _ -> pr "  int r;\n"; "-1"
+       | RInt64 _ -> pr "  int64_t r;\n"; "-1"
+       | RConstString _ -> pr "  const char *r;\n"; "NULL"
+       | RString _ -> pr "  char *r;\n"; "NULL"
+       | RStringList _ | RHashtable _ -> pr "  char **r;\n"; "NULL"
+       | RIntBool _ -> pr "  struct guestfs_int_bool *r;\n"; "NULL"
+       | RPVList n -> pr "  struct guestfs_lvm_pv_list *r;\n"; "NULL"
+       | RVGList n -> pr "  struct guestfs_lvm_vg_list *r;\n"; "NULL"
+       | RLVList n -> pr "  struct guestfs_lvm_lv_list *r;\n"; "NULL"
+       | RStat n -> pr "  struct guestfs_stat *r;\n"; "NULL"
+       | RStatVFS n -> pr "  struct guestfs_statvfs *r;\n"; "NULL" in
+      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 "    rb_raise (e_Error, \"%%s\", guestfs_last_error (g));\n";
+      pr "\n";
+
+      (match fst style with
+       | RErr ->
+          pr "  return Qnil;\n"
+       | RInt _ | RBool _ ->
+          pr "  return INT2NUM (r);\n"
+       | RInt64 _ ->
+          pr "  return ULL2NUM (r);\n"
+       | RConstString _ ->
+          pr "  return rb_str_new2 (r);\n";
+       | RString _ ->
+          pr "  VALUE rv = rb_str_new2 (r);\n";
+          pr "  free (r);\n";
+          pr "  return rv;\n";
+       | RStringList _ ->
+          pr "  int i, len = 0;\n";
+          pr "  for (i = 0; r[i] != NULL; ++i) len++;\n";
+          pr "  VALUE rv = rb_ary_new2 (len);\n";
+          pr "  for (i = 0; r[i] != NULL; ++i) {\n";
+          pr "    rb_ary_push (rv, rb_str_new2 (r[i]));\n";
+          pr "    free (r[i]);\n";
+          pr "  }\n";
+          pr "  free (r);\n";
+          pr "  return rv;\n"
+       | RIntBool _ ->
+          pr "  VALUE rv = rb_ary_new2 (2);\n";
+          pr "  rb_ary_push (rv, INT2NUM (r->i));\n";
+          pr "  rb_ary_push (rv, INT2NUM (r->b));\n";
+          pr "  guestfs_free_int_bool (r);\n";
+          pr "  return rv;\n"
+       | RPVList n ->
+          generate_ruby_lvm_code "pv" pv_cols
+       | RVGList n ->
+          generate_ruby_lvm_code "vg" vg_cols
+       | RLVList n ->
+          generate_ruby_lvm_code "lv" lv_cols
+       | RStat n ->
+          pr "  VALUE rv = rb_hash_new ();\n";
+          List.iter (
+            function
+            | name, `Int ->
+                pr "  rb_hash_aset (rv, rb_str_new2 (\"%s\"), ULL2NUM (r->%s));\n" name name
+          ) stat_cols;
+          pr "  free (r);\n";
+          pr "  return rv;\n"
+       | RStatVFS n ->
+          pr "  VALUE rv = rb_hash_new ();\n";
+          List.iter (
+            function
+            | name, `Int ->
+                pr "  rb_hash_aset (rv, rb_str_new2 (\"%s\"), ULL2NUM (r->%s));\n" name name
+          ) statvfs_cols;
+          pr "  free (r);\n";
+          pr "  return rv;\n"
+       | RHashtable _ ->
+          pr "  VALUE rv = rb_hash_new ();\n";
+          pr "  int i;\n";
+          pr "  for (i = 0; r[i] != NULL; i+=2) {\n";
+          pr "    rb_hash_aset (rv, rb_str_new2 (r[i]), rb_str_new2 (r[i+1]));\n";
+          pr "    free (r[i]);\n";
+          pr "    free (r[i+1]);\n";
+          pr "  }\n";
+          pr "  free (r);\n";
+          pr "  return rv;\n"
+      );
+
+      pr "}\n";
+      pr "\n"
+  ) all_functions;
+
+  pr "\
+/* Initialize the module. */
+void Init__guestfs ()
+{
+  m_guestfs = rb_define_module (\"Guestfs\");
+  c_guestfs = rb_define_class_under (m_guestfs, \"Guestfs\", rb_cObject);
+  e_Error = rb_define_class_under (m_guestfs, \"Error\", rb_eStandardError);
+
+  rb_define_module_function (m_guestfs, \"create\", ruby_guestfs_create, 0);
+  rb_define_method (c_guestfs, \"close\", ruby_guestfs_close, 0);
+
+";
+  (* Define the rest of the methods. *)
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      pr "  rb_define_method (c_guestfs, \"%s\",\n" name;
+      pr "        ruby_guestfs_%s, %d);\n" name (List.length (snd style))
+  ) all_functions;
+
+  pr "}\n"
+
+(* Ruby code to return an LVM struct list. *)
+and generate_ruby_lvm_code typ cols =
+  pr "  VALUE rv = rb_ary_new2 (r->len);\n";
+  pr "  int i;\n";
+  pr "  for (i = 0; i < r->len; ++i) {\n";
+  pr "    VALUE hv = rb_hash_new ();\n";
+  List.iter (
+    function
+    | name, `String ->
+       pr "    rb_hash_aset (rv, rb_str_new2 (\"%s\"), rb_str_new2 (r->val[i].%s));\n" name name
+    | name, `UUID ->
+       pr "    rb_hash_aset (rv, rb_str_new2 (\"%s\"), rb_str_new (r->val[i].%s, 32));\n" name name
+    | name, `Bytes
+    | name, `Int ->
+       pr "    rb_hash_aset (rv, rb_str_new2 (\"%s\"), ULL2NUM (r->val[i].%s));\n" name name
+    | name, `OptPercent ->
+       pr "    rb_hash_aset (rv, rb_str_new2 (\"%s\"), rb_dbl2big (r->val[i].%s));\n" name name
+  ) cols;
+  pr "    rb_ary_push (rv, hv);\n";
+  pr "  }\n";
+  pr "  guestfs_free_lvm_%s_list (r);\n" typ;
+  pr "  return rv;\n"
+
 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;
@@ -4023,6 +5155,10 @@ Run it from the top source directory using the command
   generate_fish_cmds ();
   close ();
 
   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 ();
@@ -4062,3 +5198,7 @@ Run it from the top source directory using the command
   let close = output_to "python/guestfs.py" in
   generate_python_py ();
   close ();
   let close = output_to "python/guestfs.py" in
   generate_python_py ();
   close ();
+
+  let close = output_to "ruby/ext/guestfs/_guestfs.c" in
+  generate_ruby_c ();
+  close ();