fish: Print input file and line number in error messages.
[libguestfs.git] / fish / fish.c
index 22f3359..efc74b2 100644 (file)
@@ -53,6 +53,7 @@ struct parsed_command {
   char *argv[64];
 };
 
+static void user_cancel (int);
 static void set_up_terminal (void);
 static void prepare_drives (struct drv *drv);
 static int launch (void);
@@ -61,7 +62,9 @@ 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 parse_quoted_string (char *p);
 static int execute_and_inline (const char *cmd, int exit_on_error);
+static void error_cb (guestfs_h *g, void *data, const char *msg);
 static void initialize_readline (void);
 static void cleanup_readline (void);
 #ifdef HAVE_LIBREADLINE
@@ -71,7 +74,7 @@ static void add_history_line (const char *);
 static int override_progress_bars = -1;
 
 /* Currently open libguestfs handle. */
-guestfs_h *g;
+guestfs_h *g = NULL;
 
 int read_only = 0;
 int live = 0;
@@ -88,6 +91,9 @@ int inspector = 0;
 int utf8_mode = 0;
 int have_terminfo = 0;
 int progress_bars = 0;
+int is_interactive = 0;
+const char *input_file = NULL;
+int input_lineno = 0;
 
 static void __attribute__((noreturn))
 usage (int status)
@@ -117,7 +123,7 @@ usage (int status)
              "  --keys-from-stdin    Read passphrases from stdin\n"
              "  --listen             Listen for remote commands\n"
              "  --live               Connect to a live virtual machine\n"
-             "  -m|--mount dev[:mnt] Mount dev on mnt (if omitted, /)\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"
@@ -158,6 +164,8 @@ main (int argc, char *argv[])
   bindtextdomain (PACKAGE, LOCALEBASEDIR);
   textdomain (PACKAGE);
 
+  parse_config ();
+
   set_up_terminal ();
 
   enum { HELP_OPTION = CHAR_MAX + 1 };
@@ -218,19 +226,6 @@ main (int argc, char *argv[])
     exit (EXIT_FAILURE);
   }
 
-  /* If developing, add ./appliance to the path.  Note that libtools
-   * interferes with this because uninstalled guestfish is a shell
-   * script that runs the real program with an absolute path.  Detect
-   * that too.
-   *
-   * BUT if LIBGUESTFS_PATH environment variable is already set by
-   * the user, then don't override it.
-   */
-  if (getenv ("LIBGUESTFS_PATH") == NULL &&
-      argv[0] &&
-      (argv[0][0] != '/' || strstr (argv[0], "/.libs/lt-") != NULL))
-    guestfs_set_path (g, "appliance:" GUESTFS_DEFAULT_PATH);
-
   /* CAUTION: we are careful to modify argv[0] here, only after
    * using it just above.
    *
@@ -395,6 +390,24 @@ main (int argc, char *argv[])
     }
   }
 
+  /* Decide here if this will be an interactive session.  We have to
+   * do this as soon as possible after processing the command line
+   * args.
+   */
+  is_interactive = !file && isatty (0);
+
+  /* Register a ^C handler.  We have to do this before launch could
+   * possibly be called below.
+   */
+  if (is_interactive) {
+    memset (&sa, 0, sizeof sa);
+    sa.sa_handler = user_cancel;
+    sa.sa_flags = SA_RESTART;
+    sigaction (SIGINT, &sa, NULL);
+
+    guestfs_set_pgroup (g, 1);
+  }
+
   /* Old-style -i syntax?  Since -a/-d/-N and -i was disallowed
    * previously, if we have -i without any drives but with something
    * on the command line, it must be old-style syntax.
@@ -493,11 +506,23 @@ main (int argc, char *argv[])
     }
   }
 
+  /* Get the name of the input file, for error messages, and replace
+   * the default error handler.
+   */
+  if (!is_interactive) {
+    if (file)
+      input_file = file;
+    else
+      input_file = "*stdin*";
+    guestfs_set_error_handler (g, error_cb, NULL);
+  }
+  input_lineno = 0;
+
   /* Decide if we display progress bars. */
   progress_bars =
     override_progress_bars >= 0
     ? override_progress_bars
-    : (optind >= argc && isatty (0));
+    : (optind >= argc && is_interactive);
 
   if (progress_bars)
     guestfs_set_event_callback (g, progress_callback,
@@ -505,7 +530,7 @@ main (int argc, char *argv[])
 
   /* Interactive, shell script, or command(s) on the command line? */
   if (optind >= argc) {
-    if (isatty (0))
+    if (is_interactive)
       interactive ();
     else
       shell_script ();
@@ -518,6 +543,13 @@ main (int argc, char *argv[])
   exit (EXIT_SUCCESS);
 }
 
+static void
+user_cancel (int sig)
+{
+  if (g)
+    guestfs_user_cancel (g);
+}
+
 /* The <term.h> header file which defines this has "issues". */
 extern int tgetent (char *, const char *);
 
@@ -651,6 +683,8 @@ script (int prompt)
       break;
     }
 
+    input_lineno++;
+
     pcmd = parse_command_line (buf, &exit_on_error);
     if (pcmd.status == -1 && exit_on_error)
       exit (EXIT_FAILURE);
@@ -769,9 +803,8 @@ parse_command_line (char *buf, int *exit_on_error_rtn)
      */
     if (*p == '"') {
       p++;
-      len = strcspn (p, "\"");
-      if (p[len] == '\0') {
-        fprintf (stderr, _("%s: unterminated double quote\n"), program_name);
+      len = parse_quoted_string (p);
+      if (len == -1) {
         pcmd.status = -1;
         return pcmd;
       }
@@ -782,7 +815,6 @@ parse_command_line (char *buf, int *exit_on_error_rtn)
         pcmd.status = -1;
         return pcmd;
       }
-      p[len] = '\0';
       pend = p[len+1] ? &p[len+2] : &p[len+1];
     } else if (*p == '\'') {
       p++;
@@ -846,6 +878,87 @@ parse_command_line (char *buf, int *exit_on_error_rtn)
   return pcmd;
 }
 
+static int
+hexdigit (char d)
+{
+  switch (d) {
+  case '0'...'9': return d - '0';
+  case 'a'...'f': return d - 'a' + 10;
+  case 'A'...'F': return d - 'A' + 10;
+  default: return -1;
+  }
+}
+
+/* Parse double-quoted strings, replacing backslash escape sequences
+ * with the true character.  Since the string is returned in place,
+ * the escapes must make the string shorter.
+ */
+static int
+parse_quoted_string (char *p)
+{
+  char *start = p;
+
+  for (; *p && *p != '"'; p++) {
+    if (*p == '\\') {
+      int m = 1, c;
+
+      switch (p[1]) {
+      case '\\': break;
+      case 'a': *p = '\a'; break;
+      case 'b': *p = '\b'; break;
+      case 'f': *p = '\f'; break;
+      case 'n': *p = '\n'; break;
+      case 'r': *p = '\r'; break;
+      case 't': *p = '\t'; break;
+      case 'v': *p = '\v'; break;
+      case '"': *p = '"'; break;
+      case '\'': *p = '\''; break;
+      case '?': *p = '?'; break;
+
+      case '0'...'7':           /* octal escape - always 3 digits */
+        m = 3;
+        if (p[2] >= '0' && p[2] <= '7' &&
+            p[3] >= '0' && p[3] <= '7') {
+          c = (p[1] - '0') * 0100 + (p[2] - '0') * 010 + (p[3] - '0');
+          if (c < 1 || c > 255)
+            goto error;
+          *p = c;
+        }
+        else
+          goto error;
+        break;
+
+      case 'x':                 /* hex escape - always 2 digits */
+        m = 3;
+        if (c_isxdigit (p[2]) && c_isxdigit (p[3])) {
+          c = hexdigit (p[2]) * 0x10 + hexdigit (p[3]);
+          if (c < 1 || c > 255)
+            goto error;
+          *p = c;
+        }
+        else
+          goto error;
+        break;
+
+      default:
+      error:
+        fprintf (stderr, _("%s: invalid escape sequence in string (starting at offset %d)\n"),
+                 program_name, (int) (p - start));
+        return -1;
+      }
+      memmove (p+1, p+1+m, strlen (p+1+m) + 1);
+    }
+  }
+
+  if (!*p) {
+    fprintf (stderr, _("%s: unterminated double quote\n"), program_name);
+    return -1;
+  }
+
+  *p = '\0';
+  return p - start;
+}
+
 /* Used to handle "<!" (execute command and inline result). */
 static int
 execute_and_inline (const char *cmd, int global_exit_on_error)
@@ -1096,6 +1209,14 @@ extended_help_message (void)
              "For complete documentation:         man guestfish\n"));
 }
 
+/* Error callback.  This replaces the standard libguestfs error handler. */
+static void
+error_cb (guestfs_h *g, void *data, const char *msg)
+{
+  fprintf (stderr, _("%s:%d: libguestfs: error: %s\n"),
+          input_file, input_lineno, msg);
+}
+
 void
 free_strings (char **argv)
 {
@@ -1373,16 +1494,21 @@ xwrite (int fd, const void *v_buf, size_t len)
   return 0;
 }
 
-/* Resolve the special "win:..." form for Windows-specific paths.
- * This always returns a newly allocated string which is freed by the
- * caller function in "cmds.c".
+/* Resolve the special "win:..." form for Windows-specific paths.  The
+ * generated code calls this for all device or path arguments.
+ *
+ * The function returns a newly allocated string, and the caller must
+ * free this string; else display an error and return NULL.
  */
+static char *win_prefix_drive_letter (char drive_letter, const char *path);
+
 char *
-resolve_win_path (const char *path)
+win_prefix (const char *path)
 {
   char *ret;
   size_t i;
 
+  /* If there is not a "win:..." prefix on the path, return strdup'd string. */
   if (STRCASENEQLEN (path, "win:", 4)) {
     ret = strdup (path);
     if (ret == NULL)
@@ -1392,21 +1518,27 @@ resolve_win_path (const char *path)
 
   path += 4;
 
-  /* Drop drive letter, if it's "C:". */
-  if (STRCASEEQLEN (path, "c:", 2))
-    path += 2;
-
-  if (!*path) {
-    ret = strdup ("/");
+  /* If there is a drive letter, rewrite the path. */
+  if (c_isalpha (path[0]) && path[1] == ':') {
+    char drive_letter = c_tolower (path[0]);
+    /* This returns the newly allocated string. */
+    ret = win_prefix_drive_letter (drive_letter, path + 2);
     if (ret == NULL)
+      return NULL;
+  }
+  else if (!*path) {
+    ret = strdup ("/");
+    if (ret == NULL) {
       perror ("strdup");
-    return ret;
+      return NULL;
+    }
   }
-
-  ret = strdup (path);
-  if (ret == NULL) {
-    perror ("strdup");
-    return NULL;
+  else {
+    ret = strdup (path);
+    if (ret == NULL) {
+      perror ("strdup");
+      return NULL;
+    }
   }
 
   /* Blindly convert any backslashes into forward slashes.  Is this good? */
@@ -1421,6 +1553,82 @@ resolve_win_path (const char *path)
   return ret;
 }
 
+static char *
+win_prefix_drive_letter (char drive_letter, const char *path)
+{
+  char **roots = NULL;
+  char **drives = NULL;
+  char **mountpoints = NULL;
+  char *device, *mountpoint, *ret = NULL;
+  size_t i;
+
+  /* Resolve the drive letter using the drive mappings table. */
+  roots = guestfs_inspect_get_roots (g);
+  if (roots == NULL)
+    goto out;
+  if (roots[0] == NULL) {
+    fprintf (stderr, _("%s: to use Windows drive letters, you must inspect the guest (\"-i\" option or run \"inspect-os\" command)\n"),
+             program_name);
+    goto out;
+  }
+  drives = guestfs_inspect_get_drive_mappings (g, roots[0]);
+  if (drives == NULL || drives[0] == NULL) {
+    fprintf (stderr, _("%s: to use Windows drive letters, this must be a Windows guest\n"),
+             program_name);
+    goto out;
+  }
+
+  device = NULL;
+  for (i = 0; drives[i] != NULL; i += 2) {
+    if (c_tolower (drives[i][0]) == drive_letter && drives[i][1] == '\0') {
+      device = drives[i+1];
+      break;
+    }
+  }
+
+  if (device == NULL) {
+    fprintf (stderr, _("%s: drive '%c:' not found.  To list available drives do:\n  inspect-get-drive-mappings %s\n"),
+             program_name, drive_letter, roots[0]);
+    goto out;
+  }
+
+  /* This drive letter must be mounted somewhere (we won't do it). */
+  mountpoints = guestfs_mountpoints (g);
+  if (mountpoints == NULL)
+    goto out;
+
+  mountpoint = NULL;
+  for (i = 0; mountpoints[i] != NULL; i += 2) {
+    if (STREQ (mountpoints[i], device)) {
+      mountpoint = mountpoints[i+1];
+      break;
+    }
+  }
+
+  if (mountpoint == NULL) {
+    fprintf (stderr, _("%s: to access '%c:', mount %s first.  One way to do this is:\n  umount-all\n  mount %s /\n"),
+             program_name, drive_letter, device, device);
+    goto out;
+  }
+
+  /* Rewrite the path, eg. if C: => /c then C:/foo => /c/foo */
+  if (asprintf (&ret, "%s%s%s",
+                mountpoint, STRNEQ (mountpoint, "/") ? "/" : "", path) == -1) {
+    perror ("asprintf");
+    goto out;
+  }
+
+ out:
+  if (roots)
+    free_strings (roots);
+  if (drives)
+    free_strings (drives);
+  if (mountpoints)
+    free_strings (mountpoints);
+
+  return ret;
+}
+
 /* Resolve the special FileIn paths ("-" or "-<<END" or filename).
  * The caller (cmds.c) will call free_file_in after the command has
  * run which should clean up resources.