From 52a0b767c8b2acb18bbdc076b258092dc122a1c6 Mon Sep 17 00:00:00 2001 From: Jay Berkenbilt Date: Sat, 22 Dec 2018 23:16:46 -0500 Subject: Slightly improve bash completion arg parsing --- qpdf/qpdf.cc | 81 +++++++++++++++++++++++++----- qpdf/qtest/qpdf.test | 6 +++ qpdf/qtest/qpdf/completion-quoting.out | 1 + qpdf/qtest/qpdf/completion-usage-empty.out | 1 + 4 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 qpdf/qtest/qpdf/completion-quoting.out create mode 100644 qpdf/qtest/qpdf/completion-usage-empty.out diff --git a/qpdf/qpdf.cc b/qpdf/qpdf.cc index ec75aee4..45bce84f 100644 --- a/qpdf/qpdf.cc +++ b/qpdf/qpdf.cc @@ -1414,30 +1414,85 @@ void ArgParser::handleBashArguments() { // Do a minimal job of parsing bash_line into arguments. This - // doesn't do everything the shell does, but it should be good - // enough for purposes of handling completion. We can't use - // new_argv because this has to interoperate with @file arguments. - - enum { st_top, st_quote } state = st_top; + // doesn't do everything the shell does (e.g. $(...), variable + // expansion, arithmetic, globs, etc.), but it should be good + // enough for purposes of handling completion. As we build up the + // new argv, we can't use this->new_argv because this code has to + // interoperate with @file arguments, so memory for both ways of + // fabricating argv has to be protected. + + bool last_was_backslash = false; + enum { st_top, st_squote, st_dquote } state = st_top; std::string arg; for (std::string::iterator iter = bash_line.begin(); iter != bash_line.end(); ++iter) { char ch = (*iter); - if ((state == st_top) && QUtil::is_space(ch) && (! arg.empty())) + if (last_was_backslash) { - bash_argv.push_back( - PointerHolder( - true, QUtil::copy_string(arg.c_str()))); - arg.clear(); + arg.append(1, ch); + last_was_backslash = false; + } + else if (ch == '\\') + { + last_was_backslash = true; } else { - if (ch == '"') + bool append = false; + switch (state) { - state = (state == st_top ? st_quote : st_top); + case st_top: + if (QUtil::is_space(ch)) + { + if (! arg.empty()) + { + bash_argv.push_back( + PointerHolder( + true, QUtil::copy_string(arg.c_str()))); + arg.clear(); + } + } + else if (ch == '"') + { + state = st_dquote; + } + else if (ch == '\'') + { + state = st_squote; + } + else + { + append = true; + } + break; + + case st_squote: + if (ch == '\'') + { + state = st_top; + } + else + { + append = true; + } + break; + + case st_dquote: + if (ch == '"') + { + state = st_top; + } + else + { + append = true; + } + break; + } + if (append) + { + arg.append(1, ch); } - arg.append(1, ch); } } if (bash_argv.empty()) diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test index 1919f737..f2f0579f 100644 --- a/qpdf/qtest/qpdf.test +++ b/qpdf/qtest/qpdf.test @@ -117,6 +117,12 @@ my @completion_tests = ( ['qpdf --decode-lzzz', 15, 'decode-l'], ['qpdf --decode-level=', undef, 'decode-level'], ['qpdf --check -', undef, 'later-arg'], + ['qpdf infile outfile oops --ch', undef, 'usage-empty'], + ['qpdf --encrypt \'user " password\' ', undef, 'quoting'], + ['qpdf --encrypt \'user password\' ', undef, 'quoting'], + ['qpdf --encrypt "user password" ', undef, 'quoting'], + ['qpdf --encrypt "user pass\'word" ', undef, 'quoting'], + ['qpdf --encrypt user\ password ', undef, 'quoting'], ); $n_tests += scalar(@completion_tests); foreach my $c (@completion_tests) diff --git a/qpdf/qtest/qpdf/completion-quoting.out b/qpdf/qtest/qpdf/completion-quoting.out new file mode 100644 index 00000000..0d8a0622 --- /dev/null +++ b/qpdf/qtest/qpdf/completion-quoting.out @@ -0,0 +1 @@ +owner-password diff --git a/qpdf/qtest/qpdf/completion-usage-empty.out b/qpdf/qtest/qpdf/completion-usage-empty.out new file mode 100644 index 00000000..442e725d --- /dev/null +++ b/qpdf/qtest/qpdf/completion-usage-empty.out @@ -0,0 +1 @@ +!--check -- cgit v1.2.3-54-g00ecf