fish: fuse: Add -m dev:mnt:opts to allow mount options to be specified.
[libguestfs.git] / fish / fish.c
index 265464e..65a0c1d 100644 (file)
@@ -1,5 +1,5 @@
 /* guestfish - the filesystem interactive shell
- * Copyright (C) 2009-2010 Red Hat Inc.
+ * Copyright (C) 2009-2011 Red Hat Inc.
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 #include "closeout.h"
 #include "progname.h"
 
+/* Return from parse_command_line.  See description below. */
+struct parsed_command {
+  int status;
+  char *pipe;
+  char *cmd;
+  char *argv[64];
+};
+
 static void set_up_terminal (void);
 static void prepare_drives (struct drv *drv);
 static int launch (void);
@@ -52,6 +60,8 @@ static void interactive (void);
 static void shell_script (void);
 static void script (int prompt);
 static void cmdline (char *argv[], int optind, int argc);
+static struct parsed_command parse_command_line (char *buf, int *exit_on_error_rtn);
+static int execute_and_inline (const char *cmd, int exit_on_error);
 static void initialize_readline (void);
 static void cleanup_readline (void);
 #ifdef HAVE_LIBREADLINE
@@ -64,12 +74,12 @@ static int override_progress_bars = -1;
 guestfs_h *g;
 
 int read_only = 0;
+int live = 0;
 int quit = 0;
 int verbose = 0;
 int remote_control_listen = 0;
 int remote_control_csh = 0;
 int remote_control = 0;
-int exit_on_error = 1;
 int command_num = 0;
 int keys_from_stdin = 0;
 int echo_keys = 0;
@@ -89,18 +99,9 @@ usage (int status)
     fprintf (stdout,
            _("%s: guest filesystem shell\n"
              "%s lets you edit virtual machine filesystems\n"
-             "Copyright (C) 2009-2010 Red Hat Inc.\n"
+             "Copyright (C) 2009-2011 Red Hat Inc.\n"
              "Usage:\n"
              "  %s [--options] cmd [: cmd : cmd ...]\n"
-             "  %s [--ro] -i -a disk-image\n"
-             "  %s [--ro] -i -d libvirt-domain\n"
-             "or for interactive use:\n"
-             "  %s\n"
-             "or from a shell script:\n"
-             "  %s <<EOF\n"
-             "  cmd\n"
-             "  ...\n"
-             "  EOF\n"
              "Options:\n"
              "  -h|--cmd-help        List available commands\n"
              "  -h|--cmd-help cmd    Display detailed help on 'cmd'\n"
@@ -115,7 +116,8 @@ usage (int status)
              "  -i|--inspector       Automatically mount filesystems\n"
              "  --keys-from-stdin    Read passphrases from stdin\n"
              "  --listen             Listen for remote commands\n"
-             "  -m|--mount dev[:mnt] Mount dev on mnt (if omitted, /)\n"
+             "  --live               Connect to a live virtual machine\n"
+             "  -m|--mount dev[:mnt[:opts]] Mount dev on mnt (if omitted, /)\n"
              "  -n|--no-sync         Don't autosync\n"
              "  -N|--new type        Create prepared disk (test1.img, ...)\n"
              "  --progress-bars      Enable progress bars even when not interactive\n"
@@ -125,11 +127,21 @@ usage (int status)
              "  --selinux            Enable SELinux support\n"
              "  -v|--verbose         Verbose messages\n"
              "  -V|--version         Display version and exit\n"
+             "  -w|--rw              Mount read-write\n"
              "  -x                   Echo each command before executing it\n"
+             "\n"
+             "To examine a disk image, ISO, hard disk, filesystem etc:\n"
+             "  %s [--ro|--rw] -i -a /path/to/disk.img\n"
+             "or\n"
+             "  %s [--ro|--rw] -i -d name-of-libvirt-domain\n"
+             "\n"
+             "--ro recommended to avoid any writes to the disk image.  If -i option fails\n"
+             "run again without -i and use 'run' + 'list-filesystems' + 'mount' cmds.\n"
+             "\n"
              "For more information, see the manpage %s(1).\n"),
              program_name, program_name, program_name,
              program_name, program_name, program_name,
-             program_name, program_name, program_name);
+             program_name);
   }
   exit (status);
 }
@@ -164,6 +176,7 @@ main (int argc, char *argv[])
     { "inspector", 0, 0, 'i' },
     { "keys-from-stdin", 0, 0, 0 },
     { "listen", 0, 0, 0 },
+    { "live", 0, 0, 0 },
     { "mount", 1, 0, 'm' },
     { "new", 1, 0, 'N' },
     { "no-dest-paths", 0, 0, 'D' },
@@ -268,6 +281,8 @@ main (int argc, char *argv[])
           format = optarg;
       } else if (STREQ (long_options[option_index].name, "csh")) {
         remote_control_csh = 1;
+      } else if (STREQ (long_options[option_index].name, "live")) {
+        live = 1;
       } else {
         fprintf (stderr, _("%s: unknown long option: %s (%d)\n"),
                  program_name, long_options[option_index].name, option_index);
@@ -485,7 +500,8 @@ main (int argc, char *argv[])
     : (optind >= argc && isatty (0));
 
   if (progress_bars)
-    guestfs_set_progress_callback (g, progress_callback, NULL);
+    guestfs_set_event_callback (g, progress_callback,
+                                GUESTFS_EVENT_PROGRESS, 0, NULL);
 
   /* Interactive, shell script, or command(s) on the command line? */
   if (optind >= argc) {
@@ -606,12 +622,9 @@ static void
 script (int prompt)
 {
   char *buf;
-  char *cmd;
-  char *p, *pend;
-  char *argv[64];
-  int len;
   int global_exit_on_error = !prompt;
-  int tilde_candidate;
+  int exit_on_error;
+  struct parsed_command pcmd;
 
   if (prompt) {
     printf (_("\n"
@@ -630,8 +643,6 @@ script (int prompt)
   }
 
   while (!quit) {
-    char *pipe = NULL;
-
     exit_on_error = global_exit_on_error;
 
     buf = rl_gets (prompt);
@@ -640,171 +651,243 @@ script (int prompt)
       break;
     }
 
-    /* Skip any initial whitespace before the command. */
-  again:
-    while (*buf && c_isspace (*buf))
-      buf++;
+    pcmd = parse_command_line (buf, &exit_on_error);
+    if (pcmd.status == -1 && exit_on_error)
+      exit (EXIT_FAILURE);
+    if (pcmd.status == 1) {
+      if (issue_command (pcmd.cmd, pcmd.argv, pcmd.pipe, exit_on_error) == -1) {
+        if (exit_on_error) exit (EXIT_FAILURE);
+      }
+    }
+  }
+  if (prompt) printf ("\n");
+}
+
+/* Parse a command string, splitting at whitespace, handling '!', '#' etc.
+ * This destructively updates 'buf'.
+ *
+ * 'exit_on_error_rtn' is used to pass in the global exit_on_error
+ * setting and to return the local setting (eg. if the command begins
+ * with '-').
+ *
+ * Returns in parsed_command.status:
+ *   1 = got a guestfish command (returned in cmd_rtn/argv_rtn/pipe_rtn)
+ *   0 = no guestfish command, but otherwise OK
+ *  -1 = an error
+ */
+static struct parsed_command
+parse_command_line (char *buf, int *exit_on_error_rtn)
+{
+  struct parsed_command pcmd;
+  char *p, *pend;
+  int len;
+  int tilde_candidate;
+  int r;
+  const size_t argv_len = sizeof pcmd.argv / sizeof pcmd.argv[0];
+
+  /* Note that pcmd.pipe must be set to NULL for correct usage.  Other
+   * fields do not need to be, but this silences a gcc warning.
+   */
+  memset (&pcmd, 0, sizeof pcmd);
 
-    if (!*buf) continue;
+ again:
+  /* Skip any initial whitespace before the command. */
+  while (*buf && c_isspace (*buf))
+    buf++;
 
-    /* If the next character is '#' then this is a comment. */
-    if (*buf == '#') continue;
+  if (!*buf) {
+    pcmd.status = 0;
+    return pcmd;
+  }
 
-    /* If the next character is '!' then pass the whole lot to system(3). */
-    if (*buf == '!') {
-      int r;
+  /* If the next character is '#' then this is a comment. */
+  if (*buf == '#') {
+    pcmd.status = 0;
+    return pcmd;
+  }
 
-      r = system (buf+1);
-      if (exit_on_error) {
-        if (r == -1 ||
-            (WIFSIGNALED (r) &&
-             (WTERMSIG (r) == SIGINT || WTERMSIG (r) == SIGQUIT)) ||
-            WEXITSTATUS (r) != 0)
-          exit (EXIT_FAILURE);
-      }
-      continue;
-    }
+  /* If the next character is '!' then pass the whole lot to system(3). */
+  if (*buf == '!') {
+    r = system (buf+1);
+    if (r == -1 ||
+        (WIFSIGNALED (r) &&
+         (WTERMSIG (r) == SIGINT || WTERMSIG (r) == SIGQUIT)) ||
+        WEXITSTATUS (r) != 0)
+      pcmd.status = -1;
+    else
+      pcmd.status = 0;
+    return pcmd;
+  }
 
-    /* If the next character is '-' allow the command to fail without
-     * exiting on error (just for this one command though).
-     */
-    if (*buf == '-') {
-      exit_on_error = 0;
-      buf++;
-      goto again;
-    }
+  /* If the next two characters are "<!" then pass the command to
+   * popen(3), read the result and execute it as guestfish commands.
+   */
+  if (buf[0] == '<' && buf[1] == '!') {
+    int r = execute_and_inline (&buf[2], *exit_on_error_rtn);
+    if (r == -1)
+      pcmd.status = -1;
+    else
+      pcmd.status = 0;
+    return pcmd;
+  }
 
-    /* Get the command (cannot be quoted). */
-    len = strcspn (buf, " \t");
+  /* If the next character is '-' allow the command to fail without
+   * exiting on error (just for this one command though).
+   */
+  if (*buf == '-') {
+    *exit_on_error_rtn = 0;
+    buf++;
+    goto again;
+  }
 
-    if (len == 0) continue;
+  /* Get the command (cannot be quoted). */
+  len = strcspn (buf, " \t");
 
-    cmd = buf;
-    unsigned int i = 0;
-    if (buf[len] == '\0') {
-      argv[0] = NULL;
-      goto got_command;
-    }
+  if (len == 0) {
+    pcmd.status = 0;
+    return pcmd;
+  }
 
-    buf[len] = '\0';
-    p = &buf[len+1];
-    p += strspn (p, " \t");
+  pcmd.cmd = buf;
+  unsigned int i = 0;
+  if (buf[len] == '\0') {
+    pcmd.argv[0] = NULL;
+    pcmd.status = 1;
+    return pcmd;
+  }
 
-    /* Get the parameters. */
-    while (*p && i < sizeof argv / sizeof argv[0]) {
-      tilde_candidate = 0;
+  buf[len] = '\0';
+  p = &buf[len+1];
+  p += strspn (p, " \t");
 
-      /* Parameters which start with quotes or pipes are treated
-       * specially.  Bare parameters are delimited by whitespace.
+  /* Get the parameters. */
+  while (*p && i < argv_len) {
+    tilde_candidate = 0;
+
+    /* Parameters which start with quotes or pipes are treated
+     * specially.  Bare parameters are delimited by whitespace.
+     */
+    if (*p == '"') {
+      p++;
+      len = strcspn (p, "\"");
+      if (p[len] == '\0') {
+        fprintf (stderr, _("%s: unterminated double quote\n"), program_name);
+        pcmd.status = -1;
+        return pcmd;
+      }
+      if (p[len+1] && (p[len+1] != ' ' && p[len+1] != '\t')) {
+        fprintf (stderr,
+                 _("%s: command arguments not separated by whitespace\n"),
+                 program_name);
+        pcmd.status = -1;
+        return pcmd;
+      }
+      p[len] = '\0';
+      pend = p[len+1] ? &p[len+2] : &p[len+1];
+    } else if (*p == '\'') {
+      p++;
+      len = strcspn (p, "'");
+      if (p[len] == '\0') {
+        fprintf (stderr, _("%s: unterminated single quote\n"), program_name);
+        pcmd.status = -1;
+        return pcmd;
+      }
+      if (p[len+1] && (p[len+1] != ' ' && p[len+1] != '\t')) {
+        fprintf (stderr,
+                 _("%s: command arguments not separated by whitespace\n"),
+                 program_name);
+        pcmd.status = -1;
+        return pcmd;
+      }
+      p[len] = '\0';
+      pend = p[len+1] ? &p[len+2] : &p[len+1];
+    } else if (*p == '|') {
+      *p = '\0';
+      pcmd.pipe = p+1;
+      continue;
+    } else if (*p != ' ' && *p != '\t') {
+      /* If the first character is a ~ then note that this parameter
+       * is a candidate for ~username expansion.  NB this does not
+       * apply to quoted parameters.
        */
-      if (*p == '"') {
-        p++;
-        len = strcspn (p, "\"");
-        if (p[len] == '\0') {
-          fprintf (stderr, _("%s: unterminated double quote\n"), program_name);
-          if (exit_on_error) exit (EXIT_FAILURE);
-          goto next_command;
-        }
-        if (p[len+1] && (p[len+1] != ' ' && p[len+1] != '\t')) {
-          fprintf (stderr,
-                   _("%s: command arguments not separated by whitespace\n"),
-                   program_name);
-          if (exit_on_error) exit (EXIT_FAILURE);
-          goto next_command;
-        }
+      tilde_candidate = *p == '~';
+      len = strcspn (p, " \t");
+      if (p[len]) {
         p[len] = '\0';
-        pend = p[len+1] ? &p[len+2] : &p[len+1];
-      } else if (*p == '\'') {
-        p++;
-        len = strcspn (p, "'");
-        if (p[len] == '\0') {
-          fprintf (stderr, _("%s: unterminated single quote\n"), program_name);
-          if (exit_on_error) exit (EXIT_FAILURE);
-          goto next_command;
-        }
-        if (p[len+1] && (p[len+1] != ' ' && p[len+1] != '\t')) {
-          fprintf (stderr,
-                   _("%s: command arguments not separated by whitespace\n"),
-                   program_name);
-          if (exit_on_error) exit (EXIT_FAILURE);
-          goto next_command;
-        }
-        p[len] = '\0';
-        pend = p[len+1] ? &p[len+2] : &p[len+1];
-      } else if (*p == '|') {
-        *p = '\0';
-        pipe = p+1;
-        continue;
-        /*
-      } else if (*p == '[') {
-        int c = 1;
-        p++;
-        pend = p;
-        while (*pend && c != 0) {
-          if (*pend == '[') c++;
-          else if (*pend == ']') c--;
-          pend++;
-        }
-        if (c != 0) {
-          fprintf (stderr,
-                   _("%s: unterminated \"[...]\" sequence\n"), program_name);
-          if (exit_on_error) exit (EXIT_FAILURE);
-          goto next_command;
-        }
-        if (*pend && (*pend != ' ' && *pend != '\t')) {
-          fprintf (stderr,
-                   _("%s: command arguments not separated by whitespace\n"),
-                   program_name);
-          if (exit_on_error) exit (EXIT_FAILURE);
-          goto next_command;
-        }
-        *(pend-1) = '\0';
-        */
-      } else if (*p != ' ' && *p != '\t') {
-        /* If the first character is a ~ then note that this parameter
-         * is a candidate for ~username expansion.  NB this does not
-         * apply to quoted parameters.
-         */
-        tilde_candidate = *p == '~';
-        len = strcspn (p, " \t");
-        if (p[len]) {
-          p[len] = '\0';
-          pend = &p[len+1];
-        } else
-          pend = &p[len];
-      } else {
-        fprintf (stderr, _("%s: internal error parsing string at '%s'\n"),
-                 program_name, p);
-        abort ();
-      }
+        pend = &p[len+1];
+      } else
+        pend = &p[len];
+    } else {
+      fprintf (stderr, _("%s: internal error parsing string at '%s'\n"),
+               program_name, p);
+      abort ();
+    }
 
-      if (!tilde_candidate)
-        argv[i] = p;
-      else
-        argv[i] = try_tilde_expansion (p);
-      i++;
-      p = pend;
+    if (!tilde_candidate)
+      pcmd.argv[i] = p;
+    else
+      pcmd.argv[i] = try_tilde_expansion (p);
+    i++;
+    p = pend;
 
-      if (*p)
-        p += strspn (p, " \t");
-    }
+    if (*p)
+      p += strspn (p, " \t");
+  }
 
-    if (i == sizeof argv / sizeof argv[0]) {
-      fprintf (stderr, _("%s: too many arguments\n"), program_name);
-      if (exit_on_error) exit (EXIT_FAILURE);
-      goto next_command;
-    }
+  if (i == argv_len) {
+    fprintf (stderr, _("%s: too many arguments\n"), program_name);
+    pcmd.status = -1;
+    return pcmd;
+  }
+
+  pcmd.argv[i] = NULL;
+
+  pcmd.status = 1;
+  return pcmd;
+}
+
+/* Used to handle "<!" (execute command and inline result). */
+static int
+execute_and_inline (const char *cmd, int global_exit_on_error)
+{
+  FILE *pp;
+  char *line = NULL;
+  size_t len = 0;
+  ssize_t n;
+  int exit_on_error;
+  struct parsed_command pcmd;
+
+  pp = popen (cmd, "r");
+  if (!pp) {
+    perror ("popen");
+    return -1;
+  }
 
-    argv[i] = NULL;
+  while ((n = getline (&line, &len, pp)) != -1) {
+    exit_on_error = global_exit_on_error;
+
+    /* Chomp final line ending which parse_command_line would not expect. */
+    if (n > 0 && line[n-1] == '\n')
+      line[n-1] = '\0';
 
-  got_command:
-    if (issue_command (cmd, argv, pipe) == -1) {
-      if (exit_on_error) exit (EXIT_FAILURE);
+    pcmd = parse_command_line (line, &exit_on_error);
+    if (pcmd.status == -1 && exit_on_error)
+      exit (EXIT_FAILURE);
+    if (pcmd.status == 1) {
+      if (issue_command (pcmd.cmd, pcmd.argv, pcmd.pipe, exit_on_error) == -1) {
+        if (exit_on_error) exit (EXIT_FAILURE);
+      }
     }
+  }
+
+  free (line);
 
-  next_command:;
+  if (pclose (pp) == -1) {
+    perror ("pclose");
+    return -1;
   }
-  if (prompt) printf ("\n");
+
+  return 0;
 }
 
 static void
@@ -812,6 +895,7 @@ cmdline (char *argv[], int optind, int argc)
 {
   const char *cmd;
   char **params;
+  int exit_on_error;
 
   exit_on_error = 1;
 
@@ -838,18 +922,23 @@ cmdline (char *argv[], int optind, int argc)
     optind++;
 
   if (optind == argc) {
-    if (issue_command (cmd, params, NULL) == -1 && exit_on_error)
+    if (issue_command (cmd, params, NULL, exit_on_error) == -1 && exit_on_error)
         exit (EXIT_FAILURE);
   } else {
     argv[optind] = NULL;
-    if (issue_command (cmd, params, NULL) == -1 && exit_on_error)
+    if (issue_command (cmd, params, NULL, exit_on_error) == -1 && exit_on_error)
       exit (EXIT_FAILURE);
     cmdline (argv, optind+1, argc);
   }
 }
 
+/* Note: 'rc_exit_on_error_flag' is the exit_on_error flag that we
+ * pass to the remote server (when issuing --remote commands).  It
+ * does not cause issue_command itself to exit on error.
+ */
 int
-issue_command (const char *cmd, char *argv[], const char *pipecmd)
+issue_command (const char *cmd, char *argv[], const char *pipecmd,
+               int rc_exit_on_error_flag)
 {
   int argc;
   int stdout_saved_fd = -1;
@@ -912,7 +1001,7 @@ issue_command (const char *cmd, char *argv[], const char *pipecmd)
 
   /* If --remote was set, then send this command to a remote process. */
   if (remote_control)
-    r = rc_remote (remote_control, cmd, argc, argv, exit_on_error);
+    r = rc_remote (remote_control, cmd, argc, argv, rc_exit_on_error_flag);
 
   /* Otherwise execute it locally. */
   else if (STRCASEEQ (cmd, "help")) {