CLI: Add 'flatten' option to the 'ls' command (#3276)

* Fixes #925 
* Add 'flatten' option to CLI ls command
* Add test for Group::hierarchy() and man page for ls --flatten
* Rename group sort test to align with others
This commit is contained in:
Balazs Gyurak 2019-06-19 01:42:19 +01:00 committed by Jonathan White
parent 1e915eef89
commit 05c11d1b7c
10 changed files with 141 additions and 19 deletions

View file

@ -1,6 +1,7 @@
2.5.0-Beta1 (2019-07-05) 2.5.0-Beta1 (2019-07-05)
========================= =========================
- Group sorting feature [#3282] - Group sorting feature [#3282]
- CLI: Add 'flatten' option to the 'ls' command [#3276]
2.4.3 (2019-06-12) 2.4.3 (2019-06-12)
========================= =========================

View file

@ -31,11 +31,16 @@ const QCommandLineOption List::RecursiveOption =
<< "recursive", << "recursive",
QObject::tr("Recursively list the elements of the group.")); QObject::tr("Recursively list the elements of the group."));
const QCommandLineOption List::FlattenOption = QCommandLineOption(QStringList() << "f"
<< "flatten",
QObject::tr("Flattens the output to single lines."));
List::List() List::List()
{ {
name = QString("ls"); name = QString("ls");
description = QObject::tr("List database entries."); description = QObject::tr("List database entries.");
options.append(List::RecursiveOption); options.append(List::RecursiveOption);
options.append(List::FlattenOption);
optionalArguments.append( optionalArguments.append(
{QString("group"), QObject::tr("Path of the group to list. Default is /"), QString("[group]")}); {QString("group"), QObject::tr("Path of the group to list. Default is /"), QString("[group]")});
} }
@ -51,10 +56,11 @@ int List::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
const QStringList args = parser->positionalArguments(); const QStringList args = parser->positionalArguments();
bool recursive = parser->isSet(List::RecursiveOption); bool recursive = parser->isSet(List::RecursiveOption);
bool flatten = parser->isSet(List::FlattenOption);
// No group provided, defaulting to root group. // No group provided, defaulting to root group.
if (args.size() == 1) { if (args.size() == 1) {
outputTextStream << database->rootGroup()->print(recursive) << flush; outputTextStream << database->rootGroup()->print(recursive, flatten) << flush;
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }
@ -65,6 +71,6 @@ int List::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
return EXIT_FAILURE; return EXIT_FAILURE;
} }
outputTextStream << group->print(recursive) << flush; outputTextStream << group->print(recursive, flatten) << flush;
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }

View file

@ -29,6 +29,7 @@ public:
int executeWithDatabase(QSharedPointer<Database> db, QSharedPointer<QCommandLineParser> parser); int executeWithDatabase(QSharedPointer<Database> db, QSharedPointer<QCommandLineParser> parser);
static const QCommandLineOption RecursiveOption; static const QCommandLineOption RecursiveOption;
static const QCommandLineOption FlattenOption;
}; };
#endif // KEEPASSXC_LIST_H #endif // KEEPASSXC_LIST_H

View file

@ -152,6 +152,8 @@ be printed to STDERR.
.IP "-R, --recursive" .IP "-R, --recursive"
Recursively list the elements of the group. Recursively list the elements of the group.
.IP "-f, --flatten"
Flattens the output to single lines. When this option is enabled, subgroups and subentries will be displayed with a relative group path instead of indentation.
.SS "Generate options" .SS "Generate options"

View file

@ -504,16 +504,25 @@ void Group::setParent(Database* db)
QObject::setParent(db); QObject::setParent(db);
} }
QStringList Group::hierarchy() const QStringList Group::hierarchy(int height) const
{ {
QStringList hierarchy; QStringList hierarchy;
const Group* group = this; const Group* group = this;
const Group* parent = m_parent; const Group* parent = m_parent;
if (height == 0) {
return hierarchy;
}
hierarchy.prepend(group->name()); hierarchy.prepend(group->name());
while (parent) { int level = 1;
bool heightReached = level == height;
while (parent && !heightReached) {
group = group->parentGroup(); group = group->parentGroup();
parent = group->parentGroup(); parent = group->parentGroup();
heightReached = ++level == height;
hierarchy.prepend(group->name()); hierarchy.prepend(group->name());
} }
@ -720,25 +729,34 @@ Group* Group::findGroupByPathRecursive(const QString& groupPath, const QString&
return nullptr; return nullptr;
} }
QString Group::print(bool recursive, int depth) QString Group::print(bool recursive, bool flatten, int depth)
{ {
QString response; QString response;
QString indentation = QString(" ").repeated(depth); QString prefix;
if (flatten) {
const QString separator("/");
prefix = hierarchy(depth).join(separator);
if (!prefix.isEmpty()) {
prefix += separator;
}
} else {
prefix = QString(" ").repeated(depth);
}
if (entries().isEmpty() && children().isEmpty()) { if (entries().isEmpty() && children().isEmpty()) {
response += indentation + tr("[empty]", "group has no children") + "\n"; response += prefix + tr("[empty]", "group has no children") + "\n";
return response; return response;
} }
for (Entry* entry : entries()) { for (Entry* entry : entries()) {
response += indentation + entry->title() + "\n"; response += prefix + entry->title() + "\n";
} }
for (Group* innerGroup : children()) { for (Group* innerGroup : children()) {
response += indentation + innerGroup->name() + "/\n"; response += prefix + innerGroup->name() + "/\n";
if (recursive) { if (recursive) {
response += innerGroup->print(recursive, depth + 1); response += innerGroup->print(recursive, flatten, depth + 1);
} }
} }

View file

@ -143,7 +143,7 @@ public:
Group* parentGroup(); Group* parentGroup();
const Group* parentGroup() const; const Group* parentGroup() const;
void setParent(Group* parent, int index = -1); void setParent(Group* parent, int index = -1);
QStringList hierarchy() const; QStringList hierarchy(int height = -1) const;
bool hasChildren() const; bool hasChildren() const;
Database* database(); Database* database();
@ -163,7 +163,7 @@ public:
CloneFlags groupFlags = DefaultCloneFlags) const; CloneFlags groupFlags = DefaultCloneFlags) const;
void copyDataFrom(const Group* other); void copyDataFrom(const Group* other);
QString print(bool recursive = false, int depth = 0); QString print(bool recursive = false, bool flatten = false, int depth = 0);
void addEntry(Entry* entry); void addEntry(Entry* entry);
void removeEntry(Entry* entry); void removeEntry(Entry* entry);

View file

@ -847,7 +847,38 @@ void TestCli::testList()
"eMail/\n" "eMail/\n"
" [empty]\n" " [empty]\n"
"Homebanking/\n" "Homebanking/\n"
" [empty]\n")); " Subgroup/\n"
" Subgroup Entry\n"));
pos = m_stdoutFile->pos();
Utils::Test::setNextPassword("a");
listCmd.execute({"ls", "-R", "-f", m_dbFile->fileName()});
m_stdoutFile->seek(pos);
m_stdoutFile->readLine(); // skip password prompt
QCOMPARE(m_stdoutFile->readAll(),
QByteArray("Sample Entry\n"
"General/\n"
"General/[empty]\n"
"Windows/\n"
"Windows/[empty]\n"
"Network/\n"
"Network/[empty]\n"
"Internet/\n"
"Internet/[empty]\n"
"eMail/\n"
"eMail/[empty]\n"
"Homebanking/\n"
"Homebanking/Subgroup/\n"
"Homebanking/Subgroup/Subgroup Entry\n"));
pos = m_stdoutFile->pos();
Utils::Test::setNextPassword("a");
listCmd.execute({"ls", "-R", "-f", m_dbFile->fileName(), "/Homebanking"});
m_stdoutFile->seek(pos);
m_stdoutFile->readLine(); // skip password prompt
QCOMPARE(m_stdoutFile->readAll(),
QByteArray("Subgroup/\n"
"Subgroup/Subgroup Entry\n"));
pos = m_stdoutFile->pos(); pos = m_stdoutFile->pos();
Utils::Test::setNextPassword("a"); Utils::Test::setNextPassword("a");
@ -921,7 +952,8 @@ void TestCli::testLocate()
locateCmd.execute({"locate", tmpFile.fileName(), "Entry"}); locateCmd.execute({"locate", tmpFile.fileName(), "Entry"});
m_stdoutFile->seek(pos); m_stdoutFile->seek(pos);
m_stdoutFile->readLine(); // skip password prompt m_stdoutFile->readLine(); // skip password prompt
QCOMPARE(m_stdoutFile->readAll(), QByteArray("/Sample Entry\n/General/New Entry\n")); QCOMPARE(m_stdoutFile->readAll(),
QByteArray("/Sample Entry\n/General/New Entry\n/Homebanking/Subgroup/Subgroup Entry\n"));
} }
void TestCli::testMerge() void TestCli::testMerge()

View file

@ -635,28 +635,57 @@ void TestGroup::testPrint()
Group* group1 = new Group(); Group* group1 = new Group();
group1->setName("group1"); group1->setName("group1");
group1->setParent(db->rootGroup());
Entry* entry2 = new Entry(); Entry* entry2 = new Entry();
entry2->setTitle(QString("entry2")); entry2->setTitle(QString("entry2"));
entry2->setGroup(group1); entry2->setGroup(group1);
entry2->setUuid(QUuid::createUuid()); entry2->setUuid(QUuid::createUuid());
group1->setParent(db->rootGroup()); Group* group2 = new Group();
group2->setName("group2");
group2->setParent(db->rootGroup());
Group* subGroup = new Group();
subGroup->setName("subgroup");
subGroup->setParent(group2);
Entry* entry3 = new Entry();
entry3->setTitle(QString("entry3"));
entry3->setGroup(subGroup);
entry3->setUuid(QUuid::createUuid());
output = db->rootGroup()->print(); output = db->rootGroup()->print();
QVERIFY(output.contains(QString("entry1\n"))); QVERIFY(output.contains(QString("entry1\n")));
QVERIFY(output.contains(QString("group1/\n"))); QVERIFY(output.contains(QString("group1/\n")));
QVERIFY(!output.contains(QString(" entry2\n"))); QVERIFY(!output.contains(QString(" entry2\n")));
QVERIFY(output.contains(QString("group2/\n")));
QVERIFY(!output.contains(QString(" subgroup\n")));
output = db->rootGroup()->print(true); output = db->rootGroup()->print(true);
QVERIFY(output.contains(QString("entry1\n"))); QVERIFY(output.contains(QString("entry1\n")));
QVERIFY(output.contains(QString("group1/\n"))); QVERIFY(output.contains(QString("group1/\n")));
QVERIFY(output.contains(QString(" entry2\n"))); QVERIFY(output.contains(QString(" entry2\n")));
QVERIFY(output.contains(QString("group2/\n")));
QVERIFY(output.contains(QString(" subgroup/\n")));
QVERIFY(output.contains(QString(" entry3\n")));
output = db->rootGroup()->print(true, true);
QVERIFY(output.contains(QString("entry1\n")));
QVERIFY(output.contains(QString("group1/\n")));
QVERIFY(output.contains(QString("group1/entry2\n")));
QVERIFY(output.contains(QString("group2/\n")));
QVERIFY(output.contains(QString("group2/subgroup/\n")));
QVERIFY(output.contains(QString("group2/subgroup/entry3\n")));
output = group1->print(); output = group1->print();
QVERIFY(!output.contains(QString("group1/\n"))); QVERIFY(!output.contains(QString("group1/\n")));
QVERIFY(output.contains(QString("entry2\n"))); QVERIFY(output.contains(QString("entry2\n")));
output = group2->print(true, true);
QVERIFY(!output.contains(QString("group2/\n")));
QVERIFY(output.contains(QString("subgroup/\n")));
QVERIFY(output.contains(QString("subgroup/entry3\n")));
} }
void TestGroup::testLocate() void TestGroup::testLocate()
@ -841,7 +870,7 @@ void TestGroup::testEquals()
QVERIFY(group->equals(group.data(), CompareItemDefault)); QVERIFY(group->equals(group.data(), CompareItemDefault));
} }
void TestGroup::sortChildrenRecursively() void TestGroup::testChildrenSort()
{ {
auto createTestGroupWithUnorderedChildren = []() -> Group* { auto createTestGroupWithUnorderedChildren = []() -> Group* {
Group* parent = new Group(); Group* parent = new Group();
@ -1020,3 +1049,35 @@ void TestGroup::sortChildrenRecursively()
QCOMPARE(children[8]->name(), QString("sub_000")); QCOMPARE(children[8]->name(), QString("sub_000"));
delete parent; delete parent;
} }
void TestGroup::testHierarchy()
{
Group* group1 = new Group();
group1->setName("group1");
Group* group2 = new Group();
group2->setName("group2");
group2->setParent(group1);
Group* group3 = new Group();
group3->setName("group3");
group3->setParent(group2);
QStringList hierarchy = group3->hierarchy();
QVERIFY(hierarchy.size() == 3);
QVERIFY(hierarchy.contains("group1"));
QVERIFY(hierarchy.contains("group2"));
QVERIFY(hierarchy.contains("group3"));
hierarchy = group3->hierarchy(0);
QVERIFY(hierarchy.size() == 0);
hierarchy = group3->hierarchy(1);
QVERIFY(hierarchy.size() == 1);
QVERIFY(hierarchy.contains("group3"));
hierarchy = group3->hierarchy(2);
QVERIFY(hierarchy.size() == 2);
QVERIFY(hierarchy.contains("group2"));
QVERIFY(hierarchy.contains("group3"));
}

View file

@ -45,7 +45,8 @@ private slots:
void testIsRecycled(); void testIsRecycled();
void testCopyDataFrom(); void testCopyDataFrom();
void testEquals(); void testEquals();
void sortChildrenRecursively(); void testChildrenSort();
void testHierarchy();
}; };
#endif // KEEPASSX_TESTGROUP_H #endif // KEEPASSX_TESTGROUP_H

Binary file not shown.