Skip to content

Commit

Permalink
Add interactive session mode to keepassxc-cli.
Browse files Browse the repository at this point in the history
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 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.

* Fixes keepassxreboot#3224.
* Add help command.
- This change eliminates the use of the QCommandLineParser::addHelpOption
builtin, because that causes the program to exit if the option is specified, which is not what we want for an interactive program.

* Allow users to open a new database even if one is already open
  • Loading branch information
sjamesr committed Sep 3, 2019
1 parent 0a3b19e commit 94e690f
Show file tree
Hide file tree
Showing 17 changed files with 603 additions and 64 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- Add 'Monospaced font' option to the Notes field [#3321]
- Drop to background when copy feature [#3253]
- Fix password generator issues with special characters [#3303]
- Add interactive shell mode to keepassxc-cli via 'keepassxc-cli open' [#3426]

2.4.3 (2019-06-12)
=========================
Expand Down
50 changes: 50 additions & 0 deletions cmake/FindReadline.cmake
Original file line number Diff line number Diff line change
@@ -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
)

9 changes: 9 additions & 0 deletions src/cli/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,24 @@ set(cli_SOURCES
Estimate.cpp
Extract.cpp
Generate.cpp
Help.cpp
List.cpp
Locate.cpp
Merge.cpp
Open.cpp
Remove.cpp
Show.cpp)

add_library(cli STATIC ${cli_SOURCES})
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)
target_link_libraries(keepassxc-cli
cli
Expand Down
71 changes: 53 additions & 18 deletions src/cli/Command.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,23 @@
#include "Estimate.h"
#include "Extract.h"
#include "Generate.h"
#include "Help.h"
#include "List.h"
#include "Locate.h"
#include "Merge.h"
#include "Open.h"
#include "Remove.h"
#include "Show.h"
#include "TextStream.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 =
QCommandLineOption(QStringList() << "q"
<< "quiet",
Expand All @@ -55,6 +64,7 @@ const QCommandLineOption Command::NoPasswordOption =
QMap<QString, Command*> commands;

Command::Command()
: currentDatabase(nullptr)
{
options.append(Command::QuietOption);
}
Expand All @@ -74,32 +84,55 @@ QString Command::getDescriptionLine()
return response;
}

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

QString Command::getHelpText()
{
return buildParser(this)->helpText().replace("[options]", name + " [options]");
}

QSharedPointer<QCommandLineParser> Command::getCommandLineParser(const QStringList& arguments)
{
TextStream errorTextStream(Utils::STDERR, QIODevice::WriteOnly);
QSharedPointer<QCommandLineParser> parser = buildParser(this);

QSharedPointer<QCommandLineParser> parser = QSharedPointer<QCommandLineParser>(new QCommandLineParser());
parser->setApplicationDescription(description);
for (const CommandLineArgument& positionalArgument : positionalArguments) {
parser->addPositionalArgument(
positionalArgument.name, positionalArgument.description, positionalArgument.syntax);
}
for (const CommandLineArgument& optionalArgument : optionalArguments) {
parser->addPositionalArgument(optionalArgument.name, optionalArgument.description, optionalArgument.syntax);
if (!parser->parse(arguments)) {
errorTextStream << parser->errorText() << "\n\n";
errorTextStream << getHelpText();
return {};
}
for (const QCommandLineOption& option : options) {
parser->addOption(option);
}
parser->addHelpOption();
parser->process(arguments);

if (parser->positionalArguments().size() < positionalArguments.size()) {
errorTextStream << parser->helpText().replace("[options]", name.append(" [options]"));
return QSharedPointer<QCommandLineParser>(nullptr);
errorTextStream << getHelpText();
return {};
}
if (parser->positionalArguments().size() > (positionalArguments.size() + optionalArguments.size())) {
errorTextStream << parser->helpText().replace("[options]", name.append(" [options]"));
return QSharedPointer<QCommandLineParser>(nullptr);
errorTextStream << getHelpText();
return {};
}
if (parser->isSet(HelpOption)) {
errorTextStream << getHelpText();
return {};
}
return parser;
}
Expand All @@ -116,9 +149,11 @@ void populateCommands()
commands.insert(QString("estimate"), new Estimate());
commands.insert(QString("extract"), new Extract());
commands.insert(QString("generate"), new Generate());
commands.insert(QString("help"), new Help());
commands.insert(QString("locate"), new Locate());
commands.insert(QString("ls"), new List());
commands.insert(QString("merge"), new Merge());
commands.insert(QString("open"), new Open());
commands.insert(QString("rm"), new Remove());
commands.insert(QString("show"), new Show());
}
Expand Down
4 changes: 4 additions & 0 deletions src/cli/Command.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,19 @@ class Command
virtual int execute(const QStringList& arguments) = 0;
QString name;
QString description;
QSharedPointer<Database> currentDatabase;
QList<CommandLineArgument> positionalArguments;
QList<CommandLineArgument> optionalArguments;
QList<QCommandLineOption> options;

QString getDescriptionLine();
QSharedPointer<QCommandLineParser> getCommandLineParser(const QStringList& arguments);
QString getHelpText();

static QList<Command*> getCommands();
static Command* getCommand(const QString& commandName);

static const QCommandLineOption HelpOption;
static const QCommandLineOption QuietOption;
static const QCommandLineOption KeyFileOption;
static const QCommandLineOption NoPasswordOption;
Expand Down
7 changes: 4 additions & 3 deletions src/cli/Create.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,17 @@ int Create::execute(const QStringList& arguments)
return EXIT_FAILURE;
}

Database db;
db.setKey(key);
QSharedPointer<Database> db(new Database);
db->setKey(key);

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;
return EXIT_FAILURE;
}

out << QObject::tr("Successfully created new database.") << endl;
currentDatabase = db;
return EXIT_SUCCESS;
}

Expand Down
29 changes: 21 additions & 8 deletions src/cli/DatabaseCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,32 @@ DatabaseCommand::DatabaseCommand()

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()) {
return EXIT_FAILURE;
}

const QStringList args = parser->positionalArguments();
auto db = Utils::unlockDatabase(args.at(0),
!parser->isSet(Command::NoPasswordOption),
parser->value(Command::KeyFileOption),
parser->isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT,
Utils::STDERR);
QStringList args = parser->positionalArguments();
auto db = currentDatabase;
if (!db) {
return EXIT_FAILURE;
// 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->value(Command::KeyFileOption),
parser->isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT,
Utils::STDERR);
if (!db) {
return EXIT_FAILURE;
}
}

return executeWithDatabase(db, parser);
Expand Down
43 changes: 43 additions & 0 deletions src/cli/Help.cpp
Original file line number Diff line number Diff line change
@@ -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);
Command* command = arguments.size() > 1 ? Command::getCommand(arguments.at(1)) : nullptr;
if (command) {
out << command->getHelpText();
} else {
out << "\n\n" << QObject::tr("Available commands:") << "\n";
for (Command* c : Command::getCommands()) {
out << c->getDescriptionLine();
}
}
return EXIT_SUCCESS;
}
31 changes: 31 additions & 0 deletions src/cli/Help.h
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 94e690f

Please sign in to comment.