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.
  • Loading branch information
sjamesr committed Aug 7, 2019
1 parent 7cbcea1 commit 9fc8898
Show file tree
Hide file tree
Showing 14 changed files with 398 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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 opn' [#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
)

8 changes: 8 additions & 0 deletions src/cli/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,20 @@ set(cli_SOURCES
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
8 changes: 5 additions & 3 deletions src/cli/Command.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include "List.h"
#include "Locate.h"
#include "Merge.h"
#include "Open.h"
#include "Remove.h"
#include "Show.h"
#include "TextStream.h"
Expand All @@ -54,7 +55,7 @@ const QCommandLineOption Command::NoPasswordOption =

QMap<QString, Command*> commands;

Command::Command()
Command::Command() : currentDatabase(nullptr)
{
options.append(Command::QuietOption);
}
Expand Down Expand Up @@ -94,11 +95,11 @@ QSharedPointer<QCommandLineParser> Command::getCommandLineParser(const QStringLi
parser->process(arguments);

if (parser->positionalArguments().size() < positionalArguments.size()) {
errorTextStream << parser->helpText().replace("[options]", name.append(" [options]"));
errorTextStream << parser->helpText().replace("[options]", name + " [options]");
return QSharedPointer<QCommandLineParser>(nullptr);
}
if (parser->positionalArguments().size() > (positionalArguments.size() + optionalArguments.size())) {
errorTextStream << parser->helpText().replace("[options]", name.append(" [options]"));
errorTextStream << parser->helpText().replace("[options]", name + " [options]");
return QSharedPointer<QCommandLineParser>(nullptr);
}
return parser;
Expand All @@ -119,6 +120,7 @@ void populateCommands()
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
1 change: 1 addition & 0 deletions src/cli/Command.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ 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;
Expand Down
30 changes: 22 additions & 8 deletions src/cli/DatabaseCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,33 @@ DatabaseCommand::DatabaseCommand()

int DatabaseCommand::execute(const QStringList& arguments)
{
QSharedPointer<QCommandLineParser> parser = getCommandLineParser(arguments);
QStringList amendedArgs(arguments);
if (currentDatabase) {
amendedArgs.prepend(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
37 changes: 37 additions & 0 deletions src/cli/Open.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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 <QtCore/QCommandLineParser>

#include "DatabaseCommand.h"
#include "TextStream.h"
#include "Utils.h"

Open::Open()
{
name = QString("open");
description = QObject::tr("Open a database.");
}

int Open::executeWithDatabase(QSharedPointer<Database> db, QSharedPointer<QCommandLineParser> parser)
{
Q_UNUSED(parser)
currentDatabase = db;
return EXIT_SUCCESS;
}
30 changes: 30 additions & 0 deletions src/cli/Open.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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 executeWithDatabase(QSharedPointer<Database> db, QSharedPointer<QCommandLineParser> parser) override;
};

#endif // KEEPASSXC_OPEN_H
37 changes: 37 additions & 0 deletions src/cli/Utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,41 @@ namespace Utils
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 inside_quotes = 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 (!inside_quotes && (c == ' ' || c == '\t')) {
if (!cur.isEmpty()) {
result.append(cur);
cur.clear();
}
} else if (c == '"' && (inside_quotes || i == 0 || command[i - 1].isSpace())) {
inside_quotes = !inside_quotes;
} else {
cur.append(c);
}
}

if (!cur.isEmpty()) {
result.append(cur);
}

return result;
}


} // namespace Utils
2 changes: 2 additions & 0 deletions src/cli/Utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ namespace Utils
FILE* outputDescriptor = STDOUT,
FILE* errorDescriptor = STDERR);

QStringList splitCommandString(const QString& command);

namespace Test
{
void setNextPassword(const QString& password);
Expand Down
3 changes: 3 additions & 0 deletions src/cli/keepassxc-cli.1
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ Lists the contents of a group in a database. If no group is specified, it will d
.IP "merge [options] <database1> <database2>"
Merges two databases together. The first database file is going to be replaced by the result of the merge, for that reason it is advisable to keep a backup of the two database files before attempting a merge. In the case that both databases make use of the same credentials, the \fI--same-credentials\fP or \fI-s\fP option can be used.

.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 "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.

Expand Down
Loading

0 comments on commit 9fc8898

Please sign in to comment.