CLI: Add interactive session mode command open

This change adds a GNU Readline-based interactive mode to keepassxc-cli. If GNU Readline is not available, commands are just read from stdin with no editing or auto-complete support.

DatabaseCommand is modified to add the path to the current database to the arguments passed to executeWithDatabase. In this way, instances of DatabaseCommand do not have to prompt to re-open the database after each invocation, and existing command implementations do not have to be changed to support interactive mode.

This change also introduces a new way of handling commands between interactive and batch modes.

* Fixes #3224.
* Ran make format
This commit is contained in:
James Ring 2019-09-13 09:49:03 -04:00 committed by Jonathan White
parent a07ea12ac4
commit b1eda37cca
27 changed files with 2751 additions and 2015 deletions

View File

@ -11,6 +11,7 @@
- CLI: Add `-y --yubikey` option for YubiKey [#3416](https://github.com/keepassxreboot/keepassxc/issues/3416) - CLI: Add `-y --yubikey` option for YubiKey [#3416](https://github.com/keepassxreboot/keepassxc/issues/3416)
- Add 'Monospaced font' option to the Notes field [#3321](https://github.com/keepassxreboot/keepassxc/issues/3321) - Add 'Monospaced font' option to the Notes field [#3321](https://github.com/keepassxreboot/keepassxc/issues/3321)
- CLI: Add group commands (mv, mkdir and rmdir) [#3313]. - CLI: Add group commands (mv, mkdir and rmdir) [#3313].
- CLI: Add interactive shell mode command `open` [#3224](https://github.com/keepassxreboot/keepassxc/issues/3224)
- Add "Paper Backup" aka "Export to HTML file" to the "Database" menu [#3277](https://github.com/keepassxreboot/keepassxc/pull/3277) - Add "Paper Backup" aka "Export to HTML file" to the "Database" menu [#3277](https://github.com/keepassxreboot/keepassxc/pull/3277)
### Changed ### Changed

50
cmake/FindReadline.cmake Normal file
View File

@ -0,0 +1,50 @@
# Code copied from sethhall@github
#
# - Try to find readline include dirs and libraries
#
# Usage of this module as follows:
#
# find_package(Readline)
#
# Variables used by this module, they can change the default behaviour and need
# to be set before calling find_package:
#
# Readline_ROOT_DIR Set this variable to the root installation of
# readline if the module has problems finding the
# proper installation path.
#
# Variables defined by this module:
#
# READLINE_FOUND System has readline, include and lib dirs found
# Readline_INCLUDE_DIR The readline include directories.
# Readline_LIBRARY The readline library.
find_path(Readline_ROOT_DIR
NAMES include/readline/readline.h
)
find_path(Readline_INCLUDE_DIR
NAMES readline/readline.h
HINTS ${Readline_ROOT_DIR}/include
)
find_library(Readline_LIBRARY
NAMES readline
HINTS ${Readline_ROOT_DIR}/lib
)
if(Readline_INCLUDE_DIR AND Readline_LIBRARY AND Ncurses_LIBRARY)
set(READLINE_FOUND TRUE)
else(Readline_INCLUDE_DIR AND Readline_LIBRARY AND Ncurses_LIBRARY)
find_library(Readline_LIBRARY NAMES readline)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Readline DEFAULT_MSG Readline_INCLUDE_DIR Readline_LIBRARY )
mark_as_advanced(Readline_INCLUDE_DIR Readline_LIBRARY)
endif(Readline_INCLUDE_DIR AND Readline_LIBRARY AND Ncurses_LIBRARY)
mark_as_advanced(
Readline_ROOT_DIR
Readline_INCLUDE_DIR
Readline_LIBRARY
)

View File

@ -541,10 +541,7 @@ QString BrowserAction::getDatabaseHash()
{ {
QMutexLocker locker(&m_mutex); QMutexLocker locker(&m_mutex);
QByteArray hash = QByteArray hash =
QCryptographicHash::hash( QCryptographicHash::hash(m_browserService.getDatabaseRootUuid().toUtf8(), QCryptographicHash::Sha256).toHex();
m_browserService.getDatabaseRootUuid().toUtf8(),
QCryptographicHash::Sha256)
.toHex();
return QString(hash); return QString(hash);
} }

View File

@ -45,7 +45,6 @@ const QCommandLineOption Add::GenerateOption = QCommandLineOption(QStringList()
<< "generate", << "generate",
QObject::tr("Generate a password for the entry.")); QObject::tr("Generate a password for the entry."));
Add::Add() Add::Add()
{ {
name = QString("add"); name = QString("add");

View File

@ -18,18 +18,22 @@ set(cli_SOURCES
AddGroup.cpp AddGroup.cpp
Analyze.cpp Analyze.cpp
Clip.cpp Clip.cpp
Close.cpp
Create.cpp Create.cpp
Command.cpp Command.cpp
DatabaseCommand.cpp DatabaseCommand.cpp
Diceware.cpp Diceware.cpp
Edit.cpp Edit.cpp
Estimate.cpp Estimate.cpp
Exit.cpp
Export.cpp Export.cpp
Generate.cpp Generate.cpp
Help.cpp
List.cpp List.cpp
Locate.cpp Locate.cpp
Merge.cpp Merge.cpp
Move.cpp Move.cpp
Open.cpp
Remove.cpp Remove.cpp
RemoveGroup.cpp RemoveGroup.cpp
Show.cpp) Show.cpp)
@ -37,6 +41,13 @@ set(cli_SOURCES
add_library(cli STATIC ${cli_SOURCES}) add_library(cli STATIC ${cli_SOURCES})
target_link_libraries(cli Qt5::Core Qt5::Widgets) target_link_libraries(cli Qt5::Core Qt5::Widgets)
find_package(Readline)
if (READLINE_FOUND)
target_compile_definitions(cli PUBLIC USE_READLINE)
target_link_libraries(cli readline)
endif()
add_executable(keepassxc-cli keepassxc-cli.cpp) add_executable(keepassxc-cli keepassxc-cli.cpp)
target_link_libraries(keepassxc-cli target_link_libraries(keepassxc-cli
cli cli
@ -53,6 +64,12 @@ install(TARGETS keepassxc-cli
BUNDLE DESTINATION . COMPONENT Runtime BUNDLE DESTINATION . COMPONENT Runtime
RUNTIME DESTINATION ${CLI_INSTALL_DIR} COMPONENT Runtime) RUNTIME DESTINATION ${CLI_INSTALL_DIR} COMPONENT Runtime)
if(MINGW)
install(CODE "include(BundleUtilities)
fixup_bundle(\"\${CMAKE_INSTALL_PREFIX}/keepassxc-cli.exe\" \"\" \"\")"
COMPONENT Runtime)
endif()
if(APPLE AND WITH_APP_BUNDLE) if(APPLE AND WITH_APP_BUNDLE)
add_custom_command(TARGET keepassxc-cli add_custom_command(TARGET keepassxc-cli
POST_BUILD POST_BUILD

38
src/cli/Close.cpp Normal file
View File

@ -0,0 +1,38 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Close.h"
#include <QCommandLineParser>
#include <QtGlobal>
#include "DatabaseCommand.h"
#include "TextStream.h"
#include "Utils.h"
Close::Close()
{
name = QString("close");
description = QObject::tr("Close the currently opened database.");
}
int Close::execute(const QStringList& arguments)
{
Q_UNUSED(arguments)
currentDatabase.reset(nullptr);
return EXIT_SUCCESS;
}

32
src/cli/Close.h Normal file
View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_CLOSE_H
#define KEEPASSXC_CLOSE_H
#include <QStringList>
#include "Command.h"
class Close : public Command
{
public:
Close();
int execute(const QStringList& arguments) override;
};
#endif // KEEPASSXC_CLOSE_H

View File

@ -15,8 +15,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <stdio.h> #include <utility>
#include <QMap> #include <QMap>
@ -26,22 +27,33 @@
#include "AddGroup.h" #include "AddGroup.h"
#include "Analyze.h" #include "Analyze.h"
#include "Clip.h" #include "Clip.h"
#include "Close.h"
#include "Create.h" #include "Create.h"
#include "Diceware.h" #include "Diceware.h"
#include "Edit.h" #include "Edit.h"
#include "Estimate.h" #include "Estimate.h"
#include "Exit.h"
#include "Export.h" #include "Export.h"
#include "Generate.h" #include "Generate.h"
#include "Help.h"
#include "List.h" #include "List.h"
#include "Locate.h" #include "Locate.h"
#include "Merge.h" #include "Merge.h"
#include "Move.h" #include "Move.h"
#include "Open.h"
#include "Remove.h" #include "Remove.h"
#include "RemoveGroup.h" #include "RemoveGroup.h"
#include "Show.h" #include "Show.h"
#include "TextStream.h" #include "TextStream.h"
#include "Utils.h" #include "Utils.h"
const QCommandLineOption Command::HelpOption = QCommandLineOption(QStringList()
#ifdef Q_OS_WIN
<< QStringLiteral("?")
#endif
<< QStringLiteral("h") << QStringLiteral("help"),
QObject::tr("Display this help."));
const QCommandLineOption Command::QuietOption = const QCommandLineOption Command::QuietOption =
QCommandLineOption(QStringList() << "q" QCommandLineOption(QStringList() << "q"
<< "quiet", << "quiet",
@ -61,9 +73,31 @@ const QCommandLineOption Command::YubiKeyOption =
QObject::tr("Yubikey slot used to encrypt the database."), QObject::tr("Yubikey slot used to encrypt the database."),
QObject::tr("slot")); QObject::tr("slot"));
QMap<QString, Command*> commands; namespace
{
QSharedPointer<QCommandLineParser> buildParser(Command* command)
{
auto parser = QSharedPointer<QCommandLineParser>(new QCommandLineParser());
parser->setApplicationDescription(command->description);
for (const CommandLineArgument& positionalArgument : command->positionalArguments) {
parser->addPositionalArgument(
positionalArgument.name, positionalArgument.description, positionalArgument.syntax);
}
for (const CommandLineArgument& optionalArgument : command->optionalArguments) {
parser->addPositionalArgument(optionalArgument.name, optionalArgument.description, optionalArgument.syntax);
}
for (const QCommandLineOption& option : command->options) {
parser->addOption(option);
}
parser->addOption(Command::HelpOption);
return parser;
}
} // namespace
Command::Command() Command::Command()
: currentDatabase(nullptr)
{ {
options.append(Command::QuietOption); options.append(Command::QuietOption);
} }
@ -83,70 +117,79 @@ QString Command::getDescriptionLine()
return response; return response;
} }
QString Command::getHelpText()
{
return buildParser(this)->helpText().replace("[options]", name + " [options]");
}
QSharedPointer<QCommandLineParser> Command::getCommandLineParser(const QStringList& arguments) QSharedPointer<QCommandLineParser> Command::getCommandLineParser(const QStringList& arguments)
{ {
TextStream errorTextStream(Utils::STDERR, QIODevice::WriteOnly); TextStream errorTextStream(Utils::STDERR, QIODevice::WriteOnly);
QSharedPointer<QCommandLineParser> parser = buildParser(this);
QSharedPointer<QCommandLineParser> parser = QSharedPointer<QCommandLineParser>(new QCommandLineParser()); if (!parser->parse(arguments)) {
parser->setApplicationDescription(description); errorTextStream << parser->errorText() << "\n\n";
for (const CommandLineArgument& positionalArgument : positionalArguments) { errorTextStream << getHelpText();
parser->addPositionalArgument( return {};
positionalArgument.name, positionalArgument.description, positionalArgument.syntax);
} }
for (const CommandLineArgument& optionalArgument : optionalArguments) {
parser->addPositionalArgument(optionalArgument.name, optionalArgument.description, optionalArgument.syntax);
}
for (const QCommandLineOption& option : options) {
parser->addOption(option);
}
parser->addHelpOption();
parser->process(arguments);
if (parser->positionalArguments().size() < positionalArguments.size()) { if (parser->positionalArguments().size() < positionalArguments.size()) {
errorTextStream << parser->helpText().replace("[options]", name.append(" [options]")); errorTextStream << getHelpText();
return QSharedPointer<QCommandLineParser>(nullptr); return {};
} }
if (parser->positionalArguments().size() > (positionalArguments.size() + optionalArguments.size())) { if (parser->positionalArguments().size() > (positionalArguments.size() + optionalArguments.size())) {
errorTextStream << parser->helpText().replace("[options]", name.append(" [options]")); errorTextStream << getHelpText();
return QSharedPointer<QCommandLineParser>(nullptr); return {};
}
if (parser->isSet(HelpOption)) {
errorTextStream << getHelpText();
return {};
} }
return parser; return parser;
} }
void populateCommands() namespace Commands
{ {
if (commands.isEmpty()) { QMap<QString, QSharedPointer<Command>> s_commands;
commands.insert(QString("add"), new Add());
commands.insert(QString("analyze"), new Analyze()); void setupCommands(bool interactive)
commands.insert(QString("clip"), new Clip()); {
commands.insert(QString("create"), new Create()); s_commands.clear();
commands.insert(QString("diceware"), new Diceware());
commands.insert(QString("edit"), new Edit()); s_commands.insert(QStringLiteral("add"), QSharedPointer<Command>(new Add()));
commands.insert(QString("estimate"), new Estimate()); s_commands.insert(QStringLiteral("analyze"), QSharedPointer<Command>(new Analyze()));
commands.insert(QString("export"), new Export()); s_commands.insert(QStringLiteral("clip"), QSharedPointer<Command>(new Clip()));
commands.insert(QString("generate"), new Generate()); s_commands.insert(QStringLiteral("close"), QSharedPointer<Command>(new Close()));
commands.insert(QString("locate"), new Locate()); s_commands.insert(QStringLiteral("create"), QSharedPointer<Command>(new Create()));
commands.insert(QString("ls"), new List()); s_commands.insert(QStringLiteral("diceware"), QSharedPointer<Command>(new Diceware()));
commands.insert(QString("merge"), new Merge()); s_commands.insert(QStringLiteral("edit"), QSharedPointer<Command>(new Edit()));
commands.insert(QString("mkdir"), new AddGroup()); s_commands.insert(QStringLiteral("estimate"), QSharedPointer<Command>(new Estimate()));
commands.insert(QString("mv"), new Move()); s_commands.insert(QStringLiteral("generate"), QSharedPointer<Command>(new Generate()));
commands.insert(QString("rm"), new Remove()); s_commands.insert(QStringLiteral("help"), QSharedPointer<Command>(new Help()));
commands.insert(QString("rmdir"), new RemoveGroup()); s_commands.insert(QStringLiteral("locate"), QSharedPointer<Command>(new Locate()));
commands.insert(QString("show"), new Show()); s_commands.insert(QStringLiteral("ls"), QSharedPointer<Command>(new List()));
s_commands.insert(QStringLiteral("merge"), QSharedPointer<Command>(new Merge()));
s_commands.insert(QStringLiteral("mkdir"), QSharedPointer<Command>(new AddGroup()));
s_commands.insert(QStringLiteral("mv"), QSharedPointer<Command>(new Move()));
s_commands.insert(QStringLiteral("open"), QSharedPointer<Command>(new Open()));
s_commands.insert(QStringLiteral("rm"), QSharedPointer<Command>(new Remove()));
s_commands.insert(QStringLiteral("rmdir"), QSharedPointer<Command>(new RemoveGroup()));
s_commands.insert(QStringLiteral("show"), QSharedPointer<Command>(new Show()));
if (interactive) {
s_commands.insert(QStringLiteral("exit"), QSharedPointer<Command>(new Exit("exit")));
s_commands.insert(QStringLiteral("quit"), QSharedPointer<Command>(new Exit("quit")));
} else {
s_commands.insert(QStringLiteral("export"), QSharedPointer<Command>(new Export()));
} }
} }
Command* Command::getCommand(const QString& commandName) QList<QSharedPointer<Command>> getCommands()
{ {
populateCommands(); return s_commands.values();
if (commands.contains(commandName)) {
return commands[commandName];
}
return nullptr;
} }
QList<Command*> Command::getCommands() QSharedPointer<Command> getCommand(const QString& commandName)
{ {
populateCommands(); return s_commands.value(commandName);
return commands.values();
} }
} // namespace Commands

View File

@ -44,19 +44,27 @@ public:
virtual int execute(const QStringList& arguments) = 0; virtual int execute(const QStringList& arguments) = 0;
QString name; QString name;
QString description; QString description;
QSharedPointer<Database> currentDatabase;
QList<CommandLineArgument> positionalArguments; QList<CommandLineArgument> positionalArguments;
QList<CommandLineArgument> optionalArguments; QList<CommandLineArgument> optionalArguments;
QList<QCommandLineOption> options; QList<QCommandLineOption> options;
QString getDescriptionLine(); QString getDescriptionLine();
QSharedPointer<QCommandLineParser> getCommandLineParser(const QStringList& arguments); QSharedPointer<QCommandLineParser> getCommandLineParser(const QStringList& arguments);
QString getHelpText();
static QList<Command*> getCommands(); static const QCommandLineOption HelpOption;
static Command* getCommand(const QString& commandName);
static const QCommandLineOption QuietOption; static const QCommandLineOption QuietOption;
static const QCommandLineOption KeyFileOption; static const QCommandLineOption KeyFileOption;
static const QCommandLineOption NoPasswordOption; static const QCommandLineOption NoPasswordOption;
static const QCommandLineOption YubiKeyOption; static const QCommandLineOption YubiKeyOption;
}; };
namespace Commands
{
void setupCommands(bool interactive);
QList<QSharedPointer<Command>> getCommands();
QSharedPointer<Command> getCommand(const QString& commandName);
} // namespace Commands
#endif // KEEPASSXC_COMMAND_H #endif // KEEPASSXC_COMMAND_H

View File

@ -93,16 +93,17 @@ int Create::execute(const QStringList& arguments)
return EXIT_FAILURE; return EXIT_FAILURE;
} }
Database db; QSharedPointer<Database> db(new Database);
db.setKey(key); db->setKey(key);
QString errorMessage; QString errorMessage;
if (!db.save(databaseFilename, &errorMessage, true, false)) { if (!db->save(databaseFilename, &errorMessage, true, false)) {
err << QObject::tr("Failed to save the database: %1.").arg(errorMessage) << endl; err << QObject::tr("Failed to save the database: %1.").arg(errorMessage) << endl;
return EXIT_FAILURE; return EXIT_FAILURE;
} }
out << QObject::tr("Successfully created new database.") << endl; out << QObject::tr("Successfully created new database.") << endl;
currentDatabase = db;
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }

View File

@ -31,13 +31,25 @@ DatabaseCommand::DatabaseCommand()
int DatabaseCommand::execute(const QStringList& arguments) int DatabaseCommand::execute(const QStringList& arguments)
{ {
QSharedPointer<QCommandLineParser> parser = getCommandLineParser(arguments); QStringList amendedArgs(arguments);
if (currentDatabase) {
amendedArgs.insert(1, currentDatabase->filePath());
}
QSharedPointer<QCommandLineParser> parser = getCommandLineParser(amendedArgs);
if (parser.isNull()) { if (parser.isNull()) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
const QStringList args = parser->positionalArguments(); QStringList args = parser->positionalArguments();
auto db = Utils::unlockDatabase(args.at(0), auto db = currentDatabase;
if (!db) {
// It would be nice to update currentDatabase here, but the CLI tests frequently
// re-use Command objects to exercise non-interactive behavior. Updating the current
// database confuses these tests. Because of this, we leave it up to the interactive
// mode implementation in the main command loop to update currentDatabase
// (see keepassxc-cli.cpp).
db = Utils::unlockDatabase(args.at(0),
!parser->isSet(Command::NoPasswordOption), !parser->isSet(Command::NoPasswordOption),
parser->value(Command::KeyFileOption), parser->value(Command::KeyFileOption),
parser->value(Command::YubiKeyOption), parser->value(Command::YubiKeyOption),
@ -46,6 +58,7 @@ int DatabaseCommand::execute(const QStringList& arguments)
if (!db) { if (!db) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
}
return executeWithDatabase(db, parser); return executeWithDatabase(db, parser);
} }

35
src/cli/Exit.cpp Normal file
View File

@ -0,0 +1,35 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Exit.h"
#include <QCommandLineParser>
#include <QObject>
#include <QtGlobal>
Exit::Exit(const QString& name)
{
this->name = name;
description = QObject::tr("Exit interactive mode.");
}
int Exit::execute(const QStringList& arguments)
{
Q_UNUSED(arguments)
// A placeholder only, behavior is implemented in keepassxc-cli.cpp.
return EXIT_SUCCESS;
}

33
src/cli/Exit.h Normal file
View File

@ -0,0 +1,33 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_EXIT_H
#define KEEPASSXC_EXIT_H
#include <QString>
#include <QStringList>
#include "Command.h"
class Exit : public Command
{
public:
Exit(const QString& name);
int execute(const QStringList& arguments) override;
};
#endif // KEEPASSXC_EXIT_H

View File

@ -38,7 +38,6 @@ Export::Export()
description = QObject::tr("Exports the content of a database to standard output in the specified format."); description = QObject::tr("Exports the content of a database to standard output in the specified format.");
} }
int Export::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<QCommandLineParser> parser) int Export::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<QCommandLineParser> parser)
{ {
TextStream outputTextStream(Utils::STDOUT, QIODevice::WriteOnly); TextStream outputTextStream(Utils::STDOUT, QIODevice::WriteOnly);

43
src/cli/Help.cpp Normal file
View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Help.h"
#include "Command.h"
#include "TextStream.h"
#include "Utils.h"
Help::Help()
{
name = QString("help");
description = QObject::tr("Display command help.");
}
int Help::execute(const QStringList& arguments)
{
TextStream out(Utils::STDERR, QIODevice::WriteOnly);
QSharedPointer<Command> command;
if (arguments.size() > 1 && (command = Commands::getCommand(arguments.at(1)))) {
out << command->getHelpText();
} else {
out << "\n\n" << QObject::tr("Available commands:") << "\n";
for (auto& cmd : Commands::getCommands()) {
out << cmd->getDescriptionLine();
}
}
return EXIT_SUCCESS;
}

31
src/cli/Help.h Normal file
View File

@ -0,0 +1,31 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_HELP_H
#define KEEPASSXC_HELP_H
#include "Command.h"
class Help : public Command
{
public:
Help();
~Help() override = default;
int execute(const QStringList& arguments) override;
};
#endif // KEEPASSXC_HELP_H

43
src/cli/Open.cpp Normal file
View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Open.h"
#include <QCommandLineParser>
#include "DatabaseCommand.h"
#include "TextStream.h"
#include "Utils.h"
Open::Open()
{
name = QString("open");
description = QObject::tr("Open a database.");
}
int Open::execute(const QStringList& arguments)
{
currentDatabase.reset(nullptr);
return this->DatabaseCommand::execute(arguments);
}
int Open::executeWithDatabase(QSharedPointer<Database> db, QSharedPointer<QCommandLineParser> parser)
{
Q_UNUSED(parser)
currentDatabase = db;
return EXIT_SUCCESS;
}

31
src/cli/Open.h Normal file
View File

@ -0,0 +1,31 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_OPEN_H
#define KEEPASSXC_OPEN_H
#include "DatabaseCommand.h"
class Open : public DatabaseCommand
{
public:
Open();
int execute(const QStringList& arguments) override;
int executeWithDatabase(QSharedPointer<Database> db, QSharedPointer<QCommandLineParser> parser) override;
};
#endif // KEEPASSXC_OPEN_H

View File

@ -269,4 +269,42 @@ namespace Utils
return clipProcess->exitCode(); return clipProcess->exitCode();
} }
/**
* Splits the given QString into a QString list. For example:
*
* "hello world" -> ["hello", "world"]
* "hello world" -> ["hello", "world"]
* "hello\\ world" -> ["hello world"] (i.e. backslash is an escape character
* "\"hello world\"" -> ["hello world"]
*/
QStringList splitCommandString(const QString& command)
{
QStringList result;
bool insideQuotes = false;
QString cur;
for (int i = 0; i < command.size(); ++i) {
QChar c = command[i];
if (c == '\\' && i < command.size() - 1) {
cur.append(command[i + 1]);
++i;
} else if (!insideQuotes && (c == ' ' || c == '\t')) {
if (!cur.isEmpty()) {
result.append(cur);
cur.clear();
}
} else if (c == '"' && (insideQuotes || i == 0 || command[i - 1].isSpace())) {
insideQuotes = !insideQuotes;
} else {
cur.append(c);
}
}
if (!cur.isEmpty()) {
result.append(cur);
}
return result;
}
} // namespace Utils } // namespace Utils

View File

@ -48,6 +48,8 @@ namespace Utils
FILE* outputDescriptor = STDOUT, FILE* outputDescriptor = STDOUT,
FILE* errorDescriptor = STDERR); FILE* errorDescriptor = STDERR);
QStringList splitCommandString(const QString& command);
namespace Test namespace Test
{ {
void setNextPassword(const QString& password); void setNextPassword(const QString& password);

View File

@ -18,16 +18,19 @@ Adds a new entry to a database. A password can be generated (\fI-g\fP option), o
The same password generation options as documented for the generate command can be used when the \fI-g\fP option is set. The same password generation options as documented for the generate command can be used when the \fI-g\fP option is set.
.IP "analyze [options] <database>" .IP "analyze [options] <database>"
Analyze passwords in a database for weaknesses. Analyzes passwords in a database for weaknesses.
.IP "clip [options] <database> <entry> [timeout]" .IP "clip [options] <database> <entry> [timeout]"
Copies the password or the current TOTP (\fI-t\fP option) of a database entry to the clipboard. If multiple entries with the same name exist in different groups, only the password for the first one is going to be copied. For copying the password 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. Copies the password or the current TOTP (\fI-t\fP option) of a database entry to the clipboard. If multiple entries with the same name exist in different groups, only the password for the first one is going to be copied. For copying the password 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.
.IP "close"
In interactive mode, closes the currently opened database (see \fIopen\fP).
.IP "create [options] <database>" .IP "create [options] <database>"
Creates a new database with a key file and/or password. The key file will be created if the file that is referred to does not exist. If both the key file and password are empty, no database will be created. Creates a new database with a key file and/or password. The key file will be created if the file that is referred to does not exist. If both the key file and password are empty, no database will be created.
.IP "diceware [options]" .IP "diceware [options]"
Generate a random diceware passphrase. Generates a random diceware passphrase.
.IP "edit [options] <database> <entry>" .IP "edit [options] <database> <entry>"
Edits a database entry. A password can be generated (\fI-g\fP option), or a prompt can be displayed to input the password (\fI-p\fP option). Edits a database entry. A password can be generated (\fI-g\fP option), or a prompt can be displayed to input the password (\fI-p\fP option).
@ -36,11 +39,17 @@ The same password generation options as documented for the generate command can
.IP "estimate [options] [password]" .IP "estimate [options] [password]"
Estimates the entropy of a password. The password to estimate can be provided as a positional argument, or using the standard input. Estimates the entropy of a password. The password to estimate can be provided as a positional argument, or using the standard input.
.IP "exit"
Exits interactive mode. Synonymous with \fIquit\fP.
.IP "export [options] <database>" .IP "export [options] <database>"
Exports the content of a database to standard output in the specified format (defaults to XML). Exports the content of a database to standard output in the specified format (defaults to XML).
.IP "generate [options]" .IP "generate [options]"
Generate a random password. Generates a random password.
.IP "help [command]"
Displays a list of available commands, or detailed information about the specified command.
.IP "locate [options] <database> <term>" .IP "locate [options] <database> <term>"
Locates all the entries that match a specific search term in a database. Locates all the entries that match a specific search term in a database.
@ -57,6 +66,12 @@ Adds a new group to a database.
.IP "mv [options] <database> <entry> <group>" .IP "mv [options] <database> <entry> <group>"
Moves an entry to a new group. Moves an entry to a new group.
.IP "open [options] <database>"
Opens the given database in a shell-style interactive mode. This is useful for performing multiple operations on a single database (e.g. \fIls\fP followed by \fIshow\fP).
.IP "quit"
Exits interactive mode. Synonymous with \fIexit\fP.
.IP "rm [options] <database> <entry>" .IP "rm [options] <database> <entry>"
Removes an entry from a database. If the database has a recycle bin, the entry will be moved there. If the entry is already in the recycle bin, it will be removed permanently. Removes an entry from a database. If the database has a recycle bin, the entry will be moved there. If the entry is already in the recycle bin, it will be removed permanently.
@ -74,16 +89,16 @@ Shows the title, username, password, URL and notes of a database entry. Can also
Displays debugging information. Displays debugging information.
.IP "-k, --key-file <path>" .IP "-k, --key-file <path>"
Specifies a path to a key file for unlocking the database. In a merge operation this option is used to specify the key file path for the first database. Specifies a path to a key file for unlocking the database. In a merge operation this option, is used to specify the key file path for the first database.
.IP "--no-password" .IP "--no-password"
Deactivate password key for the database. Deactivates the password key for the database.
.IP "-y, --yubikey <slot>" .IP "-y, --yubikey <slot>"
Specifies a yubikey slot for unlocking the database. In a merge operation this option is used to specify the yubikey slot for the first database. Specifies a yubikey slot for unlocking the database. In a merge operation this option is used to specify the yubikey slot for the first database.
.IP "-q, --quiet <path>" .IP "-q, --quiet <path>"
Silence password prompt and other secondary outputs. Silences password prompt and other secondary outputs.
.IP "-h, --help" .IP "-h, --help"
Displays help information. Displays help information.
@ -95,19 +110,19 @@ Displays the program version.
.SS "Merge options" .SS "Merge options"
.IP "-d, --dry-run <path>" .IP "-d, --dry-run <path>"
Only print the changes detected by the merge operation. Prints the changes detected by the merge operation without making any changes to the database.
.IP "-f, --key-file-from <path>" .IP "-f, --key-file-from <path>"
Path of the key file for the second database. Sets the path of the key file for the second database.
.IP "--no-password-from" .IP "--no-password-from"
Deactivate password key for the database to merge from. Deactivates password key for the database to merge from.
.IP "--yubikey-from <slot>" .IP "--yubikey-from <slot>"
Yubikey slot for the second database. Yubikey slot for the second database.
.IP "-s, --same-credentials" .IP "-s, --same-credentials"
Use the same credentials for unlocking both database. Uses the same credentials for unlocking both databases.
.SS "Add and edit options" .SS "Add and edit options"
@ -115,34 +130,34 @@ The same password generation options as documented for the generate command can
with those 2 commands when the -g option is set. with those 2 commands when the -g option is set.
.IP "-u, --username <username>" .IP "-u, --username <username>"
Specify the username of the entry. Specifies the username of the entry.
.IP "--url <url>" .IP "--url <url>"
Specify the URL of the entry. Specifies the URL of the entry.
.IP "-p, --password-prompt" .IP "-p, --password-prompt"
Use a password prompt for the entry's password. Uses a password prompt for the entry's password.
.IP "-g, --generate" .IP "-g, --generate"
Generate a new password for the entry. Generates a new password for the entry.
.SS "Edit options" .SS "Edit options"
.IP "-t, --title <title>" .IP "-t, --title <title>"
Specify the title of the entry. Specifies the title of the entry.
.SS "Estimate options" .SS "Estimate options"
.IP "-a, --advanced" .IP "-a, --advanced"
Perform advanced analysis on the password. Performs advanced analysis on the password.
.SS "Analyze options" .SS "Analyze options"
.IP "-H, --hibp <filename>" .IP "-H, --hibp <filename>"
Check if any passwords have been publicly leaked, by comparing against the given Checks if any passwords have been publicly leaked, by comparing against the given
list of password SHA-1 hashes, which must be in "Have I Been Pwned" format. Such list of password SHA-1 hashes, which must be in "Have I Been Pwned" format. Such
files are available from https://haveibeenpwned.com/Passwords; note that they files are available from https://haveibeenpwned.com/Passwords; note that they
are large, and so this operation typically takes some time (minutes up to an are large, and so this operation typically takes some time (minutes up to an
@ -152,31 +167,31 @@ hour or so).
.SS "Clip options" .SS "Clip options"
.IP "-t, --totp" .IP "-t, --totp"
Copy the current TOTP instead of current password to clipboard. Will report an error Copies the current TOTP instead of current password to clipboard. Will report
if no TOTP is configured for the entry. an error if no TOTP is configured for the entry.
.SS "Show options" .SS "Show options"
.IP "-a, --attributes <attribute>..." .IP "-a, --attributes <attribute>..."
Names of the attributes to show. This option can be specified more than once, 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 with each attribute shown one-per-line in the given order. If no attributes are
specified and \fI-t\fP is not specified, a summary of the default attributes is given. specified and \fI-t\fP is not specified, a summary of the default attributes is given.
.IP "-t, --totp" .IP "-t, --totp"
Also show the current TOTP. Will report an error if no TOTP is configured for the Also shows the current TOTP, reporting an error if no TOTP is configured for
entry. the entry.
.SS "Diceware options" .SS "Diceware options"
.IP "-W, --words <count>" .IP "-W, --words <count>"
Desired number of words for the generated passphrase. [Default: 7] Sets the desired number of words for the generated passphrase. [Default: 7]
.IP "-w, --word-list <path>" .IP "-w, --word-list <path>"
Path of the wordlist for the diceware generator. The wordlist must have > 1000 words, Sets the Path of the wordlist for the diceware generator. The wordlist must
otherwise the program will fail. If the wordlist has < 4000 words a warning will have > 1000 words, otherwise the program will fail. If the wordlist has < 4000
be printed to STDERR. words a warning will be printed to STDERR.
.SS "Export options" .SS "Export options"
@ -188,7 +203,7 @@ Format to use when exporting. Available choices are xml or csv. Defaults to xml.
.SS "List options" .SS "List options"
.IP "-R, --recursive" .IP "-R, --recursive"
Recursively list the elements of the group. Recursively lists the elements of the group.
.IP "-f, --flatten" .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. 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.
@ -196,22 +211,22 @@ Flattens the output to single lines. When this option is enabled, subgroups and
.SS "Generate options" .SS "Generate options"
.IP "-L, --length <length>" .IP "-L, --length <length>"
Desired length for the generated password. [Default: 16] Sets the desired length for the generated password. [Default: 16]
.IP "-l --lower" .IP "-l --lower"
Use lowercase characters for the generated password. [Default: Enabled] Uses lowercase characters for the generated password. [Default: Enabled]
.IP "-U --upper" .IP "-U --upper"
Use uppercase characters for the generated password. [Default: Enabled] Uses uppercase characters for the generated password. [Default: Enabled]
.IP "-n --numeric" .IP "-n --numeric"
Use numbers characters for the generated password. [Default: Enabled] Uses numbers characters for the generated password. [Default: Enabled]
.IP "-s --special" .IP "-s --special"
Use special characters for the generated password. [Default: Disabled] Uses special characters for the generated password. [Default: Disabled]
.IP "-e --extended" .IP "-e --extended"
Use extended ASCII characters for the generated password. [Default: Disabled] Uses extended ASCII characters for the generated password. [Default: Disabled]
.IP "-x --exclude <chars>" .IP "-x --exclude <chars>"
Comma-separated list of characters to exclude from the generated password. None is excluded by default. Comma-separated list of characters to exclude from the generated password. None is excluded by default.

View File

@ -16,14 +16,20 @@
*/ */
#include <cstdlib> #include <cstdlib>
#include <memory>
#include <QCommandLineParser> #include <QCommandLineParser>
#include <QCoreApplication> #include <QCoreApplication>
#include <QDir>
#include <QScopedPointer>
#include <QStringList> #include <QStringList>
#include "cli/TextStream.h" #include "cli/TextStream.h"
#include <cli/Command.h> #include <cli/Command.h>
#include "DatabaseCommand.h"
#include "Open.h"
#include "Utils.h"
#include "config-keepassx.h" #include "config-keepassx.h"
#include "core/Bootstrap.h" #include "core/Bootstrap.h"
#include "core/Tools.h" #include "core/Tools.h"
@ -33,6 +39,138 @@
#include <sanitizer/lsan_interface.h> #include <sanitizer/lsan_interface.h>
#endif #endif
#if defined(USE_READLINE)
#include <readline/history.h>
#include <readline/readline.h>
#endif
class LineReader
{
public:
virtual ~LineReader() = default;
virtual QString readLine(QString prompt) = 0;
virtual bool isFinished() = 0;
};
class SimpleLineReader : public LineReader
{
public:
SimpleLineReader()
: inStream(stdin, QIODevice::ReadOnly)
, outStream(stdout, QIODevice::WriteOnly)
, finished(false)
{
}
QString readLine(QString prompt) override
{
outStream << prompt;
outStream.flush();
QString result = inStream.readLine();
if (result.isNull()) {
finished = true;
}
return result;
}
bool isFinished() override
{
return finished;
}
private:
TextStream inStream;
TextStream outStream;
bool finished;
};
#if defined(USE_READLINE)
class ReadlineLineReader : public LineReader
{
public:
ReadlineLineReader()
: finished(false)
{
}
QString readLine(QString prompt) override
{
char* result = readline(prompt.toStdString().c_str());
if (!result) {
finished = true;
return {};
}
add_history(result);
QString qstr(result);
free(result);
return qstr;
}
bool isFinished() override
{
return finished;
}
private:
bool finished;
};
#endif
void enterInteractiveMode(const QStringList& arguments)
{
// Replace command list with interactive version
Commands::setupCommands(true);
Open o;
QStringList openArgs(arguments);
openArgs.removeFirst();
o.execute(openArgs);
QScopedPointer<LineReader> reader;
#if defined(USE_READLINE)
reader.reset(new ReadlineLineReader());
#else
reader.reset(new SimpleLineReader());
#endif
QSharedPointer<Database> currentDatabase(o.currentDatabase);
QString command;
while (true) {
TextStream errorTextStream(Utils::STDERR, QIODevice::WriteOnly);
QString prompt;
if (currentDatabase) {
prompt += currentDatabase->metadata()->name();
if (prompt.isEmpty()) {
prompt += QFileInfo(currentDatabase->filePath()).fileName();
}
}
prompt += "> ";
command = reader->readLine(prompt);
if (reader->isFinished()) {
return;
}
QStringList args = Utils::splitCommandString(command);
if (args.empty()) {
continue;
}
auto cmd = Commands::getCommand(args[0]);
if (!cmd) {
errorTextStream << QObject::tr("Unknown command %1").arg(args[0]) << "\n";
continue;
} else if (cmd->name == "quit" || cmd->name == "exit") {
return;
}
cmd->currentDatabase = currentDatabase;
cmd->execute(args);
currentDatabase = cmd->currentDatabase;
}
}
int main(int argc, char** argv) int main(int argc, char** argv)
{ {
if (!Crypto::init()) { if (!Crypto::init()) {
@ -44,6 +182,7 @@ int main(int argc, char** argv)
QCoreApplication::setApplicationVersion(KEEPASSXC_VERSION); QCoreApplication::setApplicationVersion(KEEPASSXC_VERSION);
Bootstrap::bootstrap(); Bootstrap::bootstrap();
Commands::setupCommands(false);
TextStream out(stdout); TextStream out(stdout);
QStringList arguments; QStringList arguments;
@ -54,7 +193,7 @@ int main(int argc, char** argv)
QString description("KeePassXC command line interface."); QString description("KeePassXC command line interface.");
description = description.append(QObject::tr("\n\nAvailable commands:\n")); description = description.append(QObject::tr("\n\nAvailable commands:\n"));
for (Command* command : Command::getCommands()) { for (auto& command : Commands::getCommands()) {
description = description.append(command->getDescriptionLine()); description = description.append(command->getDescriptionLine());
} }
parser.setApplicationDescription(description); parser.setApplicationDescription(description);
@ -84,9 +223,13 @@ int main(int argc, char** argv)
} }
QString commandName = parser.positionalArguments().at(0); QString commandName = parser.positionalArguments().at(0);
Command* command = Command::getCommand(commandName); if (commandName == "open") {
enterInteractiveMode(arguments);
return EXIT_SUCCESS;
}
if (command == nullptr) { auto command = Commands::getCommand(commandName);
if (!command) {
qCritical("Invalid command %s.", qPrintable(commandName)); qCritical("Invalid command %s.", qPrintable(commandName));
// showHelp exits the application immediately, so we need to set the // showHelp exits the application immediately, so we need to set the
// exit code here. // exit code here.

View File

@ -32,10 +32,7 @@ class YkChallengeResponseKeyCLI : public QObject, public ChallengeResponseKey
public: public:
static QUuid UUID; static QUuid UUID;
explicit YkChallengeResponseKeyCLI(int slot, explicit YkChallengeResponseKeyCLI(int slot, bool blocking, QString messageInteraction, FILE* outputDescriptor);
bool blocking,
QString messageInteraction,
FILE* outputDescriptor);
QByteArray rawKey() const override; QByteArray rawKey() const override;
bool challenge(const QByteArray& challenge) override; bool challenge(const QByteArray& challenge) override;

View File

@ -42,10 +42,12 @@
#include "cli/Estimate.h" #include "cli/Estimate.h"
#include "cli/Export.h" #include "cli/Export.h"
#include "cli/Generate.h" #include "cli/Generate.h"
#include "cli/Help.h"
#include "cli/List.h" #include "cli/List.h"
#include "cli/Locate.h" #include "cli/Locate.h"
#include "cli/Merge.h" #include "cli/Merge.h"
#include "cli/Move.h" #include "cli/Move.h"
#include "cli/Open.h"
#include "cli/Remove.h" #include "cli/Remove.h"
#include "cli/RemoveGroup.h" #include "cli/RemoveGroup.h"
#include "cli/Show.h" #include "cli/Show.h"
@ -62,6 +64,8 @@
QTEST_MAIN(TestCli) QTEST_MAIN(TestCli)
QSharedPointer<Database> globalCurrentDatabase;
void TestCli::initTestCase() void TestCli::initTestCase()
{ {
QVERIFY(Crypto::init()); QVERIFY(Crypto::init());
@ -173,24 +177,59 @@ QSharedPointer<Database> TestCli::readTestDatabase() const
return db; return db;
} }
void TestCli::testCommand() void TestCli::testBatchCommands()
{ {
QCOMPARE(Command::getCommands().size(), 17); Commands::setupCommands(false);
QVERIFY(Command::getCommand("add")); QVERIFY(Commands::getCommand("add"));
QVERIFY(Command::getCommand("analyze")); QVERIFY(Commands::getCommand("analyze"));
QVERIFY(Command::getCommand("clip")); QVERIFY(Commands::getCommand("clip"));
QVERIFY(Command::getCommand("create")); QVERIFY(Commands::getCommand("close"));
QVERIFY(Command::getCommand("diceware")); QVERIFY(Commands::getCommand("create"));
QVERIFY(Command::getCommand("edit")); QVERIFY(Commands::getCommand("diceware"));
QVERIFY(Command::getCommand("estimate")); QVERIFY(Commands::getCommand("edit"));
QVERIFY(Command::getCommand("export")); QVERIFY(Commands::getCommand("estimate"));
QVERIFY(Command::getCommand("generate")); QVERIFY(Commands::getCommand("export"));
QVERIFY(Command::getCommand("locate")); QVERIFY(Commands::getCommand("generate"));
QVERIFY(Command::getCommand("ls")); QVERIFY(Commands::getCommand("help"));
QVERIFY(Command::getCommand("merge")); QVERIFY(Commands::getCommand("locate"));
QVERIFY(Command::getCommand("rm")); QVERIFY(Commands::getCommand("ls"));
QVERIFY(Command::getCommand("show")); QVERIFY(Commands::getCommand("merge"));
QVERIFY(!Command::getCommand("doesnotexist")); QVERIFY(Commands::getCommand("mkdir"));
QVERIFY(Commands::getCommand("mv"));
QVERIFY(Commands::getCommand("open"));
QVERIFY(Commands::getCommand("rm"));
QVERIFY(Commands::getCommand("rmdir"));
QVERIFY(Commands::getCommand("show"));
QVERIFY(!Commands::getCommand("doesnotexist"));
QCOMPARE(Commands::getCommands().size(), 20);
}
void TestCli::testInteractiveCommands()
{
Commands::setupCommands(true);
QVERIFY(Commands::getCommand("add"));
QVERIFY(Commands::getCommand("analyze"));
QVERIFY(Commands::getCommand("clip"));
QVERIFY(Commands::getCommand("close"));
QVERIFY(Commands::getCommand("create"));
QVERIFY(Commands::getCommand("diceware"));
QVERIFY(Commands::getCommand("edit"));
QVERIFY(Commands::getCommand("estimate"));
QVERIFY(Commands::getCommand("exit"));
QVERIFY(Commands::getCommand("generate"));
QVERIFY(Commands::getCommand("help"));
QVERIFY(Commands::getCommand("locate"));
QVERIFY(Commands::getCommand("ls"));
QVERIFY(Commands::getCommand("merge"));
QVERIFY(Commands::getCommand("mkdir"));
QVERIFY(Commands::getCommand("mv"));
QVERIFY(Commands::getCommand("open"));
QVERIFY(Commands::getCommand("quit"));
QVERIFY(Commands::getCommand("rm"));
QVERIFY(Commands::getCommand("rmdir"));
QVERIFY(Commands::getCommand("show"));
QVERIFY(!Commands::getCommand("doesnotexist"));
QCOMPARE(Commands::getCommands().size(), 21);
} }
void TestCli::testAdd() void TestCli::testAdd()
@ -1750,3 +1789,88 @@ void TestCli::testYubiKeyOption()
QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); QCOMPARE(m_stdoutFile->readAll(), QByteArray(""));
QCOMPARE(m_stderrFile->readAll().split(':').at(0), QByteArray("Invalid YubiKey slot 3\n")); QCOMPARE(m_stderrFile->readAll().split(':').at(0), QByteArray("Invalid YubiKey slot 3\n"));
} }
namespace
{
void expectParseResult(const QString& input, const QStringList& expectedOutput)
{
QStringList result = Utils::splitCommandString(input);
QCOMPARE(result.size(), expectedOutput.size());
for (int i = 0; i < expectedOutput.size(); ++i) {
QCOMPARE(result[i], expectedOutput[i]);
}
}
} // namespace
void TestCli::testCommandParsing_data()
{
QTest::addColumn<QString>("input");
QTest::addColumn<QStringList>("expectedOutput");
QTest::newRow("basic") << "hello world" << QStringList({"hello", "world"});
QTest::newRow("basic escaping") << "hello\\ world" << QStringList({"hello world"});
QTest::newRow("quoted string") << "\"hello world\"" << QStringList({"hello world"});
QTest::newRow("multiple params") << "show Passwords/Internet" << QStringList({"show", "Passwords/Internet"});
QTest::newRow("quoted string inside param")
<< R"(ls foo\ bar\ baz"quoted")" << QStringList({"ls", "foo bar baz\"quoted\""});
QTest::newRow("multiple whitespace") << "hello world" << QStringList({"hello", "world"});
QTest::newRow("single slash char") << "\\" << QStringList({"\\"});
QTest::newRow("double backslash entry name") << "show foo\\\\\\\\bar" << QStringList({"show", "foo\\\\bar"});
}
void TestCli::testCommandParsing()
{
QFETCH(QString, input);
QFETCH(QStringList, expectedOutput);
expectParseResult(input, expectedOutput);
}
void TestCli::testOpen()
{
Open o;
Utils::Test::setNextPassword("a");
o.execute({"open", m_dbFile->fileName()});
m_stdoutFile->reset();
QVERIFY(o.currentDatabase);
List l;
// Set a current database, simulating interactive mode.
l.currentDatabase = o.currentDatabase;
l.execute({"ls"});
m_stdoutFile->reset();
QByteArray expectedOutput("Sample Entry\n"
"General/\n"
"Windows/\n"
"Network/\n"
"Internet/\n"
"eMail/\n"
"Homebanking/\n");
QByteArray actualOutput = m_stdoutFile->readAll();
actualOutput.truncate(expectedOutput.length());
QCOMPARE(actualOutput, expectedOutput);
}
void TestCli::testHelp()
{
Help h;
Commands::setupCommands(false);
{
h.execute({"help"});
m_stderrFile->reset();
QString output(m_stderrFile->readAll());
QVERIFY(output.contains(QObject::tr("Available commands")));
}
{
List l;
h.execute({"help", "ls"});
m_stderrFile->reset();
QString output(m_stderrFile->readAll());
QVERIFY(output.contains(l.description));
}
}

View File

@ -43,11 +43,13 @@ private slots:
void cleanup(); void cleanup();
void cleanupTestCase(); void cleanupTestCase();
void testCommand(); void testBatchCommands();
void testAdd(); void testAdd();
void testAddGroup(); void testAddGroup();
void testAnalyze(); void testAnalyze();
void testClip(); void testClip();
void testCommandParsing_data();
void testCommandParsing();
void testCreate(); void testCreate();
void testDiceware(); void testDiceware();
void testEdit(); void testEdit();
@ -58,10 +60,13 @@ private slots:
void testGenerate(); void testGenerate();
void testKeyFileOption(); void testKeyFileOption();
void testNoPasswordOption(); void testNoPasswordOption();
void testHelp();
void testInteractiveCommands();
void testList(); void testList();
void testLocate(); void testLocate();
void testMerge(); void testMerge();
void testMove(); void testMove();
void testOpen();
void testRemove(); void testRemove();
void testRemoveGroup(); void testRemoveGroup();
void testRemoveQuiet(); void testRemoveQuiet();

View File

@ -1214,7 +1214,6 @@ void TestMerge::testCustomData()
QCOMPARE(dbDestination->metadata()->customData()->value("key3"), QCOMPARE(dbDestination->metadata()->customData()->value("key3"),
QString("newValue")); // Old value should be replaced QString("newValue")); // Old value should be replaced
// Merging again should not do anything if the values are the same. // Merging again should not do anything if the values are the same.
m_clock->advanceSecond(1); m_clock->advanceSecond(1);
dbSource->metadata()->customData()->set("key3", "oldValue"); dbSource->metadata()->customData()->set("key3", "oldValue");
@ -1223,7 +1222,6 @@ void TestMerge::testCustomData()
QStringList changes2 = merger2.merge(); QStringList changes2 = merger2.merge();
QVERIFY(changes2.isEmpty()); QVERIFY(changes2.isEmpty());
Merger merger3(dbSource2.data(), dbDestination2.data()); Merger merger3(dbSource2.data(), dbDestination2.data());
merger3.merge(); merger3.merge();

View File

@ -26,8 +26,8 @@
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QLineEdit> #include <QLineEdit>
#include <QPushButton> #include <QPushButton>
#include <QToolBar>
#include <QTableView> #include <QTableView>
#include <QToolBar>
#include "config-keepassx-tests.h" #include "config-keepassx-tests.h"
#include "core/Bootstrap.h" #include "core/Bootstrap.h"