Mercurial > projects > pwm
view cmd.c @ 15:3380c8fd9776
Show all record fields except the password by default
author | Guido Berhoerster <guido+pwm@berhoerster.name> |
---|---|
date | Mon, 07 Aug 2017 19:11:56 +0200 |
parents | a01899a6e4bb |
children | a07665727c19 |
line wrap: on
line source
/* * Copyright (C) 2017 Guido Berhoerster <guido+pwm@berhoerster.name> * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "compat.h" #include <ctype.h> #ifdef HAVE_ERR_H #include <err.h> #endif /* HAVE_ERR_H */ #include <errno.h> #include <limits.h> #ifdef HAVE_READPASSPHRASE_H #include <readpassphrase.h> #endif /* READPASSPHRASE_H */ #include <regex.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <unistd.h> #include "cmd.h" #include "pw.h" #include "pwfile.h" #include "util.h" #define TIME_FORMAT "%Y-%m-%dT%TZ" #define TIME_SIZE (4 + 1 + 2 + 1 + 2 + 1 + 8 + 1 + 1) #define CHARS_DIGIT "0123456789" #define CHARS_UPPER "ABCDEFGHIJKLMNOPQRSTUVWXYZ" #define CHARS_LOWER "abcdefghijklmnopqrstuvwxyz" #define CHARS_PUNCT "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" enum field_type { FIELD_UNKNOWN = -1, FIELD_GROUP, FIELD_TITLE, FIELD_USERNAME, FIELD_PASSWORD, FIELD_NOTES, FIELD_URL, FIELD_MTIME, FIELD_CTIME }; enum cmd_generatepassword_arg_type { CMD_GP_ARG_UNKNOWN = -1, CMD_GP_ARG_LEN, CMD_GP_ARG_CHARS, CMD_GP_ARG_CHARCLASS }; enum charclass_type { CHARCLASS_UNKNOWN = -1, CHARCLASS_DIGIT, CHARCLASS_UPPER, CHARCLASS_LOWER, CHARCLASS_PUNCT, CHARCLASS_ALPHA, CHARCLASS_ALNUM, CHARCLASS_XDIGIT, CHARCLASS_GRAPH }; static enum cmd_return cmd_info(struct pwm_ctx *, int, char *[]); static enum cmd_return cmd_list(struct pwm_ctx *, int, char *[]); static enum cmd_return cmd_create(struct pwm_ctx *, int, char *[]); static enum cmd_return cmd_modify(struct pwm_ctx *, int, char *[]); static enum cmd_return cmd_generatepassword(struct pwm_ctx *, int, char *[]); static enum cmd_return cmd_remove(struct pwm_ctx *, int, char *[]); static enum cmd_return cmd_show(struct pwm_ctx *, int, char *[]); static enum cmd_return cmd_pipe(struct pwm_ctx *, int, char *[]); static enum cmd_return cmd_creategroup(struct pwm_ctx *, int, char *[]); static enum cmd_return cmd_removegroup(struct pwm_ctx *, int, char *[]); static enum cmd_return cmd_changepassword(struct pwm_ctx *, int, char *[]); static enum cmd_return cmd_help(struct pwm_ctx *, int, char *[]); static enum cmd_return cmd_write(struct pwm_ctx *, int, char *[]); static enum cmd_return cmd_quit(struct pwm_ctx *, int, char *[]); static const char *field_namev[] = { "group", "title", "username", "password", "notes", "url", "ctime", "mtime", NULL }; static const char *field_labels[] = { "Group: ", "Title: ", "Username: ", "Password: ", "Notes: ", "URL: ", "Created: ", "Modified: " }; static const char *cmd_generatepassword_argv[] = { "len", "char", "charclass", NULL }; static const char *charclass_namev[] = { "digit", "upper", "lower", "punct", "alpha", "alnum", "xdigit", "graph", NULL }; static const char *charclass_values[] = { CHARS_DIGIT, CHARS_UPPER, CHARS_LOWER, CHARS_PUNCT, CHARS_UPPER CHARS_LOWER, CHARS_DIGIT CHARS_UPPER CHARS_LOWER, CHARS_DIGIT "abcdef", CHARS_DIGIT CHARS_UPPER CHARS_LOWER CHARS_PUNCT }; static struct cmd cmds[] = { { "i", "info", "info", "Show metadata information about the current file", cmd_info }, { "ls", "list", "list [field~regex ...]", "List entries", cmd_list }, { "c", "create", "create field=value ...", "Create entry", cmd_create }, { "m", "modify", "modify id field=value ...", "Modify entry", cmd_modify }, { "gp", "generatepassword", "generatepassword [id] [len=n] [chars=n:chars] " "[charclass=n:class] ...", "Randomly generate a password", cmd_generatepassword }, { "rm", "remove", "remove id", "Delete entry", cmd_remove }, { "s", "show", "show id field", "Show entry", cmd_show }, { "p", "pipe", "pipe id field command", "Pipe entry to external command", cmd_pipe }, { "cg", "creategroup", "creategroup name", "Create empty group", cmd_creategroup }, { "rg", "removegroup", "removegroup name", "Delete empty group", cmd_removegroup }, { "ch", "changepassword", "changepassword", "Change password", cmd_changepassword }, { "h", "help", "help [command]", "Show help text", cmd_help }, { "w", "write", "write", "Write the database", cmd_write }, { "q", "quit", "quit", "Quit", cmd_quit }, { "Q", "Quit", "Quit", "Quit without checking", cmd_quit }, { 0 } }; static int parse_arg(char *arg, const char *namev[], int sep, char **valuep) { size_t i; size_t name_len; for (i = 0; namev[i] != NULL; i++) { name_len = strlen(namev[i]); if ((strncmp(namev[i], arg, name_len) == 0) && (arg[name_len] == sep)) { if (valuep != NULL) { *valuep = arg + name_len + 1; } return (i); } } return (-1); } static int parse_id(const char *arg, unsigned int *idp) { long x; char *p; errno = 0; x = strtol(arg, &p, 10); if ((errno != 0) || (*arg == '\0') || (*p != '\0') || (x > UINT_MAX) || (x <= 0)) { return (-1); } *idp = (unsigned int)x; return (0); } static enum cmd_return cmd_info(struct pwm_ctx *ctx, int argc, char *argv[]) { struct metadata *metadata; struct tm *tm; char timebuf[TIME_SIZE]; if (argc != 1) { return (CMD_USAGE); } metadata = pwfile_get_metadata(ctx); printf("Format: 0x%04x\n", metadata->version); if (metadata->user != NULL) { printf("User: %s\n", metadata->user); } if (metadata->user != NULL) { printf("Host: %s\n", metadata->host); } if (metadata->user != NULL) { printf("Application: %s\n", metadata->application); } tm = gmtime(&metadata->timestamp); strftime(timebuf, sizeof (timebuf), TIME_FORMAT, tm); printf("Last Saved: %s\n", timebuf); pwfile_destroy_metadata(metadata); return (CMD_OK); } static enum cmd_return cmd_list(struct pwm_ctx *ctx, int argc, char *argv[]) { int retval = CMD_ERR; int i; regex_t *group_re = NULL; regex_t *title_re = NULL; regex_t *username_re = NULL; regex_t *notes_re = NULL; regex_t *url_re = NULL; enum field_type type; char *value; regex_t **repp; int errcode; char *errbuf; size_t errbuf_size; union list_item **list = NULL; size_t j; struct record *record; for (i = 1; i < argc; i++) { type = parse_arg(argv[i], field_namev, '~', &value); if (type == FIELD_UNKNOWN) { fprintf(stderr, "bad field name \"%s\"\n", argv[i]); goto out; } if (value[0] == '\0') { /* skip empty expressions */ continue; } switch (type) { case FIELD_GROUP: repp = &group_re; break; case FIELD_TITLE: repp = &title_re; break; case FIELD_USERNAME: repp = &username_re; break; case FIELD_NOTES: repp = ¬es_re; break; case FIELD_URL: repp = &url_re; break; default: fprintf(stderr, "bad field name \"%s\"\n", argv[i]); goto out; } if (*repp == NULL) { *repp = xmalloc(sizeof (regex_t)); } else { regfree(*repp); } errcode = regcomp(*repp, value, REG_EXTENDED | REG_NOSUB); if (errcode != 0) { errbuf_size = regerror(errcode, *repp, "", 0); errbuf = xmalloc(errbuf_size); regerror(errcode, *repp, errbuf, errbuf_size); fprintf(stderr, "bad regular expression \"%s\"\n", errbuf); free(errbuf); free(*repp); *repp = NULL; goto out; } } list = pwfile_create_list(ctx); for (j = 0; list[j] != NULL; j++) { if (list[j]->any.type == ITEM_TYPE_GROUP) { printf("[%s]\n", list[j]->group.group); } else { record = pwfile_get_record(ctx, list[j]->record.id); if (((group_re == NULL) || (regexec(group_re, record->group, 0, NULL, 0) == 0)) && ((title_re == NULL) || (regexec(title_re, record->title, 0, NULL, 0) == 0)) && ((username_re == NULL) || (regexec(username_re, record->username, 0, NULL, 0) == 0)) && ((notes_re == NULL) || (regexec(notes_re, record->notes, 0, NULL, 0) == 0)) && ((url_re == NULL) || (regexec(url_re, record->url, 0, NULL, 0) == 0))) { printf("%4u %s\n", list[j]->record.id, (list[j]->record.title != NULL) ? list[j]->record.title : ""); } pwfile_destroy_record(record); } } retval = CMD_OK; out: if (group_re != NULL) { regfree(group_re); free(group_re); } if (title_re != NULL) { regfree(title_re); free(title_re); } if (username_re != NULL) { regfree(username_re); free(username_re); } if (notes_re != NULL) { regfree(notes_re); free(notes_re); } if (url_re != NULL) { regfree(url_re); free(url_re); } pwfile_destroy_list(list); return (retval); } static enum cmd_return cmd_create(struct pwm_ctx *ctx, int argc, char *argv[]) { int i; struct record record = { 0 }; enum field_type type; char *value; if (argc < 2) { return (CMD_USAGE); } for (i = 1; i < argc; i++) { type = parse_arg(argv[i], field_namev, '=', &value); if (type == FIELD_UNKNOWN) { fprintf(stderr, "bad field assignment \"%s\"\n", argv[i]); } if (value[0] == '\0') { /* skip empty assignments */ continue; } switch (type) { case FIELD_GROUP: record.group = value; break; case FIELD_TITLE: record.title = value; break; case FIELD_USERNAME: record.username = value; break; case FIELD_PASSWORD: record.password = value; break; case FIELD_NOTES: record.notes = value; break; case FIELD_URL: record.url = value; break; default: fprintf(stderr, "bad field name \"%s\"\n", argv[i]); return (CMD_ERR); } } pwfile_create_record(ctx, &record); return (CMD_OK); } static enum cmd_return cmd_modify(struct pwm_ctx *ctx, int argc, char *argv[]) { unsigned int id; int i; struct record record = { 0 }; enum field_type type; char *value; if (argc < 2) { return (CMD_USAGE); } if (parse_id(argv[1], &id) != 0) { fprintf(stderr, "invalid id %s\n", argv[1]); return (CMD_ERR); } for (i = 2; i < argc; i++) { type = parse_arg(argv[i], field_namev, '=', &value); if (type == FIELD_UNKNOWN) { fprintf(stderr, "bad field assignment \"%s\"\n", argv[i]); return (CMD_ERR); } if (value[0] == '\0') { /* skip empty assignments */ continue; } switch (type) { case FIELD_GROUP: record.group = value; break; case FIELD_TITLE: record.title = value; break; case FIELD_USERNAME: record.username = value; break; case FIELD_PASSWORD: record.password = value; break; case FIELD_NOTES: record.notes = value; break; case FIELD_URL: record.url = value; break; default: fprintf(stderr, "bad field name \"%s\"\n", argv[i]); return (CMD_ERR); } } pwfile_modify_record(ctx, id, &record); return (CMD_OK); } static enum cmd_return cmd_generatepassword(struct pwm_ctx *ctx, int argc, char *argv[]) { enum cmd_return retval = CMD_ERR; unsigned int id = 0; int i = 1; char *value = NULL; long x; char *p; size_t password_len = 16; size_t chars_min; const char *chars; struct pw_char_group *char_groupv = NULL; size_t char_groupv_len = 0; int charclass; size_t j; char password[PWS3_MAX_PASSWORD_LEN + 1] = { 0 }; /* check if first argument is an id */ if ((argc > 1) && (parse_id(argv[1], &id) == 0)) { i++; } for (; i < argc; i++) { switch (parse_arg(argv[i], cmd_generatepassword_argv, '=', &value)) { case CMD_GP_ARG_LEN: errno = 0; x = strtol(value, &p, 10); if ((errno != 0) || (*value == '\0') || (*p != '\0') || (x > PWS3_MAX_PASSWORD_LEN) || (x <= 0)) { fprintf(stderr, "invalid password length " "\"%s\"\n", argv[i]); goto out; } password_len = x; break; case CMD_GP_ARG_CHARS: errno = 0; x = strtol(value, &p, 10); if ((errno != 0) || (*value == '\0') || (*p != ':') || (x < 0) || (x > PWS3_MAX_PASSWORD_LEN)) { fprintf(stderr, "invalid minimum number of " "characters \"%s\"\n", argv[i]); goto out; } chars_min = x; chars = ++p; while (*p != '\0') { if (!isascii(*p) || !isprint(*p)) { fprintf(stderr, "invalid character in " "character group \"%s\"\n", argv[i]); goto out; } p++; } char_groupv = xrealloc(char_groupv, sizeof (struct pw_char_group) * (char_groupv_len + 1)); char_groupv[char_groupv_len].chars = chars; char_groupv[char_groupv_len].chars_min = chars_min; char_groupv_len++; break; case CMD_GP_ARG_CHARCLASS: errno = 0; x = strtol(value, &p, 10); if ((errno != 0) || (*value == '\0') || (*p != ':') || (x < 0) || (x > PWS3_MAX_PASSWORD_LEN)) { fprintf(stderr, "invalid minimum number of " "characters \"%s\"\n", argv[i]); goto out; } chars_min = x; charclass = parse_arg(++p, charclass_namev, '\0', NULL); if (charclass < 0) { fprintf(stderr, "unknown character class " "\"%s\"\n", argv[i]); goto out; } chars = charclass_values[charclass]; char_groupv = xrealloc(char_groupv, sizeof (struct pw_char_group) * (char_groupv_len + 1)); char_groupv[char_groupv_len].chars = chars; char_groupv[char_groupv_len].chars_min = chars_min; char_groupv_len++; break; default: fprintf(stderr, "invalid argument \"%s\"\n", argv[i]); retval = CMD_USAGE; goto out; } } for (j = 0; j < char_groupv_len; j++) { if (char_groupv[j].chars_min > password_len) { fprintf(stderr, "invalid minimum number of " "characters \"%zu:%s\"\n", char_groupv[j].chars_min, char_groupv[j].chars); goto out; } } if (char_groupv_len == 0) { /* use defaults */ char_groupv = xmalloc(sizeof (struct pw_char_group)); char_groupv[0].chars = charclass_values[CHARCLASS_GRAPH]; char_groupv[0].chars_min = 0; char_groupv_len++; } if (pw_genrandom(char_groupv, char_groupv_len, password, password_len) != 0) { fprintf(stderr, "failed to generate password that meets the " "given constraints\n"); goto out; } if (id != 0) { if (pwfile_modify_record(ctx, id, &(struct record){ .password = password }) != 0) { fprintf(stderr, "record %u does not exist\n", id); goto out; } } else { printf("%s\n", password); } retval = CMD_OK; out: free(char_groupv); return (retval); } static enum cmd_return cmd_remove(struct pwm_ctx *ctx, int argc, char *argv[]) { unsigned int id; if (argc != 2) { return (CMD_USAGE); } if (parse_id(argv[1], &id) != 0) { fprintf(stderr, "invalid id %s\n", argv[1]); return (CMD_ERR); } if (pwfile_remove_record(ctx, id) != 0) { fprintf(stderr, "failed to remove record %u\n", id); return (CMD_ERR); } return (CMD_OK); } static int print_field(const char *label, const char *value, int show_label, FILE *fp) { fprintf(fp, "%s%s\n", show_label ? label : "", (value != NULL) ? value : ""); if (ferror(fp)) { warn("fprintf"); return (-1); } return (0); } static void print_record(struct record *record, int fields[], int show_labels, FILE *fp) { struct tm *tm; char timebuf[TIME_SIZE]; if (fields[FIELD_TITLE]) { if (print_field(field_labels[FIELD_TITLE], record->title, show_labels, fp) != 0) { return; } } if (fields[FIELD_GROUP]) { if (print_field(field_labels[FIELD_GROUP], record->group, show_labels, fp)) { return; } } if (fields[FIELD_USERNAME]) { if (print_field(field_labels[FIELD_USERNAME], record->username, show_labels, fp)) { return; } } if (fields[FIELD_PASSWORD]) { if (print_field(field_labels[FIELD_PASSWORD], record->password, show_labels, fp)) { return; } } if (fields[FIELD_NOTES]) { if (print_field(field_labels[FIELD_NOTES], record->notes, show_labels, fp)) { return; } } if (fields[FIELD_URL]) { if (print_field(field_labels[FIELD_URL], record->url, show_labels, fp)) { return; } } if (fields[FIELD_CTIME]) { tm = gmtime(&record->ctime); strftime(timebuf, sizeof (timebuf), TIME_FORMAT, tm); if (print_field(field_labels[FIELD_CTIME], timebuf, show_labels, fp)) { return; } } if (fields[FIELD_MTIME]) { tm = gmtime(&record->mtime); strftime(timebuf, sizeof (timebuf), TIME_FORMAT, tm); if (print_field(field_labels[FIELD_MTIME], timebuf, show_labels, fp)) { return; } } } static enum cmd_return cmd_show(struct pwm_ctx *ctx, int argc, char *argv[]) { unsigned int id; struct record *record; int i; enum field_type type; int fields[COUNTOF(field_namev) - 1] = { [FIELD_GROUP] = 1, [FIELD_TITLE] = 1, [FIELD_USERNAME] = 1, [FIELD_PASSWORD] = 0, [FIELD_NOTES] = 1, [FIELD_URL] = 1, [FIELD_MTIME] = 1, [FIELD_CTIME] = 1 }; if (argc < 2) { return (CMD_USAGE); } if (parse_id(argv[1], &id) != 0) { fprintf(stderr, "invalid id %s\n", argv[1]); return (CMD_ERR); } if (argc > 2) { /* show only explicitly given field names */ memset(fields, 0, sizeof (fields)); } for (i = 2; i < argc; i++) { type = parse_arg(argv[i], field_namev, '\0', NULL); if (type < 0) { fprintf(stderr, "bad field name \"%s\"\n", argv[i]); return (CMD_ERR); } fields[type] = 1; } record = pwfile_get_record(ctx, id); if (record == NULL) { fprintf(stderr, "record %u does not exist\n", id); return (CMD_ERR); } print_record(record, fields, 1, stdout); pwfile_destroy_record(record); return (CMD_OK); } static enum cmd_return cmd_pipe(struct pwm_ctx *ctx, int argc, char *argv[]) { enum cmd_return retval = CMD_ERR; unsigned int id; struct record *record = NULL; enum field_type type; int fields[COUNTOF(field_namev) - 1] = { 0 }; FILE *fp = NULL; if (argc != 4) { return (CMD_USAGE); } if (parse_id(argv[1], &id) != 0) { fprintf(stderr, "invalid id %s\n", argv[1]); return (CMD_ERR); } type = parse_arg(argv[2], field_namev, '\0', NULL); if (type < 0) { fprintf(stderr, "bad field name \"%s\"\n", argv[2]); return (CMD_ERR); } fields[type] = 1; fp = popen(argv[3], "w"); if (fp == NULL) { warn("popen"); goto out; } record = pwfile_get_record(ctx, id); if (record == NULL) { fprintf(stderr, "record %u does not exist\n", id); goto out; } print_record(record, fields, 0, fp); retval = CMD_OK; out: pwfile_destroy_record(record); if (fp != NULL) { pclose(fp); } return (retval); } static enum cmd_return cmd_creategroup(struct pwm_ctx *ctx, int argc, char *argv[]) { if (argc != 2) { return (CMD_USAGE); } if (pwfile_create_group(ctx, argv[1]) != 0) { fprintf(stderr, "group \"%s\" already exists\n", argv[1]); return (CMD_ERR); } return (CMD_OK); } static enum cmd_return cmd_removegroup(struct pwm_ctx *ctx, int argc, char *argv[]) { if (argc != 2) { return (CMD_USAGE); } if (pwfile_remove_group(ctx, argv[1]) != 0) { fprintf(stderr, "there is no empty group \"%s\"\n", argv[1]); return (CMD_ERR); } return (CMD_OK); } static enum cmd_return cmd_changepassword(struct pwm_ctx *ctx, int argc, char *argv[]) { size_t password_len; char password_buf[PWS3_MAX_PASSWORD_LEN + 1]; char confirm_buf[PWS3_MAX_PASSWORD_LEN + 1]; if (argc > 2) { return (CMD_USAGE); } else if (argc == 2) { password_len = strlen(argv[1]); if (password_len == 0) { fprintf(stderr, "password must not be empty\n"); return (CMD_ERR); } else if (password_len + 1 > sizeof (ctx->password)) { fprintf(stderr, "password too long\n"); return (CMD_ERR); } memcpy(ctx->password, argv[1], password_len + 1); } else { if (readpassphrase("Enter password: ", password_buf, sizeof (password_buf), RPP_ECHO_OFF | RPP_REQUIRE_TTY) == NULL) { err(1, "readpassphrase"); } password_len = strlen(password_buf); if (password_len == 0) { fprintf(stderr, "password must not be empty\n"); return (CMD_ERR); } if (readpassphrase("Confirm password: ", confirm_buf, sizeof (confirm_buf), RPP_ECHO_OFF | RPP_REQUIRE_TTY) == NULL) { err(1, "readpassphrase"); } if (strcmp(password_buf, confirm_buf) != 0) { fprintf(stderr, "passwords do not match\n"); return (CMD_ERR); } memcpy(ctx->password, password_buf, password_len + 1); } return (CMD_OK); } static enum cmd_return cmd_help(struct pwm_ctx *ctx, int argc, char *argv[]) { struct cmd *cmd; if (argc > 2) { return (CMD_USAGE); } if (argc == 2) { for (cmd = cmds; cmd->cmd_func != NULL; cmd++) { if ((strcmp(argv[1], cmd->abbrev_cmd) == 0) || (strcmp(argv[1], cmd->full_cmd) == 0)) { printf("%s\n", cmd->usage); break; } } } else { printf("Commands:\n"); for (cmd = cmds; cmd->cmd_func != NULL; cmd++) { printf("%-2s %-16s %s\n", cmd->abbrev_cmd, cmd->full_cmd, cmd->description); } } return (CMD_OK); } static enum cmd_return cmd_write(struct pwm_ctx *ctx, int argc, char *argv[]) { if (argc != 1) { return (CMD_USAGE); } return ((pwfile_write_file(ctx) == 0) ? CMD_OK : CMD_ERR); } static enum cmd_return cmd_quit(struct pwm_ctx *ctx, int argc, char *argv[]) { if (argc != 1) { return (CMD_USAGE); } if ((argv[0][0] == 'q') && ctx->unsaved_changes && (ctx->prev_cmd != NULL) && (strcmp(ctx->prev_cmd, "quit") != 0)) { printf("Warning: There are unsaved changes\n"); return (CMD_OK); } return (CMD_QUIT); } struct cmd * cmd_match(const char *cmd_name) { size_t i; for (i = 0; cmds[i].cmd_func != NULL; i++) { if ((strcmp(cmds[i].abbrev_cmd, cmd_name) == 0) || (strcmp(cmds[i].full_cmd, cmd_name) == 0)) { return (&cmds[i]); } } return (NULL); }