diff --git a/.ci/build.sh b/.ci/build.sh index bd44971c..d6ae7b0c 100644 --- a/.ci/build.sh +++ b/.ci/build.sh @@ -7,3 +7,4 @@ pacman -S --noconfirm --needed git cmake lxqt-build-tools-git qt5-tools qtermwid cmake -B build -S . make -C build +ARGS="-V" make -C build test diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f6fd520..0c85875a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,7 @@ include(GNUInstallDirs) set(QTERMINAL_VERSION "1.1.0") option(UPDATE_TRANSLATIONS "Update source translation translations/*.ts files" OFF) +option(BUILD_TESTS "Builds tests" ON) if(APPLE) option(APPLEBUNDLE "Build as qterminal.app bundle" ON) @@ -36,6 +37,10 @@ endif() find_package(QTermWidget5 ${QTERMWIDGET_MINIMUM_VERSION} REQUIRED) find_package(lxqt-build-tools ${LXQTBT_MINIMUM_VERSION} REQUIRED) +if (BUILD_TESTS) + find_package(Qt5 ${QT_MINIMUM_VERSION} CONFIG REQUIRED Test) +endif() + include(LXQtPreventInSourceBuilds) include(LXQtTranslateTs) include(LXQtTranslateDesktop) @@ -75,6 +80,7 @@ set(QTERM_SRC src/fontdialog.cpp src/dbusaddressable.cpp src/tab-switcher.cpp + src/qterminalutils.cpp ) set(QTERM_MOC_SRC @@ -265,3 +271,8 @@ else() COMMAND "${CMAKE_COMMAND}" -E touch "${CMAKE_CURRENT_BINARY_DIR}/${EXE_NAME}.app/Contents/Resources/empty.lproj" COMMENT "Creating Resources/empty.lproj") endif() + +if(BUILD_TESTS) + enable_testing() + add_subdirectory(test) +endif() diff --git a/src/main.cpp b/src/main.cpp index 581be135..23ce9173 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -33,6 +33,7 @@ #include "mainwindow.h" #include "qterminalapp.h" +#include "qterminalutils.h" #include "terminalconfig.h" #define out @@ -72,7 +73,7 @@ QTerminalApp * QTerminalApp::m_instance = nullptr; exit(code); } -void parse_args(int argc, char* argv[], QString& workdir, QString & shell_command, out bool& dropMode) +void parse_args(int argc, char* argv[], QString& workdir, QStringList & shell_command, out bool& dropMode) { int next_option; dropMode = false; @@ -87,13 +88,13 @@ void parse_args(int argc, char* argv[], QString& workdir, QString & shell_comman workdir = QString::fromLocal8Bit(optarg); break; case 'e': - shell_command = QString::fromLocal8Bit(optarg); + shell_command << parse_command(QString::fromLocal8Bit(optarg)); // #15 "Raw" -e params // Passing "raw" params (like konsole -e mcedit /tmp/tmp.txt") is more preferable - then I can call QString("qterminal -e ") + cmd_line in other programs while (optind < argc) { //printf("arg: %d - %s\n", optind, argv[optind]); - shell_command += QLatin1Char(' ') + QString::fromLocal8Bit(argv[optind++]); + shell_command << QString::fromLocal8Bit(argv[optind++]); } break; case 'd': @@ -141,7 +142,8 @@ int main(int argc, char *argv[]) app->registerOnDbus(); #endif - QString workdir, shell_command; + QString workdir; + QStringList shell_command; bool dropMode; parse_args(argc, argv, workdir, shell_command, dropMode); diff --git a/src/properties.h b/src/properties.h index 7e600335..ff97df32 100644 --- a/src/properties.h +++ b/src/properties.h @@ -50,7 +50,7 @@ class Properties QPoint mainWindowPosition; QByteArray mainWindowState; //ShortcutMap shortcuts; - QString shell; + QStringList shell; QFont font; QString colorScheme; QString guiStyle; diff --git a/src/qterminalutils.cpp b/src/qterminalutils.cpp new file mode 100644 index 00000000..62ce95e2 --- /dev/null +++ b/src/qterminalutils.cpp @@ -0,0 +1,64 @@ +/*************************************************************************** + * Copyright (C) 2022 by LXQt team * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + ***************************************************************************/ + +#include + +#include "qterminalutils.h" + +QStringList parse_command(const QString& str) +{ + const QRegularExpression separator(QString::fromLatin1(R"('|(? index) + { + list << str.mid(index, nextIndex - index).replace(escapedSpace, QStringLiteral(" ")); + } + if (match.capturedLength() == 0) + { // end of string ("\z") is matched + break; + } + index = nextIndex + match.capturedLength(); + auto c = str.at(index - 1); // last matched character + if (!c.isSpace()) + { // a single quote or an unescaped double quote is matched + nextIndex = str.indexOf(c == QLatin1Char('\'') ? singleQuote : doubleQuote, index, &match); + if (nextIndex == -1) + { // the quote is not closed + break; + } + else + { + if (nextIndex > index) + { + list << str.mid(index, nextIndex - index).replace(escapedSpace, QStringLiteral(" ")); + } + index = nextIndex + match.capturedLength(); + } + } + } + return list; +} + diff --git a/src/qterminalutils.h b/src/qterminalutils.h new file mode 100644 index 00000000..0b8d782c --- /dev/null +++ b/src/qterminalutils.h @@ -0,0 +1,26 @@ +/*************************************************************************** + * Copyright (C) 2022 by LXQt team * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + ***************************************************************************/ + +#ifndef QTERMINALUTILS_H +#define QTERMINALUTILS_H + +#include +#include + +QStringList parse_command(const QString& str); + +#endif diff --git a/src/terminalconfig.cpp b/src/terminalconfig.cpp index f61e014c..1497a298 100644 --- a/src/terminalconfig.cpp +++ b/src/terminalconfig.cpp @@ -8,7 +8,7 @@ #include "properties.h" #include "termwidget.h" -TerminalConfig::TerminalConfig(const QString & wdir, const QString & shell) +TerminalConfig::TerminalConfig(const QString & wdir, const QStringList & shell) { m_workingDirectory = wdir; m_shell = shell; @@ -32,20 +32,20 @@ QString TerminalConfig::getWorkingDirectory() return QTerminalApp::Instance()->getWorkingDirectory(); } -QString TerminalConfig::getShell() +QStringList TerminalConfig::getShell() { - if (!m_shell.trimmed().isEmpty()) - return m_shell.trimmed(); - if (!Properties::Instance()->shell.trimmed().isEmpty()) - return Properties::Instance()->shell.trimmed(); + if (!m_shell.isEmpty()) + return m_shell; + if (!Properties::Instance()->shell.isEmpty()) + return Properties::Instance()->shell; QByteArray envShell = qgetenv("SHELL"); if (envShell.constData() != nullptr) { QString shellString = QString::fromLocal8Bit(envShell).trimmed(); if (!shellString.isEmpty()) - return shellString; + return QStringList{shellString}; } - return QString(); + return QStringList(); } void TerminalConfig::setWorkingDirectory(const QString &val) @@ -53,7 +53,7 @@ void TerminalConfig::setWorkingDirectory(const QString &val) m_workingDirectory = val; } -void TerminalConfig::setShell(const QString &val) +void TerminalConfig::setShell(const QStringList &val) { m_shell = val; } @@ -87,16 +87,23 @@ static QString variantToString(const QVariant& variant, QString &defaultVal) return defaultVal; } +static QStringList variantToStringList(const QVariant& variant, QStringList &defaultVal) +{ + if (variant.type() == QVariant::StringList) + return qvariant_cast(variant); + return defaultVal; +} + TerminalConfig TerminalConfig::fromDbus(const QHash &termArgs) { QString wdir = QString(); - QString shell(Properties::Instance()->shell); + QStringList shell(Properties::Instance()->shell); if (termArgs.contains(QLatin1String(DBUS_ARG_WORKDIR))) { wdir = variantToString(termArgs[QLatin1String(DBUS_ARG_WORKDIR)], wdir); } if (termArgs.contains(QLatin1String(DBUS_ARG_SHELL))) { - shell = variantToString(termArgs[QLatin1String(DBUS_ARG_SHELL)], shell); + shell = variantToStringList(termArgs[QLatin1String(DBUS_ARG_SHELL)], shell); } return TerminalConfig(wdir, shell); } diff --git a/src/terminalconfig.h b/src/terminalconfig.h index 5823fae8..4afd18fc 100644 --- a/src/terminalconfig.h +++ b/src/terminalconfig.h @@ -11,15 +11,15 @@ class TermWidget; class TerminalConfig { public: - TerminalConfig(const QString & wdir, const QString & shell); + TerminalConfig(const QString & wdir, const QStringList & shell); TerminalConfig(const TerminalConfig &cfg); TerminalConfig(); QString getWorkingDirectory(); - QString getShell(); + QStringList getShell(); void setWorkingDirectory(const QString &val); - void setShell(const QString &val); + void setShell(const QStringList &val); void provideCurrentDirectory(const QString &val); #ifdef HAVE_QDBUS @@ -29,10 +29,10 @@ class TerminalConfig private: - // True when + // True when QString m_currentDirectory; QString m_workingDirectory; - QString m_shell; + QStringList m_shell; }; -#endif \ No newline at end of file +#endif diff --git a/src/termwidget.cpp b/src/termwidget.cpp index f2c48613..f4536148 100644 --- a/src/termwidget.cpp +++ b/src/termwidget.cpp @@ -55,16 +55,13 @@ TermWidgetImpl::TermWidgetImpl(TerminalConfig &cfg, QWidget * parent) setWorkingDirectory(cfg.getWorkingDirectory()); - QString shell = cfg.getShell(); + QStringList shell = cfg.getShell(); if (!shell.isEmpty()) { - //qDebug() << "Shell program:" << shell; - QStringList parts = shell.split(QRegExp(QStringLiteral("\\s+")), Qt::SkipEmptyParts); - //qDebug() << parts; - setShellProgram(parts.at(0)); - parts.removeAt(0); - if (parts.count()) - setArgs(parts); + setShellProgram(shell.at(0)); + shell.removeAt(0); + if (!shell.isEmpty()) + setArgs(shell); } setMotionAfterPasting(Properties::Instance()->m_motionAfterPaste); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 00000000..5f332a04 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,7 @@ +qt5_wrap_cpp(QTERM_TEST_MOC qterminal_test.h) +add_executable(qterminal_test + qterminal_test.cpp + ${CMAKE_SOURCE_DIR}/src/qterminalutils.cpp + ${QTERM_TEST_MOC}) +target_link_libraries(qterminal_test Qt5::Test) +add_test(NAME qterminal_test COMMAND qterminal_test) diff --git a/test/qterminal_test.cpp b/test/qterminal_test.cpp new file mode 100644 index 00000000..c9c26513 --- /dev/null +++ b/test/qterminal_test.cpp @@ -0,0 +1,53 @@ +/*************************************************************************** + * Copyright (C) 2022 by LXQt team * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + ***************************************************************************/ + +#include "qterminal_test.h" + +#include "qterminalutils.h" + +#include + +// handy shortcut copied from liblxqt +#ifndef QL1S +#define QL1S(x) QLatin1String(x) +#endif + +void QTerminalTest::testParseCommand() +{ + /* Common usage */ + // qterminal -e 'fpad -s "PATH/ha ha"' + // qterminal -e "fpad -s \"PATH/ha ha\"" # \" is decoded as " by the shell + QCOMPARE(parse_command(QL1S(R"(fpad -s "PATH/ha ha")")), + QStringList() << QL1S("fpad") << QL1S("-s") << QL1S("PATH/ha ha")); + // qterminal -e "fpad -s 'PATH/ha ha'" + QCOMPARE(parse_command(QL1S(R"(fpad -s 'PATH/ha ha')")), + QStringList() << QL1S("fpad") << QL1S("-s") << QL1S("PATH/ha ha")); + // qterminal -e 'fpad -s PATH/ha\ ha' + // qterminal -e "fpad -s PATH/ha\ ha" + QCOMPARE(parse_command(QL1S(R"(fpad -s PATH/ha\ ha)")), + QStringList() << QL1S("fpad") << QL1S("-s") << QL1S("PATH/ha ha")); + + /* Uncommon usage */ + // qterminal -e 'fpad -s \"PATH/ha ha\"' + QCOMPARE(parse_command(QL1S(R"(fpad -s \"PATH/ha ha\")")), + QStringList() << QL1S("fpad") << QL1S("-s") << QL1S("\\\"PATH/ha") << QL1S("ha\\\"")); + // qterminal -e 'fpad -s "PATH/ha\ ha"' + QCOMPARE(parse_command(QL1S(R"(fpad -s "PATH/ha\ ha")")), + QStringList() << QL1S("fpad") << QL1S("-s") << QL1S("PATH/ha ha")); +} + +QTEST_MAIN(QTerminalTest) diff --git a/test/qterminal_test.h b/test/qterminal_test.h new file mode 100644 index 00000000..9c64ec98 --- /dev/null +++ b/test/qterminal_test.h @@ -0,0 +1,32 @@ +/*************************************************************************** + * Copyright (C) 2022 by LXQt team * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + ***************************************************************************/ + +#ifndef QTERMINAL_TEST_H +#define QTERMINAL_TEST_H + +#include + +class QTerminalTest : public QObject +{ + Q_OBJECT + + // Each private slot is a test function +private Q_SLOTS: + void testParseCommand(); +}; + +#endif