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=""
# Libevent
lldp_CHECK_LIBEVENT
-# editline
-lldp_CHECK_EDITLINE
+# readline (or something similar)
+AX_LIB_READLINE
#######################
### Options
Maintainer: Vincent Bernat <bernat@debian.org>
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
--- /dev/null
+# ===========================================================================
+# 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 <readline/readline.h>
+# # elif defined(HAVE_READLINE_H)
+# # include <readline.h>
+# # 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 <readline/history.h>
+# # elif defined(HAVE_HISTORY_H)
+# # include <history.h>
+# # 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 <vl@iki.fi>
+#
+# 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
+++ /dev/null
-#
-# 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 <stdlib.h>
-@%:@include <histedit.h>
- ]], [[
-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])
-])
Source2: lldpd.sysconfig
BuildRequires: pkgconfig
-BuildRequires: libedit-devel
+BuildRequires: readline-devel
%if %{with snmp}
BuildRequires: net-snmp-devel
BuildRequires: openssl-devel
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
#include "../ctl.h"
#include "writer.h"
+/* Readline stuff */
+#ifdef HAVE_LIBREADLINE
+# if defined(HAVE_READLINE_READLINE_H)
+# include <readline/readline.h>
+# elif defined(HAVE_READLINE_H)
+# include <readline.h>
+# 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 <readline/history.h>
+# elif defined(HAVE_HISTORY_H)
+# include <history.h>
+# else
+extern void add_history ();
+# endif
+#endif
+#undef NEWLINE
+
/* commands.c */
#define NEWLINE "<CR>"
struct cmd_node;
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 *);
/* actions.c */
void register_commands_configure(struct cmd_node *);
+/* tokenizer.c */
+int tokenize_line(const char*, int*, char***);
+void tokenize_free(int, char**);
+
#endif
* @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.
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
};
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);
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 */
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));
/* 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)] = ' ';
*/
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;
}
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);
}
/**
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
+
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <arpa/inet.h>
-#include <histedit.h>
#include <libgen.h>
#include "client.h"
}
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;
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*
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 */
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);
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 */
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;
fun:register_commands_dot3pow
...
}
+
+# libreadline has no function to free memory
+{
+ readline-internal-memory
+ Memcheck:Leak
+ fun:malloc
+ ...
+ fun:readline
+ fun:main
+}
--- /dev/null
+/* -*- mode: c; c-file-style: "openbsd" -*- */
+/*
+ * Copyright (c) 2013 Vincent Bernat <bernat@luffy.cx>
+ *
+ * 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 <string.h>
+/**
+ * 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);
+}
+