Quoting in guestfish.
[libguestfs.git] / fish / fish.c
index b0d91c7..b825626 100644 (file)
 #include <inttypes.h>
 #include <assert.h>
 
+#ifdef HAVE_LIBREADLINE
+#include <readline/readline.h>
+#include <readline/history.h>
+#endif
+
 #include <guestfs.h>
 
 #include "fish.h"
@@ -46,6 +51,9 @@ static void script (int prompt);
 static void cmdline (char *argv[], int optind, int argc);
 static int issue_command (const char *cmd, char *argv[]);
 static int parse_size (const char *str, off_t *size_rtn);
+static void initialize_readline (void);
+static void cleanup_readline (void);
+static void add_history_line (const char *);
 
 /* Currently open libguestfs handle. */
 guestfs_h *g;
@@ -114,6 +122,8 @@ main (int argc, char *argv[])
   char *p;
   int c;
 
+  initialize_readline ();
+
   /* guestfs_create is meant to be a lightweight operation, so
    * it's OK to do it early here.
    */
@@ -208,6 +218,8 @@ main (int argc, char *argv[])
   else
     cmdline (argv, optind, argc);
 
+  cleanup_readline ();
+
   exit (0);
 }
 
@@ -254,13 +266,51 @@ shell_script (void)
   script (0);
 }
 
+#define FISH "><fs> "
+
+static char *line_read = NULL;
+
+static char *
+rl_gets (int prompt)
+{
+#ifdef HAVE_LIBREADLINE
+
+  if (line_read) {
+    free (line_read);
+    line_read = NULL;
+  }
+
+  line_read = readline (prompt ? FISH : "");
+
+  if (prompt && line_read && *line_read)
+    add_history_line (line_read);
+
+#else /* !HAVE_LIBREADLINE */
+
+  static char buf[8192];
+  int len;
+
+  if (prompt) printf (FISH);
+  line_read = fgets (buf, sizeof buf, stdin);
+
+  if (line_read) {
+    len = strlen (line_read);
+    if (len > 0 && buf[len-1] == '\n') buf[len-1] = '\0';
+  }
+
+#endif /* !HAVE_LIBREADLINE */
+
+  return line_read;
+}
+
 static void
 script (int prompt)
 {
-  char buf[8192];
+  char *buf;
   char *cmd;
+  char *p, *pend;
   char *argv[64];
-  int len, i;
+  int i, len;
 
   if (prompt)
     printf ("\n"
@@ -272,32 +322,124 @@ script (int prompt)
            "\n");
 
   while (!quit) {
-    if (prompt) printf ("><fs> ");
-    if (fgets (buf, sizeof buf, stdin) == NULL) {
+    buf = rl_gets (prompt);
+    if (!buf) {
       quit = 1;
       break;
     }
 
-    len = strlen (buf);
-    if (len > 0 && buf[len-1] == '\n') buf[len-1] = '\0';
+    /* Skip any initial whitespace before the command. */
+    while (*buf && isspace (*buf))
+      buf++;
+
+    /* Get the command (cannot be quoted). */
+    len = strcspn (buf, " \t");
 
-    /* Split the buffer up at whitespace. */
-    cmd = strtok (buf, " \t");
-    if (cmd == NULL)
-      continue;
+    if (len == 0) continue;
 
+    cmd = buf;
     i = 0;
-    while (i < sizeof argv / sizeof argv[0] &&
-          (argv[i] = strtok (NULL, " \t")) != NULL)
-      i++;
+    if (buf[len] == '\0') {
+      argv[0] = NULL;
+      goto got_command;
+    }
+
+    buf[len] = '\0';
+    p = &buf[len+1];
+    p += strspn (p, " \t");
+
+    /* Get the parameters. */
+    while (*p && i < sizeof argv / sizeof argv[0]) {
+      /* Parameters which start with quotes or square brackets
+       * are treated specially.  Bare parameters are delimited
+       * by whitespace.
+       */
+      if (*p == '"') {
+       p++;
+       len = strcspn (p, "\"");
+       if (p[len] == '\0') {
+         fprintf (stderr, "guestfish: unterminated double quote\n");
+         if (!prompt) exit (1);
+         goto next_command;
+       }
+       if (p[len+1] && (p[len+1] != ' ' && p[len+1] != '\t')) {
+         fprintf (stderr, "guestfish: command arguments not separated by whitespace\n");
+         if (!prompt) exit (1);
+         goto next_command;
+       }
+       p[len] = '\0';
+       pend = &p[len+2];
+      } else if (*p == '\'') {
+       p++;
+       len = strcspn (p, "'");
+       if (p[len] == '\0') {
+         fprintf (stderr, "guestfish: unterminated single quote\n");
+         if (!prompt) exit (1);
+         goto next_command;
+       }
+       if (p[len+1] && (p[len+1] != ' ' && p[len+1] != '\t')) {
+         fprintf (stderr, "guestfish: command arguments not separated by whitespace\n");
+         if (!prompt) exit (1);
+         goto next_command;
+       }
+       p[len] = '\0';
+       pend = &p[len+2];
+       /*
+      } 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, "guestfish: unterminated \"[...]\" sequence\n");
+         if (!prompt) exit (1);
+         goto next_command;
+       }
+       if (*pend && (*pend != ' ' && *pend != '\t')) {
+         fprintf (stderr, "guestfish: command arguments not separated by whitespace\n");
+         if (!prompt) exit (1);
+         goto next_command;
+       }
+       *(pend-1) = '\0';
+       */
+      } else if (*p != ' ' && *p != '\t') {
+       len = strcspn (p, " \t");
+       if (p[len]) {
+         p[len] = '\0';
+         pend = &p[len+1];
+       } else
+         pend = &p[len];
+      } else {
+       fprintf (stderr, "guestfish: internal error parsing string at '%s'\n",
+                p);
+       abort ();
+      }
+
+      argv[i++] = p;
+      p = pend;
+
+      if (*p)
+       p += strspn (p, " \t");
+    }
+
     if (i == sizeof argv / sizeof argv[0]) {
-      fprintf (stderr, "guestfish: too many arguments in command\n");
-      exit (1);
+      fprintf (stderr, "guestfish: too many arguments\n");
+      if (!prompt) exit (1);
+      goto next_command;
     }
 
+    argv[i] = NULL;
+
+  got_command:
     if (issue_command (cmd, argv) == -1) {
       if (!prompt) exit (1);
     }
+
+  next_command:;
   }
   if (prompt) printf ("\n");
 }
@@ -514,10 +656,7 @@ is_true (const char *str)
     strcasecmp (str, "no") != 0;
 }
 
-/* This is quite inadequate for real use.  For example, there is no way
- * to specify an empty list.  We need to use a real parser to allow
- * quoting, empty lists, etc.
- */
+/* XXX We could improve list parsing. */
 char **
 parse_string_list (const char *str)
 {
@@ -527,7 +666,7 @@ parse_string_list (const char *str)
 
   argc = 1;
   for (i = 0; str[i]; ++i)
-    if (str[i] == ':') argc++;
+    if (str[i] == ' ') argc++;
 
   argv = malloc (sizeof (char *) * (argc+1));
   if (argv == NULL) { perror ("malloc"); exit (1); }
@@ -535,12 +674,63 @@ parse_string_list (const char *str)
   p = str;
   i = 0;
   while (*p) {
-    pend = strchrnul (p, ':');
+    pend = strchrnul (p, ' ');
     argv[i] = strndup (p, pend-p);
     i++;
-    p = *pend == ':' ? pend+1 : pend;
+    p = *pend == ' ' ? pend+1 : pend;
   }
   argv[i] = NULL;
 
   return argv;
 }
+
+#ifdef HAVE_LIBREADLINE
+static char histfile[1024];
+static int nr_history_lines = 0;
+#endif
+
+static void
+initialize_readline (void)
+{
+#ifdef HAVE_LIBREADLINE
+  const char *home;
+
+  home = getenv ("HOME");
+  if (home) {
+    snprintf (histfile, sizeof histfile, "%s/.guestfish", home);
+    using_history ();
+    (void) read_history (histfile);
+  }
+
+  rl_readline_name = "guestfish";
+  rl_attempted_completion_function = do_completion;
+#endif
+}
+
+static void
+cleanup_readline (void)
+{
+#ifdef HAVE_LIBREADLINE
+  int fd;
+
+  if (histfile[0] != '\0') {
+    fd = open (histfile, O_WRONLY|O_CREAT, 0644);
+    if (fd == -1) {
+      perror (histfile);
+      return;
+    }
+    close (fd);
+
+    (void) append_history (nr_history_lines, histfile);
+  }
+#endif
+}
+
+static void
+add_history_line (const char *line)
+{
+#ifdef HAVE_LIBREADLINE
+  add_history (line);
+  nr_history_lines++;
+#endif
+}