diff options
author | Oswald Buddenhagen <[email protected]> | 2010-10-14 18:05:43 +0200 |
---|---|---|
committer | Oswald Buddenhagen <[email protected]> | 2010-11-17 13:19:07 +0100 |
commit | 531c70f05bfc8355f856f2af41be533fb13b85e6 (patch) | |
tree | 33d0d30107949f92e507e26f083d5d3caa98e8b8 | |
parent | dc3ab5bf85d6ce9be2131f1a299731288d37c923 (diff) |
add Utils::QtcProcess
this is a wrapper around QProcess with these features:
- setEnvironment() takes a Utils::Environment instead of a QStringList
- instead of taking a stringlist with arguments, take a single shell
command string which is fully compatible with the system's native
shell (the bourne shell on unix and cmd.exe on windows) - with support
for environment variable expansion, and subject to the shell's
splitting and quoting rules. if the command is too complex (e.g.,
contains redirections), it is transparently executed through a real
shell.
- additionally, the class contains a set of helper functions for
manipulating (constructing, splitting, etc.) shell command lines.
in particular, it contains a shell-safe macro expander and the nested
class ArgIterator which can be used for inspecting and manipulating a
shell command line without going through the stringlist indirection
(which is potentially lossy).
some of this is based on KDE code (KShell and KMacroExpander) which i
have written myself.
-rw-r--r-- | src/libs/utils/qtcprocess.cpp | 1387 | ||||
-rw-r--r-- | src/libs/utils/qtcprocess.h | 149 | ||||
-rw-r--r-- | src/libs/utils/utils-lib.pri | 2 | ||||
-rw-r--r-- | tests/auto/auto.pro | 1 | ||||
-rw-r--r-- | tests/auto/qtcprocess/qtcprocess.pro | 22 | ||||
-rw-r--r-- | tests/auto/qtcprocess/tst_qtcprocess.cpp | 698 |
6 files changed, 2259 insertions, 0 deletions
diff --git a/src/libs/utils/qtcprocess.cpp b/src/libs/utils/qtcprocess.cpp new file mode 100644 index 00000000000..dab401d78cf --- /dev/null +++ b/src/libs/utils/qtcprocess.cpp @@ -0,0 +1,1387 @@ +/************************************************************************** +** +** This file is part of Qt Creator +** +** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation ([email protected]) +** +** Commercial Usage +** +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Nokia. +** +** GNU Lesser General Public License Usage +** +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at http://qt.nokia.com/contact. +** +**************************************************************************/ + +#include "qtcprocess.h" +#include "stringutils.h" + +#ifdef Q_OS_WIN +#include <QtCore/QDir> +#endif + +using namespace Utils; + +/** + * \fn QStringList QtcProcess::splitArgs( + * const QString &args, bool abortOnMeta, SplitError *err, const Environment *env) + * + * Splits \a args according to system shell word splitting and quoting rules. + * + * \section Unix + * + * The behavior is based on the POSIX shell and bash: + * \list + * \li Whitespace splits tokens + * \li The backslash quotes the following character + * \li A string enclosed in single quotes is not split. No shell meta + * characters are interpreted. + * \li A string enclosed in double quotes is not split. Within the string, + * the backslash quotes shell meta characters - if it is followed + * by a "meaningless" character, the backslash is output verbatim. + * \list + * If \a abortOnMeta is \c false, only the splitting and quoting rules apply, + * while other meta characters (substitutions, redirections, etc.) are ignored. + * If \a abortOnMeta is \c true, encounters of unhandled meta characters are + * treated as errors. + * + * \section Windows + * + * The behavior is defined by the Microsoft C runtime: + * \list + * \li Whitespace splits tokens + * \li A string enclosed in double quotes is not split + * \list + * \li 3N double quotes within a quoted string yield N literal quotes. + * This is not documented on MSDN. + * \endlist + * \li Backslashes have special semantics iff they are followed by a double + * quote: + * \list + * \li 2N backslashes + double quote => N backslashes and begin/end quoting + * \li 2N+1 backslashes + double quote => N backslashes + literal quote + * \endlist + * \endlist + * Qt and many other implementations comply with this standard, but many do not. + * + * If \a abortOnMeta is \c true, cmd shell semantics are applied before + * proceeding with word splitting: + * \list + * \li Cmd ignores \em all special chars between double quotes. + * Note that the quotes are \em not removed at this stage - the + * tokenization rules described above still apply. + * \li The \c circumflex is the escape char for everything including itself. + * \endlist + * As the quoting levels are independent from each other and have different + * semantics, you need a command line like \c{"foo "\^"" bar"} to get + * \c{foo " bar}. + * + * \param cmd the command to split + * \param abortOnMeta see above + * \param err if not NULL, a status code will be stored at the pointer + * target, see \ref SplitError + * \param env if not NULL, perform variable substitution with the + * given environment. + * \return a list of unquoted words or an empty list if an error occurred + */ + +#ifdef Q_OS_WIN + +inline static bool isMetaChar(ushort c) +{ + static const uchar iqm[] = { + 0x00, 0x00, 0x00, 0x00, 0x40, 0x03, 0x00, 0x50, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10 + }; // &()<>| + + return (c < sizeof(iqm) * 8) && (iqm[c / 8] & (1 << (c & 7))); +} + +static void envExpand(QString &args, const Environment *env, const QString *pwd) +{ + static const QString cdName = QLatin1String("CD"); + int off = 0; + next: + for (int prev = -1, that; + (that = args.indexOf(QLatin1Char('%'), off)) >= 0; + prev = that, off = that + 1) { + if (prev >= 0) { + const QString var = args.mid(prev + 1, that - prev - 1).toUpper(); + const QString val = (var == cdName && pwd && !pwd->isEmpty()) + ? QDir::toNativeSeparators(*pwd) : env->value(var); + if (!val.isEmpty()) { // Empty values are impossible, so this is an existence check + args.replace(prev, that - prev + 1, val); + off = prev + val.length(); + goto next; + } + } + } +} + +QString QtcProcess::prepareArgs(const QString &_args, SplitError *err, + const Environment *env, const QString *pwd) +{ + QString args(_args); + + if (env) { + envExpand(args, env, pwd); + } else { + if (args.indexOf(QLatin1Char('%')) >= 0) { + if (err) + *err = FoundMeta; + return QString(); + } + } + + if (!args.isEmpty() && args.unicode()[0].unicode() == '@') + args.remove(0, 1); + + for (int p = 0; p < args.length(); p++) { + ushort c = args.unicode()[p].unicode(); + if (c == '^') { + args.remove(p, 1); + } else if (c == '"') { + do { + if (++p == args.length()) + break; // For cmd, this is no error. + } while (args.unicode()[p].unicode() != '"'); + } else if (isMetaChar(c)) { + if (err) + *err = FoundMeta; + return QString(); + } + } + + if (err) + *err = SplitOk; + return args; +} + +inline static bool isWhiteSpace(ushort c) +{ + return c == ' ' || c == '\t'; +} + +static QStringList doSplitArgs(const QString &args, QtcProcess::SplitError *err) +{ + QStringList ret; + + if (err) + *err = QtcProcess::SplitOk; + + int p = 0; + const int length = args.length(); + forever { + forever { + if (p == length) + return ret; + if (!isWhiteSpace(args.unicode()[p].unicode())) + break; + ++p; + } + + QString arg; + bool inquote = false; + forever { + bool copy = true; // copy this char + int bslashes = 0; // number of preceding backslashes to insert + while (p < length && args.unicode()[p] == QLatin1Char('\\')) { + ++p; + ++bslashes; + } + if (p < length && args.unicode()[p] == QLatin1Char('"')) { + if (!(bslashes & 1)) { + // Even number of backslashes, so the quote is not escaped. + if (inquote) { + if (p + 1 < length && args.unicode()[p + 1] == QLatin1Char('"')) { + // This is not documented on MSDN. + // Two consecutive quotes make a literal quote. Brain damage: + // this still closes the quoting, so a 3rd quote is required, + // which makes the runtime's quoting run out of sync with the + // shell's one unless the 2nd quote is escaped. + ++p; + } else { + // Closing quote + copy = false; + } + inquote = false; + } else { + // Opening quote + copy = false; + inquote = true; + } + } + bslashes >>= 1; + } + + while (--bslashes >= 0) + arg.append(QLatin1Char('\\')); + + if (p == length || (!inquote && isWhiteSpace(args.unicode()[p].unicode()))) { + ret.append(arg); + if (inquote) { + if (err) + *err = QtcProcess::BadQuoting; + return QStringList(); + } + break; + } + + if (copy) + arg.append(args.unicode()[p]); + ++p; + } + } + //not reached +} + +QStringList QtcProcess::splitArgs(const QString &_args, bool abortOnMeta, SplitError *err, + const Environment *env, const QString *pwd) +{ + if (abortOnMeta) { + SplitError perr; + if (!err) + err = &perr; + QString args = prepareArgs(_args, &perr, env, pwd); + if (*err != SplitOk) + return QStringList(); + return doSplitArgs(args, err); + } else { + QString args = _args; + if (env) + envExpand(args, env, pwd); + return doSplitArgs(args, err); + } +} + +#else // Q_OS_WIN + +inline static bool isQuoteMeta(QChar cUnicode) +{ + char c = cUnicode.toAscii(); + return c == '\\' || c == '\'' || c == '"' || c == '$'; +} + +inline static bool isMeta(QChar cUnicode) +{ + static const uchar iqm[] = { + 0x00, 0x00, 0x00, 0x00, 0xdc, 0x07, 0x00, 0xd8, + 0x00, 0x00, 0x00, 0x38, 0x01, 0x00, 0x00, 0x38 + }; // \'"$`<>|;&(){}*?#[] + + uint c = cUnicode.unicode(); + + return (c < sizeof(iqm) * 8) && (iqm[c / 8] & (1 << (c & 7))); +} + +QStringList QtcProcess::splitArgs(const QString &args, bool abortOnMeta, SplitError *err, + const Environment *env, const QString *pwd) +{ + static const QString pwdName = QLatin1String("PWD"); + QStringList ret; + + for (int pos = 0; ; ) { + QChar c; + do { + if (pos >= args.length()) + goto okret; + c = args.unicode()[pos++]; + } while (c.isSpace()); + QString cret; + bool hadWord = false; + do { + if (c == QLatin1Char('\'')) { + int spos = pos; + do { + if (pos >= args.length()) + goto quoteerr; + c = args.unicode()[pos++]; + } while (c != QLatin1Char('\'')); + cret += args.mid(spos, pos-spos-1); + hadWord = true; + } else if (c == QLatin1Char('"')) { + for (;;) { + if (pos >= args.length()) + goto quoteerr; + c = args.unicode()[pos++]; + nextq: + if (c == QLatin1Char('"')) + break; + if (c == QLatin1Char('\\')) { + if (pos >= args.length()) + goto quoteerr; + c = args.unicode()[pos++]; + if (c != QLatin1Char('"') && + c != QLatin1Char('\\') && + !(abortOnMeta && + (c == QLatin1Char('$') || + c == QLatin1Char('`')))) + cret += QLatin1Char('\\'); + } else if (c == QLatin1Char('$') && env) { + if (pos >= args.length()) + goto quoteerr; + c = args.unicode()[pos++]; + bool braced = false; + if (c == QLatin1Char('{')) { + if (pos >= args.length()) + goto quoteerr; + c = args.unicode()[pos++]; + braced = true; + } + QString var; + while (c.isLetterOrNumber() || c == QLatin1Char('_')) { + var += c; + if (pos >= args.length()) + goto quoteerr; + c = args.unicode()[pos++]; + } + if (var == pwdName && pwd && !pwd->isEmpty()) { + cret += *pwd; + } else { + Environment::const_iterator vit = env->constFind(var); + if (vit == env->constEnd()) { + if (abortOnMeta) + goto metaerr; // Assume this is a shell builtin + } else { + cret += *vit; + } + } + if (!braced) + goto nextq; + if (c != QLatin1Char('}')) { + if (abortOnMeta) + goto metaerr; // Assume this is a complex expansion + goto quoteerr; // Otherwise it's just garbage + } + continue; + } else if (abortOnMeta && + (c == QLatin1Char('$') || + c == QLatin1Char('`'))) { + goto metaerr; + } + cret += c; + } + hadWord = true; + } else if (c == QLatin1Char('$') && env) { + if (pos >= args.length()) + goto quoteerr; // Bash just takes it verbatim, but whatever + c = args.unicode()[pos++]; + bool braced = false; + if (c == QLatin1Char('{')) { + if (pos >= args.length()) + goto quoteerr; + c = args.unicode()[pos++]; + braced = true; + } + QString var; + while (c.isLetterOrNumber() || c == QLatin1Char('_')) { + var += c; + if (pos >= args.length()) { + if (braced) + goto quoteerr; + c = QLatin1Char(' '); + break; + } + c = args.unicode()[pos++]; + } + QString val; + if (var == pwdName && pwd && !pwd->isEmpty()) { + val = *pwd; + } else { + Environment::const_iterator vit = env->constFind(var); + if (vit == env->constEnd()) { + if (abortOnMeta) + goto metaerr; // Assume this is a shell builtin + } else { + val = *vit; + } + } + for (int i = 0; i < val.length(); i++) { + QChar cc = val.unicode()[i]; + if (cc == 9 || cc == 10 || cc == 32) { + if (hadWord) { + ret += cret; + cret.clear(); + hadWord = false; + } + } else { + cret += cc; + hadWord = true; + } + } + if (!braced) + goto nextc; + if (c != QLatin1Char('}')) { + if (abortOnMeta) + goto metaerr; // Assume this is a complex expansion + goto quoteerr; // Otherwise it's just garbage + } + } else { + if (c == QLatin1Char('\\')) { + if (pos >= args.length()) + goto quoteerr; + c = args.unicode()[pos++]; + } else if (abortOnMeta && isMeta(c)) { + goto metaerr; + } + cret += c; + hadWord = true; + } + if (pos >= args.length()) + break; + c = args.unicode()[pos++]; + nextc: ; + } while (!c.isSpace()); + if (hadWord) + ret += cret; + } + + okret: + if (err) + *err = SplitOk; + return ret; + + quoteerr: + if (err) + *err = BadQuoting; + return QStringList(); + + metaerr: + if (err) + *err = FoundMeta; + return QStringList(); +} + +#endif // Q_OS_WIN + +inline static bool isSpecialCharUnix(ushort c) +{ + // Chars that should be quoted (TM). This includes: + static const uchar iqm[] = { + 0xff, 0xff, 0xff, 0xff, 0xdf, 0x07, 0x00, 0xd8, + 0x00, 0x00, 0x00, 0x38, 0x01, 0x00, 0x00, 0x78 + }; // 0-32 \'"$`<>|;&(){}*?#!~[] + + return (c < sizeof(iqm) * 8) && (iqm[c / 8] & (1 << (c & 7))); +} + +inline static bool hasSpecialCharsUnix(const QString &arg) +{ + for (int x = arg.length() - 1; x >= 0; --x) + if (isSpecialCharUnix(arg.unicode()[x].unicode())) + return true; + return false; +} + +QString QtcProcess::quoteArgUnix(const QString &arg) +{ + if (!arg.length()) + return QString::fromLatin1("''"); + + QString ret(arg); + if (hasSpecialCharsUnix(ret)) { + ret.replace(QLatin1Char('\''), QLatin1String("'\\''")); + ret.prepend(QLatin1Char('\'')); + ret.append(QLatin1Char('\'')); + } + return ret; +} + +void QtcProcess::addArgUnix(QString *args, const QString &arg) +{ + if (!args->isEmpty()) + *args += QLatin1Char(' '); + *args += quoteArgUnix(arg); +} + +QString QtcProcess::joinArgsUnix(const QStringList &args) +{ + QString ret; + foreach (const QString &arg, args) + addArgUnix(&ret, arg); + return ret; +} + +#ifdef Q_OS_WIN +inline static bool isSpecialChar(ushort c) +{ + // Chars that should be quoted (TM). This includes: + // - control chars & space + // - the shell meta chars "&()<>^| + // - the potential separators ,;= + static const uchar iqm[] = { + 0xff, 0xff, 0xff, 0xff, 0x45, 0x13, 0x00, 0x78, + 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x10 + }; + + return (c < sizeof(iqm) * 8) && (iqm[c / 8] & (1 << (c & 7))); +} + +inline static bool hasSpecialChars(const QString &arg) +{ + for (int x = arg.length() - 1; x >= 0; --x) + if (isSpecialChar(arg.unicode()[x].unicode())) + return true; + return false; +} + +QString QtcProcess::quoteArg(const QString &arg) +{ + if (!arg.length()) + return QString::fromLatin1("\"\""); + + QString ret(arg); + if (hasSpecialChars(ret)) { + // Quotes are escaped and their preceding backslashes are doubled. + // It's impossible to escape anything inside a quoted string on cmd + // level, so the outer quoting must be "suspended". + ret.replace(QRegExp(QLatin1String("(\\\\*)\"")), QLatin1String("\"\\1\\1\\^\"\"")); + // The argument must not end with a \ since this would be interpreted + // as escaping the quote -- rather put the \ behind the quote: e.g. + // rather use "foo"\ than "foo\" + ret.replace(QRegExp(QLatin1String("(\\\\*)$")), QLatin1String("\"\\1")); + ret.prepend(QLatin1Char('"')); + } + // FIXME: Without this, quoting is not foolproof. But it needs support in the process setup, etc. + //ret.replace('%', QLatin1String("%PERCENT_SIGN%")); + return ret; +} + +void QtcProcess::addArg(QString *args, const QString &arg) +{ + if (!args->isEmpty()) + *args += QLatin1Char(' '); + *args += quoteArg(arg); +} + +QString QtcProcess::joinArgs(const QStringList &args) +{ + QString ret; + foreach (const QString &arg, args) + addArg(&ret, arg); + return ret; +} +#endif + +void QtcProcess::addArgs(QString *args, const QString &inArgs) +{ + if (!inArgs.isEmpty()) { + if (!args->isEmpty()) + *args += QLatin1Char(' '); + *args += inArgs; + } +} + +void QtcProcess::addArgs(QString *args, const QStringList &inArgs) +{ + foreach (const QString &arg, inArgs) + addArg(args, arg); +} + +#ifdef Q_OS_WIN +void QtcProcess::prepareCommand(const QString &command, const QString &arguments, + QString *outCmd, QString *outArgs, + const Environment *env, const QString *pwd) +{ + QtcProcess::SplitError err; + *outArgs = QtcProcess::prepareArgs(arguments, &err, env, pwd); + if (err == QtcProcess::SplitOk) { + *outCmd = command; + } else { + *outCmd = QString::fromLatin1(qgetenv("COMSPEC")); + *outArgs = QLatin1String("/v:off /s /c \"") + + quoteArg(QDir::toNativeSeparators(command)) + QLatin1Char(' ') + arguments + + QLatin1Char('"'); + } +} +#else +bool QtcProcess::prepareCommand(const QString &command, const QString &arguments, + QString *outCmd, QStringList *outArgs, + const Environment *env, const QString *pwd) +{ + QtcProcess::SplitError err; + *outArgs = QtcProcess::prepareArgs(arguments, &err, env, pwd); + if (err == QtcProcess::SplitOk) { + *outCmd = command; + } else { + if (err != QtcProcess::FoundMeta) + return false; + *outCmd = QLatin1String("/bin/sh"); + *outArgs << QLatin1String("-c") << (quoteArg(command) + QLatin1Char(' ') + arguments); + } + return true; +} +#endif + +void QtcProcess::start() +{ + Environment env; + if (m_haveEnv) { + env = m_environment; + QProcess::setEnvironment(env.toStringList()); + } else { + env = Environment::systemEnvironment(); + } + + const QString &workDir = workingDirectory(); + QString command; +#ifdef Q_OS_WIN + QString arguments; + prepareCommand(m_command, m_arguments, &command, &arguments, &env, &workDir); + setNativeArguments(arguments); + QProcess::start(command, QStringList()); +#else + QStringList arguments; + if (!prepareCommand(m_command, m_arguments, &command, &arguments, &env, &workDir)) { + setErrorString(tr("Error in command line.")); + // Should be FailedToStart, but we cannot set the process error from the outside, + // so it would be inconsistent. + emit error(QProcess::UnknownError); + return; + } + QProcess::start(command, arguments); +#endif +} + +#ifdef Q_OS_WIN + +// This function assumes that the resulting string will be quoted. +// That's irrelevant if it does not contain quotes itself. +static int quoteArgInternal(QString &ret, int bslashes) +{ + // Quotes are escaped and their preceding backslashes are doubled. + // It's impossible to escape anything inside a quoted string on cmd + // level, so the outer quoting must be "suspended". + const QChar bs(QLatin1Char('\\')), dq(QLatin1Char('"')); + for (int p = 0; p < ret.length(); p++) { + if (ret.at(p) == bs) { + bslashes++; + } else { + if (ret.at(p) == dq) { + if (bslashes) { + ret.insert(p, QString(bslashes, bs)); + p += bslashes; + } + ret.insert(p, QLatin1String("\"\\^\"")); + p += 4; + } + bslashes = 0; + } + } + return bslashes; +} + +#else + +// The main state of the Unix shell parser +enum MxQuoting { MxBasic, MxSingleQuote, MxDoubleQuote, MxParen, MxSubst, MxGroup, MxMath }; +typedef struct { + MxQuoting current; + // Bizarrely enough, double quoting has an impact on the behavior of some + // complex expressions within the quoted string. + bool dquote; +} MxState; +Q_DECLARE_TYPEINFO(MxState, Q_PRIMITIVE_TYPE); + +// Pushed state for the case where a $(()) expansion turns out bogus +typedef struct { + QString str; + int pos, varPos; +} MxSave; +Q_DECLARE_TYPEINFO(MxSave, Q_MOVABLE_TYPE); + +#include <QtCore/QStack> + +#endif + +// TODO: This documentation is relevant for end-users. Where to put it? +/** + * Perform safe macro expansion (substitution) on a string for use + * in shell commands. + * + * \section Unix notes + * + * Explicitly supported shell constructs: + * \\ '' "" {} () $(()) ${} $() `` + * + * Implicitly supported shell constructs: + * (()) + * + * Unsupported shell constructs that will cause problems: + * \list + * \li Shortened \c{case $v in pat)} syntax. Use \c{case $v in (pat)} instead. + * \li Bash-style \c{$""} and \c{$''} string quoting syntax. + * \endlist + * + * The rest of the shell (incl. bash) syntax is simply ignored, + * as it is not expected to cause problems. + * + * Security considerations: + * \list + * \li Backslash-escaping an expando is treated as a quoting error + * \li Do not put expandos into double quoted substitutions: + * \badcode + * "${VAR:-%{macro}}" + * \endcode + * \li Do not put expandos into command line arguments which are nested + * shell commands: + * \badcode + * sh -c 'foo \%{file}' + * \endcode + * \goodcode + * file=\%{file} sh -c 'foo "$file"' + * \endcode + * \endlist + * + * \section Windows notes + * + * All quoting syntax supported by splitArgs() is supported here as well. + * Additionally, command grouping via parentheses is recognized - note + * however, that the parser is much stricter about unquoted parentheses + * than cmd itself. + * The rest of the cmd syntax is simply ignored, as it is not expected + * to cause problems. + * + * Security considerations: + * \list + * \li Circumflex-escaping an expando is treated as a quoting error + * \li Closing double quotes right before expandos and opening double quotes + * right after expandos are treated as quoting errors + * \li Do not put expandos into nested commands: + * \badcode + * for /f "usebackq" \%v in (`foo \%{file}`) do \@echo \%v + * \endcode + * \li A macro's value must not contain anything which may be interpreted + * as an environment variable expansion. A solution is replacing any + * percent signs with a fixed string like \c{\%PERCENT_SIGN\%} and + * injecting \c{PERCENT_SIGN=\%} into the shell's environment. + * \li Enabling delayed environment variable expansion (cmd /v:on) should have + * no security implications, but may still wreak havoc due to the + * need for doubling circumflexes if any exclamation marks are present, + * and the need to circumflex-escape the exclamation marks themselves. + * \endlist + * + * \param cmd pointer to the string in which macros are expanded in-place + * \param mx pointer to a macro expander instance + * \return false if the string could not be parsed and therefore no safe + * substitution was possible + */ +bool QtcProcess::expandMacros(QString *cmd, AbstractMacroExpander *mx) +{ + QString str = *cmd; + if (str.isEmpty()) + return true; + + QString rsts; + int varLen; + int varPos = 0; + if (!(varLen = mx->findMacro(str, &varPos, &rsts))) + return true; + + int pos = 0; + +#ifdef Q_OS_WIN + enum { // cmd.exe parsing state + ShellBasic, // initial state + ShellQuoted, // double-quoted state => *no* other meta chars are interpreted + ShellEscaped // circumflex-escaped state => next char is not interpreted + } shellState = ShellBasic; + enum { // CommandLineToArgv() parsing state and some more + CrtBasic, // initial state + CrtNeedWord, // after empty expando; insert empty argument if whitespace follows + CrtInWord, // in non-whitespace + CrtClosed, // previous char closed the double-quoting + CrtHadQuote, // closed double-quoting after an expando + // The remaining two need to be numerically higher + CrtQuoted, // double-quoted state => spaces don't split tokens + CrtNeedQuote // expando opened quote; close if no expando follows + } crtState = CrtBasic; + int bslashes = 0; // previous chars were manual backslashes + int rbslashes = 0; // trailing backslashes in replacement + + forever { + if (pos == varPos) { + if (shellState == ShellEscaped) + return false; // Circumflex'd quoted expando would be Bad (TM). + if ((shellState == ShellQuoted) != (crtState == CrtQuoted)) + return false; // CRT quoting out of sync with shell quoting. Ahoy to Redmond. + rbslashes += bslashes; + bslashes = 0; + if (crtState < CrtQuoted) { + if (rsts.isEmpty()) { + if (crtState == CrtBasic) { + // Outside any quoting and the string is empty, so put + // a pair of quotes. Delaying that is just pedantry. + crtState = CrtNeedWord; + } + } else { + if (hasSpecialChars(rsts)) { + if (crtState == CrtClosed) { + // Quoted expando right after closing quote. Can't do that. + return false; + } + int tbslashes = quoteArgInternal(rsts, 0); + rsts.prepend(QLatin1Char('"')); + if (rbslashes) + rsts.prepend(QString(rbslashes, QLatin1Char('\\'))); + crtState = CrtNeedQuote; + rbslashes = tbslashes; + } else { + crtState = CrtInWord; // We know that this string contains no spaces. + // We know that this string contains no quotes, + // so the function won't make a mess. + rbslashes = quoteArgInternal(rsts, rbslashes); + } + } + } else { + rbslashes = quoteArgInternal(rsts, rbslashes); + } + str.replace(pos, varLen, rsts); + pos += rsts.length(); + varPos = pos; + if (!(varLen = mx->findMacro(str, &varPos, &rsts))) { + // Don't leave immediately, as we may be in CrtNeedWord state which could + // be still resolved, or we may have inserted trailing backslashes. + varPos = INT_MAX; + } + continue; + } + if (crtState == CrtNeedQuote) { + if (rbslashes) { + str.insert(pos, QString(rbslashes, QLatin1Char('\\'))); + pos += rbslashes; + varPos += rbslashes; + rbslashes = 0; + } + str.insert(pos, QLatin1Char('"')); + pos++; + varPos++; + crtState = CrtHadQuote; + } + ushort cc = str.unicode()[pos].unicode(); + if (shellState == ShellBasic && cc == '^') { + shellState = ShellEscaped; + } else { + if (!cc || cc == ' ' || cc == '\t') { + if (crtState < CrtQuoted) { + if (crtState == CrtNeedWord) { + str.insert(pos, QLatin1String("\"\"")); + pos += 2; + varPos += 2; + } + crtState = CrtBasic; + } + if (!cc) + break; + bslashes = 0; + rbslashes = 0; + } else { + if (cc == '\\') { + bslashes++; + if (crtState < CrtQuoted) + crtState = CrtInWord; + } else { + if (cc == '"') { + if (shellState != ShellEscaped) + shellState = (shellState == ShellQuoted) ? ShellBasic : ShellQuoted; + if (rbslashes) { + // Offset -1: skip possible circumflex. We have at least + // one backslash, so a fixed offset is ok. + str.insert(pos - 1, QString(rbslashes, QLatin1Char('\\'))); + pos += rbslashes; + varPos += rbslashes; + } + if (!(bslashes & 1)) { + // Even number of backslashes, so the quote is not escaped. + switch (crtState) { + case CrtQuoted: + // Closing quote + crtState = CrtClosed; + break; + case CrtClosed: + // Two consecutive quotes make a literal quote - and + // still close quoting. See QtcProcess::quoteArg(). + crtState = CrtInWord; + break; + case CrtHadQuote: + // Opening quote right after quoted expando. Can't do that. + return false; + default: + // Opening quote + crtState = CrtQuoted; + break; + } + } else if (crtState < CrtQuoted) { + crtState = CrtInWord; + } + } else if (crtState < CrtQuoted) { + crtState = CrtInWord; + } + bslashes = 0; + rbslashes = 0; + } + } + if (varPos == INT_MAX && !rbslashes) + break; + if (shellState == ShellEscaped) + shellState = ShellBasic; + } + pos++; + } +#else + MxState state = { MxBasic, false }; + QStack<MxState> sstack; + QStack<MxSave> ostack; + + while (pos < str.length()) { + if (pos == varPos) { + // Our expansion rules trigger in any context + if (state.dquote) { + // We are within a double-quoted string. Escape relevant meta characters. + rsts.replace(QRegExp(QLatin1String("([$`\"\\\\])")), QLatin1String("\\\\1")); + } else if (state.current == MxSingleQuote) { + // We are within a single-quoted string. "Suspend" single-quoting and put a + // single escaped quote for each single quote inside the string. + rsts.replace(QLatin1Char('\''), QLatin1String("'\\''")); + } else if (rsts.isEmpty() || hasSpecialCharsUnix(rsts)) { + // String contains "quote-worthy" characters. Use single quoting - but + // that choice is arbitrary. + rsts.replace(QLatin1Char('\''), QLatin1String("'\\''")); + rsts.prepend(QLatin1Char('\'')); + rsts.append(QLatin1Char('\'')); + } // Else just use the string verbatim. + str.replace(pos, varLen, rsts); + pos += rsts.length(); + varPos = pos; + if (!(varLen = mx->findMacro(str, &varPos, &rsts))) + break; + continue; + } + ushort cc = str.unicode()[pos].unicode(); + if (state.current == MxSingleQuote) { + // Single quoted context - only the single quote has any special meaning. + if (cc == '\'') + state = sstack.pop(); + } else if (cc == '\\') { + // In any other context, the backslash starts an escape. + pos += 2; + if (varPos < pos) + return false; // Backslash'd quoted expando would be Bad (TM). + continue; + } else if (cc == '$') { + cc = str.unicode()[++pos].unicode(); + if (cc == '(') { + sstack.push(state); + if (str.unicode()[pos + 1].unicode() == '(') { + // $(( starts a math expression. This may also be a $( ( in fact, + // so we push the current string and offset on a stack so we can retry. + MxSave sav = { str, pos + 2, varPos }; + ostack.push(sav); + state.current = MxMath; + pos += 2; + continue; + } else { + // $( starts a command substitution. This actually "opens a new context" + // which overrides surrounding double quoting. + state.current = MxParen; + state.dquote = false; + } + } else if (cc == '{') { + // ${ starts a "braced" variable substitution. + sstack.push(state); + state.current = MxSubst; + } // Else assume that a "bare" variable substitution has started + } else if (cc == '`') { + // Backticks are evil, as every shell interprets escapes within them differently, + // which is a danger for the quoting of our own expansions. + // So we just apply *our* rules (which match bash) and transform it into a POSIX + // command substitution which has clear semantics. + str.replace(pos, 1, QLatin1String("$( " )); // add space -> avoid creating $(( + varPos += 2; + int pos2 = pos += 3; + forever { + if (pos2 >= str.length()) + return false; // Syntax error - unterminated backtick expression. + cc = str.unicode()[pos2].unicode(); + if (cc == '`') + break; + if (cc == '\\') { + cc = str.unicode()[++pos2].unicode(); + if (cc == '$' || cc == '`' || cc == '\\' || + (cc == '"' && state.dquote)) + { + str.remove(pos2 - 1, 1); + if (varPos >= pos2) + varPos--; + continue; + } + } + pos2++; + } + str[pos2] = QLatin1Char(')'); + sstack.push(state); + state.current = MxParen; + state.dquote = false; + continue; + } else if (state.current == MxDoubleQuote) { + // (Truly) double quoted context - only remaining special char is the closing quote. + if (cc == '"') + state = sstack.pop(); + } else if (cc == '\'') { + // Start single quote if we are not in "inherited" double quoted context. + if (!state.dquote) { + sstack.push(state); + state.current = MxSingleQuote; + } + } else if (cc == '"') { + // Same for double quoting. + if (!state.dquote) { + sstack.push(state); + state.current = MxDoubleQuote; + state.dquote = true; + } + } else if (state.current == MxSubst) { + // "Braced" substitution context - only remaining special char is the closing brace. + if (cc == '}') + state = sstack.pop(); + } else if (cc == ')') { + if (state.current == MxMath) { + if (str.unicode()[pos + 1].unicode() == ')') { + state = sstack.pop(); + pos += 2; + } else { + // False hit: the $(( was a $( ( in fact. + // ash does not care (and will complain), but bash actually parses it. + varPos = ostack.top().varPos; + pos = ostack.top().pos; + str = ostack.top().str; + ostack.pop(); + state.current = MxParen; + state.dquote = false; + sstack.push(state); + } + continue; + } else if (state.current == MxParen) { + state = sstack.pop(); + } else { + break; // Syntax error - excess closing parenthesis. + } + } else if (cc == '}') { + if (state.current == MxGroup) + state = sstack.pop(); + else + break; // Syntax error - excess closing brace. + } else if (cc == '(') { + // Context-saving command grouping. + sstack.push(state); + state.current = MxParen; + } else if (cc == '{') { + // Plain command grouping. + sstack.push(state); + state.current = MxGroup; + } + pos++; + } + // FIXME? May complain if (!sstack.empty()), but we don't really care anyway. +#endif + + *cmd = str; + return true; +} + +QString QtcProcess::expandMacros(const QString &str, AbstractMacroExpander *mx) +{ + QString ret = str; + expandMacros(&ret, mx); + return ret; +} + +bool QtcProcess::ArgIterator::next() +{ + // We delay the setting of m_prev so we can still delete the last argument + // after we find that there are no more arguments. It's a bit of a hack ... + int prev = m_pos; + + m_simple = true; + m_value.clear(); + +#ifdef Q_OS_WIN + enum { // cmd.exe parsing state + ShellBasic, // initial state + ShellQuoted, // double-quoted state => *no* other meta chars are interpreted + ShellEscaped // circumflex-escaped state => next char is not interpreted + } shellState = ShellBasic; + enum { // CommandLineToArgv() parsing state and some more + CrtBasic, // initial state + CrtInWord, // in non-whitespace + CrtClosed, // previous char closed the double-quoting + CrtQuoted // double-quoted state => spaces don't split tokens + } crtState = CrtBasic; + enum { NoVar, NewVar, FullVar } varState = NoVar; // inside a potential env variable expansion + int bslashes = 0; // number of preceding backslashes + + for (;; m_pos++) { + ushort cc = m_pos < m_str->length() ? m_str->unicode()[m_pos].unicode() : 0; + if (shellState == ShellBasic && cc == '^') { + varState = NoVar; + shellState = ShellEscaped; + } else if ((shellState == ShellBasic && isMetaChar(cc)) || !cc) { // A "bit" simplistic ... + // We ignore crtQuote state here. Whatever ... + doReturn: + if (m_simple) + while (--bslashes >= 0) + m_value += QLatin1Char('\\'); + else + m_value.clear(); + if (crtState != CrtBasic) { + m_prev = prev; + return true; + } + return false; + } else { + if (crtState != CrtQuoted && (cc == ' ' || cc == '\t')) { + if (crtState != CrtBasic) { + // We'll lose shellQuote state here. Whatever ... + goto doReturn; + } + } else { + if (cc == '\\') { + bslashes++; + if (crtState != CrtQuoted) + crtState = CrtInWord; + varState = NoVar; + } else { + if (cc == '"') { + varState = NoVar; + if (shellState != ShellEscaped) + shellState = (shellState == ShellQuoted) ? ShellBasic : ShellQuoted; + int obslashes = bslashes; + bslashes >>= 1; + if (!(obslashes & 1)) { + // Even number of backslashes, so the quote is not escaped. + switch (crtState) { + case CrtQuoted: + // Closing quote + crtState = CrtClosed; + continue; + case CrtClosed: + // Two consecutive quotes make a literal quote - and + // still close quoting. See quoteArg(). + crtState = CrtInWord; + break; + default: + // Opening quote + crtState = CrtQuoted; + continue; + } + } else if (crtState != CrtQuoted) { + crtState = CrtInWord; + } + } else { + if (cc == '%') { + if (varState == FullVar) { + m_simple = false; + varState = NoVar; + } else { + varState = NewVar; + } + } else if (varState != NoVar) { + // This check doesn't really reflect cmd reality, but it is an + // approximation of what would be sane. + varState = (cc == '_' || cc == '-' || cc == '.' + || QChar(cc).isLetterOrNumber()) ? FullVar : NoVar; + + } + if (crtState != CrtQuoted) + crtState = CrtInWord; + } + for (; bslashes > 0; bslashes--) + m_value += QLatin1Char('\\'); + m_value += QChar(cc); + } + } + if (shellState == ShellEscaped) + shellState = ShellBasic; + } + } +#else + MxState state = { MxBasic, false }; + QStack<MxState> sstack; + QStack<int> ostack; + bool hadWord = false; + + for (; m_pos < m_str->length(); m_pos++) { + ushort cc = m_str->unicode()[m_pos].unicode(); + if (state.current == MxSingleQuote) { + if (cc == '\'') { + state = sstack.pop(); + continue; + } + } else if (cc == '\\') { + if (++m_pos >= m_str->length()) + break; + cc = m_str->unicode()[m_pos].unicode(); + if (state.dquote && cc != '"' && cc != '\\' && cc != '$' && cc != '`') + m_value += QLatin1Char('\\'); + } else if (cc == '$') { + if (++m_pos >= m_str->length()) + break; + cc = m_str->unicode()[m_pos].unicode(); + if (cc == '(') { + sstack.push(state); + if (++m_pos >= m_str->length()) + break; + if (m_str->unicode()[m_pos].unicode() == '(') { + ostack.push(m_pos); + state.current = MxMath; + } else { + state.dquote = false; + state.current = MxParen; + // m_pos too far by one now - whatever. + } + } else if (cc == '{') { + sstack.push(state); + state.current = MxSubst; + } else { + // m_pos too far by one now - whatever. + } + m_simple = false; + hadWord = true; + continue; + } else if (cc == '`') { + forever { + if (++m_pos >= m_str->length()) { + m_simple = false; + m_prev = prev; + return true; + } + cc = m_str->unicode()[m_pos].unicode(); + if (cc == '`') + break; + if (cc == '\\') + m_pos++; // m_pos may be too far by one now - whatever. + } + m_simple = false; + hadWord = true; + continue; + } else if (state.current == MxDoubleQuote) { + if (cc == '"') { + state = sstack.pop(); + continue; + } + } else if (cc == '\'') { + if (!state.dquote) { + sstack.push(state); + state.current = MxSingleQuote; + hadWord = true; + continue; + } + } else if (cc == '"') { + if (!state.dquote) { + sstack.push(state); + state.dquote = true; + state.current = MxDoubleQuote; + hadWord = true; + continue; + } + } else if (state.current == MxSubst) { + if (cc == '}') + state = sstack.pop(); + continue; // Not simple anyway + } else if (cc == ')') { + if (state.current == MxMath) { + if (++m_pos >= m_str->length()) + break; + if (m_str->unicode()[m_pos].unicode() == ')') { + ostack.pop(); + state = sstack.pop(); + } else { + // false hit: the $(( was a $( ( in fact. + // ash does not care, but bash does. + m_pos = ostack.pop(); + state.current = MxParen; + state.dquote = false; + sstack.push(state); + } + continue; + } else if (state.current == MxParen) { + state = sstack.pop(); + continue; + } else { + break; + } +#if 0 // MxGroup is impossible, see below. + } else if (cc == '}') { + if (state.current == MxGroup) { + state = sstack.pop(); + continue; + } +#endif + } else if (cc == '(') { + sstack.push(state); + state.current = MxParen; + m_simple = false; + hadWord = true; +#if 0 // Should match only at the beginning of a command, which we never have currently. + } else if (cc == '{') { + sstack.push(state); + state.current = MxGroup; + m_simple = false; + hadWord = true; + continue; +#endif + } else if (cc == '<' || cc == '>' || cc == '&' || cc == '|' || cc == ';') { + if (sstack.isEmpty()) + break; + } else if (cc == ' ' || cc == '\t') { + if (!hadWord) + continue; + if (sstack.isEmpty()) + break; + } + m_value += QChar(cc); + hadWord = true; + } + // TODO: Possibly complain here if (!sstack.empty()) + if (!m_simple) + m_value.clear(); + if (hadWord) { + m_prev = prev; + return true; + } + return false; +#endif +} + +void QtcProcess::ArgIterator::deleteArg() +{ + if (!m_prev) + while (m_pos < m_str->length() && m_str->at(m_pos).isSpace()) + m_pos++; + m_str->remove(m_prev, m_pos - m_prev); + m_pos = m_prev; +} + +void QtcProcess::ArgIterator::appendArg(const QString &str) +{ + const QString qstr = quoteArg(str); + if (!m_pos) + m_str->insert(0, qstr + QLatin1Char(' ')); + else + m_str->insert(m_pos, QLatin1Char(' ') + qstr); + m_pos += qstr.length() + 1; +} diff --git a/src/libs/utils/qtcprocess.h b/src/libs/utils/qtcprocess.h new file mode 100644 index 00000000000..e7439d8c27c --- /dev/null +++ b/src/libs/utils/qtcprocess.h @@ -0,0 +1,149 @@ +/************************************************************************** +** +** This file is part of Qt Creator +** +** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation ([email protected]) +** +** Commercial Usage +** +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Nokia. +** +** GNU Lesser General Public License Usage +** +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at http://qt.nokia.com/contact. +** +**************************************************************************/ + +#ifndef QTCPROCESS_H +#define QTCPROCESS_H + +#include <QProcess> + +#include "utils_global.h" + +#include "environment.h" + +namespace Utils { +class AbstractMacroExpander; + +/*! + This class provides functionality for dealing with shell-quoted process arguments. +*/ +class QTCREATOR_UTILS_EXPORT QtcProcess : public QProcess +{ + Q_OBJECT + +public: + QtcProcess(QObject *parent = 0) : QProcess(parent), m_haveEnv(false) {} + void setEnvironment(const Environment &env) + { m_environment = env; m_haveEnv = true; } + void setCommand(const QString &command, const QString &arguments) + { m_command = command; m_arguments = arguments; } + void start(); + + enum SplitError { + SplitOk = 0, //! All went just fine + BadQuoting, //! Command contains quoting errors + FoundMeta //! Command contains complex shell constructs + }; + + //! Quote a single argument for usage in a unix shell command + static QString quoteArgUnix(const QString &arg); + //! Quote a single argument and append it to a unix shell command + static void addArgUnix(QString *args, const QString &arg); + //! Join an argument list into a unix shell command + static QString joinArgsUnix(const QStringList &args); +#ifdef Q_OS_WIN + //! Quote a single argument for usage in a shell command + static QString quoteArg(const QString &arg); + //! Quote a single argument and append it to a shell command + static void addArg(QString *args, const QString &arg); + //! Join an argument list into a shell command + static QString joinArgs(const QStringList &args); + //! Prepare argument of a shell command for feeding into QProcess + static QString prepareArgs(const QString &cmd, SplitError *err, + const Environment *env = 0, const QString *pwd = 0); + //! Prepare a shell command for feeding into QProcess + static void prepareCommand(const QString &command, const QString &arguments, + QString *outCmd, QString *outArgs, + const Environment *env = 0, const QString *pwd = 0); +#else + static QString quoteArg(const QString &arg) { return quoteArgUnix(arg); } + static void addArg(QString *args, const QString &arg) { addArgUnix(args, arg); } + static QString joinArgs(const QStringList &args) { return joinArgsUnix(args); } + static QStringList prepareArgs(const QString &cmd, SplitError *err, + const Environment *env = 0, const QString *pwd = 0) + { return splitArgs(cmd, true, err, env, pwd); } + static bool prepareCommand(const QString &command, const QString &arguments, + QString *outCmd, QStringList *outArgs, + const Environment *env = 0, const QString *pwd = 0); +#endif + //! Quote and append each argument to a shell command + static void addArgs(QString *args, const QStringList &inArgs); + //! Append already quoted arguments to a shell command + static void addArgs(QString *args, const QString &inArgs); + //! Split a shell command into separate arguments. ArgIterator is usually a better choice. + static QStringList splitArgs(const QString &cmd, bool abortOnMeta = false, SplitError *err = 0, + const Environment *env = 0, const QString *pwd = 0); + //! Safely replace the expandos in a shell command + static bool expandMacros(QString *cmd, AbstractMacroExpander *mx); + static QString expandMacros(const QString &str, AbstractMacroExpander *mx); + + /*! Iterate over arguments from a command line. + * Assumes that the name of the actual command is *not* part of the line. + * Terminates after the first command if the command line is complex. + */ + class QTCREATOR_UTILS_EXPORT ArgIterator { + public: + ArgIterator(QString *str) : m_str(str), m_pos(0), m_prev(-1) {} + //! Get the next argument. Returns false on encountering end of first command. + bool next(); + //! True iff the argument is a plain string, possibly after unquoting. + bool isSimple() const { return m_simple; } + //! Return the string value of the current argument if it is simple, otherwise empty. + QString value() const { return m_value; } + //! Delete the last argument fetched via next() from the command line. + void deleteArg(); + //! Insert argument into the command line after the last one fetched via next(). + //! This may be used before the first call to next() to insert at the front. + void appendArg(const QString &str); + private: + QString *m_str, m_value; + int m_pos, m_prev; + bool m_simple; + }; + + class QTCREATOR_UTILS_EXPORT ConstArgIterator { + public: + ConstArgIterator(const QString &str) : m_str(str), m_ait(&m_str) {} + bool next() { return m_ait.next(); } + bool isSimple() const { return m_ait.isSimple(); } + QString value() const { return m_ait.value(); } + private: + QString m_str; + ArgIterator m_ait; + }; + +private: + QString m_command; + QString m_arguments; + Environment m_environment; + bool m_haveEnv; +}; + +} + +#endif // QTCPROCESS_H diff --git a/src/libs/utils/utils-lib.pri b/src/libs/utils/utils-lib.pri index aa2e65cf11d..245bb7ecdd2 100644 --- a/src/libs/utils/utils-lib.pri +++ b/src/libs/utils/utils-lib.pri @@ -8,6 +8,7 @@ INCLUDEPATH += $$PWD QT += network SOURCES += $$PWD/environment.cpp \ + $$PWD/qtcprocess.cpp \ $$PWD/reloadpromptutils.cpp \ $$PWD/stringutils.cpp \ $$PWD/filesearch.cpp \ @@ -66,6 +67,7 @@ unix:!macx { SOURCES += $$PWD/unixutils.cpp } HEADERS += $$PWD/environment.h \ + $$PWD/qtcprocess.h \ $$PWD/utils_global.h \ $$PWD/reloadpromptutils.h \ $$PWD/stringutils.h \ diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index d67486117bc..a041af70533 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -12,6 +12,7 @@ SUBDIRS += \ # icheckbuild \ # profilewriter \ ioutils \ + qtcprocess \ utils_stringutils \ filesearch diff --git a/tests/auto/qtcprocess/qtcprocess.pro b/tests/auto/qtcprocess/qtcprocess.pro new file mode 100644 index 00000000000..037015df4a7 --- /dev/null +++ b/tests/auto/qtcprocess/qtcprocess.pro @@ -0,0 +1,22 @@ +CONFIG += qtestlib testcase +TEMPLATE = app +CONFIG -= app_bundle +DEFINES += QTCREATOR_UTILS_LIB + +UTILS_PATH = ../../../src/libs/utils + +INCLUDEPATH += $$UTILS_PATH/.. +DEPENDPATH += $$UTILS_PATH/.. + +SOURCES += \ + tst_qtcprocess.cpp \ + $$UTILS_PATH/qtcprocess.cpp \ + $$UTILS_PATH/stringutils.cpp \ + $$UTILS_PATH/environment.cpp +HEADERS += \ + $$UTILS_PATH/qtcprocess.h \ + $$UTILS_PATH/environment.h \ + $$UTILS_PATH/stringutils.h \ + $$UTILS_PATH/utils_global.h + +TARGET = tst_$$TARGET diff --git a/tests/auto/qtcprocess/tst_qtcprocess.cpp b/tests/auto/qtcprocess/tst_qtcprocess.cpp new file mode 100644 index 00000000000..a6ef5529149 --- /dev/null +++ b/tests/auto/qtcprocess/tst_qtcprocess.cpp @@ -0,0 +1,698 @@ +/************************************************************************** +** +** This file is part of Qt Creator +** +** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation ([email protected]) +** +** Commercial Usage +** +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Nokia. +** +** GNU Lesser General Public License Usage +** +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at http://qt.nokia.com/contact. +** +**************************************************************************/ + +#include <utils/qtcprocess.h> +#include <utils/stringutils.h> +#include <utils/environment.h> + +#include <QtTest/QtTest> + +using namespace Utils; + +class MacroMapExpander : public AbstractQtcMacroExpander { +public: + virtual bool resolveMacro(const QString &name, QString *ret) + { + QHash<QString, QString>::const_iterator it = m_map.constFind(name); + if (it != m_map.constEnd()) { + *ret = it.value(); + return true; + } + return false; + } + void insert(const QString &key, const QString &value) { m_map.insert(key, value); } +private: + QHash<QString, QString> m_map; +}; + +class tst_QtcProcess : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void splitArgs_data(); + void splitArgs(); + void prepareArgs_data(); + void prepareArgs(); + void prepareArgsEnv_data(); + void prepareArgsEnv(); + void expandMacros_data(); + void expandMacros(); + void iterations_data(); + void iterations(); + void iteratorEdits(); + +private: + Environment env; + MacroMapExpander mx; +}; + +void tst_QtcProcess::initTestCase() +{ + env.set("empty", ""); + env.set("word", "hi"); + env.set("words", "hi ho"); + env.set("spacedwords", " hi ho sucker "); + +#ifdef Q_OS_WIN + mx.insert("a", "hi"); + mx.insert("aa", "hi ho"); + + mx.insert("b", "h\\i"); + mx.insert("c", "\\hi"); + mx.insert("d", "hi\\"); + mx.insert("ba", "h\\i ho"); + mx.insert("ca", "\\hi ho"); + mx.insert("da", "hi ho\\"); + + mx.insert("e", "h\"i"); + mx.insert("f", "\"hi"); + mx.insert("g", "hi\""); + + mx.insert("h", "h\\\"i"); + mx.insert("i", "\\\"hi"); + mx.insert("j", "hi\\\""); + + mx.insert("k", "&special;"); + + mx.insert("x", "\\"); + mx.insert("y", "\""); +#else + mx.insert("a", "hi"); + mx.insert("b", "hi ho"); + mx.insert("c", "&special;"); + mx.insert("d", "h\\i"); + mx.insert("e", "h\"i"); + mx.insert("f", "h'i"); +#endif + mx.insert("z", ""); +} + +Q_DECLARE_METATYPE(QtcProcess::SplitError) + +void tst_QtcProcess::splitArgs_data() +{ + QTest::addColumn<QString>("in"); + QTest::addColumn<QString>("out"); + QTest::addColumn<QtcProcess::SplitError>("err"); + + static const struct { + const char * const in; + const char * const out; + const QtcProcess::SplitError err; + } vals[] = { +#ifdef Q_OS_WIN + { "", "", QtcProcess::SplitOk }, + { " ", "", QtcProcess::SplitOk }, + { "hi", "hi", QtcProcess::SplitOk }, + { "hi ho", "hi ho", QtcProcess::SplitOk }, + { " hi ho ", "hi ho", QtcProcess::SplitOk }, + { "\"hi ho\" \"hi\" ho ", "\"hi ho\" hi ho", QtcProcess::SplitOk }, + { "\\", "\\", QtcProcess::SplitOk }, + { "\\\"", "\"\"\\^\"\"\"", QtcProcess::SplitOk }, + { "\"hi\"\"\"ho\"", "\"hi\"\\^\"\"ho\"", QtcProcess::SplitOk }, + { "\\\\\\\"", "\"\"\\\\\\^\"\"\"", QtcProcess::SplitOk }, + { " ^^ ", "\"^^\"", QtcProcess::SplitOk }, + { "hi\"", "", QtcProcess::BadQuoting }, + { "hi\"dood", "", QtcProcess::BadQuoting }, + { "%var%", "%var%", QtcProcess::SplitOk }, +#else + { "", "", QtcProcess::SplitOk }, + { " ", "", QtcProcess::SplitOk }, + { "hi", "hi", QtcProcess::SplitOk }, + { "hi ho", "hi ho", QtcProcess::SplitOk }, + { " hi ho ", "hi ho", QtcProcess::SplitOk }, + { "'hi ho' \"hi\" ho ", "'hi ho' hi ho", QtcProcess::SplitOk }, + { " \\ ", "' '", QtcProcess::SplitOk }, + { " \\\" ", "'\"'", QtcProcess::SplitOk }, + { " '\"' ", "'\"'", QtcProcess::SplitOk }, + { " \"\\\"\" ", "'\"'", QtcProcess::SplitOk }, + { "hi'", "", QtcProcess::BadQuoting }, + { "hi\"dood", "", QtcProcess::BadQuoting }, + { "$var", "'$var'", QtcProcess::SplitOk }, +#endif + }; + + for (unsigned i = 0; i < sizeof(vals)/sizeof(vals[0]); i++) + QTest::newRow(vals[i].in) << QString::fromLatin1(vals[i].in) + << QString::fromLatin1(vals[i].out) + << vals[i].err; +} + +void tst_QtcProcess::splitArgs() +{ + QFETCH(QString, in); + QFETCH(QString, out); + QFETCH(QtcProcess::SplitError, err); + + QtcProcess::SplitError outerr; + QString outstr = QtcProcess::joinArgs(QtcProcess::splitArgs(in, false, &outerr)); + QCOMPARE(outerr, err); + if (err == QtcProcess::SplitOk) + QCOMPARE(outstr, out); +} + +void tst_QtcProcess::prepareArgs_data() +{ + QTest::addColumn<QString>("in"); + QTest::addColumn<QString>("out"); + QTest::addColumn<QtcProcess::SplitError>("err"); + + static const struct { + const char * const in; + const char * const out; + const QtcProcess::SplitError err; + } vals[] = { +#ifdef Q_OS_WIN + { "", "", QtcProcess::SplitOk }, + { " ", " ", QtcProcess::SplitOk }, + { "hi", "hi", QtcProcess::SplitOk }, + { "hi ho", "hi ho", QtcProcess::SplitOk }, + { " hi ho ", " hi ho ", QtcProcess::SplitOk }, + { "\"hi ho\" \"hi\" ho ", "\"hi ho\" \"hi\" ho ", QtcProcess::SplitOk }, + { "\\", "\\", QtcProcess::SplitOk }, + { "\\\"", "\\\"", QtcProcess::SplitOk }, + { "\"hi\"\"ho\"", "\"hi\"\"ho\"", QtcProcess::SplitOk }, + { "\\\\\\\"", "\\\\\\\"", QtcProcess::SplitOk }, + { "^^", "^", QtcProcess::SplitOk }, + { "hi\"", "hi\"", QtcProcess::SplitOk }, + { "hi\"dood", "hi\"dood", QtcProcess::SplitOk }, + { "%var%", "", QtcProcess::FoundMeta }, + { "echo hi > file", "", QtcProcess::FoundMeta }, +#else + { "", "", QtcProcess::SplitOk }, + { " ", "", QtcProcess::SplitOk }, + { "hi", "hi", QtcProcess::SplitOk }, + { "hi ho", "hi ho", QtcProcess::SplitOk }, + { " hi ho ", "hi ho", QtcProcess::SplitOk }, + { "'hi ho' \"hi\" ho ", "'hi ho' hi ho", QtcProcess::SplitOk }, + { " \\ ", "' '", QtcProcess::SplitOk }, + { "hi'", "", QtcProcess::BadQuoting }, + { "hi\"dood", "", QtcProcess::BadQuoting }, + { "$var", "", QtcProcess::FoundMeta }, +#endif + }; + + for (unsigned i = 0; i < sizeof(vals)/sizeof(vals[0]); i++) + QTest::newRow(vals[i].in) << QString::fromLatin1(vals[i].in) + << QString::fromLatin1(vals[i].out) + << vals[i].err; +} + +void tst_QtcProcess::prepareArgs() +{ + QFETCH(QString, in); + QFETCH(QString, out); + QFETCH(QtcProcess::SplitError, err); + + QtcProcess::SplitError outerr; + QString outstr; +#ifdef Q_OS_WIN + outstr = QtcProcess::prepareArgs(in, &outerr); +#else + outstr = QtcProcess::joinArgs(QtcProcess::prepareArgs(in, &outerr)); +#endif + QCOMPARE(outerr, err); + if (err == QtcProcess::SplitOk) + QCOMPARE(outstr, out); +} + +void tst_QtcProcess::prepareArgsEnv_data() +{ + QTest::addColumn<QString>("in"); + QTest::addColumn<QString>("out"); + QTest::addColumn<QtcProcess::SplitError>("err"); + + static const struct { + const char * const in; + const char * const out; + const QtcProcess::SplitError err; + } vals[] = { +#ifdef Q_OS_WIN + { "", "", QtcProcess::SplitOk }, + { " ", " ", QtcProcess::SplitOk }, + { "hi", "hi", QtcProcess::SplitOk }, + { "hi ho", "hi ho", QtcProcess::SplitOk }, + { " hi ho ", " hi ho ", QtcProcess::SplitOk }, + { "\"hi ho\" \"hi\" ho ", "\"hi ho\" \"hi\" ho ", QtcProcess::SplitOk }, + { "\\", "\\", QtcProcess::SplitOk }, + { "\\\"", "\\\"", QtcProcess::SplitOk }, + { "\"hi\"\"ho\"", "\"hi\"\"ho\"", QtcProcess::SplitOk }, + { "\\\\\\\"", "\\\\\\\"", QtcProcess::SplitOk }, + { "^^", "^", QtcProcess::SplitOk }, + { "hi\"", "hi\"", QtcProcess::SplitOk }, + { "hi\"dood", "hi\"dood", QtcProcess::SplitOk }, + { "%empty%", "%empty%", QtcProcess::SplitOk }, // Yep, no empty variables on Windows. + { "%word%", "hi", QtcProcess::SplitOk }, + { " %word% ", " hi ", QtcProcess::SplitOk }, + { "%words%", "hi ho", QtcProcess::SplitOk }, + { "%nonsense%words%", "%nonsensehi ho", QtcProcess::SplitOk }, + { "fail%nonsense%words%", "fail%nonsensehi ho", QtcProcess::SplitOk }, + { "%words%words%", "hi howords%", QtcProcess::SplitOk }, + { "%words%%words%", "hi hohi ho", QtcProcess::SplitOk }, + { "echo hi > file", "", QtcProcess::FoundMeta }, +#else + { "", "", QtcProcess::SplitOk }, + { " ", "", QtcProcess::SplitOk }, + { "hi", "hi", QtcProcess::SplitOk }, + { "hi ho", "hi ho", QtcProcess::SplitOk }, + { " hi ho ", "hi ho", QtcProcess::SplitOk }, + { "'hi ho' \"hi\" ho ", "'hi ho' hi ho", QtcProcess::SplitOk }, + { " \\ ", "' '", QtcProcess::SplitOk }, + { "hi'", "", QtcProcess::BadQuoting }, + { "hi\"dood", "", QtcProcess::BadQuoting }, + { "$empty", "", QtcProcess::SplitOk }, + { "$word", "hi", QtcProcess::SplitOk }, + { " $word ", "hi", QtcProcess::SplitOk }, + { "${word}", "hi", QtcProcess::SplitOk }, + { " ${word} ", "hi", QtcProcess::SplitOk }, + { "$words", "hi ho", QtcProcess::SplitOk }, + { "$spacedwords", "hi ho sucker", QtcProcess::SplitOk }, + { "hi${empty}ho", "hiho", QtcProcess::SplitOk }, + { "hi${words}ho", "hihi hoho", QtcProcess::SplitOk }, + { "hi${spacedwords}ho", "hi hi ho sucker ho", QtcProcess::SplitOk }, + { "${", "", QtcProcess::BadQuoting }, + { "${var", "", QtcProcess::BadQuoting }, + { "${var ", "", QtcProcess::FoundMeta }, + { "\"hi${words}ho\"", "'hihi hoho'", QtcProcess::SplitOk }, + { "\"hi${spacedwords}ho\"", "'hi hi ho sucker ho'", QtcProcess::SplitOk }, + { "\"${", "", QtcProcess::BadQuoting }, + { "\"${var", "", QtcProcess::BadQuoting }, + { "\"${var ", "", QtcProcess::FoundMeta }, +#endif + }; + + for (unsigned i = 0; i < sizeof(vals)/sizeof(vals[0]); i++) + QTest::newRow(vals[i].in) << QString::fromLatin1(vals[i].in) + << QString::fromLatin1(vals[i].out) + << vals[i].err; +} + +void tst_QtcProcess::prepareArgsEnv() +{ + QFETCH(QString, in); + QFETCH(QString, out); + QFETCH(QtcProcess::SplitError, err); + + QtcProcess::SplitError outerr; + QString outstr; +#ifdef Q_OS_WIN + outstr = QtcProcess::prepareArgs(in, &outerr, &env); +#else + outstr = QtcProcess::joinArgs(QtcProcess::prepareArgs(in, &outerr, &env)); +#endif + QCOMPARE(outerr, err); + if (err == QtcProcess::SplitOk) + QCOMPARE(outstr, out); +} + +void tst_QtcProcess::expandMacros_data() + +{ + QTest::addColumn<QString>("in"); + QTest::addColumn<QString>("out"); + QChar sp(QLatin1Char(' ')); + + static const struct { + const char * const in; + const char * const out; + } vals[] = { +#ifdef Q_OS_WIN + { "plain", 0 }, + { "%{a}", "hi" }, + { "%{aa}", "\"hi ho\"" }, + { "%{b}", "h\\i" }, + { "%{c}", "\\hi" }, + { "%{d}", "hi\\" }, + { "%{ba}", "\"h\\i ho\"" }, + { "%{ca}", "\"\\hi ho\"" }, + { "%{da}", "\"hi ho\\\\\"" }, // or "\"hi ho\"\\" + { "%{e}", "\"h\"\\^\"\"i\"" }, + { "%{f}", "\"\"\\^\"\"hi\"" }, + { "%{g}", "\"hi\"\\^\"\"\"" }, + { "%{h}", "\"h\\\\\"\\^\"\"i\"" }, + { "%{i}", "\"\\\\\"\\^\"\"hi\"" }, + { "%{j}", "\"hi\\\\\"\\^\"\"\"" }, + { "%{k}", "\"&special;\"" }, + { "%{x}", "\\" }, + { "%{y}", "\"\"\\^\"\"\"" }, + { "%{z}", "\"\"" }, + { "^%{z}%{z}", "^%{z}%{z}" }, // stupid user check + + { "quoted", 0 }, + { "\"%{a}\"", "\"hi\"" }, + { "\"%{aa}\"", "\"hi ho\"" }, + { "\"%{b}\"", "\"h\\i\"" }, + { "\"%{c}\"", "\"\\hi\"" }, + { "\"%{d}\"", "\"hi\\\\\"" }, + { "\"%{ba}\"", "\"h\\i ho\"" }, + { "\"%{ca}\"", "\"\\hi ho\"" }, + { "\"%{da}\"", "\"hi ho\\\\\"" }, + { "\"%{e}\"", "\"h\"\\^\"\"i\"" }, + { "\"%{f}\"", "\"\"\\^\"\"hi\"" }, + { "\"%{g}\"", "\"hi\"\\^\"\"\"" }, + { "\"%{h}\"", "\"h\\\\\"\\^\"\"i\"" }, + { "\"%{i}\"", "\"\\\\\"\\^\"\"hi\"" }, + { "\"%{j}\"", "\"hi\\\\\"\\^\"\"\"" }, + { "\"%{k}\"", "\"&special;\"" }, + { "\"%{x}\"", "\"\\\\\"" }, + { "\"%{y}\"", "\"\"\\^\"\"\"" }, + { "\"%{z}\"", "\"\"" }, + + { "leading bs", 0 }, + { "\\%{a}", "\\hi" }, + { "\\%{aa}", "\\\\\"hi ho\"" }, + { "\\%{b}", "\\h\\i" }, + { "\\%{c}", "\\\\hi" }, + { "\\%{d}", "\\hi\\" }, + { "\\%{ba}", "\\\\\"h\\i ho\"" }, + { "\\%{ca}", "\\\\\"\\hi ho\"" }, + { "\\%{da}", "\\\\\"hi ho\\\\\"" }, + { "\\%{e}", "\\\\\"h\"\\^\"\"i\"" }, + { "\\%{f}", "\\\\\"\"\\^\"\"hi\"" }, + { "\\%{g}", "\\\\\"hi\"\\^\"\"\"" }, + { "\\%{h}", "\\\\\"h\\\\\"\\^\"\"i\"" }, + { "\\%{i}", "\\\\\"\\\\\"\\^\"\"hi\"" }, + { "\\%{j}", "\\\\\"hi\\\\\"\\^\"\"\"" }, + { "\\%{x}", "\\\\" }, + { "\\%{y}", "\\\\\"\"\\^\"\"\"" }, + { "\\%{z}", "\\" }, + + { "trailing bs", 0 }, + { "%{a}\\", "hi\\" }, + { "%{aa}\\", "\"hi ho\"\\" }, + { "%{b}\\", "h\\i\\" }, + { "%{c}\\", "\\hi\\" }, + { "%{d}\\", "hi\\\\" }, + { "%{ba}\\", "\"h\\i ho\"\\" }, + { "%{ca}\\", "\"\\hi ho\"\\" }, + { "%{da}\\", "\"hi ho\\\\\"\\" }, + { "%{e}\\", "\"h\"\\^\"\"i\"\\" }, + { "%{f}\\", "\"\"\\^\"\"hi\"\\" }, + { "%{g}\\", "\"hi\"\\^\"\"\"\\" }, + { "%{h}\\", "\"h\\\\\"\\^\"\"i\"\\" }, + { "%{i}\\", "\"\\\\\"\\^\"\"hi\"\\" }, + { "%{j}\\", "\"hi\\\\\"\\^\"\"\"\\" }, + { "%{x}\\", "\\\\" }, + { "%{y}\\", "\"\"\\^\"\"\"\\" }, + { "%{z}\\", "\\" }, + + { "bs-enclosed", 0 }, + { "\\%{a}\\", "\\hi\\" }, + { "\\%{aa}\\", "\\\\\"hi ho\"\\" }, + { "\\%{b}\\", "\\h\\i\\" }, + { "\\%{c}\\", "\\\\hi\\" }, + { "\\%{d}\\", "\\hi\\\\" }, + { "\\%{ba}\\", "\\\\\"h\\i ho\"\\" }, + { "\\%{ca}\\", "\\\\\"\\hi ho\"\\" }, + { "\\%{da}\\", "\\\\\"hi ho\\\\\"\\" }, + { "\\%{e}\\", "\\\\\"h\"\\^\"\"i\"\\" }, + { "\\%{f}\\", "\\\\\"\"\\^\"\"hi\"\\" }, + { "\\%{g}\\", "\\\\\"hi\"\\^\"\"\"\\" }, + { "\\%{h}\\", "\\\\\"h\\\\\"\\^\"\"i\"\\" }, + { "\\%{i}\\", "\\\\\"\\\\\"\\^\"\"hi\"\\" }, + { "\\%{j}\\", "\\\\\"hi\\\\\"\\^\"\"\"\\" }, + { "\\%{x}\\", "\\\\\\" }, + { "\\%{y}\\", "\\\\\"\"\\^\"\"\"\\" }, + { "\\%{z}\\", "\\\\" }, + + { "bs-enclosed and trailing literal quote", 0 }, + { "\\%{a}\\\\\\^\"", "\\hi\\\\\\^\"" }, + { "\\%{aa}\\\\\\^\"", "\\\\\"hi ho\"\\\\\\^\"" }, + { "\\%{b}\\\\\\^\"", "\\h\\i\\\\\\^\"" }, + { "\\%{c}\\\\\\^\"", "\\\\hi\\\\\\^\"" }, + { "\\%{d}\\\\\\^\"", "\\hi\\\\\\\\\\^\"" }, + { "\\%{ba}\\\\\\^\"", "\\\\\"h\\i ho\"\\\\\\^\"" }, + { "\\%{ca}\\\\\\^\"", "\\\\\"\\hi ho\"\\\\\\^\"" }, + { "\\%{da}\\\\\\^\"", "\\\\\"hi ho\\\\\"\\\\\\^\"" }, + { "\\%{e}\\\\\\^\"", "\\\\\"h\"\\^\"\"i\"\\\\\\^\"" }, + { "\\%{f}\\\\\\^\"", "\\\\\"\"\\^\"\"hi\"\\\\\\^\"" }, + { "\\%{g}\\\\\\^\"", "\\\\\"hi\"\\^\"\"\"\\\\\\^\"" }, + { "\\%{h}\\\\\\^\"", "\\\\\"h\\\\\"\\^\"\"i\"\\\\\\^\"" }, + { "\\%{i}\\\\\\^\"", "\\\\\"\\\\\"\\^\"\"hi\"\\\\\\^\"" }, + { "\\%{j}\\\\\\^\"", "\\\\\"hi\\\\\"\\^\"\"\"\\\\\\^\"" }, + { "\\%{x}\\\\\\^\"", "\\\\\\\\\\\\\\^\"" }, + { "\\%{y}\\\\\\^\"", "\\\\\"\"\\^\"\"\"\\\\\\^\"" }, + { "\\%{z}\\\\\\^\"", "\\\\\\\\\\^\"" }, + + { "bs-enclosed and trailing unclosed quote", 0 }, + { "\\%{a}\\\\\"", "\\hi\\\\\"" }, + { "\\%{aa}\\\\\"", "\\\\\"hi ho\"\\\\\"" }, + { "\\%{b}\\\\\"", "\\h\\i\\\\\"" }, + { "\\%{c}\\\\\"", "\\\\hi\\\\\"" }, + { "\\%{d}\\\\\"", "\\hi\\\\\\\\\"" }, + { "\\%{ba}\\\\\"", "\\\\\"h\\i ho\"\\\\\"" }, + { "\\%{ca}\\\\\"", "\\\\\"\\hi ho\"\\\\\"" }, + { "\\%{da}\\\\\"", "\\\\\"hi ho\\\\\"\\\\\"" }, + { "\\%{e}\\\\\"", "\\\\\"h\"\\^\"\"i\"\\\\\"" }, + { "\\%{f}\\\\\"", "\\\\\"\"\\^\"\"hi\"\\\\\"" }, + { "\\%{g}\\\\\"", "\\\\\"hi\"\\^\"\"\"\\\\\"" }, + { "\\%{h}\\\\\"", "\\\\\"h\\\\\"\\^\"\"i\"\\\\\"" }, + { "\\%{i}\\\\\"", "\\\\\"\\\\\"\\^\"\"hi\"\\\\\"" }, + { "\\%{j}\\\\\"", "\\\\\"hi\\\\\"\\^\"\"\"\\\\\"" }, + { "\\%{x}\\\\\"", "\\\\\\\\\\\\\"" }, + { "\\%{y}\\\\\"", "\\\\\"\"\\^\"\"\"\\\\\"" }, + { "\\%{z}\\\\\"", "\\\\\\\\\"" }, + + { "multi-var", 0 }, + { "%{x}%{y}%{z}", "\\\\\"\"\\^\"\"\"" }, + { "%{x}%{z}%{y}%{z}", "\\\\\"\"\\^\"\"\"" }, + { "%{x}%{z}%{y}", "\\\\\"\"\\^\"\"\"" }, + { "%{x}\\^\"%{z}", "\\\\\\^\"" }, + { "%{x}%{z}\\^\"%{z}", "\\\\\\^\"" }, + { "%{x}%{z}\\^\"", "\\\\\\^\"" }, + { "%{x}\\%{z}", "\\\\" }, + { "%{x}%{z}\\%{z}", "\\\\" }, + { "%{x}%{z}\\", "\\\\" }, + { "%{aa}%{a}", "\"hi hohi\"" }, + { "%{aa}%{aa}", "\"hi hohi ho\"" }, + { "%{aa}:%{aa}", "\"hi ho\":\"hi ho\"" }, + { "hallo ^|%{aa}^|", "hallo ^|\"hi ho\"^|" }, + + { "quoted multi-var", 0 }, + { "\"%{x}%{y}%{z}\"", "\"\\\\\"\\^\"\"\"" }, + { "\"%{x}%{z}%{y}%{z}\"", "\"\\\\\"\\^\"\"\"" }, + { "\"%{x}%{z}%{y}\"", "\"\\\\\"\\^\"\"\"" }, + { "\"%{x}\"^\"\"%{z}\"", "\"\\\\\"^\"\"\"" }, + { "\"%{x}%{z}\"^\"\"%{z}\"", "\"\\\\\"^\"\"\"" }, + { "\"%{x}%{z}\"^\"\"\"", "\"\\\\\"^\"\"\"" }, + { "\"%{x}\\%{z}\"", "\"\\\\\\\\\"" }, + { "\"%{x}%{z}\\%{z}\"", "\"\\\\\\\\\"" }, + { "\"%{x}%{z}\\\\\"", "\"\\\\\\\\\"" }, + { "\"%{aa}%{a}\"", "\"hi hohi\"" }, + { "\"%{aa}%{aa}\"", "\"hi hohi ho\"" }, + { "\"%{aa}:%{aa}\"", "\"hi ho:hi ho\"" }, +#else + { "plain", 0 }, + { "%{a}", "hi" }, + { "%{b}", "'hi ho'" }, + { "%{c}", "'&special;'" }, + { "%{d}", "'h\\i'" }, + { "%{e}", "'h\"i'" }, + { "%{f}", "'h'\\''i'" }, + { "%{z}", "''" }, + { "\\%{z}%{z}", "\\%{z}%{z}" }, // stupid user check + + { "single-quoted", 0 }, + { "'%{a}'", "'hi'" }, + { "'%{b}'", "'hi ho'" }, + { "'%{c}'", "'&special;'" }, + { "'%{d}'", "'h\\i'" }, + { "'%{e}'", "'h\"i'" }, + { "'%{f}'", "'h'\\''i'" }, + { "'%{z}'", "''" }, + + { "double-quoted", 0 }, + { "\"%{a}\"", "\"hi\"" }, + { "\"%{b}\"", "\"hi ho\"" }, + { "\"%{c}\"", "\"&special;\"" }, + { "\"%{d}\"", "\"h\\\\i\"" }, + { "\"%{e}\"", "\"h\\\"i\"" }, + { "\"%{f}\"", "\"h'i\"" }, + { "\"%{z}\"", "\"\"" }, + + { "complex", 0 }, + { "echo \"$(echo %{a})\"", "echo \"$(echo hi)\"" }, + { "echo \"$(echo %{b})\"", "echo \"$(echo 'hi ho')\"" }, + { "echo \"$(echo \"%{a}\")\"", "echo \"$(echo \"hi\")\"" }, + // These make no sense shell-wise, but they test expando nesting + { "echo \"%{echo %{a}}\"", "echo \"%{echo hi}\"" }, + { "echo \"%{echo %{b}}\"", "echo \"%{echo hi ho}\"" }, + { "echo \"%{echo \"%{a}\"}\"", "echo \"%{echo \"hi\"}\"" }, +#endif + }; + + const char *title = 0; + for (unsigned i = 0; i < sizeof(vals)/sizeof(vals[0]); i++) { + if (!vals[i].out) { + title = vals[i].in; + } else { + char buf[80]; + sprintf(buf, "%s: %s", title, vals[i].in); + QTest::newRow(buf) << QString::fromLatin1(vals[i].in) + << QString::fromLatin1(vals[i].out); + sprintf(buf, "padded %s: %s", title, vals[i].in); + QTest::newRow(buf) << (sp + QString::fromLatin1(vals[i].in) + sp) + << (sp + QString::fromLatin1(vals[i].out) + sp); + } + } +} + +void tst_QtcProcess::expandMacros() +{ + QFETCH(QString, in); + QFETCH(QString, out); + + QtcProcess::expandMacros(&in, &mx); + QCOMPARE(in, out); +} + +void tst_QtcProcess::iterations_data() +{ + QTest::addColumn<QString>("in"); + QTest::addColumn<QString>("out"); + + static const struct { + const char * const in; + const char * const out; + } vals[] = { +#ifdef Q_OS_WIN + { "", "" }, + { "hi", "hi" }, + { " hi ", "hi" }, + { "hi ho", "hi ho" }, + { "\"hi ho\" sucker", "\"hi ho\" sucker" }, + { "\"hi\"^\"\"ho\" sucker", "\"hi\"\\^\"\"ho\" sucker" }, + { "\"hi\"\\^\"\"ho\" sucker", "\"hi\"\\^\"\"ho\" sucker" }, + { "hi^|ho", "\"hi|ho\"" }, + { "c:\\", "c:\\" }, + { "\"c:\\\\\"", "c:\\" }, + { "\\hi\\ho", "\\hi\\ho" }, + { "hi null%", "hi null%" }, + { "hi null% ho", "hi null% ho" }, + { "hi null%here ho", "hi null%here ho" }, + { "hi null%here%too ho", "hi {} ho" }, + { "echo hello | more", "echo hello" }, + { "echo hello| more", "echo hello" }, +#else + { "", "" }, + { " ", "" }, + { "hi", "hi" }, + { " hi ", "hi" }, + { "'hi'", "hi" }, + { "hi ho", "hi ho" }, + { "\"hi ho\" sucker", "'hi ho' sucker" }, + { "\"hi\\\"ho\" sucker", "'hi\"ho' sucker" }, + { "\"hi'ho\" sucker", "'hi'\\''ho' sucker" }, + { "'hi ho' sucker", "'hi ho' sucker" }, + { "\\\\", "'\\'" }, + { "'\\'", "'\\'" }, + { "hi 'null${here}too' ho", "hi 'null${here}too' ho" }, + { "hi null${here}too ho", "hi {} ho" }, + { "hi $(echo $dollar cent) ho", "hi {} ho" }, + { "hi `echo $dollar \\`echo cent\\` | cat` ho", "hi {} ho" }, + { "echo hello | more", "echo hello" }, + { "echo hello| more", "echo hello" }, +#endif + }; + + for (unsigned i = 0; i < sizeof(vals)/sizeof(vals[0]); i++) + QTest::newRow(vals[i].in) << QString::fromLatin1(vals[i].in) + << QString::fromLatin1(vals[i].out); +} + +void tst_QtcProcess::iterations() +{ + QFETCH(QString, in); + QFETCH(QString, out); + + QString outstr; + for (QtcProcess::ArgIterator ait(&in); ait.next(); ) + if (ait.isSimple()) + QtcProcess::addArg(&outstr, ait.value()); + else + QtcProcess::addArgs(&outstr, "{}"); + QCOMPARE(outstr, out); +} + +void tst_QtcProcess::iteratorEdits() +{ + QString in1 = "one two three", in2 = in1, in3 = in1, in4 = in1, in5 = in1; + + QtcProcess::ArgIterator ait1(&in1); + QVERIFY(ait1.next()); + ait1.deleteArg(); + QVERIFY(ait1.next()); + QVERIFY(ait1.next()); + QVERIFY(!ait1.next()); + QCOMPARE(in1, QString::fromLatin1("two three")); + ait1.appendArg("four"); + QCOMPARE(in1, QString::fromLatin1("two three four")); + + QtcProcess::ArgIterator ait2(&in2); + QVERIFY(ait2.next()); + QVERIFY(ait2.next()); + ait2.deleteArg(); + QVERIFY(ait2.next()); + ait2.appendArg("four"); + QVERIFY(!ait2.next()); + QCOMPARE(in2, QString::fromLatin1("one three four")); + + QtcProcess::ArgIterator ait3(&in3); + QVERIFY(ait3.next()); + ait3.appendArg("one-b"); + QVERIFY(ait3.next()); + QVERIFY(ait3.next()); + ait3.deleteArg(); + QVERIFY(!ait3.next()); + QCOMPARE(in3, QString::fromLatin1("one one-b two")); + + QtcProcess::ArgIterator ait4(&in4); + ait4.appendArg("pre-one"); + QVERIFY(ait4.next()); + QVERIFY(ait4.next()); + QVERIFY(ait4.next()); + ait4.deleteArg(); + QVERIFY(!ait4.next()); + QCOMPARE(in4, QString::fromLatin1("pre-one one two")); + + QtcProcess::ArgIterator ait5(&in5); + QVERIFY(ait5.next()); + QVERIFY(ait5.next()); + QVERIFY(ait5.next()); + QVERIFY(!ait5.next()); + ait5.deleteArg(); + QVERIFY(!ait5.next()); + QCOMPARE(in5, QString::fromLatin1("one two")); +} + +QTEST_MAIN(tst_QtcProcess) + +#include "tst_qtcprocess.moc" |