Improve CLI Show and Clip Commands

* Fixes #11767
* When requesting TOTP, only output that value and then exit
* Add explicit requests for UUID and Tags for both show and clip commands.
* When showing all values, also include UUID and Tags.
* Only hide the attribute names when specifically requesting them by name
This commit is contained in:
Jonathan White 2025-05-10 09:03:47 -04:00
parent f53c7e5af5
commit 87ccc85247
No known key found for this signature in database
GPG key ID: 440FC65F2E0C6E01
9 changed files with 151 additions and 131 deletions

View file

@ -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__>::

View file

@ -7635,10 +7635,6 @@ Do you want to overwrite it?</source>
<source>Entry %1 not found.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>ERROR: Please specify one of --attribute or --totp, not both.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Entry with path %1 has no TOTP set up.</source>
<translation type="unfinished"></translation>
@ -8352,18 +8348,10 @@ Available commands:
<source>Search term.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Show the entry&apos;s current TOTP.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Show the protected attributes in clear text.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Show all the attributes of the entry.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Show the attachments of the entry.</source>
<translation type="unfinished"></translation>
@ -9248,6 +9236,34 @@ This option is deprecated, use --set-key-file instead.</source>
<source>Tags</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy the entry&apos;s UUID to the clipboard.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy the entry&apos;s tag list to the clipboard.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>ERROR: Cannot specify multiple options at once (--attribute, --totp, --uuid, --tags).</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Only show the entry&apos;s current TOTP.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Show the entry&apos;s UUID.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Show the entry&apos;s tags.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Show all the attributes of the entry, including UUID and Tags.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtIOCompressor</name>

View file

@ -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> 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> 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);

View file

@ -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;
};

View file

@ -24,14 +24,21 @@
#include <QCommandLineParser>
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> 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> 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> 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> 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> database, QSharedPointer<
}
}
if (showTotp) {
out << entry->totp() << Qt::endl;
}
return encounteredError ? EXIT_FAILURE : EXIT_SUCCESS;
}

View file

@ -28,6 +28,8 @@ public:
int executeWithDatabase(QSharedPointer<Database> db, QSharedPointer<QCommandLineParser> 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;

View file

@ -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;

View file

@ -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

View file

@ -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()