Viewing: parser.c
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (C) 2001 Cluster File Systems, Inc.
*
* Copyright (c) 2014, 2017, Intel Corporation.
*
*/
/*
* This file is part of Lustre, http://www.lustre.org/
*
* libcfs/libcfs/parser.c
*
* A command line parser.
*
*/
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <getopt.h>
#include <malloc.h>
#ifdef HAVE_LIBREADLINE
# include <readline/history.h>
# include <readline/readline.h>
#endif /* HAVE_LIBREADLINE */
#include <string.h>
#include <unistd.h>
#include <libcfs/util/parser.h>
#include <linux/lustre/lustre_ver.h>
/* Top level of commands */
static command_t top_level[MAXCMDS];
/* Set to 1 if user types exit or quit */
static int done;
/*
* Normally, the parser will quit when an error occurs in non-interacive
* mode. Setting this to non-zero will force it to keep buggering on.
*/
static int ignore_errors;
static char *skipwhitespace(char *s);
static char *skiptowhitespace(char *s);
static command_t *find_cmd(char *name, command_t cmds[], char **next);
static int process(char *s, char **next, command_t *lookup, command_t **result,
char **prev);
static int line2args(char *line, char **argv, int maxargs);
static int cfs_parser_commands(command_t *cmds);
static int cfs_parser_execarg(int argc, char **argv, command_t cmds[]);
static int cfs_parser_list_commands(const command_t *cmdlist, int line_len,
int col_num);
static int cfs_parser_list(int argc, char **argv);
static int cfs_parser_help(int argc, char **argv);
static int cfs_parser_quit(int argc, char **argv);
static int cfs_parser_version(int argc, char **argv);
static int cfs_parser_ignore_errors(int argc, char **argv);
command_t override_cmdlist[] = {
{ .pc_name = "quit", .pc_func = cfs_parser_quit, .pc_help = "quit" },
{ .pc_name = "exit", .pc_func = cfs_parser_quit, .pc_help = "exit" },
{ .pc_name = "help", .pc_func = cfs_parser_help,
.pc_help = "provide useful information about a command" },
{ .pc_name = "--help", .pc_func = cfs_parser_help,
.pc_help = "provide useful information about a command" },
{ .pc_name = "version", .pc_func = cfs_parser_version,
.pc_help = "show program version" },
{ .pc_name = "--version", .pc_func = cfs_parser_version,
.pc_help = "show program version" },
{ .pc_name = "list-commands", .pc_func = cfs_parser_list,
.pc_help = "list available commands" },
{ .pc_name = "--list-commands", .pc_func = cfs_parser_list,
.pc_help = "list available commands" },
{ .pc_name = "--ignore_errors", .pc_func = cfs_parser_ignore_errors,
.pc_help = "ignore errors that occur during script processing"},
{ .pc_name = "ignore_errors", .pc_func = cfs_parser_ignore_errors,
.pc_help = "ignore errors that occur during script processing"},
{ .pc_name = 0, .pc_func = NULL, .pc_help = 0 }
};
static char *skipwhitespace(char *s)
{
char *t;
int len;
len = (int)strlen(s);
for (t = s; t <= s + len && isspace(*t); t++)
;
return t;
}
static char *skiptowhitespace(char *s)
{
char *t;
for (t = s; *t && !isspace(*t); t++)
;
return t;
}
static int line2args(char *line, char **argv, int maxargs)
{
char *arg;
int i = 0;
arg = strtok(line, " \t");
if (!arg || maxargs < 1)
return 0;
argv[i++] = arg;
while ((arg = strtok(NULL, " \t")) != NULL && i < maxargs)
argv[i++] = arg;
return i;
}
/* find a command -- return it if unique otherwise print alternatives */
static command_t *cfs_parser_findargcmd(char *name, command_t cmds[])
{
command_t *cmd;
for (cmd = cmds; cmd->pc_name; cmd++) {
if (strcmp(name, cmd->pc_name) == 0)
return cmd;
}
return NULL;
}
static int cfs_parser_ignore_errors(int argc, char **argv)
{
(void) argc;
(void) argv;
ignore_errors = 1;
return 0;
}
int cfs_parser(int argc, char **argv, command_t cmds[])
{
command_t *cmd;
int rc = 0;
int i = 0;
done = 0;
if (cmds == NULL)
return -ENOENT;
for (cmd = override_cmdlist; cmd->pc_name && i < MAXCMDS; cmd++)
top_level[i++] = *cmd;
for (cmd = cmds; cmd->pc_name && i < MAXCMDS; cmd++)
top_level[i++] = *cmd;
/* Null-terminate top_level array properly */
if (i < MAXCMDS)
memset(&top_level[i], 0, sizeof(command_t));
if (argc == 2 && (strcmp(argv[0], "help") == 0))
return CMD_HELP;
if (argc > 1)
rc = cfs_parser_execarg(argc - 1, argv + 1, cmds);
else
rc = cfs_parser_commands(cmds);
return rc;
}
static int cfs_parser_execarg(int argc, char **argv, command_t cmds[])
{
command_t *cmd;
cmd = cfs_parser_findargcmd(argv[0], override_cmdlist);
if (!cmd)
cmd = cfs_parser_findargcmd(argv[0], cmds);
if (cmd && cmd->pc_func) {
int rc = cmd->pc_func(argc, argv);
if (rc == CMD_HELP) {
fprintf(stdout, "%s\n", cmd->pc_help);
fflush(stdout);
}
return rc;
}
fprintf(stderr,
"%s: '%s' is not a valid command. See '%s --list-commands'.\n",
program_invocation_short_name, argv[0],
program_invocation_short_name);
return -1;
}
/*
* Returns the command_t * (NULL if not found) corresponding to a
* _partial_ match with the first token in name. It sets *next to
* point to the following token. Does not modify *name.
*/
static command_t *find_cmd(char *name, command_t cmds[], char **next)
{
int i, len;
if (!cmds || !name)
return NULL;
/*
* This sets name to point to the first non-white space character,
* and next to the first whitespace after name, len to the length: do
* this with strtok
*/
name = skipwhitespace(name);
*next = skiptowhitespace(name);
len = (int)(*next - name);
if (len == 0)
return NULL;
for (i = 0; cmds[i].pc_name; i++) {
if (strncasecmp(name, cmds[i].pc_name, len) == 0) {
*next = skipwhitespace(*next);
return &cmds[i];
}
}
return NULL;
}
/*
* Recursively process a command line string s and find the command
* corresponding to it. This can be ambiguous, full, incomplete,
* non-existent.
*/
static int process(char *s, char **next, command_t *lookup,
command_t **result, char **prev)
{
static int depth;
*result = find_cmd(s, lookup, next);
*prev = s;
/* non existent */
if (!*result)
return CMD_NONE;
/*
* found entry: is it ambigous, i.e. not exact command name and
* more than one command in the list matches. Note that find_cmd
* points to the first ambiguous entry
*/
if (strncasecmp(s, (*result)->pc_name, strlen((*result)->pc_name))) {
char *another_next;
int found_another = 0;
command_t *another_result = find_cmd(s, (*result) + 1,
&another_next);
while (another_result) {
if (strncasecmp(s, another_result->pc_name,
strlen(another_result->pc_name)) == 0) {
*result = another_result;
*next = another_next;
goto got_it;
}
another_result = find_cmd(s, another_result + 1,
&another_next);
found_another = 1;
/*
* In some circumstances, process will fail to find a
* suitable command. We want to be able to escape both
* the while loop and the recursion. So, track the
* number of times we've been here and give up if
* things start to get out-of-hand.
*/
if (depth > 50)
return CMD_NONE;
depth++;
}
if (found_another)
return CMD_AMBIG;
}
got_it:
/* found a unique command: component or full? */
if ((*result)->pc_func)
return CMD_COMPLETE;
if (**next == '\0')
return CMD_INCOMPLETE;
return process(*next, next, (*result)->pc_sub_cmd,
result, prev);
}
#ifdef HAVE_LIBREADLINE
static command_t *match_tbl; /* Command completion against this table */
static char *command_generator(const char *text, int state)
{
static int index, len;
char *name;
/* Do we have a match table? */
if (!match_tbl)
return NULL;
/* If this is the first time called on this word, state is 0 */
if (!state) {
index = 0;
len = (int)strlen(text);
}
/* Return next name in the command list that paritally matches test */
while ((name = (match_tbl + index)->pc_name)) {
index++;
if (strncasecmp(name, text, len) == 0)
return strdup(name);
}
/* No more matches */
return NULL;
}
/* probably called by readline */
static char **command_completion(const char *text, int start, int end)
{
command_t *table;
char *pos;
match_tbl = top_level;
for (table = find_cmd(rl_line_buffer, match_tbl, &pos);
table; table = find_cmd(pos, match_tbl, &pos)) {
if (*(pos - 1) == ' ')
match_tbl = table->pc_sub_cmd;
}
return rl_completion_matches(text, command_generator);
}
#endif
/* take a string and execute the function or print help */
static int execute_line(char *line)
{
command_t *cmd, *ambig;
char *prev;
char *next, *tmp;
char *argv[MAXARGS];
int i;
int rc = 0;
switch (process(line, &next, top_level, &cmd, &prev)) {
case CMD_AMBIG:
fprintf(stderr, "Ambiguous command \'%s\'\nOptions: ", line);
while ((ambig = find_cmd(prev, cmd, &tmp))) {
fprintf(stderr, "%s ", ambig->pc_name);
cmd = ambig + 1;
}
fprintf(stderr, "\n");
break;
case CMD_NONE:
cfs_parser_help(1, NULL);
break;
case CMD_INCOMPLETE:
if (cmd == NULL || cmd->pc_sub_cmd == NULL) {
fprintf(stderr, "'%s' incomplete command.\n", line);
return rc;
}
fprintf(stderr,
"'%s' incomplete command. Use '%s x' where x is one of:\n\t",
line, line);
for (i = 0; cmd->pc_sub_cmd[i].pc_name; i++)
fprintf(stderr, "%s ", cmd->pc_sub_cmd[i].pc_name);
fprintf(stderr, "\n");
break;
case CMD_COMPLETE:
optind = 0;
i = line2args(line, argv, MAXARGS);
rc = cmd->pc_func(i, argv);
if (rc == CMD_HELP) {
fprintf(stdout, "%s\n", cmd->pc_help);
fflush(stdout);
}
break;
}
return rc;
}
#ifdef HAVE_LIBREADLINE
static void noop_int_fn(int unused) { }
static void noop_void_fn(void) { }
#endif
/*
* just in case you're ever in an airplane and discover you
* forgot to install readline-dev. :)
*/
static int init_input(void)
{
int interactive = isatty(fileno(stdin));
#ifdef HAVE_LIBREADLINE
using_history();
stifle_history(HISTORY);
if (!interactive) {
rl_prep_term_function = noop_int_fn;
rl_deprep_term_function = noop_void_fn;
}
rl_attempted_completion_function = command_completion;
rl_completion_entry_function = command_generator;
#endif
return interactive;
}
#ifndef HAVE_LIBREADLINE
#define add_history(s)
static char *readline(char *prompt)
{
int size = 2048;
char *line = malloc(size);
char *ptr = line;
int c;
int eof = 0;
if (!line)
return NULL;
if (prompt)
printf("%s", prompt);
while (1) {
if ((c = fgetc(stdin)) != EOF) {
if (c == '\n')
goto out;
*ptr++ = (char)c;
if (ptr - line >= size - 1) {
char *tmp;
size *= 2;
tmp = malloc(size);
if (!tmp)
goto outfree;
memcpy(tmp, line, ptr - line);
ptr = tmp + (ptr - line);
free(line);
line = tmp;
}
} else {
eof = 1;
if (ferror(stdin) || feof(stdin))
goto outfree;
goto out;
}
}
out:
*ptr = 0;
if (eof && (strlen(line) == 0)) {
free(line);
line = NULL;
}
return line;
outfree:
free(line);
return NULL;
}
#endif
/* this is the command execution machine */
static int cfs_parser_commands(command_t *cmds)
{
char *line, *s;
int rc = 0, save_error = 0;
int interactive;
interactive = init_input();
while (!done) {
line = readline(interactive ? "> " : NULL);
if (!line)
break;
s = skipwhitespace(line);
if (*s) {
add_history(s);
rc = execute_line(s);
}
/* stop on error if not-interactive */
if (rc != 0 && !interactive) {
if (save_error == 0)
save_error = rc;
if (!ignore_errors)
done = 1;
}
free(line);
}
if (save_error)
rc = save_error;
return rc;
}
static int cfs_parser_help(int argc, char **argv)
{
char line[1024];
char *next, *prev, *tmp;
command_t *result, *ambig;
int i;
if (argc == 1) {
printf("usage: %s [COMMAND] [OPTIONS]... [ARGS]\n",
program_invocation_short_name);
printf("Without any parameters, interactive mode is invoked\n");
printf("Try '%s help <COMMAND>', or '%s --list-commands' for a list of commands.\n",
program_invocation_short_name,
program_invocation_short_name);
return 0;
}
/*
* Joining command line arguments without space is not critical here
* because of this string is used for search a help topic and assume
* that only one argument will be (the name of topic). For example:
* lst > help ping run
* pingrun: Unknown command.
*/
line[0] = '\0';
for (i = 1; i < argc; i++) {
if (strlen(argv[i]) >= sizeof(line) - strlen(line) - 1)
return -E2BIG;
/*
* The function strlcat() cannot be used here because of
* this function is used in LNet utils that is not linked
* with libcfs.a.
*/
strncat(line, argv[i], sizeof(line) - strlen(line) - 1);
}
switch (process(line, &next, top_level, &result, &prev)) {
case CMD_COMPLETE:
fprintf(stderr, "%s: %s\n", line, result->pc_help);
break;
case CMD_NONE:
fprintf(stderr, "%s: '%s' is not a valid command. See '%s --list-commands'.\n",
program_invocation_short_name, line,
program_invocation_short_name);
break;
case CMD_INCOMPLETE:
fprintf(stderr,
"'%s' incomplete command. Use '%s x' where x is one of:\n",
line, line);
fprintf(stderr, "\t");
for (i = 0; result->pc_sub_cmd[i].pc_name; i++)
fprintf(stderr, "%s ", result->pc_sub_cmd[i].pc_name);
fprintf(stderr, "\n");
break;
case CMD_AMBIG:
fprintf(stderr, "Ambiguous command \'%s\'\nOptions: ", line);
while ((ambig = find_cmd(prev, result, &tmp))) {
fprintf(stderr, "%s ", ambig->pc_name);
result = ambig + 1;
}
fprintf(stderr, "\n");
break;
}
return 0;
}
/**
* cfs_parser_list_commands() - Output a list of the supported commands.
* @cmdlist: Array of structures describing the commands.
* @line_len: Length of output line.
* @col_num: The number of commands printed in a single row.
*
* The commands and subcommands supported by the utility are printed, arranged
* into several columns for readability.
*
* Return: The number of items that were printed.
*/
static int cfs_parser_list_commands(const command_t *cmdlist, int line_len,
int col_num)
{
int char_max;
int count = 0;
int col = 0;
int nprinted = 0;
int offset = 0;
char_max = line_len / col_num;
for (; cmdlist->pc_name; cmdlist++) {
if (!cmdlist->pc_func && !cmdlist->pc_sub_cmd)
break;
count++;
nprinted = fprintf(stdout, "%-*s ", char_max - offset - 1,
cmdlist->pc_name);
/*
* when a column is too wide, save offset so subsequent columns
* can be aligned properly
*/
offset = offset + nprinted - char_max;
offset = offset > 0 ? offset : 0;
col++;
if (col >= col_num) {
fprintf(stdout, "\n");
col = 0;
offset = 0;
}
}
if (col != 0)
fprintf(stdout, "\n");
return count;
}
static int cfs_parser_quit(int argc, char **argv)
{
(void) argc;
(void) argv;
done = 1;
return 0;
}
static int cfs_parser_version(int argc, char **argv)
{
(void) argc;
(void) argv;
fprintf(stdout, "%s %s\n", program_invocation_short_name,
LUSTRE_VERSION_STRING);
return 0;
}
static int cfs_parser_list(int argc, char **argv)
{
command_t *cmd;
int num_cmds_listed;
(void) argc;
(void) argv;
cmd = top_level;
while (cmd->pc_name != NULL) {
if (!cmd->pc_func) {
/*
* print the command category
*/
printf("\n%s\n", cmd->pc_name);
cmd++;
}
num_cmds_listed = cfs_parser_list_commands(cmd, 80, 4);
cmd += num_cmds_listed;
}
return 0;
}