diff --git a/docs/man/keepassxc-cli.1.adoc b/docs/man/keepassxc-cli.1.adoc
index 5fd076f6e..1db079b1d 100644
--- a/docs/man/keepassxc-cli.1.adoc
+++ b/docs/man/keepassxc-cli.1.adoc
@@ -52,11 +52,12 @@ It provides the ability to query and modify the entries of a KeePass database, d
Removes the named attachment from an entry.
*clip* [_options_] <__database__> <__entry__> [_timeout_]::
- Copies an attribute or the current TOTP (if the *-t* option is specified) of a database entry to the clipboard.
+ Copies an attribute, current TOTP value, UUID, or tags list of a database entry to the clipboard.
If no attribute name is specified using the *-a* option, the password is copied.
If multiple entries with the same name exist in different groups, only the attribute for the first one is copied.
For copying the attribute of an entry in a specific group, the group path to the entry should be specified as well, instead of just the name.
Optionally, a timeout in seconds can be specified to automatically clear the clipboard, the default timeout is 10 seconds, set to 0 to disable.
+ Note: an error will be thrown if you specify multiple options at once (eg, *--uuid* and *-a*).
*close*::
In interactive mode, closes the currently opened database (see *open*).
@@ -143,8 +144,8 @@ It provides the ability to query and modify the entries of a KeePass database, d
Searches all entries that match a specific search term in a database.
*show* [_options_] <__database__> <__entry__>::
- Shows the title, username, password, URL and notes of a database entry.
- Can also show the current TOTP.
+ Shows the title, username, password, URL and notes of a database entry by default.
+ Can also show the current TOTP, entry UUID, and tags list.
Regarding the occurrence of multiple entries with the same name in different groups, everything stated in the *clip* command section also applies here.
== OPTIONS
@@ -235,6 +236,12 @@ 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.
+*--uuid*::
+ Copies the UUID of the entry to the clipboard.
+
+*--tags*::
+ Copies the tags of the entry to the clipboard.
+
*-b*, *--best*::
Try to find and copy to clipboard a unique entry matching the input
If a unique matching entry is found it will be copied to the clipboard.
@@ -262,7 +269,6 @@ The same password generation options as documented for the generate command can
*-a*, *--attributes* <__attribute__>...::
Shows the named attributes.
This option can be specified more than once, with each attribute shown one-per-line in the given order.
- If no attributes are specified and *-t* is not specified, a summary of the default attributes is given.
Protected attributes will be displayed in clear text if specified explicitly by this option.
*--all*::
@@ -275,7 +281,13 @@ The same password generation options as documented for the generate command can
Shows the attachment names along with the size of the attachments.
*-t*, *--totp*::
- Also shows the current TOTP, reporting an error if no TOTP is configured for the entry.
+ Shows the current TOTP and then exits. An error is thrown if no TOTP is configured for the entry.
+
+*--uuid*::
+ Shows the UUID of the entry.
+
+*--tags*::
+ Shows the tag list of the entry.
=== Diceware options
*-W*, *--words* <__count__>::
diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts
index 183837fae..96bdeb4e7 100644
--- a/share/translations/keepassxc_en.ts
+++ b/share/translations/keepassxc_en.ts
@@ -7635,10 +7635,6 @@ Do you want to overwrite it?
Entry %1 not found.
-
- ERROR: Please specify one of --attribute or --totp, not both.
-
- Entry with path %1 has no TOTP set up.
@@ -8352,18 +8348,10 @@ Available commands:
Search term.
-
- Show the entry's current TOTP.
-
- Show the protected attributes in clear text.
-
- Show all the attributes of the entry.
-
- Show the attachments of the entry.
@@ -9248,6 +9236,34 @@ This option is deprecated, use --set-key-file instead.
Tags
+
+ Copy the entry's UUID to the clipboard.
+
+
+
+ Copy the entry's tag list to the clipboard.
+
+
+
+ ERROR: Cannot specify multiple options at once (--attribute, --totp, --uuid, --tags).
+
+
+
+ Only show the entry's current TOTP.
+
+
+
+ Show the entry's UUID.
+
+
+
+ Show the entry's tags.
+
+
+
+ Show all the attributes of the entry, including UUID and Tags.
+
+ QtIOCompressor
diff --git a/src/cli/Clip.cpp b/src/cli/Clip.cpp
index 364072fdd..ea8d02b95 100644
--- a/src/cli/Clip.cpp
+++ b/src/cli/Clip.cpp
@@ -37,6 +37,12 @@ const QCommandLineOption Clip::TotpOption =
QCommandLineOption(QStringList() << "t" << "totp",
QObject::tr("Copy the current TOTP to the clipboard (equivalent to \"-a totp\")."));
+const QCommandLineOption Clip::UuidOption =
+ QCommandLineOption(QStringList() << "uuid", QObject::tr("Copy the entry's UUID to the clipboard."));
+
+const QCommandLineOption Clip::TagsOption =
+ QCommandLineOption(QStringList() << "tags", QObject::tr("Copy the entry's tag list to the clipboard."));
+
const QCommandLineOption Clip::BestMatchOption =
QCommandLineOption(QStringList() << "b" << "best-match",
QObject::tr("Must match only one entry, otherwise a list of possible matches is shown."));
@@ -47,6 +53,8 @@ Clip::Clip()
description = QObject::tr("Copy an entry's attribute to the clipboard.");
options.append(Clip::AttributeOption);
options.append(Clip::TotpOption);
+ options.append(Clip::UuidOption);
+ options.append(Clip::TagsOption);
options.append(Clip::BestMatchOption);
positionalArguments.append(
{QString("entry"), QObject::tr("Path of the entry to clip.", "clip = copy to clipboard"), QString("")});
@@ -99,8 +107,13 @@ int Clip::executeWithDatabase(QSharedPointer database, QSharedPointer<
return EXIT_FAILURE;
}
- if (parser->isSet(AttributeOption) && parser->isSet(TotpOption)) {
- err << QObject::tr("ERROR: Please specify one of --attribute or --totp, not both.") << Qt::endl;
+ auto optionCount = parser->isSet(AttributeOption) ? 1 : 0;
+ optionCount += parser->isSet(TotpOption) ? 1 : 0;
+ optionCount += parser->isSet(UuidOption) ? 1 : 0;
+ optionCount += parser->isSet(TagsOption) ? 1 : 0;
+ if (optionCount > 1) {
+ err << QObject::tr("ERROR: Cannot specify multiple options at once (--attribute, --totp, --uuid, --tags).")
+ << Qt::endl;
return EXIT_FAILURE;
}
@@ -113,11 +126,16 @@ int Clip::executeWithDatabase(QSharedPointer database, QSharedPointer<
return EXIT_FAILURE;
}
- selectedAttribute = "totp";
- found = true;
value = entry->totp();
- } else if (Utils::EntryFieldNames.contains(selectedAttribute)) {
- value = Utils::getTopLevelField(entry, selectedAttribute);
+ selectedAttribute = "TOTP";
+ found = true;
+ } else if (parser->isSet(UuidOption)) {
+ value = entry->uuid().toString();
+ selectedAttribute = "UUID";
+ found = true;
+ } else if (parser->isSet(TagsOption)) {
+ value = entry->tags();
+ selectedAttribute = "Tags";
found = true;
} else {
QStringList attrs = Utils::findAttributes(*entry->attributes(), selectedAttribute);
diff --git a/src/cli/Clip.h b/src/cli/Clip.h
index a8afb6951..8f017ddbb 100644
--- a/src/cli/Clip.h
+++ b/src/cli/Clip.h
@@ -29,6 +29,8 @@ public:
static const QCommandLineOption AttributeOption;
static const QCommandLineOption TotpOption;
+ static const QCommandLineOption UuidOption;
+ static const QCommandLineOption TagsOption;
static const QCommandLineOption BestMatchOption;
};
diff --git a/src/cli/Show.cpp b/src/cli/Show.cpp
index a65809a16..574ba884f 100644
--- a/src/cli/Show.cpp
+++ b/src/cli/Show.cpp
@@ -24,14 +24,21 @@
#include
const QCommandLineOption Show::TotpOption =
- QCommandLineOption(QStringList() << "t" << "totp", QObject::tr("Show the entry's current TOTP."));
+ QCommandLineOption(QStringList() << "t" << "totp", QObject::tr("Only show the entry's current TOTP."));
+
+const QCommandLineOption Show::UuidOption =
+ QCommandLineOption(QStringList() << "uuid", QObject::tr("Show the entry's UUID."));
+
+const QCommandLineOption Show::TagsOption =
+ QCommandLineOption(QStringList() << "tags", QObject::tr("Show the entry's tags."));
const QCommandLineOption Show::ProtectedAttributesOption =
QCommandLineOption(QStringList() << "s" << "show-protected",
QObject::tr("Show the protected attributes in clear text."));
const QCommandLineOption Show::AllAttributesOption =
- QCommandLineOption(QStringList() << "all", QObject::tr("Show all the attributes of the entry."));
+ QCommandLineOption(QStringList() << "all",
+ QObject::tr("Show all the attributes of the entry, including UUID and Tags."));
const QCommandLineOption Show::AttachmentsOption =
QCommandLineOption(QStringList() << "show-attachments", QObject::tr("Show the attachments of the entry."));
@@ -49,6 +56,8 @@ Show::Show()
name = QString("show");
description = QObject::tr("Show an entry's information.");
options.append(Show::TotpOption);
+ options.append(Show::UuidOption);
+ options.append(Show::TagsOption);
options.append(Show::AttributesOption);
options.append(Show::ProtectedAttributesOption);
options.append(Show::AllAttributesOption);
@@ -63,9 +72,10 @@ int Show::executeWithDatabase(QSharedPointer database, QSharedPointer<
const QStringList args = parser->positionalArguments();
const QString& entryPath = args.at(1);
- bool showTotp = parser->isSet(Show::TotpOption);
bool showProtectedAttributes = parser->isSet(Show::ProtectedAttributesOption);
bool showAllAttributes = parser->isSet(Show::AllAttributesOption);
+ bool showUuid = parser->isSet(Show::UuidOption);
+ bool showTags = parser->isSet(Show::TagsOption);
QStringList attributes = parser->values(Show::AttributesOption);
Entry* entry = database->rootGroup()->findEntryByPath(entryPath);
@@ -74,18 +84,23 @@ int Show::executeWithDatabase(QSharedPointer database, QSharedPointer<
return EXIT_FAILURE;
}
- if (showTotp && !entry->hasTotp()) {
- err << QObject::tr("Entry with path %1 has no TOTP set up.").arg(entryPath) << Qt::endl;
- return EXIT_FAILURE;
+ // Early exit if the user only wants to show the TOTP
+ if (parser->isSet(Show::TotpOption)) {
+ if (!entry->hasTotp()) {
+ err << QObject::tr("Entry with path %1 has no TOTP set up.").arg(entryPath) << Qt::endl;
+ return EXIT_FAILURE;
+ }
+
+ out << entry->totp() << Qt::endl;
+ return EXIT_SUCCESS;
}
- bool attributesWereSpecified = true;
+ bool attributesWereSpecified = !showUuid && !showTags;
if (showAllAttributes) {
attributesWereSpecified = false;
+ showUuid = true;
+ showTags = true;
attributes = EntryAttributes::DefaultAttributes;
- for (QString fieldName : Utils::EntryFieldNames) {
- attributes.append(fieldName);
- }
// Adding the custom attributes after the default attributes so that
// the default attributes are always shown first.
for (QString attributeName : entry->attributes()->keys()) {
@@ -94,26 +109,16 @@ int Show::executeWithDatabase(QSharedPointer database, QSharedPointer<
}
attributes.append(attributeName);
}
- } else if (attributes.isEmpty() && !showTotp) {
+ } else if (attributes.isEmpty() && !showUuid && !showTags) {
// If no attributes are specified, output the default attribute set.
attributesWereSpecified = false;
attributes = EntryAttributes::DefaultAttributes;
- for (QString fieldName : Utils::EntryFieldNames) {
- attributes.append(fieldName);
- }
+ showTags = true;
}
// Iterate over the attributes and output them line-by-line.
bool encounteredError = false;
for (const QString& attributeName : asConst(attributes)) {
- if (Utils::EntryFieldNames.contains(attributeName)) {
- if (!attributesWereSpecified) {
- out << attributeName << ": ";
- }
- out << Utils::getTopLevelField(entry, attributeName) << Qt::endl;
- continue;
- }
-
QStringList attrs = Utils::findAttributes(*entry->attributes(), attributeName);
if (attrs.isEmpty()) {
encounteredError = true;
@@ -137,6 +142,14 @@ int Show::executeWithDatabase(QSharedPointer database, QSharedPointer<
}
}
+ // Output UUID and Tags if a certain field wasn't specified
+ if (showTags) {
+ out << "Tags: " << entry->tags() << Qt::endl;
+ }
+ if (showUuid) {
+ out << "UUID: " << entry->uuid().toString() << Qt::endl;
+ }
+
if (parser->isSet(Show::AttachmentsOption)) {
// Separate attachment output from attributes output via a newline.
out << Qt::endl;
@@ -156,9 +169,5 @@ int Show::executeWithDatabase(QSharedPointer database, QSharedPointer<
}
}
- if (showTotp) {
- out << entry->totp() << Qt::endl;
- }
-
return encounteredError ? EXIT_FAILURE : EXIT_SUCCESS;
}
diff --git a/src/cli/Show.h b/src/cli/Show.h
index ca00a815f..939e91ac8 100644
--- a/src/cli/Show.h
+++ b/src/cli/Show.h
@@ -28,6 +28,8 @@ public:
int executeWithDatabase(QSharedPointer db, QSharedPointer parser) override;
static const QCommandLineOption TotpOption;
+ static const QCommandLineOption UuidOption;
+ static const QCommandLineOption TagsOption;
static const QCommandLineOption AllAttributesOption;
static const QCommandLineOption AttributesOption;
static const QCommandLineOption ProtectedAttributesOption;
diff --git a/src/cli/Utils.cpp b/src/cli/Utils.cpp
index ae4874c43..022b9fea0 100644
--- a/src/cli/Utils.cpp
+++ b/src/cli/Utils.cpp
@@ -395,17 +395,6 @@ namespace Utils
return result;
}
- QString getTopLevelField(const Entry* entry, const QString& fieldName)
- {
- if (fieldName == UuidFieldName) {
- return entry->uuid().toString();
- }
- if (fieldName == TagsFieldName) {
- return entry->tags();
- }
- return "";
- }
-
QStringList findAttributes(const EntryAttributes& attributes, const QString& name)
{
QStringList result;
diff --git a/src/cli/Utils.h b/src/cli/Utils.h
index 6a272fc62..eee9b3cec 100644
--- a/src/cli/Utils.h
+++ b/src/cli/Utils.h
@@ -34,10 +34,6 @@ namespace Utils
extern QTextStream STDIN;
extern QTextStream DEVNULL;
- static const QString UuidFieldName = "Uuid";
- static const QString TagsFieldName = "Tags";
- static const QStringList EntryFieldNames(QStringList() << UuidFieldName << TagsFieldName);
-
void setDefaultTextStreams();
void resetTextStreams();
@@ -61,10 +57,6 @@ namespace Utils
* (case-insensitive).
*/
QStringList findAttributes(const EntryAttributes& attributes, const QString& name);
- /**
- * Get the value of a top-level Entry field using its name.
- */
- QString getTopLevelField(const Entry* entry, const QString& fieldName);
}; // namespace Utils
#endif // KEEPASSXC_UTILS_H
diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp
index 9b2e5ab8f..b0aa82344 100644
--- a/tests/TestCli.cpp
+++ b/tests/TestCli.cpp
@@ -673,14 +673,14 @@ void TestCli::testClip()
// Uuid (top-level field)
setInput("a");
- execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0", "-a", "Uuid"});
+ execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0", "--uuid"});
QTRY_COMPARE(clipboard->text(), QString("{9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}"));
// TOTP
setInput("a");
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0", "--totp"});
QTRY_VERIFY(isTotp(clipboard->text()));
- QCOMPARE(m_stdout->readLine(), QByteArray("Entry's \"totp\" attribute copied to the clipboard!\n"));
+ QCOMPARE(m_stdout->readLine(), QByteArray("Entry's \"TOTP\" attribute copied to the clipboard!\n"));
// Test Unicode
setInput("a");
@@ -725,7 +725,7 @@ void TestCli::testClip()
setInput("a");
execCmd(clipCmd, {"clip", m_dbFile2->fileName(), "--attribute", "Username", "--totp", "/Sample Entry", "0"});
- QVERIFY(m_stderr->readAll().contains("ERROR: Please specify one of --attribute or --totp, not both.\n"));
+ QVERIFY(m_stderr->readAll().contains("ERROR: Cannot specify multiple options at once"));
// Best option
setInput("a");
@@ -2077,72 +2077,55 @@ void TestCli::testShow()
QVERIFY(!showCmd.name.isEmpty());
QVERIFY(showCmd.getDescriptionLine().contains(showCmd.name));
+ const QByteArray expectTitle("Title: Sample Entry");
+ const QByteArray expectUserName("UserName: User Name");
+ const QByteArray expectUrl("URL: http://www.somesite.com/");
+ const QByteArray expectUuid("UUID: {9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}");
+ const QByteArray expectNotes("Notes: Notes");
+ const QByteArray expectTags("Tags: ");
+
setInput("a");
execCmd(showCmd, {"show", m_dbFile->fileName(), "/Sample Entry"});
m_stderr->readLine(); // Skip password prompt
QCOMPARE(m_stderr->readAll(), QByteArray());
- QCOMPARE(m_stdout->readAll(),
- QByteArray("Title: Sample Entry\n"
- "UserName: User Name\n"
- "Password: PROTECTED\n"
- "URL: http://www.somesite.com/\n"
- "Notes: Notes\n"
- "Uuid: {9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}\n"
- "Tags: \n"));
+ auto out = m_stdout->readAll();
+ QVERIFY(out.contains(expectTitle));
+ QVERIFY(out.contains(expectUserName));
+ QVERIFY(out.contains(expectUrl));
+ QVERIFY(out.contains(expectNotes));
+ QVERIFY(out.contains(expectTags));
+ QVERIFY(!out.contains(expectUuid));
+ QVERIFY(out.contains("Password: PROTECTED"));
setInput("a");
execCmd(showCmd, {"show", "-s", m_dbFile->fileName(), "/Sample Entry"});
- QCOMPARE(m_stdout->readAll(),
- QByteArray("Title: Sample Entry\n"
- "UserName: User Name\n"
- "Password: Password\n"
- "URL: http://www.somesite.com/\n"
- "Notes: Notes\n"
- "Uuid: {9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}\n"
- "Tags: \n"));
+ out = m_stdout->readAll();
+ QVERIFY(out.contains("Password: Password"));
setInput("a");
execCmd(showCmd, {"show", m_dbFile->fileName(), "-q", "/Sample Entry"});
QCOMPARE(m_stderr->readAll(), QByteArray());
- QCOMPARE(m_stdout->readAll(),
- QByteArray("Title: Sample Entry\n"
- "UserName: User Name\n"
- "Password: PROTECTED\n"
- "URL: http://www.somesite.com/\n"
- "Notes: Notes\n"
- "Uuid: {9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}\n"
- "Tags: \n"));
+ out = m_stdout->readAll();
+ QVERIFY(out.contains(expectTitle));
+ QVERIFY(out.contains(expectUserName));
+ QVERIFY(out.contains(expectUrl));
+ QVERIFY(out.contains(expectNotes));
+ QVERIFY(out.contains(expectTags));
+ QVERIFY(!out.contains(expectUuid));
setInput("a");
execCmd(showCmd, {"show", m_dbFile->fileName(), "--show-attachments", "/Sample Entry"});
m_stderr->readLine(); // Skip password prompt
QCOMPARE(m_stderr->readAll(), QByteArray());
- QCOMPARE(m_stdout->readAll(),
- QByteArray("Title: Sample Entry\n"
- "UserName: User Name\n"
- "Password: PROTECTED\n"
- "URL: http://www.somesite.com/\n"
- "Notes: Notes\n"
- "Uuid: {9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}\n"
- "Tags: \n"
- "\n"
- "Attachments:\n"
- " Sample attachment.txt (15 B)\n"));
+ out = m_stdout->readAll();
+ QVERIFY(out.contains("Attachments:\n Sample attachment.txt (15 B)"));
setInput("a");
execCmd(showCmd, {"show", m_dbFile->fileName(), "--show-attachments", "/Homebanking/Subgroup/Subgroup Entry"});
m_stderr->readLine(); // Skip password prompt
QCOMPARE(m_stderr->readAll(), QByteArray());
- QCOMPARE(m_stdout->readAll(),
- QByteArray("Title: Subgroup Entry\n"
- "UserName: Bank User Name\n"
- "Password: PROTECTED\n"
- "URL: https://www.bank.com\n"
- "Notes: Important note\n"
- "Uuid: {20b183fd-6878-4506-a50b-06d30792aa10}\n"
- "Tags: \n"
- "\n"
- "No attachments present.\n"));
+ out = m_stdout->readAll();
+ QVERIFY(out.contains("No attachments present."));
setInput("a");
execCmd(showCmd, {"show", "-a", "Title", m_dbFile->fileName(), "/Sample Entry"});
@@ -2153,8 +2136,8 @@ void TestCli::testShow()
QCOMPARE(m_stdout->readAll(), QByteArray("Password\n"));
setInput("a");
- execCmd(showCmd, {"show", "-a", "Uuid", m_dbFile->fileName(), "/Sample Entry"});
- QCOMPARE(m_stdout->readAll(), QByteArray("{9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}\n"));
+ execCmd(showCmd, {"show", "--uuid", m_dbFile->fileName(), "/Sample Entry"});
+ QVERIFY(m_stdout->readAll().contains(expectUuid));
setInput("a");
execCmd(showCmd, {"show", "-a", "Title", "-a", "URL", m_dbFile->fileName(), "/Sample Entry"});
@@ -2178,9 +2161,9 @@ void TestCli::testShow()
execCmd(showCmd, {"show", "-t", m_dbFile->fileName(), "/Sample Entry"});
QVERIFY(isTotp(m_stdout->readAll()));
+ // TOTP paramter short circuits any other parameter
setInput("a");
execCmd(showCmd, {"show", "-a", "Title", m_dbFile->fileName(), "--totp", "/Sample Entry"});
- QCOMPARE(m_stdout->readLine(), QByteArray("Sample Entry\n"));
QVERIFY(isTotp(m_stdout->readAll()));
setInput("a");
@@ -2196,18 +2179,15 @@ void TestCli::testShow()
setInput("a");
execCmd(showCmd, {"show", "--all", m_dbFile->fileName(), "/Sample Entry"});
- QCOMPARE(m_stdout->readAll(),
- QByteArray("Title: Sample Entry\n"
- "UserName: User Name\n"
- "Password: PROTECTED\n"
- "URL: http://www.somesite.com/\n"
- "Notes: Notes\n"
- "Uuid: {9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}\n"
- "Tags: \n"
- "TOTP Seed: PROTECTED\n"
- "TOTP Settings: 30;6\n"
- "TestAttribute1: b\n"
- "testattribute1: a\n"));
+ out = m_stdout->readAll();
+ QVERIFY(out.contains(expectTitle));
+ QVERIFY(out.contains(expectUserName));
+ QVERIFY(out.contains(expectUuid));
+ QVERIFY(out.contains(expectTags));
+ QVERIFY(out.contains("TOTP Seed: PROTECTED"));
+ QVERIFY(out.contains("TOTP Settings: 30;6"));
+ QVERIFY(out.contains("TestAttribute1: b"));
+ QVERIFY(out.contains("testattribute1: a"));
}
void TestCli::testInvalidDbFiles()