mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-08-11 07:50:31 -04:00
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:
parent
1e915eef89
commit
05c11d1b7c
10 changed files with 141 additions and 19 deletions
|
@ -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)
|
||||||
=========================
|
=========================
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
|
|
@ -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.
Loading…
Add table
Add a link
Reference in a new issue