]> git.neil.brown.name Git - lafs-utils.git/commitdiff
Add new "lafs" program
authorNeilBrown <neilb@suse.de>
Sat, 12 Mar 2011 07:58:32 +0000 (18:58 +1100)
committerNeilBrown <neilb@suse.de>
Sat, 12 Mar 2011 07:58:32 +0000 (18:58 +1100)
"lafs" is similar to "debugfs" for ext[234].

It allows a LaFS to be examined and modified.
Various commands can be enterred, or read from a file.

This preliminary check-in only provides the infrastructure
for reading and parsing commands together with support for
context sensitive completion and help using readline.

Commands implemented are:
  ?  help  exit quit

Naturally more will follow.

Signed-off-by: NeilBrown <neilb@suse.de>
.gitignore
tools/Makefile
tools/lafs.c [new file with mode: 0644]

index bffc514fb92d846f5d878a053ba41982889a8a05..0d064a381e83c01ae275de1da80e607add911f83 100644 (file)
@@ -1,4 +1,5 @@
 *.o
 *.a
 mkfs.lafs
+lafs
 core
index c54b1e86542abb7f0852dcf674d75cd90f76e041..592289abc9e4f9f8522c9d87c7572d208610d024 100644 (file)
 CPPFLAGS = -I../include
 CFLAGS = -Wall -Werror -g
 LDFLAGS = -L../lib
-LDLIBS = -llafs -ltalloc
+LDLIBS = -llafs -ltalloc -lreadline
 
-all : mkfs.lafs
+all : mkfs.lafs lafs
 
 mkfs.lafs : mkfs.lafs.o ../lib/liblafs.a
+
+lafs : lafs.o ../lib/liblafs.a
diff --git a/tools/lafs.c b/tools/lafs.c
new file mode 100644 (file)
index 0000000..af8e00b
--- /dev/null
@@ -0,0 +1,684 @@
+/*
+ * lafs - Examine and manipulate and LaFS image
+ *
+ *  This program is essentially a front-end to liblafs.  It allows
+ *  a LaFS filesystem to be examined and modified.  All interesting
+ *  manipulations are function calls in to liblafs.
+ *  The program simply parses textual commands converting them into
+ *  library calls, and provides a readline interface with autocompletion
+ *  and context-sensitive help.
+ *
+ * Copyright (C) 2011 NeilBrown <neil@brown.name>
+ *
+ *    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
+ *    the Free Software Foundation; either version 2 of the License, or
+ *    (at your option) any later version.
+ *
+ *    This program is distributed in the hope that it will be useful,
+ *    but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *    GNU General Public License for more details.
+ *
+ *    You should have received a copy of the GNU General Public License
+ *    along with this program; if not, write to the Free Software
+ *    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ *    Author: Neil Brown
+ *    Email: <neil@brown.name>
+ *
+ */
+
+#define _GNU_SOURCE
+#include <sys/types.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <readline/readline.h>
+#include <readline/history.h>
+#include <lafs/lafs.h>
+#include <talloc.h>
+
+/* This is the global state which is passed around among
+ * the various commands.
+ */
+struct state {
+       struct lafs *lafs;
+       int done;
+};
+
+/* Every command can have arguments, both positional and
+ * named.
+ * Named arguments are specified with a leading hyphen, though
+ * multiple hyphens may be given in input (completion chooses 2).
+ * Tags for positional arguments must not start with '-', and is used
+ * only for help messages.
+ * Named arguments are normally followed by an '=' and a value.  If an argument is
+ * defined as a 'flag' then no '=' is expected.
+ * A tag argument can provide a value to a positional argument, and the
+ * command can easily determine if the tag version was used.
+ * Each tag may only be given once.
+ * A positional argument can be a subcommand which links to a new set
+ * of argument descriptions.
+ * types are:
+ *   flag:  expect --tagname      This is either present or not.
+ *   opaque:                      An uninterpreted string, often a number.
+ *   external:                    An external filename - completion is possible.
+ *   internal:                    An internal filename - completion might be possible.
+ *   subcommand:                  one of a list of subcommands.
+ * Any unique prefix of a tag is allowed to match.
+ */
+enum argtype { flag, opaque, external, internal, subcommand, terminal };
+static struct args {
+       char *tag;
+       enum argtype type;
+       int pos;
+       struct cmd *subcmd;
+       char *desc;
+} lafs_args[];
+#define TERMINAL_ARG {NULL, terminal, 0, NULL, NULL}
+
+/* When a positional parameter is identified as 'subcommand' it is associated
+ * with a list of 'struct cmd' identifying the possible subcommands.
+ * Any unique prefix of a command is allowed to match.
+ * The initial argument list description allows a single argument which is
+ * a 'subcommand' identifying all the commands that lafs understands.
+ */
+static struct cmd {
+       char *name;
+       void (*cmd)(struct state *st, void **args);
+       struct args *args;
+       char *help;
+} lafs_cmds[];
+
+/* Find a command in a list.  The word must be an exact match, or
+ * a unique prefix.
+ */
+static struct cmd *find_cmd(struct cmd *cmds, char *word)
+{
+       int c;
+       int l = strlen(word);
+       int best = -1;
+
+       for (c = 0; cmds[c].name; c++) {
+               if (strcmp(word, cmds[c].name) == 0)
+                       return cmds+c;
+               if (strncmp(word, cmds[c].name, l) == 0) {
+                       if (best == -1)
+                               best = c;
+                       else
+                               best = -2;
+               }
+       }
+       if (best < 0)
+               return NULL;
+       return cmds + best;
+}
+
+/* Find a tag in an argument list.  The given tag (up to the given length)
+ * must be an exact match or a unique prefix.
+ */
+static int find_tag(struct args *args, const char *tag, int len)
+{
+       int i;
+       int best = -1;
+
+       for (i = 0; args[i].type != terminal; i++)
+               if (args[i].tag[0] == '-') {
+                       if (strncmp(tag, args[i].tag+1, len) != 0)
+                               continue;
+                       if (strlen(args[i].tag+1) == len)
+                               return i;
+                       if (best == -1)
+                               best = i;
+                       else
+                               best = -2;
+               }
+       return best;
+}
+
+
+/* Return the first word on the line, modifying the line in-place and
+ * updating *line to be ready to get the next word.
+ *
+ * Space/tab separates words.  ' or " quotes words.
+ * \ protects quotes and spaces.
+ */
+static char *take_word(char **line)
+{
+       char *rv = *line; /* the buffer we will return - if non-empty */
+       char *lp = rv;    /* the line we are examining */
+       char *wp = rv;    /* the end of the word we are creating. */
+       static char *delim = " \t\n";
+       char quote = '\0';
+
+       while (*lp && strchr(delim, *lp) != NULL)
+               lp++;
+
+       while (*lp && (quote || strchr(delim, *lp) == NULL)) {
+               if (quote && *lp == quote) {
+                       lp++;
+                       continue;
+               }
+               switch(*lp) {
+               case '\'':
+               case '"':
+                       if (quote == *lp) {
+                               quote = '\0';
+                               continue;
+                       }
+                       if (!quote) {
+                               quote = *lp++;
+                               continue;
+                       }
+                       break;
+               case '\\':
+                       if (lp[1] == '\'' || lp[1] == '"' || lp[1] == ' ')
+                               lp++;
+                       break;
+               }
+               *wp++ = *lp++;
+       }
+       if (*lp)
+               lp++;
+       *line = lp;
+       *wp = '\0';
+       if (wp > rv)
+               return rv;
+       return NULL;
+}
+
+/* Return an array of void* corresponding to the entries in
+ * 'args'.  If an arg is present, the array entry is not NULL.
+ * For flags, the array entry will be the full flag.
+ * for --tag=, the array entry will be from after the '='
+ * for positional, the array entry will be the whole arg.
+ * where a tag can fill a positional, the value is placed in both
+ * slots.
+ * If an arg is a subcommand, then the 'struct cmd' point is placed
+ * in the return array rather than a string.
+ * The 'args' pointer is updated to the most precise subcommand.
+ * '*offsetp' is set to the offset in the returned array of the 
+ * first argument in the new 'args' list.
+ * '*lastp' is set to the last tag in 'args' that was matched.
+ * '*error' is an error message if something when astray.
+ *
+ * Named arguments can appear before, after, or among positional
+ * arguments.
+ */
+static void **parse_line(struct args **argsp, char *line, int *offsetp,
+                        int *lastp, char **error)
+{
+       void **rv;
+       int i;
+       char *w;
+       int offset = 0;
+       int size;
+       struct args *args = *argsp;
+
+       if (lastp)
+               *lastp = -1;
+
+       for (i = 0; args[i].type != terminal; i++)
+               ;
+       rv = calloc(i+offset, sizeof(char*));
+       size = i+offset;
+
+       while ((w = take_word(&line)) != NULL) {
+               if (*w == '-') {
+                       /* Find the matching tag. */
+                       char *t = w, *e;
+                       int n, n2;
+                       while (*t == '-')
+                               t++;
+                       e = t;
+                       while (*e && *e != '=')
+                               e++;
+                       n = n2 = find_tag(args, t, e-t);
+                       if (n < 0) {
+                               asprintf(error, "Unrecognised option: %s", w);
+                               break;
+                       }
+                       if (lastp)
+                               *lastp = n;
+                       if (args[n].pos >= 0)
+                               n2 = args[n].pos;
+                       if (rv[n+offset] != NULL || rv[n2+offset] != NULL) {
+                               asprintf(error, "Duplicate option: %s", w);
+                               break;
+                       } else {
+                               if (*e == '=')
+                                       w = e+1;
+                               rv[n+offset] = w;
+                               rv[n2+offset] = w;
+                       }
+               } else {
+                       /* must be next positional */
+                       for (i=0;
+                            args[i].tag[0] != '-' && args[i].type != terminal;
+                            i++)
+                               if (rv[i+offset] == NULL)
+                                       break;
+                       if (args[i].tag[0] == '-' || args[i].type == terminal) {
+                               /* No positions left */
+                               asprintf(error, "Extra positional parameter: %s", w);
+                               break;
+                       }
+                       rv[i+offset] = w;
+                       /* If this is a subcommand arg then we need to
+                        * parse the remaining args in the context of the
+                        * given subcommand - if it exists.
+                        */
+                       if (args[i].type == subcommand) {
+                               struct cmd *c = find_cmd(args[i].subcmd, w);
+                               rv[i+offset] = c;
+                               if (c) {
+                                       args = c->args;
+                                       *argsp = args;
+                                       offset += i+1;
+                                       if (lastp)
+                                               *lastp = -1;
+                                       for (i = 0; args[i].type != terminal; i++)
+                                               ;
+                                       rv = realloc(rv, (i+offset) * sizeof(void*));
+                                       while (size < i + offset) {
+                                               rv[size] = NULL;
+                                               size++;
+                                       }
+                               } else
+                                       asprintf(error, "Unrecognised command: %s",w);
+                       }
+               }
+       }
+       if (offsetp)
+               *offsetp = offset;
+       return rv;
+}
+
+/* parse and execute the given command line against the given state. */
+static int execute_line(struct state *st, char *line)
+{
+       struct cmd *c;
+       struct args *args = lafs_args;
+       void **arglist;
+       char *error = NULL;
+
+       arglist = parse_line(&args, line, NULL, NULL, &error);
+       
+       if (error) {
+               fprintf(stderr, "lafs: %s\n", error);
+               free(error);
+               free(arglist);
+               return -1;
+       }
+       c = (struct cmd*)arglist[0];
+
+       c->cmd(st, arglist);
+       free(arglist);
+       return 1;
+}
+
+static char **complete_in_context(const char *word, int start, int end);
+
+/* 'interact' is the main interface when used interactively.
+ * It reads lines using 'readline' and executes them.
+ * 'readline' is configured to provide context sensitive completion
+ * and help.
+ */
+static void interact(void)
+{
+       struct state st = {0};
+       st.lafs = lafs_alloc();
+       rl_attempted_completion_function = complete_in_context;
+       rl_basic_word_break_characters = " \t\n=";
+       rl_completer_quote_characters = "\"'";
+       rl_initialize();
+       
+       while (!st.done) {
+               char *line = readline("LaFS: ");
+
+               if (!line) {
+                       printf("\n");
+                       break;
+               }
+
+               if (*line)
+                       add_history(line);
+               execute_line(&st, line);
+
+               free(line);
+       }
+}
+
+/* 'runfile' is the alternate interface when a regular file is
+ * given with commands.  It reads and executes commands until
+ * it finds an error.
+ */
+static void runfile(FILE *f)
+{
+       struct state st = {0};
+       st.lafs = lafs_alloc();
+
+       while (!st.done) {
+               char *line = NULL;
+               size_t len, size;
+
+               len = getline(&line, &size, f);
+
+               if (execute_line(&st, line) < 0)
+                       st.done = 1;
+
+               free(line);
+       }
+}
+
+
+int main(int argc, char *argv[])
+{
+       if (argc > 1) {
+               if (strcmp(argv[1], "-") == 0)
+                       runfile(stdin);
+               else {
+                       FILE *f = fopen(argv[1], "r");
+                       if (f)
+                               runfile(f);
+                       else
+                               fprintf(stderr, "lafs: cannot open %s\n", argv[1]);
+               }
+       } else
+               interact();
+       exit(0);
+}
+
+/*
+ * Here be routines to provide context sensitive completion and
+ * help.
+ * Completion understands:
+ *  - lists of subcommands
+ *  - lists of tags for options
+ *  - options that expect filenames, whether internal or external.
+ * Before listing possible matches, the description of the expected
+ * option is given.
+ */
+
+/* cmd_gen is used to generate a list of matching commands.
+ * 'gen_cmds' must be initialised to point to the list.
+ */
+static struct cmd *gen_cmds;
+static char *cmd_gen(const char *prefix, int state)
+{
+       static int next;
+       int len = strlen(prefix);
+       if (state == 0)
+               next = 0;
+       for ( ; gen_cmds[next].name ; next++)
+               if (strncmp(prefix, gen_cmds[next].name, len) == 0) {
+                       next++;
+                       return strdup(gen_cmds[next-1].name);
+               }
+       return NULL;
+}
+
+/* tag_gen is used to generate a list of expected tags.
+ * 'gen_args' is the relevant argument list.
+ * 'gen_found' is used to determine which tags have already been given,
+ *    so they are not offered again.
+ * generated tags always start "--" even if user types "-ta".
+ */
+static struct args *gen_args;
+static void **gen_found;
+static char *tag_gen(const char *prefix, int state)
+{
+       static int next;
+       int len;
+
+       if (state == 0)
+               next = 0;
+
+       while (*prefix == '-')
+               prefix++;
+       len = strlen(prefix);
+
+       for ( ; gen_args[next].type != terminal; next++) {
+               char *c;
+               if (gen_args[next].tag[0] != '-')
+                       continue;
+               if (gen_found[next])
+                       continue;
+               if (gen_args[next].pos >= 0 &&
+                   gen_found[gen_args[next].pos])
+                       continue;
+               if (strncmp(prefix, gen_args[next].tag+1, len) != 0)
+                       continue;
+
+               c = malloc(2 + strlen(gen_args[next].tag+1) + 2);
+               strcpy(c, "--");
+               strcpy(c+2, gen_args[next].tag+1);
+               if (gen_args[next].type != flag) {
+                       strcat(c, "=");
+                       rl_completion_suppress_append = 1;
+               }
+               next++;
+               return c;
+       }
+       return NULL;
+}
+
+/*
+ * This is the brains of the completion handler.
+ * We parse the line-so-far to determine way options have already
+ * been provided, and so what is left to provide.
+ * We then look at the given prefix to determine if a positional or
+ * tag argument is most likely, and provide relevant completions.
+ */
+static char **complete_in_context(const char *prefix, int start, int end)
+{
+       static char *buf = NULL;
+       static int bufsize = 0;
+
+       char *line;
+       void **arglist;
+       struct args *args;
+       int offset, last;
+       int p;
+       char *error = NULL;
+       char **matches = NULL;
+
+       while (bufsize < start+1) {
+               bufsize += 80;
+               buf = realloc(buf, bufsize);
+       }
+       memcpy(buf, rl_line_buffer, start);
+       buf[start] = 0;
+       line = buf;
+
+       args = lafs_args;
+       arglist = parse_line(&args, line, &offset, &last, &error);
+
+       if (error) {
+               printf("\n *** %s ***\n", error);
+               free(error);
+               goto after_message;
+       }
+
+       if (start && rl_line_buffer[start-1] == '=' &&
+           last >= 0 && arglist[last+offset] &&
+           ((char*)arglist[last+offset])[0] == '\0')
+               p = last;
+       else {
+               last = -1;
+               for (p = 0; args[p].tag[0] != '-' && args[p].pos != terminal; p++)
+                       if (arglist[p+offset] == NULL)
+                               break;
+               if (args[p].tag[0] == '-' || args[p].pos == terminal)
+                       p = -1;
+       }
+       /* 'p' is the arg we expect here, either first positional arg that
+        *  we haven't seen, or tag that we have "--tag=" for. */
+
+       if (last >= 0 || (p >= 0 && (!*prefix || *prefix != '-'))) {
+               switch(args[p].type) {
+               case subcommand:
+                       gen_cmds = args[p].subcmd;
+                       matches = rl_completion_matches(prefix, cmd_gen);
+                       break;
+               case external:
+                       matches = rl_completion_matches(
+                               prefix, rl_filename_completion_function);
+                       break;
+               default:
+                       break;
+               }
+               if (rl_completion_type == '?') {
+                       printf("\n *** Please give: %s ***", args[p].desc);
+                       if (!matches) {
+                               printf("\n");
+                               rl_on_new_line();
+                       }
+               }
+               rl_attempted_completion_over = 1;
+               return matches;
+       }
+       if (!*prefix || *prefix == '-') {
+               gen_args = args;
+               gen_found = arglist + offset;
+               rl_attempted_completion_over = 1;
+               return rl_completion_matches(prefix, tag_gen);
+       }
+
+       printf("\n *** No further positional arguments expected:"
+              " try '-' instead ***\n");
+after_message:
+       rl_on_new_line();
+       rl_attempted_completion_over = 1;
+       return NULL;
+}
+
+/***********************************************************************8
+ * Here are the commands.
+ * Each command X must define
+ *   static char help_X = "One line of description for the command";
+ *   static struct args args_X[] = { list of arg definitions; TERMINAL_ARG};
+ *   static void c_X(struct state *st, void *args) { implement command ;}
+ * and must be listed in lafs_cmds below.
+ */
+
+/****** EXIT ******/
+static char help_exit[] = "Exit lafs";
+static struct args args_exit[] = { TERMINAL_ARG };
+static void c_exit(struct state *st, void **args)
+{
+       st->done = 1;
+}
+/****** QUIT ******/
+#define help_quit help_exit
+#define c_quit c_exit
+#define args_quit args_exit
+
+/****** HELP ******/
+static char help_help[] = "Print help for a command or all commands";
+static struct args args_help[] = {
+       { "COMMAND", subcommand, -1, lafs_cmds, "Command to display help for"},
+       { "-all", flag,      -1, NULL, "List brief help for all commands"},
+       TERMINAL_ARG
+};
+
+static void c_help(struct state *st, void **args)
+{
+       int c;
+       struct cmd *cmd = args[1];
+
+       if (cmd == NULL && args[2] == NULL) {
+               for (c = 0; lafs_cmds[c].name; c++) {
+                       printf("%-9s ", lafs_cmds[c].name);
+                       if ((c%8)==7)
+                               printf("\n");
+               }
+               printf("\n");
+               return;
+       }
+
+       if (cmd) {
+               printf("%s: %s\n", cmd->name, cmd->help);
+               if (cmd->args[0].type != terminal) {
+                       int i;
+                       printf(" Usage: %s", cmd->name);
+                       for (i=0; cmd->args[i].type != terminal; i++) {
+                               struct args *a = cmd->args+i;
+                               if (a->tag[0] == '-') {
+                                       printf(" [--options...]");
+                                       break;
+                               }
+                               printf(" %s", a->tag);
+                       }
+                       printf("\n");
+                       printf(" Arguments:\n");
+                       for (i=0; cmd->args[i].type != terminal; i++) {
+                               struct args *a = cmd->args+i;
+                               if (a->tag[0] != '-') {
+                                       printf("  %-15s: %s\n", a->tag, a->desc);
+                               } else
+                                       printf("  -%-14s: %s\n", a->tag, a->desc);
+                       }
+               }
+       } else {
+               printf("-------------------------------------------\n");
+               for (c=0; lafs_cmds[c].name; c++)
+                       printf("%-9s: %s\n", lafs_cmds[c].name, lafs_cmds[c].help);
+       }
+}
+
+/****** NEWFS ******/
+static char help_newfs[] = "Create a new LaFS filesystem, which can then be stored on one or more devices.";
+static struct args args_newfs[] = {
+       { "BLOCK-SIZE",  opaque, -1, NULL, "Block size, 512..4096, defaults to 1024"},
+       { "-block-size", opaque,  0, NULL, "Block size, 512..4096, defaults to 1024"},
+       { "-state-size", opaque, -1, NULL, "Size of state block, defaults to 1024"},
+       { "-uuid",       opaque, -1, NULL, "UUID - normally randomly generated"},
+       { "-black", opaque, -1, NULL, "nothing (just testing)"},
+       TERMINAL_ARG
+};
+static void c_newfs(struct state *st, void **args)
+{
+       /* FIXME */
+       return;
+}
+
+/****** STORE ******/
+static char help_store[] = "Create a file in the LaFS from an external file";
+static struct args args_store[] = {
+       { "FROM", external, -1, NULL, "File to copy into LaFS"},
+       { "TO",   internal, -1, NULL, "Where to store file in LaFS"},
+       { "-from", external, 0, NULL, "File to copy into LaFS"},
+       TERMINAL_ARG
+};
+static void c_store(struct state *st, void **args)
+{
+       char *from = args[1];
+       char *to = args[2];
+       if (!from)
+               printf("ERROR: Source file is missing\n");
+       else if (!to)
+               printf("ERROR: destination file name is missing\n");
+       else
+               printf("Oh how I wish I could copy %s to %s\n", from, to);
+}
+
+/* list of all commands - preferably in alphabetical order */
+#define CMD(x) {#x, c_##x, args_##x, help_##x}
+static struct cmd lafs_cmds[] = {
+       {"?", c_help, args_help, help_help},
+       CMD(exit),
+       CMD(help),
+       CMD(newfs),
+       CMD(quit),
+       CMD(store),
+       { NULL, NULL, NULL, NULL}
+};
+
+static struct args lafs_args[] = {
+       { "COMMAND",    subcommand, -1, lafs_cmds, "Command for lafs to execute"},
+       TERMINAL_ARG
+};