From 23e7fa38bd58167bbf8e47b0f56a0667b912928f Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Sun, 6 Jan 2013 10:47:21 +0100 Subject: [PATCH] lldpcli: switch to GNU Readline. libedit is not available on old versions of Linux distro. We try to use Readline API instead which may be compatible with the libedit version available in BSD. This means that we have to write our own tokenizer. This also means that we can make Readline optional. --- .travis.yml | 2 +- configure.ac | 4 +- debian/control | 2 +- m4/ax_lib_readline.m4 | 113 ++++++++++++++++++++++++++++ m4/libedit.m4 | 40 ---------- redhat/lldpd.spec | 2 +- src/client/Makefile.am | 6 +- src/client/client.h | 35 ++++++++- src/client/commands.c | 28 +++---- src/client/lldpcli.c | 161 +++++++++++++++------------------------- src/client/lldpctl.supp | 10 +++ src/client/tokenizer.c | 114 ++++++++++++++++++++++++++++ 12 files changed, 353 insertions(+), 164 deletions(-) create mode 100644 m4/ax_lib_readline.m4 delete mode 100644 m4/libedit.m4 create mode 100644 src/client/tokenizer.c diff --git a/.travis.yml b/.travis.yml index 4026df2d..6b0ecc64 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: "python" install: - "sudo apt-get -qq update" - "sudo apt-get -y install automake autoconf libtool pkg-config" - - "sudo apt-get -y install libsnmp-dev libxml2-dev libjansson-dev libevent-dev libedit-dev check" + - "sudo apt-get -y install libsnmp-dev libxml2-dev libjansson-dev libevent-dev libreadline-dev check" script: "./autogen.sh && ./configure $LLDPD_CONFIG_ARGS && make && make check && make distcheck && sudo make install" env: - LLDPD_CONFIG_ARGS="" diff --git a/configure.ac b/configure.ac index cc0327ef..50ca5b4c 100644 --- a/configure.ac +++ b/configure.ac @@ -100,8 +100,8 @@ PKG_CHECK_MODULES([CHECK], [check >= 0.9.4], [have_check=yes], [have_check=no]) # Libevent lldp_CHECK_LIBEVENT -# editline -lldp_CHECK_EDITLINE +# readline (or something similar) +AX_LIB_READLINE ####################### ### Options diff --git a/debian/control b/debian/control index 6db271c3..6676c6dc 100644 --- a/debian/control +++ b/debian/control @@ -4,7 +4,7 @@ Priority: optional Maintainer: Vincent Bernat Build-Depends: debhelper (>= 5), cdbs, autotools-dev, libsnmp15-dev | libsnmp9-dev | libsnmp-dev, - libxml2-dev, libevent-dev, libedit-dev, pkg-config + libxml2-dev, libevent-dev, libreadline-dev, pkg-config Standards-Version: 3.8.4 Homepage: https://github.com/vincentbernat/lldpd/wiki diff --git a/m4/ax_lib_readline.m4 b/m4/ax_lib_readline.m4 new file mode 100644 index 00000000..439db7f1 --- /dev/null +++ b/m4/ax_lib_readline.m4 @@ -0,0 +1,113 @@ +# =========================================================================== +# http://www.gnu.org/software/autoconf-archive/ax_lib_readline.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_LIB_READLINE +# +# DESCRIPTION +# +# Searches for a readline compatible library. If found, defines +# `HAVE_LIBREADLINE'. If the found library has the `add_history' function, +# sets also `HAVE_READLINE_HISTORY'. Also checks for the locations of the +# necessary include files and sets `HAVE_READLINE_H' or +# `HAVE_READLINE_READLINE_H' and `HAVE_READLINE_HISTORY_H' or +# 'HAVE_HISTORY_H' if the corresponding include files exists. +# +# The libraries that may be readline compatible are `libedit', +# `libeditline' and `libreadline'. Sometimes we need to link a termcap +# library for readline to work, this macro tests these cases too by trying +# to link with `libtermcap', `libcurses' or `libncurses' before giving up. +# +# Here is an example of how to use the information provided by this macro +# to perform the necessary includes or declarations in a C file: +# +# #ifdef HAVE_LIBREADLINE +# # if defined(HAVE_READLINE_READLINE_H) +# # include +# # elif defined(HAVE_READLINE_H) +# # include +# # else /* !defined(HAVE_READLINE_H) */ +# extern char *readline (); +# # endif /* !defined(HAVE_READLINE_H) */ +# char *cmdline = NULL; +# #else /* !defined(HAVE_READLINE_READLINE_H) */ +# /* no readline */ +# #endif /* HAVE_LIBREADLINE */ +# +# #ifdef HAVE_READLINE_HISTORY +# # if defined(HAVE_READLINE_HISTORY_H) +# # include +# # elif defined(HAVE_HISTORY_H) +# # include +# # else /* !defined(HAVE_HISTORY_H) */ +# extern void add_history (); +# extern int write_history (); +# extern int read_history (); +# # endif /* defined(HAVE_READLINE_HISTORY_H) */ +# /* no history */ +# #endif /* HAVE_READLINE_HISTORY */ +# +# LICENSE +# +# Copyright (c) 2008 Ville Laurikari +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 6 + +AU_ALIAS([VL_LIB_READLINE], [AX_LIB_READLINE]) +AC_DEFUN([AX_LIB_READLINE], [ + AC_CACHE_CHECK([for a readline compatible library], + ax_cv_lib_readline, [ + _save_LIBS="$LIBS" + for readline_lib in readline edit editline; do + for termcap_lib in "" termcap curses ncurses; do + if test -z "$termcap_lib"; then + TRY_LIB="-l$readline_lib" + else + TRY_LIB="-l$readline_lib -l$termcap_lib" + fi + LIBS="$ORIG_LIBS $TRY_LIB" + AC_TRY_LINK_FUNC(readline, ax_cv_lib_readline="$TRY_LIB") + if test -n "$ax_cv_lib_readline"; then + break + fi + done + if test -n "$ax_cv_lib_readline"; then + break + fi + done + if test -z "$ax_cv_lib_readline"; then + ax_cv_lib_readline="no" + fi + LIBS="$_save_LIBS" + ]) + + if test "$ax_cv_lib_readline" != "no"; then + READLINE_LIBS="$ax_cv_lib_readline" + AC_SUBST(READLINE_LIBS) + + _save_LIBS="$LIBS" + LIBS="$LIBS $READLINE_LIBS" + AC_DEFINE(HAVE_LIBREADLINE, 1, + [Define if you have a readline compatible library]) + AC_CHECK_HEADERS(readline.h readline/readline.h) + AC_CACHE_CHECK([whether readline supports history], + ax_cv_lib_readline_history, [ + ax_cv_lib_readline_history="no" + AC_TRY_LINK_FUNC(add_history, ax_cv_lib_readline_history="yes") + ]) + if test "$ax_cv_lib_readline_history" = "yes"; then + AC_DEFINE(HAVE_READLINE_HISTORY, 1, + [Define if your readline library has \`add_history']) + AC_CHECK_HEADERS(history.h readline/history.h) + fi + + LIBS="$_save_LIBS" + fi +])dnl diff --git a/m4/libedit.m4 b/m4/libedit.m4 deleted file mode 100644 index 10d67fb3..00000000 --- a/m4/libedit.m4 +++ /dev/null @@ -1,40 +0,0 @@ -# -# lldp_CHECK_EDITLINE -# - -AC_DEFUN([lldp_CHECK_EDITLINE], [ - _save_LIBS="$LIBS" - _save_CFLAGS="$CFLAGS" - - # First, try with pkg-config - PKG_CHECK_MODULES([EDITLINE], [libedit >= 2.9], [], [ - # Nothing appropriate. Maybe it is installed anyway. - AC_CHECK_HEADER([histedit.h], [], - [AC_MSG_ERROR([*** unable to find editline/libedit])]) - EDITLINE_CFLAGS="" - EDITLINE_LIBS="-ledit -lcurses" - ]) - - # Check if everything works as expected - LIBS="$LIBS $EDITLINE_LIBS" - CFLAGS="$CFLAGS $EDITLINE_CFLAGS" - AC_MSG_CHECKING([if libedit version is compatible]) - AC_COMPILE_IFELSE( - [AC_LANG_PROGRAM([[ -@%:@include -@%:@include - ]], [[ -int i = H_SETSIZE; (void)i; -el_init("", NULL, NULL, NULL); -exit(0); - ]])], - [ AC_MSG_RESULT([yes]) ], - [ AC_MSG_RESULT([no]) - AC_MSG_ERROR([*** libedit does not work as expected])])]) - - LIBS="$_save_LIBS" - CFLAGS="$_save_CFLAGS" - - AC_SUBST([EDITLINE_CFLAGS]) - AC_SUBST([EDITLINE_LIBS]) -]) diff --git a/redhat/lldpd.spec b/redhat/lldpd.spec index d4d836e9..737777ac 100644 --- a/redhat/lldpd.spec +++ b/redhat/lldpd.spec @@ -38,7 +38,7 @@ Source1: lldpd.init%{?suse_version:.suse} Source2: lldpd.sysconfig BuildRequires: pkgconfig -BuildRequires: libedit-devel +BuildRequires: readline-devel %if %{with snmp} BuildRequires: net-snmp-devel BuildRequires: openssl-devel diff --git a/src/client/Makefile.am b/src/client/Makefile.am index 590c5220..f7eb1bc9 100644 --- a/src/client/Makefile.am +++ b/src/client/Makefile.am @@ -10,13 +10,13 @@ uninstall-local: lldpcli_SOURCES = client.h lldpcli.c display.c actions.c \ commands.c show.c \ - misc.c \ + misc.c tokenizer.c \ writer.h text_writer.c kv_writer.c lldpcli_LDADD = \ $(top_builddir)/src/libcommon-daemon-client.la \ $(top_builddir)/src/lib/liblldpctl.la \ - @EDITLINE_LIBS@ -lldpcli_CFLAGS = $(AM_CFLAGS) @EDITLINE_CFLAGS@ + @READLINE_LIBS@ +lldpcli_CFLAGS = $(AM_CFLAGS) if USE_XML lldpcli_SOURCES += xml_writer.c diff --git a/src/client/client.h b/src/client/client.h index 1cf91c14..524612ff 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -28,6 +28,35 @@ #include "../ctl.h" #include "writer.h" +/* Readline stuff */ +#ifdef HAVE_LIBREADLINE +# if defined(HAVE_READLINE_READLINE_H) +# include +# elif defined(HAVE_READLINE_H) +# include +# else +extern char *readline(); +extern char *rl_line_buffer +extern int rl_point; +extern int rl_insert_text(const char*); +extern int rl_delete_text(int, int); +extern int rl_ding(void); +extern int rl_crlf(void); +extern int rl_on_new_line(void); +extern int rl_bind_key(int, int(*f)(int, int)); +# endif +#endif +#ifdef HAVE_READLINE_HISTORY +# if defined(HAVE_READLINE_HISTORY_H) +# include +# elif defined(HAVE_HISTORY_H) +# include +# else +extern void add_history (); +# endif +#endif +#undef NEWLINE + /* commands.c */ #define NEWLINE "" struct cmd_node; @@ -49,7 +78,7 @@ int cmdenv_pop(struct cmd_env*, int); int commands_execute(struct lldpctl_conn_t *, struct writer *, struct cmd_node *, int argc, const char **argv); char *commands_complete(struct cmd_node *, int argc, const char **argv, - int cursorc, int cursoro, int all); + int all); /* helpers */ int cmd_check_no_env(struct cmd_env *, void *); int cmd_check_env(struct cmd_env *, void *); @@ -89,4 +118,8 @@ void register_commands_watch(struct cmd_node *); /* actions.c */ void register_commands_configure(struct cmd_node *); +/* tokenizer.c */ +int tokenize_line(const char*, int*, char***); +void tokenize_free(int, char**); + #endif diff --git a/src/client/commands.c b/src/client/commands.c index 7eee64a6..366e2cf6 100644 --- a/src/client/commands.c +++ b/src/client/commands.c @@ -298,8 +298,6 @@ struct candidate_word { * @param root Root node we want to start from. * @param argc Number of arguments. * @param argv Array of arguments. - * @param cursorc On which argument the cursor is. -1 if no completion is required. - * @param cursoro On which offset the cursor is. -1 if no completion is required. * @param word Completed word. Or NULL when no completion is required. * @param all When completing, display possible completions even if only one choice is possible. * @return 0 on success, -1 otherwise. @@ -307,13 +305,13 @@ struct candidate_word { static int _commands_execute(struct lldpctl_conn_t *conn, struct writer *w, struct cmd_node *root, int argc, const char **argv, - int cursorc, int cursoro, char **word, int all) + char **word, int all) { - int n, rc = 0, completion = (cursorc != -1 && cursoro != -1 && word != NULL); + int n, rc = 0, completion = (word != NULL); struct cmd_env env = { .elements = TAILQ_HEAD_INITIALIZER(env.elements), .stack = TAILQ_HEAD_INITIALIZER(env.stack), - .argc = completion?cursorc:argc, + .argc = argc, .argv = argv, .argp = 0 }; @@ -329,8 +327,10 @@ _commands_execute(struct lldpctl_conn_t *conn, struct writer *w, while ((current = cmdenv_top(&env))) { struct cmd_node *candidate, *best = NULL; const char *token = (env.argp < env.argc) ? env.argv[env.argp] : - (env.argp == env.argc && !completion) ? NEWLINE : NULL; - if (token == NULL) goto end; + (env.argp == env.argc) ? NEWLINE : NULL; + if (token == NULL || + (completion && env.argp == env.argc - 1)) + goto end; if (!completion) log_debug("lldpctl", "process argument %02d: `%s`", env.argp, token); @@ -389,7 +389,7 @@ end: log_warnx("lldpctl", "incomplete command"); rc = -1; } - } else if (rc == 0 && env.argp == env.argc) { + } else if (rc == 0 && env.argp == env.argc - 1) { /* We need to complete. Let's build the list of candidate words. */ struct cmd_node *candidate = NULL; int maxl = 10; /* Max length of a word */ @@ -398,7 +398,8 @@ end: current = cmdenv_top(&env); TAILQ_FOREACH(candidate, ¤t->subentries, next) { if ((!candidate->token || - !strncmp(env.argv[cursorc], candidate->token, cursoro)) && + !strncmp(env.argv[env.argc - 1], candidate->token, + strlen(env.argv[env.argc -1 ]))) && (!candidate->validate || candidate->validate(&env, candidate->arg) == 1)) { struct candidate_word *cword = malloc(sizeof(struct candidate_word)); @@ -435,7 +436,8 @@ end: /* If the prefix is complete, add a space, otherwise, * just return it as is. */ if (!all && strcmp(prefix, NEWLINE) && - strlen(prefix) > 0 && cursoro < strlen(prefix)) { + strlen(prefix) > 0 && + strlen(env.argv[env.argc-1]) < strlen(prefix)) { TAILQ_FOREACH(cword, &words, next) { if (cword->word && !strcmp(prefix, cword->word)) { prefix[strlen(prefix)] = ' '; @@ -474,11 +476,11 @@ end: */ char * commands_complete(struct cmd_node *root, int argc, const char **argv, - int cursorc, int cursoro, int all) + int all) { char *word = NULL; if (_commands_execute(NULL, NULL, root, argc, argv, - cursorc, cursoro, &word, all) == 0) + &word, all) == 0) return word; return NULL; } @@ -490,7 +492,7 @@ int commands_execute(struct lldpctl_conn_t *conn, struct writer *w, struct cmd_node *root, int argc, const char **argv) { - return _commands_execute(conn, w, root, argc, argv, -1, -1, NULL, 0); + return _commands_execute(conn, w, root, argc, argv, NULL, 0); } /** diff --git a/src/client/lldpcli.c b/src/client/lldpcli.c index 92324e88..641fe9f8 100644 --- a/src/client/lldpcli.c +++ b/src/client/lldpcli.c @@ -15,6 +15,7 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ + #define _GNU_SOURCE #include #include @@ -26,7 +27,6 @@ #include #include #include -#include #include #include "client.h" @@ -64,12 +64,16 @@ is_privileged() } static char* -prompt(EditLine *el) +prompt() { +#define CESC "\033" int privileged = is_privileged(); - if (privileged) - return "[lldpcli] # "; - return "[lldpcli] $ "; + if (isatty(STDIN_FILENO)) { + if (privileged) + return "[lldpcli] # "; + return "[lldpcli] $ "; + } + return ""; } static int must_exit = 0; @@ -112,50 +116,55 @@ cmd_update(struct lldpctl_conn_t *conn, struct writer *w, return 1; } -static unsigned char -_cmd_complete(EditLine *el, int ch, int all) +static int +_cmd_complete(int all) { - int rc = CC_ERROR; - Tokenizer *eltok; - if ((eltok = tok_init(NULL)) == NULL) + char **argv = NULL; + int argc = 0; + int rc = 1; + char *line = malloc(strlen(rl_line_buffer) + 2); + if (!line) return -1; + strcpy(line, rl_line_buffer); + line[rl_point] = 2; /* empty character, will force a word */ + line[rl_point+1] = 0; + + if (tokenize_line(line, &argc, &argv) != 0) goto end; - const LineInfo *li = el_line(el); - - const char **argv; - char *compl; - int argc, cursorc, cursoro; - if (tok_line(eltok, li, &argc, &argv, &cursorc, &cursoro) != 0) - goto end; - compl = commands_complete(root, argc, argv, cursorc, cursoro, all); + char *compl = commands_complete(root, argc, (const char **)argv, all); if (compl) { - el_deletestr(el, cursoro); - if (el_insertstr(el, compl) == -1) { + int from = rl_point - strlen(argv[argc-1]); + rl_delete_text(from, rl_point); + rl_point = from; + if (rl_insert_text(compl) < 0) { free(compl); goto end; } free(compl); - rc = CC_REDISPLAY; + rc = 0; goto end; } - /* No completion or several completion available. We beep. */ - el_beep(el); - rc = CC_REDISPLAY; + /* No completion or several completion available. */ + if (!all) rl_ding(); + rl_crlf(); + rl_on_new_line(); + rc = 0; end: - if (eltok) tok_end(eltok); + free(line); + tokenize_free(argc, argv); return rc; } -static unsigned char -cmd_complete(EditLine *el, int ch) +static int +cmd_complete(int count, int ch) { - return _cmd_complete(el, ch, 0); + return _cmd_complete(0); } -static unsigned char -cmd_help(EditLine *el, int ch) +static int +cmd_help(int count, int ch) { - return _cmd_complete(el, ch, 1); + return _cmd_complete(1); } static struct cmd_node* @@ -200,11 +209,6 @@ main(int argc, char *argv[]) lldpctl_conn_t *conn = NULL; struct writer *w; - EditLine *el = NULL; - History *elhistory = NULL; - HistEvent elhistev; - Tokenizer *eltok = NULL; - char *interfaces = NULL; /* Get and parse command line options */ @@ -244,51 +248,15 @@ main(int argc, char *argv[]) free(prev); } must_exit = 1; - goto skipeditline; - } else { - if (optind < argc) { - /* More arguments! */ - must_exit = 1; - } - } - - /* Init editline */ - log_debug("lldpctl", "init editline"); - el = el_init("lldpctl", stdin, stdout, stderr); - if (el == NULL) { - log_warnx("lldpctl", "unable to setup editline"); - goto end; - } - el_set(el, EL_PROMPT, prompt); - el_set(el, EL_SIGNAL, 1); - el_set(el, EL_EDITOR, "emacs"); - /* If on a TTY, setup completion */ - if (isatty(STDERR_FILENO)) { - el_set(el, EL_ADDFN, "command_complete", - "Execute completion", cmd_complete); - el_set(el, EL_ADDFN, "command_help", - "Show completion", cmd_help); - el_set(el, EL_BIND, "^I", "command_complete", NULL); - el_set(el, EL_BIND, "?", "command_help", NULL); - } - - /* Init history */ - elhistory = history_init(); - if (elhistory == NULL) { - log_warnx("lldpctl", "unable to enable history"); + } else if (optind < argc) { + /* More arguments! */ + must_exit = 1; } else { - history(elhistory, &elhistev, H_SETSIZE, 800); - el_set(el, EL_HIST, history, elhistory); + /* Shell session */ + rl_bind_key('?', cmd_help); + rl_bind_key('\t', cmd_complete); } - /* Init tokenizer */ - eltok = tok_init(NULL); - if (eltok == NULL) { - log_warnx("lldpctl", "unable to initialize tokenizer"); - goto end; - } - -skipeditline: /* Make a connection */ log_debug("lldpctl", "connect to lldpd"); conn = lldpctl_new(NULL, NULL, NULL); @@ -297,34 +265,25 @@ skipeditline: do { const char *line; - const char **cargv; - int count, n, cargc; - + char **cargv; + int n, cargc; if (!is_lldpctl(NULL) && (optind >= argc)) { - /* Read a new line. */ - line = el_gets(el, &count); - if (line == NULL) break; + line = readline(prompt()); + if (line == NULL) break; /* EOF */ /* Tokenize it */ log_debug("lldpctl", "tokenize command line"); - n = tok_str(eltok, line, &cargc, &cargv); + n = tokenize_line(line, &cargc, &cargv); switch (n) { case -1: log_warnx("lldpctl", "internal error while tokenizing"); goto end; case 1: - case 2: - case 3: - /* TODO: handle multiline statements */ log_warnx("lldpctl", "unmatched quotes"); - tok_reset(eltok); - continue; - } - if (cargc == 0) { - tok_reset(eltok); continue; } - if (elhistory) history(elhistory, &elhistev, H_ENTER, line); + if (cargc == 0) continue; + add_history(line); } /* Init output formatter */ @@ -340,33 +299,31 @@ skipeditline: if (is_lldpctl(NULL)) { if (!interfaces) { - cargv = (const char*[]){ "show", "neighbors", "details" }; + cargv = (char*[]){ "show", "neighbors", "details" }; cargc = 3; } else { - cargv = (const char*[]){ "show", "neighbors", "ports", interfaces, "details" }; + cargv = (char*[]){ "show", "neighbors", "ports", interfaces, "details" }; cargc = 5; } } else if (optind < argc) { - cargv = (const char **)argv; + cargv = argv; cargv = &cargv[optind]; cargc = argc - optind; } /* Execute command */ if (commands_execute(conn, w, - root, cargc, cargv) != 0) + root, cargc, (const char **)cargv) != 0) log_info("lldpctl", "an error occurred while executing last command"); w->finish(w); - if (eltok) tok_reset(eltok); + if (!is_lldpctl(NULL) && optind >= argc) + tokenize_free(cargc, cargv); } while (!must_exit); rc = EXIT_SUCCESS; end: if (conn) lldpctl_release(conn); - if (eltok) tok_end(eltok); - if (elhistory) history_end(elhistory); - if (el) el_end(el); if (root) commands_free(root); free(interfaces); return rc; diff --git a/src/client/lldpctl.supp b/src/client/lldpctl.supp index 20168992..97804534 100644 --- a/src/client/lldpctl.supp +++ b/src/client/lldpctl.supp @@ -33,3 +33,13 @@ fun:register_commands_dot3pow ... } + +# libreadline has no function to free memory +{ + readline-internal-memory + Memcheck:Leak + fun:malloc + ... + fun:readline + fun:main +} diff --git a/src/client/tokenizer.c b/src/client/tokenizer.c new file mode 100644 index 00000000..784114c4 --- /dev/null +++ b/src/client/tokenizer.c @@ -0,0 +1,114 @@ +/* -*- mode: c; c-file-style: "openbsd" -*- */ +/* + * Copyright (c) 2013 Vincent Bernat + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "client.h" + +#include +/** + * Tokenize the given line. We support quoted strings and escaped characters + * with backslash. + * + * @param line Line to tokenize. + * @param argv Will get an array of arguments tokenized. + * @param argc Will get the number of tokenized arguments. + * @return 0 on success, -1 on internal error, 1 on unmatched quotes + */ +int +tokenize_line(const char *line, int *argc, char ***argv) +{ + int iargc = 0; char **iargv = NULL; + char *ifs = " \n\t"; + char *quotes = "'\""; + char *escapes = "\\"; + char empty = 2; /* Empty character, will be removed from output + * but will mark a word. */ + + /* Escape handle. Also escape quoted characters. */ + int escaped = 0; + int ipos = 0; + char quote = 0; + char input[2*strlen(line) + 2]; + memset(input, 0, 2*strlen(line) + 4); + for (int pos = 0; line[pos]; pos++) { + if (!escaped && strchr(escapes, line[pos])) + escaped = 1; + else if (!escaped && strchr(quotes, line[pos]) && !quote) { + input[ipos++] = empty; + input[ipos++] = '!'; + quote = line[pos]; + } else if (!escaped && quote == line[pos]) + quote = 0; + else { + input[ipos++] = line[pos]; + input[ipos++] = (escaped || quote)?'!':' '; + escaped = 0; + } + } + if (escaped || quote) return 1; + /* Trick to not have to handle \0 in a special way */ + input[ipos++] = ifs[0]; + input[ipos++] = ' '; + + /* Tokenize, we don't have to handle quotes anymore */ + int wbegin = -1; /* Offset of the beginning of the current word */ + +#define CURRENT (input[2*pos]) +#define ESCAPED (input[2*pos+1] != ' ') + for (int pos = 0; CURRENT; pos++) { + if (wbegin == -1) { + if (!ESCAPED && strchr(ifs, CURRENT)) + /* IFS while not in a word, continue. */ + continue; + /* Start a word. */ + wbegin = pos; + continue; + } + if (ESCAPED || !strchr(ifs, CURRENT)) + /* Regular character in a word. */ + continue; + + /* End of word. */ + char *word = calloc(1, pos - wbegin + 1); + if (!word) goto error; + int i,j; + for (i = wbegin, j = 0; + i != pos; + i++) + if (input[2*i] != empty) word[j++] = input[2*i]; + char **nargv = realloc(iargv, sizeof(char*) * (iargc + 1)); + if (!nargv) goto error; + nargv[iargc++] = word; + iargv = nargv; + wbegin = -1; + } + + *argc = iargc; + *argv = iargv; + return 0; + +error: + tokenize_free(iargc, iargv); + return -1; +} + +void +tokenize_free(int argc, char **argv) +{ + while (argc) free(argv[--argc]); + free(argv); +} + -- 2.39.5