diff --git a/docs/man/keepassxc-cli.1.adoc b/docs/man/keepassxc-cli.1.adoc index e5d0745a..c0b0deaa 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 1bd5cc4b..0e2e6fb9 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 291e6329..a8afb695 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 348afb67..fb0bb591 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 a8e6eabb..066e28e5 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 00000000..b447d625 Binary files /dev/null and b/tests/data/NewDatabaseMulti.kdbx differ