[libvirt] [RFC PATCH] virsh: new echo command

* tools/virsh.c (cmdEcho): New command. (commands): Add it. (vshCmdOptType): Add VSH_OT_ARGV. (vshCmddefGetData): Special case new opt flag. (vshCommandOptArgv): New function. --- Not complete yet, but this shows what I'm thinking of. Adding the echo command has two benefits: 1. It will let me add unit tests for the recent virsh command line improvements - echo back arbitrary strings to make sure quoting is as desired. This part works with what I have here, before I ran out of time to finish this today. 2. Make it easier for a user on the command line to conver an arbitrary string into something safe for shell evalution and/or XML usage, by munging the input in a way that it can be reused in the desired context. Not yet implemented; hence the RFC. It exploits the fact that "--" is consumed as the end-of-options, hence, there is no way for "" to be recognized as a valid option name, so the only way we can encounter VSH_OT_ARGV is via the new argv handling, at which point we can handle all remaining command line arguments. tools/virsh.c | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 files changed, 80 insertions(+), 8 deletions(-) diff --git a/tools/virsh.c b/tools/virsh.c index 89c2e1e..f361658 100644 --- a/tools/virsh.c +++ b/tools/virsh.c @@ -119,11 +119,12 @@ typedef enum { * vshCmdOptType - command option type */ typedef enum { - VSH_OT_NONE = 0, /* none */ - VSH_OT_BOOL, /* boolean option */ - VSH_OT_STRING, /* string option */ - VSH_OT_INT, /* int option */ - VSH_OT_DATA /* string data (as non-option) */ + VSH_OT_NONE = 0, /* none */ + VSH_OT_BOOL, /* boolean option */ + VSH_OT_STRING, /* string option */ + VSH_OT_INT, /* int option */ + VSH_OT_DATA, /* string data (as non-option) */ + VSH_OT_ARGV /* remaining arguments, opt->name should be "" */ } vshCmdOptType; /* @@ -230,6 +231,7 @@ static char *vshCommandOptString(const vshCmd *cmd, const char *name, static long long vshCommandOptLongLong(const vshCmd *cmd, const char *name, int *found); static int vshCommandOptBool(const vshCmd *cmd, const char *name); +static char *vshCommandOptArgv(const vshCmd *cmd, int count); #define VSH_BYID (1 << 1) #define VSH_BYUUID (1 << 2) @@ -8917,6 +8919,54 @@ cmdPwd(vshControl *ctl, const vshCmd *cmd ATTRIBUTE_UNUSED) #endif /* + * "echo" command + */ +static const vshCmdInfo info_echo[] = { + {"help", N_("echo arguments")}, + {"desc", N_("Echo back arguments, possibly with quoting.")}, + {NULL, NULL} +}; + +static const vshCmdOptDef opts_echo[] = { + {"shell", VSH_OT_BOOL, 0, N_("escape for shell use")}, + {"xml", VSH_OT_BOOL, 0, N_("escape for XML use")}, + {"", VSH_OT_ARGV, 0, N_("arguments to echo")}, + {NULL, 0, 0, NULL} +}; + +/* Exists mainly for debugging virsh, but also handy for adding back + * quotes for later evaluation. + */ +static int +cmdEcho (vshControl *ctl ATTRIBUTE_UNUSED, const vshCmd *cmd) +{ + bool shell = false; + bool xml = false; + int count = 0; + char *arg; + virBuffer buf = VIR_BUFFER_INITIALIZER; + + if (vshCommandOptBool(cmd, "shell")) + shell = true; + if (vshCommandOptBool(cmd, "xml")) + xml = true; + + while ((arg = vshCommandOptArgv(cmd, count)) != NULL) { + /* TODO - use buf */ + if (xml) { + /* TODO - use virBufferEscapeString */ + } + if (shell) { + /* TODO - add '' and escape embedded ' */ + } + vshPrint(ctl, "%s%s", count ? " " : "", arg); + count++; + } + + return TRUE; +} + +/* * "edit" command */ static const vshCmdInfo info_edit[] = { @@ -9545,6 +9595,7 @@ static const vshCmdDef commands[] = { {"domxml-from-native", cmdDomXMLFromNative, opts_domxmlfromnative, info_domxmlfromnative}, {"domxml-to-native", cmdDomXMLToNative, opts_domxmltonative, info_domxmltonative}, {"dumpxml", cmdDumpXML, opts_dumpxml, info_dumpxml}, + {"echo", cmdEcho, opts_echo, info_echo}, {"edit", cmdEdit, opts_edit, info_edit}, {"find-storage-pool-sources", cmdPoolDiscoverSources, opts_find_storage_pool_sources, info_find_storage_pool_sources}, @@ -9707,8 +9758,8 @@ vshCmddefGetData(const vshCmdDef * cmd, int data_ct) const vshCmdOptDef *opt; for (opt = cmd->opts; opt && opt->name; opt++) { - if (opt->type == VSH_OT_DATA) { - if (data_ct == 0) + if (opt->type >= VSH_OT_DATA) { + if (data_ct == 0 || opt->type == VSH_OT_ARGV) return opt; else data_ct--; @@ -9970,6 +10021,27 @@ vshCommandOptBool(const vshCmd *cmd, const char *name) return vshCommandOpt(cmd, name) ? TRUE : FALSE; } +/* + * Returns the COUNT argv argument, or NULL after last argument. + * + * Requires that a VSH_OT_ARGV option with the name "" be last in the + * list of supported options in CMD->def->opts. + */ +static char * +vshCommandOptArgv(const vshCmd *cmd, int count) +{ + vshCmdOpt *opt = cmd->opts; + + while (opt) { + if (opt->def && opt->def->type == VSH_OT_ARGV) { + if (count-- == 0) + return opt->data; + } + opt = opt->next; + } + return NULL; +} + /* Determine whether CMD->opts includes an option with name OPTNAME. If not, give a diagnostic and return false. If so, return true. */ -- 1.7.2.3

On Tue, Oct 12, 2010 at 05:39:24PM -0600, Eric Blake wrote:
* tools/virsh.c (cmdEcho): New command. (commands): Add it. (vshCmdOptType): Add VSH_OT_ARGV. (vshCmddefGetData): Special case new opt flag. (vshCommandOptArgv): New function. ---
Not complete yet, but this shows what I'm thinking of. Adding the echo command has two benefits:
1. It will let me add unit tests for the recent virsh command line improvements - echo back arbitrary strings to make sure quoting is as desired. This part works with what I have here, before I ran out of time to finish this today.
2. Make it easier for a user on the command line to conver an arbitrary string into something safe for shell evalution and/or XML usage, by munging the input in a way that it can be reused in the desired context. Not yet implemented; hence the RFC.
It exploits the fact that "--" is consumed as the end-of-options, hence, there is no way for "" to be recognized as a valid option name, so the only way we can encounter VSH_OT_ARGV is via the new argv handling, at which point we can handle all remaining command line arguments.
Interesting, so you could use echo to build scripts where escaping it automatically provided by an earlier use of virsh. Sounds good to me ! Daniel -- Daniel Veillard | libxml Gnome XML XSLT toolkit http://xmlsoft.org/ daniel@veillard.com | Rpmfind RPM search engine http://rpmfind.net/ http://veillard.com/ | virtualization library http://libvirt.org/

* tools/virsh.c (vshCmdOptType): Add VSH_OT_ARGV. Delete unused VSH_OT_NONE. (vshCmddefGetData): Special case new opt flag. (vshCmddefHelp): Display help for argv. (vshCommandOptArgv): New function. --- Here's the completed series. tools/virsh.c | 75 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 files changed, 59 insertions(+), 16 deletions(-) diff --git a/tools/virsh.c b/tools/virsh.c index 89c2e1e..54b1bbc 100644 --- a/tools/virsh.c +++ b/tools/virsh.c @@ -119,11 +119,11 @@ typedef enum { * vshCmdOptType - command option type */ typedef enum { - VSH_OT_NONE = 0, /* none */ - VSH_OT_BOOL, /* boolean option */ - VSH_OT_STRING, /* string option */ - VSH_OT_INT, /* int option */ - VSH_OT_DATA /* string data (as non-option) */ + VSH_OT_BOOL, /* boolean option */ + VSH_OT_STRING, /* string option */ + VSH_OT_INT, /* int option */ + VSH_OT_DATA, /* string data (as non-option) */ + VSH_OT_ARGV /* remaining arguments, opt->name should be "" */ } vshCmdOptType; /* @@ -230,6 +230,7 @@ static char *vshCommandOptString(const vshCmd *cmd, const char *name, static long long vshCommandOptLongLong(const vshCmd *cmd, const char *name, int *found); static int vshCommandOptBool(const vshCmd *cmd, const char *name); +static char *vshCommandOptArgv(const vshCmd *cmd, int count); #define VSH_BYID (1 << 1) #define VSH_BYUUID (1 << 2) @@ -9707,8 +9708,8 @@ vshCmddefGetData(const vshCmdDef * cmd, int data_ct) const vshCmdOptDef *opt; for (opt = cmd->opts; opt && opt->name; opt++) { - if (opt->type == VSH_OT_DATA) { - if (data_ct == 0) + if (opt->type >= VSH_OT_DATA) { + if (data_ct == 0 || opt->type == VSH_OT_ARGV) return opt; else data_ct--; @@ -9784,18 +9785,28 @@ vshCmddefHelp(vshControl *ctl, const char *cmdname) const vshCmdOptDef *opt; for (opt = def->opts; opt->name; opt++) { const char *fmt; - if (opt->type == VSH_OT_BOOL) + switch (opt->type) { + case VSH_OT_BOOL: fmt = "[--%s]"; - else if (opt->type == VSH_OT_INT) + break; + case VSH_OT_INT: /* xgettext:c-format */ fmt = _("[--%s <number>]"); - else if (opt->type == VSH_OT_STRING) + break; + case VSH_OT_STRING: /* xgettext:c-format */ fmt = _("[--%s <string>]"); - else if (opt->type == VSH_OT_DATA) + break; + case VSH_OT_DATA: fmt = ((opt->flag & VSH_OFLAG_REQ) ? "<%s>" : "[<%s>]"); - else + break; + case VSH_OT_ARGV: + /* xgettext:c-format */ + fmt = _("[<string>]..."); + break; + default: assert(0); + } fputc(' ', stdout); fprintf(stdout, fmt, opt->name); } @@ -9812,15 +9823,26 @@ vshCmddefHelp(vshControl *ctl, const char *cmdname) const vshCmdOptDef *opt; fputs(_("\n OPTIONS\n"), stdout); for (opt = def->opts; opt->name; opt++) { - if (opt->type == VSH_OT_BOOL) + switch (opt->type) { + case VSH_OT_BOOL: snprintf(buf, sizeof(buf), "--%s", opt->name); - else if (opt->type == VSH_OT_INT) + break; + case VSH_OT_INT: snprintf(buf, sizeof(buf), _("--%s <number>"), opt->name); - else if (opt->type == VSH_OT_STRING) + break; + case VSH_OT_STRING: snprintf(buf, sizeof(buf), _("--%s <string>"), opt->name); - else if (opt->type == VSH_OT_DATA) + break; + case VSH_OT_DATA: snprintf(buf, sizeof(buf), _("[--%s] <string>"), opt->name); + break; + case VSH_OT_ARGV: + /* Not really an option. */ + continue; + default: + assert(0); + } fprintf(stdout, " %-15s %s\n", buf, _(opt->help)); } @@ -9970,6 +9992,27 @@ vshCommandOptBool(const vshCmd *cmd, const char *name) return vshCommandOpt(cmd, name) ? TRUE : FALSE; } +/* + * Returns the COUNT argv argument, or NULL after last argument. + * + * Requires that a VSH_OT_ARGV option with the name "" be last in the + * list of supported options in CMD->def->opts. + */ +static char * +vshCommandOptArgv(const vshCmd *cmd, int count) +{ + vshCmdOpt *opt = cmd->opts; + + while (opt) { + if (opt->def && opt->def->type == VSH_OT_ARGV) { + if (count-- == 0) + return opt->data; + } + opt = opt->next; + } + return NULL; +} + /* Determine whether CMD->opts includes an option with name OPTNAME. If not, give a diagnostic and return false. If so, return true. */ -- 1.7.2.3

2010/10/15 Eric Blake <eblake@redhat.com>:
* tools/virsh.c (vshCmdOptType): Add VSH_OT_ARGV. Delete unused VSH_OT_NONE. (vshCmddefGetData): Special case new opt flag. (vshCmddefHelp): Display help for argv. (vshCommandOptArgv): New function. ---
Here's the completed series.
tools/virsh.c | 75 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 files changed, 59 insertions(+), 16 deletions(-)
diff --git a/tools/virsh.c b/tools/virsh.c index 89c2e1e..54b1bbc 100644 --- a/tools/virsh.c +++ b/tools/virsh.c @@ -119,11 +119,11 @@ typedef enum { * vshCmdOptType - command option type */ typedef enum { - VSH_OT_NONE = 0, /* none */ - VSH_OT_BOOL, /* boolean option */ - VSH_OT_STRING, /* string option */ - VSH_OT_INT, /* int option */ - VSH_OT_DATA /* string data (as non-option) */ + VSH_OT_BOOL, /* boolean option */
You lost (or explicitly removed) the = 0 here, but that's okay.
+ VSH_OT_STRING, /* string option */ + VSH_OT_INT, /* int option */ + VSH_OT_DATA, /* string data (as non-option) */ + VSH_OT_ARGV /* remaining arguments, opt->name should be "" */ } vshCmdOptType;
ACK. Matthias

On 10/15/2010 03:25 PM, Matthias Bolte wrote:
typedef enum { - VSH_OT_NONE = 0, /* none */ - VSH_OT_BOOL, /* boolean option */ - VSH_OT_STRING, /* string option */ - VSH_OT_INT, /* int option */ - VSH_OT_DATA /* string data (as non-option) */ + VSH_OT_BOOL, /* boolean option */
You lost (or explicitly removed) the = 0 here, but that's okay.
Explicitly dropped (C89 defaults enums to start from 0, and I tend to favor minimal code solutions). Besides, and since this enum is not exported, it doesn't matter _what_ we start with, as long as the values are distinct, so the compiler default is as good as any explicit setting.
+ VSH_OT_STRING, /* string option */ + VSH_OT_INT, /* int option */ + VSH_OT_DATA, /* string data (as non-option) */ + VSH_OT_ARGV /* remaining arguments, opt->name should be "" */ } vshCmdOptType;
ACK.
You really try to cover all edge cases here :)
Thanks - I sure tried! (although I didn't use gcov, so I probably missed some). I've pushed the series now. -- Eric Blake eblake@redhat.com +1-801-349-2682 Libvirt virtualization library http://libvirt.org

* tools/virsh.c (cmdEcho): New command. (commands): Add it. * tools/virsh.pod (echo): Document it. --- tools/virsh.c | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ tools/virsh.pod | 7 +++++ 2 files changed, 82 insertions(+), 0 deletions(-) diff --git a/tools/virsh.c b/tools/virsh.c index 54b1bbc..17cc763 100644 --- a/tools/virsh.c +++ b/tools/virsh.c @@ -8918,6 +8918,80 @@ cmdPwd(vshControl *ctl, const vshCmd *cmd ATTRIBUTE_UNUSED) #endif /* + * "echo" command + */ +static const vshCmdInfo info_echo[] = { + {"help", N_("echo arguments")}, + {"desc", N_("Echo back arguments, possibly with quoting.")}, + {NULL, NULL} +}; + +static const vshCmdOptDef opts_echo[] = { + {"shell", VSH_OT_BOOL, 0, N_("escape for shell use")}, + {"xml", VSH_OT_BOOL, 0, N_("escape for XML use")}, + {"", VSH_OT_ARGV, 0, N_("arguments to echo")}, + {NULL, 0, 0, NULL} +}; + +/* Exists mainly for debugging virsh, but also handy for adding back + * quotes for later evaluation. + */ +static int +cmdEcho (vshControl *ctl ATTRIBUTE_UNUSED, const vshCmd *cmd) +{ + bool shell = false; + bool xml = false; + int count = 0; + char *arg; + virBuffer buf = VIR_BUFFER_INITIALIZER; + + if (vshCommandOptBool(cmd, "shell")) + shell = true; + if (vshCommandOptBool(cmd, "xml")) + xml = true; + + while ((arg = vshCommandOptArgv(cmd, count)) != NULL) { + bool close_quote = false; + char *q; + + if (count) + virBufferAddChar(&buf, ' '); + /* Add outer '' only if arg included shell metacharacters. */ + if (shell && + (strpbrk(arg, "\r\t\n !\"#$&'()*;<>?[\\]^`{|}~") || !*arg)) { + virBufferAddChar(&buf, '\''); + close_quote = true; + } + if (xml) { + virBufferEscapeString(&buf, "%s", arg); + } else { + if (shell && (q = strchr(arg, '\''))) { + do { + virBufferAdd(&buf, arg, q - arg); + virBufferAddLit(&buf, "'\\''"); + arg = q + 1; + q = strchr(arg, '\''); + } while (q); + } + virBufferAdd(&buf, arg, strlen(arg)); + } + if (close_quote) + virBufferAddChar(&buf, '\''); + count++; + } + + if (virBufferError(&buf)) { + vshPrint(ctl, "%s", _("Failed to allocate XML buffer")); + return FALSE; + } + arg = virBufferContentAndReset(&buf); + if (arg) + vshPrint(ctl, "%s", arg); + VIR_FREE(arg); + return TRUE; +} + +/* * "edit" command */ static const vshCmdInfo info_edit[] = { @@ -9546,6 +9620,7 @@ static const vshCmdDef commands[] = { {"domxml-from-native", cmdDomXMLFromNative, opts_domxmlfromnative, info_domxmlfromnative}, {"domxml-to-native", cmdDomXMLToNative, opts_domxmltonative, info_domxmltonative}, {"dumpxml", cmdDumpXML, opts_dumpxml, info_dumpxml}, + {"echo", cmdEcho, opts_echo, info_echo}, {"edit", cmdEdit, opts_edit, info_edit}, {"find-storage-pool-sources", cmdPoolDiscoverSources, opts_find_storage_pool_sources, info_find_storage_pool_sources}, diff --git a/tools/virsh.pod b/tools/virsh.pod index f65f6d4..d662b78 100644 --- a/tools/virsh.pod +++ b/tools/virsh.pod @@ -409,6 +409,13 @@ Using I<--security-info> security sensitive information will also be included in the XML dump. I<--update-cpu> updates domain CPU requirements according to host CPU. +=item B<echo> optional I<--shell> I<--xml> I<arg>... + +Echo back each I<arg>, separated by space. If I<--shell> is +specified, then the output will be single-quoted where needed, so that +it is suitable for reuse in a shell context. If I<--xml> is +specified, then the output will be escaped for use in XML. + =item B<edit> I<domain-id> Edit the XML configuration file for a domain. -- 1.7.2.3

2010/10/15 Eric Blake <eblake@redhat.com>:
* tools/virsh.c (cmdEcho): New command. (commands): Add it. * tools/virsh.pod (echo): Document it. --- tools/virsh.c | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ tools/virsh.pod | 7 +++++ 2 files changed, 82 insertions(+), 0 deletions(-)
ACK. Matthias

* tests/virshtest.c (mymain): Add tests of command parsing and echo command. --- No patch series is complete without decent tests. This should cover all of the recent virsh command-line parsing improvements (various quoting styles, empty argument support, -- support), as well as stress-testing the new echo command. tests/virshtest.c | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 97 insertions(+), 0 deletions(-) diff --git a/tests/virshtest.c b/tests/virshtest.c index f6790bc..8ec97a8 100644 --- a/tests/virshtest.c +++ b/tests/virshtest.c @@ -220,6 +220,17 @@ static int testCompareDomstateByName(const void *data ATTRIBUTE_UNUSED) { return testCompareOutputLit(exp, NULL, argv); } +struct testInfo { + const char *const *argv; + const char *result; +}; + +static int testCompareEcho(const void *data) { + const struct testInfo *info = data; + return testCompareOutputLit(info->result, NULL, info->argv); +} + + static int mymain(int argc, char **argv) { @@ -309,6 +320,92 @@ mymain(int argc, char **argv) 1, testCompareDomstateByName, NULL) != 0) ret = -1; + /* It's a bit awkward listing result before argument, but that's a + * limitation of C99 vararg macros. */ +#define DO_TEST(i, result, ...) \ + do { \ + const char *myargv[] = { VIRSH_DEFAULT, __VA_ARGS__, NULL }; \ + const struct testInfo info = { myargv, result }; \ + if (virtTestRun("virsh echo " #i, \ + 1, testCompareEcho, &info) < 0) \ + ret = -1; \ + } while (0) + + /* Arg parsing quote removal tests. */ + DO_TEST(0, "\n", + "echo"); + DO_TEST(1, "a\n", + "echo", "a"); + DO_TEST(2, "a b\n", + "echo", "a", "b"); + DO_TEST(3, "a b\n", + "echo a \t b"); + DO_TEST(4, "a \t b\n", + "echo \"a \t b\""); + DO_TEST(5, "a \t b\n", + "echo 'a \t b'"); + DO_TEST(6, "a \t b\n", + "echo a\\ \\\t\\ b"); + DO_TEST(7, "\n\n", + "echo ; echo"); + DO_TEST(8, "a\nb\n", + ";echo a; ; echo b;"); + DO_TEST(9, "' \" \\;echo\ta\n", + "echo", "'", "\"", "\\;echo\ta"); + DO_TEST(10, "' \" ;echo a\n", + "echo \\' \\\" \\;echo\ta"); + DO_TEST(11, "' \" \\\na\n", + "echo \\' \\\" \\\\;echo\ta"); + DO_TEST(12, "' \" \\\\\n", + "echo \"'\" '\"' '\\'\"\\\\\""); + + /* Tests of echo flags. */ + DO_TEST(13, "a A 0 + * ; . ' \" / ? = \n < > &\n", + "echo", "a", "A", "0", "+", "*", ";", ".", "'", "\"", "/", "?", + "=", " ", "\n", "<", ">", "&"); + DO_TEST(14, "a A 0 + '*' ';' . ''\\''' '\"' / '?' = ' ' '\n' '<' '>' '&'\n", + "echo", "--shell", "a", "A", "0", "+", "*", ";", ".", "'", "\"", + "/", "?", "=", " ", "\n", "<", ">", "&"); + DO_TEST(15, "a A 0 + * ; . ' " / ? = \n < > &\n", + "echo", "--xml", "a", "A", "0", "+", "*", ";", ".", "'", "\"", + "/", "?", "=", " ", "\n", "<", ">", "&"); + DO_TEST(16, "a A 0 + '*' ';' . ''' '"' / '?' = ' ' '\n' '<'" + " '>' '&'\n", + "echo", "--shell", "--xml", "a", "A", "0", "+", "*", ";", ".", "'", + "\"", "/", "?", "=", " ", "\n", "<", ">", "&"); + DO_TEST(17, "\n", + "echo", ""); + DO_TEST(18, "''\n", + "echo", "--shell", ""); + DO_TEST(19, "\n", + "echo", "--xml", ""); + DO_TEST(20, "''\n", + "echo", "--xml", "--shell", ""); + DO_TEST(21, "\n", + "echo ''"); + DO_TEST(22, "''\n", + "echo --shell \"\""); + DO_TEST(23, "\n", + "echo --xml ''"); + DO_TEST(24, "''\n", + "echo --xml --shell \"\"''"); + + /* Tests of -- handling. */ + DO_TEST(25, "a\n", + "--", "echo", "--shell", "a"); + DO_TEST(26, "a\n", + "--", "echo", "a", "--shell"); + DO_TEST(27, "a --shell\n", + "--", "echo", "--", "a", "--shell"); + DO_TEST(28, "-- --shell a\n", + "echo", "--", "--", "--shell", "a"); + DO_TEST(29, "a\n", + "echo --s\\h'e'\"l\"l -- a"); + DO_TEST(30, "--shell a\n", + "echo \t '-'\"-\" \t --shell \t a"); + +#undef DO_TEST + return(ret==0 ? EXIT_SUCCESS : EXIT_FAILURE); } -- 1.7.2.3

2010/10/15 Eric Blake <eblake@redhat.com>:
* tests/virshtest.c (mymain): Add tests of command parsing and echo command. ---
No patch series is complete without decent tests. This should cover all of the recent virsh command-line parsing improvements (various quoting styles, empty argument support, -- support), as well as stress-testing the new echo command.
tests/virshtest.c | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 97 insertions(+), 0 deletions(-)
You really try to cover all edge cases here :) ACK. Matthias
participants (3)
-
Daniel Veillard
-
Eric Blake
-
Matthias Bolte