From 13a805bd79182bbd7d0ef89ba5ac1b359d00c478 Mon Sep 17 00:00:00 2001 From: Mooffie Date: Fri, 18 Nov 2016 13:14:18 +0200 Subject: [PATCH] Ticket #3730: extfs: introduce a command-line tool for parsing file lists. We introduce 'mc_parse_ls_l', a tool for parsing file-listings in format similar to that of 'ls -l'. This format is used by the various extfs helpers. We'll use this tool, in the next commit, to build a tester. Signed-off-by: Andrew Borodin --- configure.ac | 1 + tests/src/Makefile.am | 2 +- tests/src/extfs-helpers-listcmd/Makefile.am | 20 + .../src/extfs-helpers-listcmd/mc_parse_ls_l.c | 405 ++++++++++++++++++ tests/src/extfs-helpers-listcmd/run | 15 + 5 files changed, 442 insertions(+), 1 deletion(-) create mode 100644 tests/src/extfs-helpers-listcmd/Makefile.am create mode 100644 tests/src/extfs-helpers-listcmd/mc_parse_ls_l.c create mode 100755 tests/src/extfs-helpers-listcmd/run diff --git a/configure.ac b/configure.ac index 04423335b..a8d144e01 100644 --- a/configure.ac +++ b/configure.ac @@ -638,6 +638,7 @@ tests/src/Makefile tests/src/filemanager/Makefile tests/src/editor/Makefile tests/src/editor/test-data.txt +tests/src/extfs-helpers-listcmd/Makefile ]) fi diff --git a/tests/src/Makefile.am b/tests/src/Makefile.am index 74eecf612..0ab1c6da7 100644 --- a/tests/src/Makefile.am +++ b/tests/src/Makefile.am @@ -1,6 +1,6 @@ PACKAGE_STRING = "/src" -SUBDIRS = . filemanager +SUBDIRS = . filemanager extfs-helpers-listcmd if USE_INTERNAL_EDIT SUBDIRS += editor diff --git a/tests/src/extfs-helpers-listcmd/Makefile.am b/tests/src/extfs-helpers-listcmd/Makefile.am new file mode 100644 index 000000000..043fa8acc --- /dev/null +++ b/tests/src/extfs-helpers-listcmd/Makefile.am @@ -0,0 +1,20 @@ +PACKAGE_STRING = "/src/extfs-helpers-listcmd" + +AM_CPPFLAGS = $(GLIB_CFLAGS) -I$(top_srcdir) + +# This lets mc_parse_ls_l.c override MC's message() without the linker +# complaining about multiple definitions. +AM_LDFLAGS = @TESTS_LDFLAGS@ + +LIBS = $(top_builddir)/lib/libmc.la + +# Programs/scripts to build on 'make check'. +check_PROGRAMS = mc_parse_ls_l + +# Tests to run on 'make check' +TESTS = run + +mc_parse_ls_l_SOURCES = \ + mc_parse_ls_l.c + +EXTRA_DIST = run diff --git a/tests/src/extfs-helpers-listcmd/mc_parse_ls_l.c b/tests/src/extfs-helpers-listcmd/mc_parse_ls_l.c new file mode 100644 index 000000000..40b0149ea --- /dev/null +++ b/tests/src/extfs-helpers-listcmd/mc_parse_ls_l.c @@ -0,0 +1,405 @@ +/* + A parser for file-listings formatted like 'ls -l'. + + Copyright (C) 2016 + Free Software Foundation, Inc. + + This file is part of the Midnight Commander. + + The Midnight Commander 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 3 of the License, + or (at your option) any later version. + + The Midnight Commander 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, see . + */ + +/** \file + * \brief A parser for file-listings formatted like 'ls -l'. + * + * This program parses file-listings the same way MC does. + * It's basically a wrapper around vfs_parse_ls_lga(). + * You can feed it the output of any of extfs's helpers. + * + * After parsing, the data is written out to stdout. The output + * format can be either YAML or a format similar to 'ls -l'. + * + * This program is the main tool our tester script is going to use. + */ + +#include + +#include +#include + +#include "lib/global.h" + +#include "lib/vfs/utilvfs.h" /* vfs_parse_ls_lga() */ +#include "lib/util.h" /* string_perm() */ +#include "lib/timefmt.h" /* FMT_LOCALTIME */ +#include "lib/widget.h" /* for the prototype of message() only */ + +/*** global variables ****************************************************************************/ + +/*** file scope macro definitions ****************************************************************/ + +/*** file scope type declarations ****************************************************************/ + +typedef enum +{ + FORMAT_YAML, + FORMAT_LS +} output_format_t; + +/*** file scope variables ************************************************************************/ + +/* Command-line options. */ +static gboolean opt_drop_mtime = FALSE; +static gboolean opt_drop_ids = FALSE; +static gboolean opt_symbolic_ids = FALSE; +static output_format_t opt_output_format = FORMAT_LS; + +/* Misc. */ +static int error_count = 0; + +/* forward declarations */ +static gboolean +parse_format_name_argument (const gchar * option_name, const gchar * value, gpointer data, + GError ** error); + +static GOptionEntry entries[] = { + {"drop-mtime", 0, 0, G_OPTION_ARG_NONE, &opt_drop_mtime, "Don't include mtime in the output.", + NULL}, + {"drop-ids", 0, 0, G_OPTION_ARG_NONE, &opt_drop_ids, "Don't include uid/gid in the output.", + NULL}, + {"symbolic-ids", 0, 0, G_OPTION_ARG_NONE, &opt_symbolic_ids, + "Print the strings '<>'/'<>' instead of the numeric IDs when they match the process' uid/gid.", + NULL}, + {"format", 'f', 0, G_OPTION_ARG_CALLBACK, parse_format_name_argument, + "Output format. Default: ls.", ""}, + {NULL, '\0', 0, 0, NULL, NULL, NULL} /* Make the compiler shut up by initializing everything. */ +}; + +/*** file scope functions ************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ +/** + * Command-line handling. + */ +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +parse_format_name_argument (const gchar * option_name, const gchar * value, gpointer data, + GError ** error) +{ + (void) option_name; + (void) data; + + if (strcmp (value, "yaml") == 0) + opt_output_format = FORMAT_YAML; + else if (strcmp (value, "ls") == 0) + opt_output_format = FORMAT_LS; + else + { + g_set_error (error, MC_ERROR, G_OPTION_ERROR_FAILED, "unknown output format '%s'", value); + return FALSE; + } + + return TRUE; +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +parse_command_line (int *argc, char **argv[]) +{ + GError *error = NULL; + GOptionContext *context; + + context = + g_option_context_new + ("- Parses its input, which is expected to be in a format similar to 'ls -l', and writes the result to stdout."); + g_option_context_add_main_entries (context, entries, NULL); + if (!g_option_context_parse (context, argc, argv, &error)) + { + g_print ("option parsing failed: %s\n", error->message); + return FALSE; + } + + return TRUE; +} + +/* --------------------------------------------------------------------------------------------- */ +/** + * Utility functions. + */ +/* --------------------------------------------------------------------------------------------- */ + +static const char * +my_itoa (int i) +{ + static char buf[BUF_SMALL]; + + sprintf (buf, "%d", i); + return buf; +} + +/* --------------------------------------------------------------------------------------------- */ + +/** + * Returns the uid as-is, or as "<>" if the same as current user. + */ +static const char * +symbolic_uid (uid_t uid) +{ + return (opt_symbolic_ids && uid == getuid ())? "<>" : my_itoa ((int) uid); +} + +/* --------------------------------------------------------------------------------------------- */ + +static const char * +symbolic_gid (gid_t gid) +{ + return (opt_symbolic_ids && gid == getgid ())? "<>" : my_itoa ((int) gid); +} + +/* --------------------------------------------------------------------------------------------- */ + +/** + * Cuts off a string's line-end (as in Perl). + */ +static void +chomp (char *s) +{ + int i; + + i = strlen (s); + + /* Code taken from vfs_parse_ls_lga(), with modifications. */ + if ((--i >= 0) && (s[i] == '\r' || s[i] == '\n')) + s[i] = '\0'; + if ((--i >= 0) && (s[i] == '\r' || s[i] == '\n')) + s[i] = '\0'; +} + +/* --------------------------------------------------------------------------------------------- */ + +static const char * +string_date (time_t time) +{ + static char buf[BUF_SMALL]; + + /* + * If we ever want to be able to re-parse the output of this program, + * we'll need to use the American brain-damaged MM-DD-YYYY instead of + * YYYY-MM-DD because vfs_parse_ls_lga() doesn't currently recognize + * the latter. + */ + FMT_LOCALTIME (buf, sizeof buf, "%Y-%m-%d %H:%M:%S", time); + return buf; +} + +/* --------------------------------------------------------------------------------------------- */ + +/** + * Override MC's message(). + * + * vfs_parse_ls_lga() calls this on error. Since MC's uses tty/widgets, it + * will crash us. We replace it with a plain version. + */ +void +message (int flags, const char *title, const char *text, ...) +{ + char *p; + va_list ap; + + (void) flags; + (void) title; + + va_start (ap, text); + p = g_strdup_vprintf (text, ap); + va_end (ap); + printf ("message(): vfs_parse_ls_lga(): parsing error at: %s\n", p); + g_free (p); +} + +/* --------------------------------------------------------------------------------------------- */ +/** + * YAML output format. + */ +/* --------------------------------------------------------------------------------------------- */ + +static void +yaml_dump_stbuf (const struct stat *st) +{ + /* Various casts and flags here were taken/inspired by info_show_info(). */ + printf (" perm: %s\n", string_perm (st->st_mode)); + if (!opt_drop_ids) + { + printf (" uid: %s\n", symbolic_uid (st->st_uid)); + printf (" gid: %s\n", symbolic_gid (st->st_gid)); + } + printf (" size: %" PRIuMAX "\n", (uintmax_t) st->st_size); + printf (" nlink: %d\n", (int) st->st_nlink); + if (!opt_drop_mtime) + printf (" mtime: %s\n", string_date (st->st_mtime)); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +yaml_dump_string (const char *name, const char *val) +{ + char *q; + + q = g_shell_quote (val); + printf (" %s: %s\n", name, q); + g_free (q); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +yaml_dump_record (gboolean success, const char *input_line, const struct stat *st, + const char *filename, const char *linkname) +{ + printf ("-\n"); /* Start new item in the list. */ + + if (success) + { + if (filename != NULL) + yaml_dump_string ("name", filename); + if (linkname != NULL) + yaml_dump_string ("linkname", linkname); + yaml_dump_stbuf (st); + } + else + { + yaml_dump_string ("cannot parse input line", input_line); + } +} + +/* --------------------------------------------------------------------------------------------- */ +/** + * 'ls' output format. + */ +/* --------------------------------------------------------------------------------------------- */ + +static void +ls_dump_stbuf (const struct stat *st) +{ + /* Various casts and flags here were taken/inspired by info_show_info(). */ + printf ("%s %3d ", string_perm (st->st_mode), (int) st->st_nlink); + if (!opt_drop_ids) + { + printf ("%8s ", symbolic_uid (st->st_uid)); + printf ("%8s ", symbolic_gid (st->st_gid)); + } + printf ("%10" PRIuMAX " ", (uintmax_t) st->st_size); + if (!opt_drop_mtime) + printf ("%s ", string_date (st->st_mtime)); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +ls_dump_record (gboolean success, const char *input_line, const struct stat *st, + const char *filename, const char *linkname) +{ + if (success) + { + ls_dump_stbuf (st); + if (filename != NULL) + printf ("%s", filename); + if (linkname != NULL) + printf (" -> %s", linkname); + printf ("\n"); + } + else + { + printf ("cannot parse input line: '%s'\n", input_line); + } +} + +/* ------------------------------------------------------------------------------ */ +/** + * Main code. + */ +/* ------------------------------------------------------------------------------ */ + +static void +process_ls_line (const char *line) +{ + struct stat st; + char *filename, *linkname; + gboolean success; + + memset (&st, 0, sizeof st); + filename = NULL; + linkname = NULL; + + success = vfs_parse_ls_lga (line, &st, &filename, &linkname, NULL); + + if (!success) + error_count++; + + if (opt_output_format == FORMAT_YAML) + yaml_dump_record (success, line, &st, filename, linkname); + else + ls_dump_record (success, line, &st, filename, linkname); + + g_free (filename); + g_free (linkname); +} + +/* ------------------------------------------------------------------------------ */ + +static void +process_input (FILE * input) +{ + char line[BUF_4K]; + + while (fgets (line, sizeof line, input) != NULL) + { + chomp (line); /* Not mandatory. Makes error messages nicer. */ + if (strncmp (line, "total ", 6) == 0) /* Convenience only: makes 'ls -l' parse cleanly. */ + continue; + process_ls_line (line); + } +} + +/* ------------------------------------------------------------------------------ */ + +int +main (int argc, char *argv[]) +{ + FILE *input; + + if (!parse_command_line (&argc, &argv)) + return EXIT_FAILURE; + + if (argc >= 2) + { + input = fopen (argv[1], "r"); + if (input == NULL) + { + perror (argv[1]); + return EXIT_FAILURE; + } + } + else + { + input = stdin; + } + + process_input (input); + + return (error_count > 0) ? EXIT_FAILURE : EXIT_SUCCESS; +} + +/* ------------------------------------------------------------------------------ */ diff --git a/tests/src/extfs-helpers-listcmd/run b/tests/src/extfs-helpers-listcmd/run new file mode 100755 index 000000000..7f905e047 --- /dev/null +++ b/tests/src/extfs-helpers-listcmd/run @@ -0,0 +1,15 @@ +#!/bin/sh + +echo +echo "This is a placeholder." +echo "It will be replaced in the future with a script that does the real" +echo "work of testing the helpers." +echo +echo "Here's a silly test to check whether 'ls', had it been an extfs helper," +echo "works properly:" +echo + +LC_ALL=C ls -l | ./mc_parse_ls_l + +# (LC_ALL=C is meant to prevent date formats MC can't parse. This might +# still not be foolproof, but that's just a demonstration.)