diff --git a/src/cmd/Makefile.am b/src/cmd/Makefile.am index 013b711c334a..64b56831c287 100644 --- a/src/cmd/Makefile.am +++ b/src/cmd/Makefile.am @@ -10,6 +10,7 @@ AM_CPPFLAGS = \ $(ZMQ_CFLAGS) fluxcmd_ldadd = \ + $(top_builddir)/src/common/libkvs/libkvs.la \ $(top_builddir)/src/common/libflux-internal.la \ $(top_builddir)/src/common/libflux-core.la \ $(top_builddir)/src/common/libflux-optparse.la \ diff --git a/src/cmd/flux-kvs.c b/src/cmd/flux-kvs.c index b984e93a56f6..595ce9252bb5 100644 --- a/src/cmd/flux-kvs.c +++ b/src/cmd/flux-kvs.c @@ -25,15 +25,18 @@ #if HAVE_CONFIG_H #include "config.h" #endif +#include #include #include #include #include #include +#include #include "src/common/libutil/xzmalloc.h" #include "src/common/libutil/log.h" #include "src/common/libutil/readall.h" +#include "src/common/libkvs/treeobj.h" int cmd_get (optparse_t *p, int argc, char **argv); int cmd_put (optparse_t *p, int argc, char **argv); @@ -48,9 +51,12 @@ int cmd_dropcache (optparse_t *p, int argc, char **argv); int cmd_copy (optparse_t *p, int argc, char **argv); int cmd_move (optparse_t *p, int argc, char **argv); int cmd_dir (optparse_t *p, int argc, char **argv); +int cmd_ls (optparse_t *p, int argc, char **argv); static void dump_kvs_dir (kvsdir_t *dir, bool Ropt, bool dopt); +#define min(a,b) ((a)<(b)?(a):(b)) + static struct optparse_option dir_opts[] = { { .name = "recursive", .key = 'R', .has_arg = 0, .usage = "Recursively display keys under subdirectories", @@ -61,6 +67,28 @@ static struct optparse_option dir_opts[] = { OPTPARSE_TABLE_END }; +static struct optparse_option ls_opts[] = { + { .name = "recursive", .key = 'R', .has_arg = 0, + .usage = "List directory recursively", + }, + { .name = "directory", .key = 'd', .has_arg = 0, + .usage = "List directory instead of contents", + }, + { .name = "width", .key = 'w', .has_arg = 1, + .usage = "Set output width to COLS. 0 means no limit", + }, + { .name = "1", .key = '1', .has_arg = 0, + .usage = "Force one entry per line", + }, + { .name = "classify", .key = 'F', .has_arg = 0, + .usage = "Append indicator (one of .@) to entries", + }, + { .name = "columns", .key = 'C', .has_arg = 0, + .usage = "List entries by columns", + }, + OPTPARSE_TABLE_END +}; + static struct optparse_option watch_opts[] = { { .name = "recursive", .key = 'R', .has_arg = 0, .usage = "Recursively display keys under subdirectories", @@ -116,6 +144,13 @@ static struct optparse_subcommand subcommands[] = { 0, dir_opts }, + { "ls", + "[-R] [-d] [-w COLS] [-1] [-W] [-C] [key...]", + "List directory", + cmd_ls, + 0, + ls_opts + }, { "unlink", "key [key...]", "Remove key", @@ -816,6 +851,251 @@ int cmd_dir (optparse_t *p, int argc, char **argv) return (0); } +/* Find longest name in a directory. + */ +static int get_dir_maxname (kvsdir_t *dir) +{ + kvsitr_t *itr; + const char *name; + int max = 0; + + if (!(itr = kvsitr_create (dir))) + log_err_exit ("kvsitr_create"); + while ((name = kvsitr_next (itr))) + if (max < strlen (name)) + max = strlen (name); + kvsitr_destroy (itr); + return max; +} + +/* Find longest name in a list of names. + */ +static int get_list_maxname (zlist_t *names) +{ + const char *name; + int max = 0; + + name = zlist_first (names); + while (name) { + if (max < strlen (name)) + max = strlen (name); + name = zlist_next (names); + } + return max; +} + +/* Determine terminal window width to use for columnar output. + * --width=COLS or -1 force the width to COLS or 1, respectively. + * If not forced and not a tty, use 1, unless -C, then use 80. + * Otherwise ask the tty how wide it is. + * N.B. --width=0 forces "unlimited" width. + */ +static int get_window_width (optparse_t *p, int fd) +{ + struct winsize w; + int width; + + if (optparse_hasopt (p, "1")) + return 1; + if ((width = optparse_get_int (p, "width", -1)) >= 0) + return width; + if (ioctl (fd, TIOCGWINSZ, &w) == 0) + return w.ws_col; + if (optparse_hasopt (p, "columns")) + return 80; + return 1; +} + +/* List the content of 'dir', arranging output in columns that fit 'win_width'. + */ +static void list_kvs_dir_single (kvsdir_t *dir, int win_width, optparse_t *p) +{ + kvsitr_t *itr; + const char *name; + int col_width = get_dir_maxname (dir) + 4; + int col = 0; + char *namebuf; + + if (!(namebuf = malloc (col_width + 2))) + log_err_exit ("malloc"); + if (!(itr = kvsitr_create (dir))) + log_err_exit ("kvsitr_create"); + while ((name = kvsitr_next (itr))) { + if (win_width != 0 && col + col_width > win_width && col > 0) { + printf ("\n"); + col = 0; + } + strcpy (namebuf, name); + if (optparse_hasopt (p, "classify")) { + if (kvsdir_isdir (dir, name)) + strcat (namebuf, "."); + else if (kvsdir_issymlink (dir, name)) + strcat (namebuf, "@"); + } + printf ("%-*s", min (col_width, win_width), namebuf); + col += col_width; + } + printf ("\n"); + kvsitr_destroy (itr); + free (namebuf); +} + +/* List contents of directory pointed to by 'key'. + * This function is called recursively if the -R option was specified. + */ +static void list_kvs_dir (flux_t *h, const char *key, optparse_t *p, + int win_width, bool print_label, bool print_vspace) +{ + flux_future_t *f; + kvsdir_t *dir; + kvsitr_t *itr; + const char *name, *json_str; + + if (!(f = flux_kvs_lookup (h, FLUX_KVS_READDIR, key)) + || flux_kvs_lookup_get (f, &json_str) < 0) { + log_err_exit ("%s", key); + goto done; + } + + if (print_label) + printf ("%s%s:\n", print_vspace ? "\n" : "", key); + if (!(dir = kvsdir_create (h, NULL, key, json_str))) + log_err_exit ("kvsdir_create"); + list_kvs_dir_single (dir, win_width, p); + + if (optparse_hasopt (p, "recursive")) { + if (!(itr = kvsitr_create (dir))) + log_err_exit ("kvsitr_create"); + while ((name = kvsitr_next (itr))) { + if (kvsdir_isdir (dir, name)) { + char *nkey; + if (!(nkey = kvsdir_key_at (dir, name))) { + log_err ("%s: kvsdir_key_at failed", name); + continue; + } + list_kvs_dir (h, nkey, p, win_width, print_label, true); + free (nkey); + } + } + kvsdir_destroy (dir); + } +done: + flux_future_destroy (f); +} + +/* List keys, arranging in columns so that they fit within 'win_width'. + * The keys are popped off the list and freed. + */ +static void list_kvs_keys (zlist_t *singles, int win_width) +{ + char *name; + int col_width = get_list_maxname (singles) + 4; + int col = 0; + + while ((name = zlist_pop (singles))) { + if (win_width != 0 && col + col_width > win_width && col > 0) { + printf ("\n"); + col = 0; + } + printf ("%-*s", min (col_width, win_width), name); + col += col_width; + free (name); + } + printf ("\n"); +} + +/* Put key in 'dirs' or 'singles' list, depending on whether + * its contents are to be listed or not. If -F is specified, + * 'singles' key names are decorated based on their type. + */ +static void categorize_key (optparse_t *p, const char *key, + zlist_t *dirs, zlist_t *singles) +{ + flux_t *h = (flux_t *)optparse_get_data (p, "flux_handle"); + flux_future_t *f; + const char *json_str; + char *nkey; + json_t *treeobj = NULL; + + if (!(f = flux_kvs_lookup (h, FLUX_KVS_TREEOBJ, key))) + log_err_exit ("flux_kvs_lookup"); + if (flux_kvs_lookup_get (f, &json_str) < 0) { + fprintf (stderr, "%s: %s\n", key, flux_strerror (errno)); + goto done; + } + if (!(treeobj = treeobj_decode (json_str))) + log_err_exit ("%s: metadata decode error", key); + if (treeobj_is_dir (treeobj) || treeobj_is_dirref (treeobj)) { + if (optparse_hasopt (p, "directory")) { + nkey = xasprintf ("%s%s", key, + optparse_hasopt (p, "classify") ? "." : ""); + if (zlist_append (singles, nkey) < 0) + log_err_exit ("zlist_append"); + } + else + if (zlist_append (dirs, xstrdup (key)) < 0) + log_err_exit ("zlist_append"); + } + else if (treeobj_is_val (treeobj) || treeobj_is_valref (treeobj)) { + if (zlist_append (singles, xstrdup (key)) < 0) + log_err_exit ("zlist_append"); + } + else if (treeobj_is_symlink (treeobj)) { + nkey = xasprintf ("%s%s", key, + optparse_hasopt (p, "classify") ? "@" : ""); + if (zlist_append (singles, nkey) < 0) + log_err_exit ("zlist_append"); + } +done: + json_decref (treeobj); + flux_future_destroy (f); + return; +} + +/* Mimic ls(1). + * Strategy: sort keys from command line into two lists: 'dirs' for + * directories to be expanded, and 'singles' for others. + * First display the singles, then display directories, optionally recursing. + * Assume "." if no keys were specified on the command line. + */ +int cmd_ls (optparse_t *p, int argc, char **argv) +{ + flux_t *h = (flux_t *)optparse_get_data (p, "flux_handle"); + int optindex = optparse_option_index (p); + int win_width = get_window_width (p, STDOUT_FILENO); + zlist_t *dirs, *singles; + char *key; + bool print_label = false; // print "name:" before directory contents + bool print_vspace = false; // print vertical space before label + + if (!(dirs = zlist_new ()) || !(singles = zlist_new ())) + log_err_exit ("zlist_new"); + + if (optindex == argc) + categorize_key (p, ".", dirs, singles); + while (optindex < argc) + categorize_key (p, argv[optindex++], dirs, singles); + + if (zlist_size (singles) > 0) { + list_kvs_keys (singles, win_width); + print_label = true; + print_vspace = true; + } + + if (optparse_hasopt (p, "recursive") || zlist_size (dirs) > 1) + print_label = true; + while ((key = zlist_pop (dirs))) { + list_kvs_dir (h, key, p, win_width, print_label, print_vspace); + print_vspace = true; + free (key); + } + + zlist_destroy (&dirs); + zlist_destroy (&singles); + + return (0); +} + int cmd_copy (optparse_t *p, int argc, char **argv) { flux_t *h = (flux_t *)optparse_get_data (p, "flux_handle");