diff --git a/docs/man/keepassxc-cli.1.adoc b/docs/man/keepassxc-cli.1.adoc index e5d0745a6..c0b0deaa7 100644 --- a/docs/man/keepassxc-cli.1.adoc +++ b/docs/man/keepassxc-cli.1.adoc @@ -207,6 +207,11 @@ The same password generation options as documented for the generate command can Copies the current TOTP instead of the specified attribute to the clipboard. Will report an error if no TOTP is configured for the entry. +*-b*, *--best*:: + Try to find and copy to clipboard a unique entry matching the input (similar to *-locate*) + If a unique matching entry is found it will be copied to the clipboard. + If multiple entries are found they will be listed to refine the search. (no clip performed) + === Create options *-k*, *--set-key-file* <__path__>:: Set the key file for the database. diff --git a/src/cli/Clip.cpp b/src/cli/Clip.cpp index 1bd5cc4ba..0e2e6fb9b 100644 --- a/src/cli/Clip.cpp +++ b/src/cli/Clip.cpp @@ -25,6 +25,7 @@ #include "cli/Utils.h" #include "core/Database.h" #include "core/Entry.h" +#include "core/Global.h" #include "core/Group.h" const QCommandLineOption Clip::AttributeOption = QCommandLineOption( @@ -39,12 +40,18 @@ const QCommandLineOption Clip::TotpOption = << "totp", QObject::tr("Copy the current TOTP to the clipboard (equivalent to \"-a totp\").")); +const QCommandLineOption Clip::BestMatchOption = QCommandLineOption( + QStringList() << "b" + << "best-match", + QObject::tr("Try to find the unique entry matching, will fail and display the list of matches otherwise.")); + Clip::Clip() { name = QString("clip"); description = QObject::tr("Copy an entry's attribute to the clipboard."); options.append(Clip::AttributeOption); options.append(Clip::TotpOption); + options.append(Clip::BestMatchOption); positionalArguments.append( {QString("entry"), QObject::tr("Path of the entry to clip.", "clip = copy to clipboard"), QString("")}); optionalArguments.append( @@ -57,12 +64,31 @@ int Clip::executeWithDatabase(QSharedPointer database, QSharedPointer< auto& err = Utils::STDERR; const QStringList args = parser->positionalArguments(); - const QString& entryPath = args.at(1); + QString bestEntryPath; + QString timeout; if (args.size() == 3) { timeout = args.at(2); } + if (parser->isSet(Clip::BestMatchOption)) { + QStringList results = database->rootGroup()->locate(args.at(1)); + if (results.count() > 1) { + err << QObject::tr("Multiple entries matching:") << endl; + for (const QString& result : asConst(results)) { + err << result << endl; + } + return EXIT_FAILURE; + } else { + bestEntryPath = (results.isEmpty()) ? args.at(1) : results[0]; + out << QObject::tr("Matching \"%1\" entry used.").arg(bestEntryPath) << endl; + } + } else { + bestEntryPath = args.at(1); + } + + const QString& entryPath = bestEntryPath; + int timeoutSeconds = 0; if (!timeout.isEmpty() && timeout.toInt() <= 0) { err << QObject::tr("Invalid timeout value %1.").arg(timeout) << endl; diff --git a/src/cli/Clip.h b/src/cli/Clip.h index 291e63295..a8afb6951 100644 --- a/src/cli/Clip.h +++ b/src/cli/Clip.h @@ -29,6 +29,7 @@ public: static const QCommandLineOption AttributeOption; static const QCommandLineOption TotpOption; + static const QCommandLineOption BestMatchOption; }; #endif // KEEPASSXC_CLIP_H diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index 348afb670..fb0bb5918 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -86,6 +86,9 @@ void TestCli::init() m_dbFile2.reset(new TemporaryFile()); m_dbFile2->copyFromFile(file.arg("NewDatabase2.kdbx")); + m_dbFileMulti.reset(new TemporaryFile()); + m_dbFileMulti->copyFromFile(file.arg("NewDatabaseMulti.kdbx")); + m_xmlFile.reset(new TemporaryFile()); m_xmlFile->copyFromFile(file.arg("NewDatabase.xml")); @@ -115,6 +118,7 @@ void TestCli::cleanup() { m_dbFile.reset(); m_dbFile2.reset(); + m_dbFileMulti.reset(); m_keyFileProtectedDbFile.reset(); m_keyFileProtectedNoPasswordDbFile.reset(); m_yubiKeyProtectedDbFile.reset(); @@ -504,6 +508,17 @@ void TestCli::testClip() setInput("a"); execCmd(clipCmd, {"clip", m_dbFile2->fileName(), "--attribute", "Username", "--totp", "/Sample Entry"}); QVERIFY(m_stderr->readAll().contains("ERROR: Please specify one of --attribute or --totp, not both.\n")); + + // Best option + setInput("a"); + execCmd(clipCmd, {"clip", m_dbFileMulti->fileName(), "Multi", "-b"}); + QByteArray errorChoices = m_stderr->readAll(); + QVERIFY(errorChoices.contains("Multi Entry 1")); + QVERIFY(errorChoices.contains("Multi Entry 2")); + + setInput("a"); + execCmd(clipCmd, {"clip", m_dbFileMulti->fileName(), "Entry 2", "-b"}); + QTRY_COMPARE(clipboard->text(), QString("Password2")); } void TestCli::testCreate() diff --git a/tests/TestCli.h b/tests/TestCli.h index a8e6eabbb..066e28e58 100644 --- a/tests/TestCli.h +++ b/tests/TestCli.h @@ -83,6 +83,7 @@ private slots: private: QScopedPointer m_dbFile; QScopedPointer m_dbFile2; + QScopedPointer m_dbFileMulti; QScopedPointer m_xmlFile; QScopedPointer m_keyFileProtectedDbFile; QScopedPointer m_keyFileProtectedNoPasswordDbFile; diff --git a/tests/data/NewDatabaseMulti.kdbx b/tests/data/NewDatabaseMulti.kdbx new file mode 100644 index 000000000..b447d6258 Binary files /dev/null and b/tests/data/NewDatabaseMulti.kdbx differ