diff --git a/autotests/kshelltest.cpp b/autotests/kshelltest.cpp index 54834ea..88cf8db 100644 --- a/autotests/kshelltest.cpp +++ b/autotests/kshelltest.cpp @@ -1,253 +1,262 @@ /* This file is part of the KDE libraries Copyright (c) 2003,2007-2008 Oswald Buddenhagen Copyright (c) 2005 Thomas Braxton This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include class KShellTest : public QObject { Q_OBJECT private Q_SLOTS: void tildeExpand(); + void tildeCollapse(); void quoteArg(); void joinArgs(); void splitJoin(); void quoteSplit(); void quoteSplit_data(); void abortOnMeta(); }; // The expansion of ~me isn't exactly QDir::homePath(), in case $HOME has a trailing slash, it's kept. static QString myHomePath() { #ifdef Q_OS_WIN return QDir::homePath(); #else return QString::fromLocal8Bit(qgetenv("HOME")); #endif } void KShellTest::tildeExpand() { QString me(KUser().loginName()); QCOMPARE(KShell::tildeExpand(QStringLiteral("~")), QDir::homePath()); QCOMPARE(KShell::tildeExpand(QStringLiteral("~/dir")), QString(QDir::homePath() + QStringLiteral("/dir"))); QCOMPARE(KShell::tildeExpand(QLatin1Char('~') + me), myHomePath()); QCOMPARE(KShell::tildeExpand(QLatin1Char('~') + me + QStringLiteral("/dir")), QString(myHomePath() + QStringLiteral("/dir"))); #ifdef Q_OS_WIN QCOMPARE(KShell::tildeExpand(QStringLiteral("^~") + me), QString(QLatin1Char('~') + me)); #else QCOMPARE(KShell::tildeExpand(QStringLiteral("\\~") + me), QString(QStringLiteral("~") + me)); #endif } +void +KShellTest::tildeCollapse() +{ + QCOMPARE(KShell::tildeCollapse(QDir::homePath()), QStringLiteral("~")); + QCOMPARE(KShell::tildeCollapse(QDir::homePath() + QStringLiteral("/Documents")), QStringLiteral("~/Documents")); + QCOMPARE(KShell::tildeCollapse(QStringLiteral("/test/") + QDir::homePath()), QStringLiteral("/test/") + QDir::homePath()); +} + void KShellTest::quoteArg() { #ifdef Q_OS_WIN QCOMPARE(KShell::quoteArg(QStringLiteral("a space")), QStringLiteral("\"a space\"")); QCOMPARE(KShell::quoteArg(QStringLiteral("fds\\\"")), QStringLiteral("fds\\\\\\^\"")); QCOMPARE(KShell::quoteArg(QStringLiteral("\\\\foo")), QStringLiteral("\\\\foo")); QCOMPARE(KShell::quoteArg(QStringLiteral("\"asdf\"")), QStringLiteral("\\^\"asdf\\^\"")); QCOMPARE(KShell::quoteArg(QStringLiteral("with\\")), QStringLiteral("\"with\\\\\"")); QCOMPARE(KShell::quoteArg(QStringLiteral("\\\\")), QStringLiteral("\"\\\\\\\\\"")); QCOMPARE(KShell::quoteArg(QStringLiteral("\"a space\\\"")), QStringLiteral("\\^\"\"a space\"\\\\\\^\"")); QCOMPARE(KShell::quoteArg(QStringLiteral("as df\\")), QStringLiteral("\"as df\\\\\"")); QCOMPARE(KShell::quoteArg(QStringLiteral("foo bar\"\\\"bla")), QStringLiteral("\"foo bar\"\\^\"\\\\\\^\"\"bla\"")); QCOMPARE(KShell::quoteArg(QStringLiteral("a % space")), QStringLiteral("\"a %PERCENT_SIGN% space\"")); #else QCOMPARE(KShell::quoteArg(QStringLiteral("a space")), QStringLiteral("'a space'")); #endif } void KShellTest::joinArgs() { QStringList list; list << QStringLiteral("this") << QStringLiteral("is") << QStringLiteral("a") << QStringLiteral("test"); QCOMPARE(KShell::joinArgs(list), QStringLiteral("this is a test")); } static QString sj(const QString &str, KShell::Options flags, KShell::Errors *ret) { return KShell::joinArgs(KShell::splitArgs(str, flags, ret)); } void KShellTest::splitJoin() { KShell::Errors err = KShell::NoError; #ifdef Q_OS_WIN QCOMPARE(sj(QStringLiteral("\"(sulli)\" text"), KShell::NoOptions, &err), QStringLiteral("\"(sulli)\" text")); QVERIFY(err == KShell::NoError); QCOMPARE(sj(QStringLiteral(" ha\\ lo "), KShell::NoOptions, &err), QStringLiteral("\"ha\\\\\" lo")); QVERIFY(err == KShell::NoError); QCOMPARE(sj(QStringLiteral("say \" error"), KShell::NoOptions, &err), QString()); QVERIFY(err == KShell::BadQuoting); QCOMPARE(sj(QStringLiteral("no \" error\""), KShell::NoOptions, &err), QStringLiteral("no \" error\"")); QVERIFY(err == KShell::NoError); QCOMPARE(sj(QStringLiteral("say \" still error"), KShell::NoOptions, &err), QString()); QVERIFY(err == KShell::BadQuoting); QCOMPARE(sj(QStringLiteral("BLA;asdf sdfess d"), KShell::NoOptions, &err), QStringLiteral("\"BLA;asdf\" sdfess d")); QVERIFY(err == KShell::NoError); QCOMPARE(sj(QStringLiteral("B\"L\"A&sdf FOO|bar sdf wer "), KShell::NoOptions, &err), QStringLiteral("\"BLA&sdf\" \"FOO|bar\" sdf wer")); QVERIFY(err == KShell::NoError); QCOMPARE(sj(QStringLiteral("\"\"\"just \"\" fine\"\"\""), KShell::NoOptions, &err), QStringLiteral("\\^\"\"just \"\\^\"\" fine\"\\^\"")); QVERIFY(err == KShell::NoError); #else QCOMPARE(sj(QString::fromUtf8("\"~qU4rK\" 'text' 'jo'\"jo\" $'crap' $'\\\\\\'\\e\\x21' ha\\ lo \\a"), KShell::NoOptions, &err), QString::fromUtf8("'~qU4rK' text jojo crap '\\'\\''\x1b!' 'ha lo' a")); QVERIFY(err == KShell::NoError); QCOMPARE(sj(QStringLiteral("\"~qU4rK\" 'text'"), KShell::TildeExpand, &err), QStringLiteral("'~qU4rK' text")); QVERIFY(err == KShell::NoError); QCOMPARE(sj(QStringLiteral("~\"qU4rK\" 'text'"), KShell::TildeExpand, &err), QStringLiteral("'~qU4rK' text")); QVERIFY(err == KShell::NoError); QCOMPARE(sj(QStringLiteral("~/\"dir\" 'text'"), KShell::TildeExpand, &err), QString(QDir::homePath() + QStringLiteral("/dir text"))); QVERIFY(err == KShell::NoError); QCOMPARE(sj(QStringLiteral("~ 'text' ~"), KShell::TildeExpand, &err), QString(QDir::homePath() + QStringLiteral(" text ") + QDir::homePath())); QVERIFY(err == KShell::NoError); QCOMPARE(sj(QStringLiteral("\\~ blah"), KShell::TildeExpand, &err), QStringLiteral("'~' blah")); QVERIFY(err == KShell::NoError); QCOMPARE(sj(QStringLiteral("~qU4rK ~") + KUser().loginName(), KShell::TildeExpand, &err), QString(QStringLiteral("'~qU4rK' ") + myHomePath())); QVERIFY(err == KShell::NoError); const QString unicodeSpaceFileName = QStringLiteral("test テスト.txt"); // #345140 QCOMPARE(sj(unicodeSpaceFileName, KShell::AbortOnMeta | KShell::TildeExpand, &err), unicodeSpaceFileName); QVERIFY(err == KShell::NoError); #endif } void KShellTest::quoteSplit_data() { QTest::addColumn("string"); QTest::newRow("no space") << QStringLiteral("hiho"); QTest::newRow("regular space") << QStringLiteral("hi there"); QTest::newRow("special space") << QString::fromUtf8("如何定期清潔典型的電風扇 講義.pdf"); } void KShellTest::quoteSplit() { QFETCH(QString, string); // Splitting a quote arg should always just return one argument const QStringList args = KShell::splitArgs(KShell::quoteArg(string)); QCOMPARE(args.count(), 1); } void KShellTest::abortOnMeta() { KShell::Errors err1 = KShell::NoError, err2 = KShell::NoError; QCOMPARE(sj(QStringLiteral("text"), KShell::AbortOnMeta, &err1), QStringLiteral("text")); QVERIFY(err1 == KShell::NoError); #ifdef Q_OS_WIN QVERIFY(KShell::splitArgs(QStringLiteral("BLA & asdf sdfess d"), KShell::AbortOnMeta, &err1).isEmpty()); QVERIFY(err1 == KShell::FoundMeta); QVERIFY(KShell::splitArgs(QStringLiteral("foo %PATH% bar"), KShell::AbortOnMeta, &err1).isEmpty()); QVERIFY(err1 == KShell::FoundMeta); QCOMPARE(sj(QStringLiteral("foo %PERCENT_SIGN% bar"), KShell::AbortOnMeta, &err1), QStringLiteral("foo %PERCENT_SIGN% bar")); QVERIFY(err1 == KShell::NoError); QCOMPARE(sj(QStringLiteral("@foo ^& bar"), KShell::AbortOnMeta, &err1), QStringLiteral("foo \"&\" bar")); QVERIFY(err1 == KShell::NoError); QCOMPARE(sj(QStringLiteral("\"BLA|asdf\" sdfess d"), KShell::AbortOnMeta, &err1), QStringLiteral("\"BLA|asdf\" sdfess d")); QVERIFY(err1 == KShell::NoError); QCOMPARE(sj(QStringLiteral("B\"L\"A\"|\"sdf \"FOO | bar\" sdf wer"), KShell::AbortOnMeta, &err1), QStringLiteral("\"BLA|sdf\" \"FOO | bar\" sdf wer")); QVERIFY(err1 == KShell::NoError); QCOMPARE(sj(QStringLiteral("b-q me \\\\^|\\\\\\^\""), KShell::AbortOnMeta, &err1), QStringLiteral("b-q me \"\\\\|\"\\\\\\^\"")); QVERIFY(err1 == KShell::NoError); #else QCOMPARE(sj(QStringLiteral("say \" error"), KShell::NoOptions, &err1), QString()); QVERIFY(err1 != KShell::NoError); QCOMPARE(sj(QStringLiteral("say \" still error"), KShell::AbortOnMeta, &err1), QString()); QVERIFY(err1 != KShell::NoError); QVERIFY(sj(QStringLiteral("say `echo no error`"), KShell::NoOptions, &err1) != sj(QStringLiteral("say `echo no error`"), KShell::AbortOnMeta, &err2)); QVERIFY(err1 != err2); QVERIFY(sj(QStringLiteral("BLA=say echo meta"), KShell::NoOptions, &err1) != sj(QStringLiteral("BLA=say echo meta"), KShell::AbortOnMeta, &err2)); QVERIFY(err1 != err2); QVERIFY(sj(QStringLiteral("B\"L\"A=say FOO=bar echo meta"), KShell::NoOptions, &err1) == sj(QStringLiteral("B\"L\"A=say FOO=bar echo meta"), KShell::AbortOnMeta, &err2)); #endif } QTEST_MAIN(KShellTest) #include "kshelltest.moc" diff --git a/src/lib/util/kshell.cpp b/src/lib/util/kshell.cpp index c30d711..751990a 100644 --- a/src/lib/util/kshell.cpp +++ b/src/lib/util/kshell.cpp @@ -1,70 +1,83 @@ /* This file is part of the KDE libraries Copyright (c) 2003,2007 Oswald Buddenhagen This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kshell.h" #include "kshell_p.h" #include "kuser.h" #include QString KShell::homeDir(const QString &user) { if (user.isEmpty()) { return QDir::homePath(); } return KUser(user).homeDir(); } QString KShell::joinArgs(const QStringList &args) { QString ret; for (QStringList::ConstIterator it = args.begin(); it != args.end(); ++it) { if (!ret.isEmpty()) { ret.append(QLatin1Char(' ')); } ret.append(quoteArg(*it)); } return ret; } #ifdef Q_OS_WIN # define ESCAPE '^' #else # define ESCAPE '\\' #endif QString KShell::tildeExpand(const QString &fname) { if (!fname.isEmpty() && fname[0] == QLatin1Char('~')) { int pos = fname.indexOf(QLatin1Char('/')); if (pos < 0) { return homeDir(fname.mid(1)); } QString ret = homeDir(fname.mid(1, pos - 1)); if (!ret.isNull()) { ret += fname.midRef(pos); } return ret; } else if (fname.length() > 1 && fname[0] == QLatin1Char(ESCAPE) && fname[1] == QLatin1Char('~')) { return fname.mid(1); } return fname; } + +QString KShell::tildeCollapse(const QString &path) +{ + if (!path.isEmpty()) { + const auto homePath = QDir::homePath(); + if (path.startsWith(homePath)) { + auto newPath = path; + newPath.replace(0, homePath.length(), QLatin1Char('~')); + return newPath; + } + } + return path; +} diff --git a/src/lib/util/kshell.h b/src/lib/util/kshell.h index c48f5ab..0aa36ad 100644 --- a/src/lib/util/kshell.h +++ b/src/lib/util/kshell.h @@ -1,199 +1,209 @@ /* This file is part of the KDE libraries Copyright (c) 2003,2007 Oswald Buddenhagen This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KSHELL_H #define KSHELL_H #include #include class QStringList; class QString; /** * \namespace KShell * Emulates some basic system shell functionality. * @see KStringHandler */ namespace KShell { /** * Flags for splitArgs(). */ enum Option { NoOptions = 0, /** * Perform tilde expansion. * On Windows, this flag is ignored, as the Windows shell has no * equivalent functionality. */ TildeExpand = 1, /** * Put the parser into full shell mode and bail out if a too complex * construct is encountered. * A particular purpose of this flag is finding out whether the * command line being split would be executable directly (via * KProcess::setProgram()) or whether it needs to be run through * a real shell (via KProcess::setShellCommand()). Note, however, * that shell builtins are @em not recognized - you need to do that * yourself (compare with a list of known commands or verify that an * executable exists for the named command). * * Meta characters that cause a bail-out are the command separators * @c semicolon and @c ampersand, the redirection symbols @c less-than, * @c greater-than and the @c pipe @c symbol and the grouping symbols * opening and closing @c parentheses. * * Further meta characters on *NIX are the grouping symbols * opening and closing @c braces, the command substitution symbol * @c backquote, the generic substitution symbol @c dollar (if * not followed by an apostrophe), the wildcards @c asterisk, * @c question @c mark and opening and closing @c square @c brackets * and the comment symbol @c hash @c mark. * Additionally, a variable assignment in the first word is recognized. * * A further meta character on Windows is the environment variable * expansion symbol @c percent. Occurrences of @c \%PERCENT_SIGN% as * inserted by quoteArg() are converted back and cause no bail-out, * though. */ AbortOnMeta = 2 }; Q_DECLARE_FLAGS(Options, Option) /** * Status codes from splitArgs() */ enum Errors { /** * Success. */ NoError = 0, /** * Indicates a parsing error, like an unterminated quoted string. */ BadQuoting, /** * The AbortOnMeta flag was set and an unhandled shell meta character * was encountered. */ FoundMeta }; /** * Splits @p cmd according to system shell word splitting and quoting rules. * Can optionally perform tilde expansion and/or abort if it finds shell * meta characters it cannot process. * * On *NIX the behavior is based on the POSIX shell and bash: * - Whitespace splits tokens * - The backslash quotes the following character * - A string enclosed in single quotes is not split. No shell meta * characters are interpreted. * - 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. * - A string enclosed in $'' is not split. Within the string, the * backslash has a similar meaning to the one in C strings. Consult * the bash manual for more information. * * On Windows, the behavior is defined by the Microsoft C runtime. Qt and * many other implementations comply with this standard, but many do not. * - Whitespace splits tokens * - A string enclosed in double quotes is not split * - 2N double quotes within a quoted string yield N literal quotes. * This is not documented on MSDN. * - Backslashes have special semantics iff they are followed by a double * quote: * - 2N backslashes + double quote => N backslashes and begin/end quoting * - 2N+1 backslashes + double quote => N backslashes + literal quote * * If AbortOnMeta is used on Windows, this function applies cmd shell * semantics before proceeding with word splitting: * - 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. * - The @c circumflex is the escape char for everything including * itself. * * @param cmd the command to split * @param flags operation flags, see \ref Option * @param err if not NULL, a status code will be stored at the pointer * target, see \ref Errors * @return a list of unquoted words or an empty list if an error occurred */ KCOREADDONS_EXPORT QStringList splitArgs(const QString &cmd, Options flags = NoOptions, Errors *err = nullptr); /** * Quotes and joins @p args together according to system shell rules. * * If the output is fed back into splitArgs(), the AbortOnMeta flag * needs to be used on Windows. On *NIX, no such requirement exists. * * See quoteArg() for more info. * * @param args a list of strings to quote and join * @return a command suitable for shell execution */ KCOREADDONS_EXPORT QString joinArgs(const QStringList &args); /** * Quotes @p arg according to system shell rules. * * This function can be used to quote an argument string such that * the shell processes it properly. This is e.g. necessary for * user-provided file names which may contain spaces or quotes. * It also prevents expansion of wild cards and environment variables. * * On *NIX, the output is POSIX shell compliant. * On Windows, it is compliant with the argument splitting code of the * Microsoft C runtime and the cmd shell used together. * Occurrences of the @c percent @c sign are replaced with * @c \%PERCENT_SIGN% to prevent spurious variable expansion; * related KDE functions are prepared for this. * * @param arg the argument to quote * @return the quoted argument */ KCOREADDONS_EXPORT QString quoteArg(const QString &arg); /** * Performs tilde expansion on @p path. Interprets "~/path" and * "~user/path". If the path starts with an escaped tilde ("\~" on UNIX, * "^~" on Windows), the escape char is removed and the path is returned * as is. * * Note that if @p path starts with a tilde but cannot be properly expanded, * this function will return an empty string. * * @param path the path to tilde-expand * @return the expanded path */ KCOREADDONS_EXPORT QString tildeExpand(const QString &path); + +/** + * Performs tilde collapse on @p path. If path did not start by the user + * homedir returns path unchanged. + * + * @param path the path to tilde-collpase + * @return the collapsed path + * @since 5.67 + */ +KCOREADDONS_EXPORT QString tildeCollapse(const QString &path); } Q_DECLARE_OPERATORS_FOR_FLAGS(KShell::Options) #endif /* KSHELL_H */