diff --git a/CHANGELOG b/CHANGELOG index 4a6c9d3f1..043fe67c5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ 2.5.0-Beta1 (2019-07-05) ========================= - Group sorting feature [#3282] +- CLI: Add 'flatten' option to the 'ls' command [#3276] 2.4.3 (2019-06-12) ========================= diff --git a/src/cli/List.cpp b/src/cli/List.cpp index 37eb6f847..7f6ea128f 100644 --- a/src/cli/List.cpp +++ b/src/cli/List.cpp @@ -31,11 +31,16 @@ const QCommandLineOption List::RecursiveOption = << "recursive", 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() { name = QString("ls"); description = QObject::tr("List database entries."); options.append(List::RecursiveOption); + options.append(List::FlattenOption); optionalArguments.append( {QString("group"), QObject::tr("Path of the group to list. Default is /"), QString("[group]")}); } @@ -51,10 +56,11 @@ int List::executeWithDatabase(QSharedPointer database, QSharedPointer< const QStringList args = parser->positionalArguments(); bool recursive = parser->isSet(List::RecursiveOption); + bool flatten = parser->isSet(List::FlattenOption); // No group provided, defaulting to root group. if (args.size() == 1) { - outputTextStream << database->rootGroup()->print(recursive) << flush; + outputTextStream << database->rootGroup()->print(recursive, flatten) << flush; return EXIT_SUCCESS; } @@ -65,6 +71,6 @@ int List::executeWithDatabase(QSharedPointer database, QSharedPointer< return EXIT_FAILURE; } - outputTextStream << group->print(recursive) << flush; + outputTextStream << group->print(recursive, flatten) << flush; return EXIT_SUCCESS; } diff --git a/src/cli/List.h b/src/cli/List.h index 51026271b..1b5797077 100644 --- a/src/cli/List.h +++ b/src/cli/List.h @@ -29,6 +29,7 @@ public: int executeWithDatabase(QSharedPointer db, QSharedPointer parser); static const QCommandLineOption RecursiveOption; + static const QCommandLineOption FlattenOption; }; #endif // KEEPASSXC_LIST_H diff --git a/src/cli/keepassxc-cli.1 b/src/cli/keepassxc-cli.1 index bd7f5d5c5..511eeea4d 100644 --- a/src/cli/keepassxc-cli.1 +++ b/src/cli/keepassxc-cli.1 @@ -152,6 +152,8 @@ be printed to STDERR. .IP "-R, --recursive" 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" diff --git a/src/core/Group.cpp b/src/core/Group.cpp index a5d5087f0..39c4a932c 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -504,16 +504,25 @@ void Group::setParent(Database* db) QObject::setParent(db); } -QStringList Group::hierarchy() const +QStringList Group::hierarchy(int height) const { QStringList hierarchy; const Group* group = this; const Group* parent = m_parent; + + if (height == 0) { + return hierarchy; + } + hierarchy.prepend(group->name()); - while (parent) { + int level = 1; + bool heightReached = level == height; + + while (parent && !heightReached) { group = group->parentGroup(); parent = group->parentGroup(); + heightReached = ++level == height; hierarchy.prepend(group->name()); } @@ -720,25 +729,34 @@ Group* Group::findGroupByPathRecursive(const QString& groupPath, const QString& return nullptr; } -QString Group::print(bool recursive, int depth) +QString Group::print(bool recursive, bool flatten, int depth) { - 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()) { - response += indentation + tr("[empty]", "group has no children") + "\n"; + response += prefix + tr("[empty]", "group has no children") + "\n"; return response; } for (Entry* entry : entries()) { - response += indentation + entry->title() + "\n"; + response += prefix + entry->title() + "\n"; } for (Group* innerGroup : children()) { - response += indentation + innerGroup->name() + "/\n"; + response += prefix + innerGroup->name() + "/\n"; if (recursive) { - response += innerGroup->print(recursive, depth + 1); + response += innerGroup->print(recursive, flatten, depth + 1); } } diff --git a/src/core/Group.h b/src/core/Group.h index 048c08854..e9dbcdb90 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -143,7 +143,7 @@ public: Group* parentGroup(); const Group* parentGroup() const; void setParent(Group* parent, int index = -1); - QStringList hierarchy() const; + QStringList hierarchy(int height = -1) const; bool hasChildren() const; Database* database(); @@ -163,7 +163,7 @@ public: CloneFlags groupFlags = DefaultCloneFlags) const; 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 removeEntry(Entry* entry); diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index b1da5f1a1..530e1eabe 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -847,7 +847,38 @@ void TestCli::testList() "eMail/\n" " [empty]\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(); Utils::Test::setNextPassword("a"); @@ -921,7 +952,8 @@ void TestCli::testLocate() locateCmd.execute({"locate", tmpFile.fileName(), "Entry"}); m_stdoutFile->seek(pos); 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() diff --git a/tests/TestGroup.cpp b/tests/TestGroup.cpp index 7d42f161a..9c3e52231 100644 --- a/tests/TestGroup.cpp +++ b/tests/TestGroup.cpp @@ -635,28 +635,57 @@ void TestGroup::testPrint() Group* group1 = new Group(); group1->setName("group1"); + group1->setParent(db->rootGroup()); Entry* entry2 = new Entry(); - entry2->setTitle(QString("entry2")); entry2->setGroup(group1); 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(); QVERIFY(output.contains(QString("entry1\n"))); QVERIFY(output.contains(QString("group1/\n"))); QVERIFY(!output.contains(QString(" entry2\n"))); + QVERIFY(output.contains(QString("group2/\n"))); + QVERIFY(!output.contains(QString(" subgroup\n"))); output = db->rootGroup()->print(true); QVERIFY(output.contains(QString("entry1\n"))); QVERIFY(output.contains(QString("group1/\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(); QVERIFY(!output.contains(QString("group1/\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() @@ -841,7 +870,7 @@ void TestGroup::testEquals() QVERIFY(group->equals(group.data(), CompareItemDefault)); } -void TestGroup::sortChildrenRecursively() +void TestGroup::testChildrenSort() { auto createTestGroupWithUnorderedChildren = []() -> Group* { Group* parent = new Group(); @@ -1020,3 +1049,35 @@ void TestGroup::sortChildrenRecursively() QCOMPARE(children[8]->name(), QString("sub_000")); 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")); +} diff --git a/tests/TestGroup.h b/tests/TestGroup.h index 9355a0c98..6242e4ff4 100644 --- a/tests/TestGroup.h +++ b/tests/TestGroup.h @@ -45,7 +45,8 @@ private slots: void testIsRecycled(); void testCopyDataFrom(); void testEquals(); - void sortChildrenRecursively(); + void testChildrenSort(); + void testHierarchy(); }; #endif // KEEPASSX_TESTGROUP_H diff --git a/tests/data/NewDatabase.kdbx b/tests/data/NewDatabase.kdbx index a8dfb5bd5..3008cce7c 100644 Binary files a/tests/data/NewDatabase.kdbx and b/tests/data/NewDatabase.kdbx differ