aboutsummaryrefslogtreecommitdiffstats
path: root/SQLiteStudio3/sqlitestudiocli
diff options
context:
space:
mode:
authorLibravatarUnit 193 <unit193@ubuntu.com>2014-12-06 17:33:25 -0500
committerLibravatarUnit 193 <unit193@ubuntu.com>2014-12-06 17:33:25 -0500
commit7167ce41b61d2ba2cdb526777a4233eb84a3b66a (patch)
treea35c14143716e1f2c98f808c81f89426045a946f /SQLiteStudio3/sqlitestudiocli
Imported Upstream version 2.99.6upstream/2.99.6
Diffstat (limited to 'SQLiteStudio3/sqlitestudiocli')
-rw-r--r--SQLiteStudio3/sqlitestudiocli/cli.cpp321
-rw-r--r--SQLiteStudio3/sqlitestudiocli/cli.h62
-rw-r--r--SQLiteStudio3/sqlitestudiocli/cli_config.cpp55
-rw-r--r--SQLiteStudio3/sqlitestudiocli/cli_config.h38
-rw-r--r--SQLiteStudio3/sqlitestudiocli/clicommandexecutor.cpp24
-rw-r--r--SQLiteStudio3/sqlitestudiocli/clicommandexecutor.h26
-rw-r--r--SQLiteStudio3/sqlitestudiocli/clicommandsyntax.cpp433
-rw-r--r--SQLiteStudio3/sqlitestudiocli/clicommandsyntax.h97
-rw-r--r--SQLiteStudio3/sqlitestudiocli/clicompleter.cpp173
-rw-r--r--SQLiteStudio3/sqlitestudiocli/clicompleter.h30
-rw-r--r--SQLiteStudio3/sqlitestudiocli/climsghandler.cpp38
-rw-r--r--SQLiteStudio3/sqlitestudiocli/climsghandler.h9
-rw-r--r--SQLiteStudio3/sqlitestudiocli/cliutils.cpp99
-rw-r--r--SQLiteStudio3/sqlitestudiocli/cliutils.h22
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommand.cpp334
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommand.h97
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandadd.cpp36
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandadd.h15
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandcd.cpp34
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandcd.h15
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandclose.cpp51
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandclose.h15
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommanddblist.cpp86
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommanddblist.h15
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommanddesc.cpp26
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommanddesc.h16
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommanddir.cpp50
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommanddir.h15
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandexit.cpp26
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandexit.h15
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandfactory.cpp85
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandfactory.h25
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandhelp.cpp86
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandhelp.h19
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandhistory.cpp81
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandhistory.h26
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandmode.cpp65
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandmode.h21
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandnullvalue.cpp32
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandnullvalue.h15
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandopen.cpp84
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandopen.h15
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandpwd.cpp28
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandpwd.h15
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandremove.cpp51
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandremove.h15
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandsql.cpp506
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandsql.h66
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandtables.cpp79
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandtables.h21
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandtree.cpp154
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommandtree.h31
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommanduse.cpp64
-rw-r--r--SQLiteStudio3/sqlitestudiocli/commands/clicommanduse.h15
-rw-r--r--SQLiteStudio3/sqlitestudiocli/main.cpp92
-rw-r--r--SQLiteStudio3/sqlitestudiocli/sqlitestudiocli.pro95
56 files changed, 4059 insertions, 0 deletions
diff --git a/SQLiteStudio3/sqlitestudiocli/cli.cpp b/SQLiteStudio3/sqlitestudiocli/cli.cpp
new file mode 100644
index 0000000..a663e84
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/cli.cpp
@@ -0,0 +1,321 @@
+#include "cli.h"
+#include "services/config.h"
+#include "cli_config.h"
+#include "services/dbmanager.h"
+#include "commands/clicommandfactory.h"
+#include "commands/clicommand.h"
+#include "qio.h"
+#include "common/utils.h"
+#include "common/utils_sql.h"
+#include "climsghandler.h"
+#include "clicompleter.h"
+#include <QCoreApplication>
+#include <QThread>
+#include <QFile>
+#include <QSet>
+#include <QStringList>
+#include <QLibrary>
+
+#if defined(Q_OS_WIN32)
+#include "readline.h"
+#elif defined(Q_OS_UNIX)
+#include <readline/readline.h>
+#include <readline/history.h>
+#endif
+
+CLI* CLI::instance = nullptr;
+
+CLI::CLI(QObject* parent) :
+ QObject(parent)
+{
+ setCurrentDb(nullptr);
+
+ using_history();
+
+#ifdef Q_OS_UNIX
+ history_base = 0; // for some reason this was set to 1 under Unix, making 1st history entry to be always ommited
+#endif
+
+
+ loadHistory();
+ CliCompleter::getInstance()->init(this);
+}
+
+CLI::~CLI()
+{
+}
+
+CLI* CLI::getInstance()
+{
+ if (!instance)
+ instance = new CLI();
+
+ return instance;
+}
+
+void CLI::start()
+{
+ thread = new QThread(this);
+
+ CliCommandFactory::init();
+
+ connect(thread, &QThread::started, this, &CLI::doWork);
+ connect(thread, &QThread::finished, this, &CLI::done);
+ this->moveToThread(thread);
+
+ if (!getCurrentDb()) // it could be set by openDbFile() from main().
+ {
+ Db* db = DBLIST->getByName(CFG_CLI.Console.DefaultDatabase.get());
+ if (db)
+ {
+ setCurrentDb(db);
+ }
+ else
+ {
+ QList<Db*> dbList = DBLIST->getDbList();
+ if (dbList.size() > 0)
+ setCurrentDb(dbList[0]);
+ else
+ setCurrentDb(nullptr);
+ }
+ }
+
+ qOut << QString("\n%1 (%2)\n------------------------\n\n").arg(QCoreApplication::applicationName()).arg(QCoreApplication::applicationVersion());
+ qOut.flush();
+
+ if (getCurrentDb())
+ qOut << tr("Current database: %1").arg(getCurrentDb()->getName()) << "\n";
+ else
+ qOut << tr("No current working database is set.") << "\n";
+
+ qOut << tr("Type %1 for help").arg(".help") << "\n\n";
+
+ qOut.flush();
+
+ thread->start();
+}
+
+void CLI::setCurrentDb(Db* db)
+{
+ currentDb = db;
+ if (db && !db->isOpen())
+ db->open();
+}
+
+Db* CLI::getCurrentDb() const
+{
+ return currentDb;
+}
+
+void CLI::exit()
+{
+ doExit = true;
+}
+
+QStringList CLI::getHistory() const
+{
+ QStringList cfgHistory;
+
+ int length = historyLength();
+
+ QString line;
+ HIST_ENTRY* entry = nullptr;
+ for (int i = 0; i < length; i++)
+ {
+ entry = history_get(i);
+ if (!entry)
+ {
+ qWarning() << "Null history entry for i =" << i;
+ continue;
+ }
+
+ line = QString::fromLocal8Bit(entry->line);
+ if (line.isEmpty())
+ continue;
+
+ cfgHistory << line;
+ }
+ return cfgHistory;
+}
+
+void CLI::println(const QString &msg)
+{
+ qOut << msg << "\n";
+ qOut.flush();
+}
+
+int CLI::historyLength() const
+{
+#if defined(Q_OS_WIN)
+ return history_length();
+#elif defined(Q_OS_UNIX)
+ return history_length;
+#endif
+}
+
+void CLI::waitForExecution()
+{
+ executionFinished = false;
+ while (!executionFinished)
+ {
+ qApp->processEvents();
+ QThread::usleep(20);
+ }
+}
+
+bool CLI::isComplete(const QString& contents) const
+{
+ if (contents.startsWith(CFG_CLI.Console.CommandPrefixChar.get()))
+ return true;
+
+ Dialect dialect = Dialect::Sqlite3;
+ if (getCurrentDb())
+ dialect = getCurrentDb()->getDialect();
+
+ bool complete = true;
+ splitQueries(contents, dialect, true, &complete);
+ return complete;
+}
+
+void CLI::loadHistory()
+{
+ foreach (const QString& line, CFG->getCliHistory())
+ {
+ if (!line.isEmpty())
+ add_history(line.toLocal8Bit().data());
+ }
+}
+
+void CLI::addHistory(const QString& text)
+{
+ if (text == lastHistoryEntry)
+ return;
+
+ CFG->addCliHistory(text);
+
+ add_history(text.toLocal8Bit().data());
+ if (historyLength() > CFG_CORE.Console.HistorySize.get())
+#ifdef Q_OS_OSX
+ free(remove_history(0));
+#else
+ free_history_entry(remove_history(0));
+#endif
+
+ lastHistoryEntry = text;
+}
+
+QString CLI::getLine() const
+{
+ return line;
+}
+
+void CLI::applyHistoryLimit()
+{
+ CFG->applyCliHistoryLimit();
+ while (historyLength() > CFG_CORE.Console.HistorySize.get())
+#ifdef Q_OS_OSX
+ free(remove_history(0));
+#else
+ free_history_entry(remove_history(0));
+#endif
+}
+
+void CLI::openDbFile(const QString& path)
+{
+ QString name = DBLIST->quickAddDb(path, QHash<QString,QVariant>());
+ if (name.isNull())
+ {
+ println(tr("Could not add database %1 to list.").arg(path));
+ return;
+ }
+ Db* db = DBLIST->getByName(name);
+ setCurrentDb(db);
+}
+
+void CLI::doWork()
+{
+ static const QString prompt = "%1>";
+
+ CliCommand* cliCommand = nullptr;
+ QString cmd;
+ QStringList cmdArgs;
+ QString cPrompt;
+ char *cline = nullptr;
+ while (!doExit)
+ {
+ line.clear();
+
+ while (!doExit && (line.isEmpty() || !isComplete(line)))
+ {
+ if (getCurrentDb())
+ {
+ cPrompt = getCurrentDb()->getName();
+ if (!getCurrentDb()->isOpen())
+ cPrompt += " ["+tr("closed")+"]";
+
+ cPrompt = prompt.arg(cPrompt);
+ }
+ else
+ cPrompt = prompt.arg("");
+
+ if (!line.isEmpty())
+ {
+ cPrompt = pad("->", -cPrompt.length(), ' ');
+ line += "\n";
+ }
+
+ cline = readline(cPrompt.toLocal8Bit().data());
+
+ line += cline;
+ free(cline);
+ }
+ addHistory(line);
+
+ if (line.startsWith(CFG_CLI.Console.CommandPrefixChar.get()))
+ {
+ cmdArgs = tokenizeArgs(line.mid(1));
+ cmd = cmdArgs.takeAt(0);
+ cliCommand = CliCommandFactory::getCommand(cmd);
+ if (!cliCommand)
+ {
+ println("No such command.");
+ continue;
+ }
+ }
+ else
+ {
+ cliCommand = CliCommandFactory::getCommand("query");
+ cmdArgs.clear();
+ cmdArgs << line;
+ }
+
+ cliCommand->setup(this);
+ if (!cliCommand->parseArgs(cmdArgs))
+ {
+ delete cliCommand;
+ continue;
+ }
+
+ cliCommand->moveToThread(qApp->thread());
+ emit execCommand(cliCommand);
+ waitForExecution();
+ }
+
+ thread->quit();
+}
+
+void CLI::done()
+{
+ qApp->exit();
+}
+
+void CLI::executionComplete()
+{
+ executionFinished = true;
+}
+
+void CLI::clearHistory()
+{
+ clear_history();
+ CFG->clearCliHistory();
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/cli.h b/SQLiteStudio3/sqlitestudiocli/cli.h
new file mode 100644
index 0000000..6f7c927
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/cli.h
@@ -0,0 +1,62 @@
+#ifndef CLI_H
+#define CLI_H
+
+#include "db/db.h"
+#include <QObject>
+#include <QTextStream>
+#include <QStringList>
+#include <QTime>
+
+class QThread;
+class QFile;
+class DbManager;
+class CliCommand;
+
+class CLI : public QObject
+{
+ Q_OBJECT
+
+ public:
+ ~CLI();
+
+ static CLI* getInstance();
+
+ void start();
+ void setCurrentDb(Db* db);
+ Db* getCurrentDb() const;
+ void exit();
+ QStringList getHistory() const;
+ QString getLine() const;
+ void applyHistoryLimit();
+
+ private:
+ explicit CLI(QObject* parent = nullptr);
+
+ void waitForExecution();
+ bool isComplete(const QString& contents) const;
+ void loadHistory();
+ void addHistory(const QString& text);
+ void println(const QString& msg = QString());
+ int historyLength() const;
+
+ static CLI* instance;
+
+ QString lastHistoryEntry;
+ QThread* thread = nullptr;
+ Db* currentDb = nullptr;
+ bool executionFinished = false;
+ bool doExit = false;
+ QString line;
+
+ signals:
+ void execCommand(CliCommand* cmd);
+
+ public slots:
+ void doWork();
+ void done();
+ void executionComplete();
+ void clearHistory();
+ void openDbFile(const QString& path);
+};
+
+#endif // CLI_H
diff --git a/SQLiteStudio3/sqlitestudiocli/cli_config.cpp b/SQLiteStudio3/sqlitestudiocli/cli_config.cpp
new file mode 100644
index 0000000..93c2b53
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/cli_config.cpp
@@ -0,0 +1,55 @@
+#include "cli_config.h"
+
+CFG_DEFINE(Cli)
+
+CliResultsDisplay::Mode CliResultsDisplay::mode(const QString& mode)
+{
+ if (mode == "ROW")
+ return ROW;
+
+ if (mode == "FIXED")
+ return FIXED;
+
+ if (mode == "COLUMNS")
+ return COLUMNS;
+
+ return CLASSIC;
+}
+
+QString CliResultsDisplay::mode(CliResultsDisplay::Mode mode)
+{
+ switch (mode)
+ {
+ case ROW:
+ return "ROW";
+ case FIXED:
+ return "FIXED";
+ case CLASSIC:
+ return "CLASSIC";
+ case COLUMNS:
+ return "COLUMNS";
+ }
+ return "CLASSIC";
+}
+
+
+void CliResultsDisplay::staticInit()
+{
+ qRegisterMetaType<CliResultsDisplay::Mode>();
+ qRegisterMetaTypeStreamOperators<CliResultsDisplay::Mode>();
+}
+
+
+QDataStream& operator<<(QDataStream& out, const CliResultsDisplay::Mode& mode)
+{
+ out << static_cast<int>(mode);
+ return out;
+}
+
+QDataStream& operator>>(QDataStream& in, CliResultsDisplay::Mode& mode)
+{
+ int modeEnum;
+ in >> modeEnum;
+ mode = static_cast<CliResultsDisplay::Mode>(modeEnum);
+ return in;
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/cli_config.h b/SQLiteStudio3/sqlitestudiocli/cli_config.h
new file mode 100644
index 0000000..7827462
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/cli_config.h
@@ -0,0 +1,38 @@
+#ifndef CLI_CONFIG_H
+#define CLI_CONFIG_H
+
+#include "config_builder.h"
+
+namespace CliResultsDisplay
+{
+ enum Mode
+ {
+ CLASSIC = 0,
+ FIXED = 1,
+ ROW = 2,
+ COLUMNS = 3
+ };
+
+ Mode mode(const QString& mode);
+ QString mode(Mode mode);
+ void staticInit();
+
+}
+
+QDataStream &operator<<(QDataStream &out, const CliResultsDisplay::Mode& mode);
+QDataStream &operator>>(QDataStream &in, CliResultsDisplay::Mode& mode);
+
+Q_DECLARE_METATYPE(CliResultsDisplay::Mode)
+
+CFG_CATEGORIES(Cli,
+ CFG_CATEGORY(Console,
+ CFG_ENTRY(QString, DefaultDatabase, QString())
+ CFG_ENTRY(QString, CommandPrefixChar, ".")
+ CFG_ENTRY(CliResultsDisplay::Mode, ResultsDisplayMode, CliResultsDisplay::CLASSIC)
+ CFG_ENTRY(QString, NullValue, "")
+ )
+)
+
+#define CFG_CLI CFG_INSTANCE(Cli)
+
+#endif // CLI_CONFIG_H
diff --git a/SQLiteStudio3/sqlitestudiocli/clicommandexecutor.cpp b/SQLiteStudio3/sqlitestudiocli/clicommandexecutor.cpp
new file mode 100644
index 0000000..6a3072e
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/clicommandexecutor.cpp
@@ -0,0 +1,24 @@
+#include "clicommandexecutor.h"
+#include "commands/clicommand.h"
+
+CliCommandExecutor::CliCommandExecutor(QObject *parent) :
+ QObject(parent)
+{
+}
+
+void CliCommandExecutor::execCommand(CliCommand* cmd)
+{
+ connect(cmd, SIGNAL(execComplete()), this, SLOT(asyncExecutionComplete()));
+ cmd->execute();
+ if (!cmd->isAsyncExecution())
+ {
+ delete cmd;
+ emit executionComplete();
+ }
+}
+
+void CliCommandExecutor::asyncExecutionComplete()
+{
+ sender()->deleteLater();
+ emit executionComplete();
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/clicommandexecutor.h b/SQLiteStudio3/sqlitestudiocli/clicommandexecutor.h
new file mode 100644
index 0000000..0312e51
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/clicommandexecutor.h
@@ -0,0 +1,26 @@
+#ifndef CLICOMMANDEXECUTOR_H
+#define CLICOMMANDEXECUTOR_H
+
+#include <QObject>
+#include <QStringList>
+
+class CliCommand;
+
+class CliCommandExecutor : public QObject
+{
+ Q_OBJECT
+
+ public:
+ explicit CliCommandExecutor(QObject *parent = 0);
+
+ signals:
+ void executionComplete();
+
+ public slots:
+ void execCommand(CliCommand* cmd);
+
+ private slots:
+ void asyncExecutionComplete();
+};
+
+#endif // CLICOMMANDEXECUTOR_H
diff --git a/SQLiteStudio3/sqlitestudiocli/clicommandsyntax.cpp b/SQLiteStudio3/sqlitestudiocli/clicommandsyntax.cpp
new file mode 100644
index 0000000..732b0f0
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/clicommandsyntax.cpp
@@ -0,0 +1,433 @@
+#include "clicommandsyntax.h"
+#include "commands/clicommand.h"
+#include "commands/clicommandfactory.h"
+#include "cli.h"
+#include <QDebug>
+
+CliCommandSyntax::CliCommandSyntax()
+{
+}
+
+CliCommandSyntax::~CliCommandSyntax()
+{
+ foreach (Argument* arg, arguments)
+ delete arg;
+
+ arguments.clear();
+ argumentMap.clear();
+
+ foreach (Option* opt, options)
+ delete opt;
+
+ optionMap.clear();
+ optionsLongNameMap.clear();
+ optionsShortNameMap.clear();
+ options.clear();
+}
+
+void CliCommandSyntax::addArgument(int id, const QString& name, bool mandatory)
+{
+ addArgumentInternal(id, {name}, mandatory, Argument::REGULAR);
+}
+
+void CliCommandSyntax::addStrictArgument(int id, const QStringList& values, bool mandatory)
+{
+ addArgumentInternal(id, values, mandatory, Argument::STRICT);
+}
+
+void CliCommandSyntax::addAlternatedArgument(int id, const QStringList& names, bool mandatory)
+{
+ addArgumentInternal(id, names, mandatory, Argument::ALTERNATED);
+}
+
+void CliCommandSyntax::addOptionShort(int id, const QString& shortName)
+{
+ addOptionInternal(id, shortName, QString(), QString());
+}
+
+void CliCommandSyntax::addOptionLong(int id, const QString& longName)
+{
+ addOptionInternal(id, QString(), longName, QString());
+}
+
+void CliCommandSyntax::addOption(int id, const QString& shortName, const QString& longName)
+{
+ addOptionInternal(id, shortName, longName, QString());
+}
+
+void CliCommandSyntax::addOptionWithArgShort(int id, const QString& shortName, const QString& argName)
+{
+ addOptionInternal(id, shortName, QString(), argName);
+}
+
+void CliCommandSyntax::addOptionWithArgLong(int id, const QString& longName, const QString& argName)
+{
+ addOptionInternal(id, QString(), longName, argName);
+}
+
+void CliCommandSyntax::addOptionWithArg(int id, const QString& shortName, const QString& longName, const QString& argName)
+{
+ addOptionInternal(id, shortName, longName, argName);
+}
+
+CliCommandSyntax::Argument* CliCommandSyntax::addArgumentInternal(int id, const QStringList& names, bool mandatory, Argument::Type type)
+{
+ checkNewArgument(mandatory);
+
+ Argument* arg = new Argument;
+ arg->mandatory = mandatory;
+ arg->id = id;
+ arg->type = type;
+ arg->names = names;
+ arguments << arg;
+ argumentMap[id] = arg;
+ return arg;
+}
+
+CliCommandSyntax::Option* CliCommandSyntax::addOptionInternal(int id, const QString& shortName, const QString& longName, const QString& argName)
+{
+ Option* opt = new Option;
+ opt->shortName = shortName;
+ opt->longName = longName;
+ opt->id = id;
+ opt->argName = argName;
+ optionMap[id] = opt;
+ options << opt;
+ optionsShortNameMap[shortName] = opt;
+ optionsLongNameMap[longName] = opt;
+ return opt;
+}
+
+bool CliCommandSyntax::getStrictArgumentCount() const
+{
+ return strictArgumentCount;
+}
+
+void CliCommandSyntax::setStrictArgumentCount(bool value)
+{
+ strictArgumentCount = value;
+}
+
+bool CliCommandSyntax::parse(const QStringList& args)
+{
+ pastOptions = (options.size() == 0);
+ lastParsedOption = nullptr;
+ QString arg;
+ bool res = false;
+ int argCnt = args.size();
+ for (int argIdx = 0; argIdx < argCnt; argIdx++)
+ {
+ arg = args[argIdx];
+
+ if (arg == "--help")
+ {
+ CliCommand* help = CliCommandFactory::getCommand("help");
+ help->setup(CLI::getInstance());
+ help->parseArgs({getName()});
+ help->execute();
+ return false;
+ }
+ else if (pastOptions)
+ {
+ res = parseArg(arg);
+ }
+ else if (arg == "--")
+ {
+ pastOptions = true;
+ res = true;
+ }
+ else if (arg.startsWith("-"))
+ {
+ res = parseOpt(arg, args, argIdx);
+ }
+ else
+ {
+ pastOptions = true;
+ res = parseArg(arg);
+ }
+
+ if (!res)
+ return false;
+ }
+
+ if (strictArgumentCount && argPosition < requiredArguments())
+ {
+ parsingErrorText = QObject::tr("Insufficient number of arguments.");
+ return false;
+ }
+
+ return true;
+}
+
+QString CliCommandSyntax::getErrorText() const
+{
+ return parsingErrorText;
+}
+
+QStringList CliCommandSyntax::getStrictArgumentCandidates()
+{
+ QStringList results;
+ if (!pastOptions)
+ {
+ if (lastParsedOption && !lastParsedOption->argName.isEmpty())
+ return results; // this case is covered by getRegularArgumentCandidates()
+
+ foreach (Option* opt, options)
+ {
+ if (opt->requested)
+ continue;
+
+ if (!opt->shortName.isEmpty())
+ results << "-"+opt->shortName;
+
+ if (!opt->longName.isEmpty())
+ results << "--"+opt->longName;
+ }
+ results << "--";
+ }
+
+ if (argPosition < arguments.size() && arguments[argPosition]->type == Argument::STRICT)
+ results += arguments[argPosition]->names;
+
+ return results;
+}
+
+QList<int> CliCommandSyntax::getRegularArgumentCandidates()
+{
+ QList<int> results;
+
+ if (!pastOptions && lastParsedOption && !lastParsedOption->argName.isEmpty())
+ {
+ // We're exactly at the spot where the option argument is expected
+ results << lastParsedOption->id;
+ return results;
+ }
+
+ if (argPosition < arguments.size())
+ {
+ switch (arguments[argPosition]->type)
+ {
+ case Argument::REGULAR:
+ case Argument::ALTERNATED:
+ results << arguments[argPosition]->id;
+ break;
+ case Argument::STRICT:
+ break;
+ }
+ }
+
+ return results;
+}
+
+void CliCommandSyntax::addAlias(const QString& alias)
+{
+ aliases << alias;
+}
+
+QStringList CliCommandSyntax::getAliases() const
+{
+ return aliases;
+}
+
+QString CliCommandSyntax::getSyntaxDefinition() const
+{
+ return getSyntaxDefinition(name);
+}
+
+QString CliCommandSyntax::getSyntaxDefinition(const QString& usedName) const
+{
+ static const QString mandatoryArgTempl = "<%1>";
+ static const QString optionalArgTempl = "[<%1>]";
+ static const QString mandatoryStrictTempl = "%1";
+ static const QString optionalStrictTempl = "[%1]";
+ static const QString optionTempl = "[%1]";
+ static const QString optionWithArgTempl = "[%1 <%2>]";
+
+ QStringList words;
+ words << usedName;
+
+ QString optName;
+ QStringList optNameParts;
+ foreach (Option* opt, options)
+ {
+ optNameParts.clear();;
+ if (!opt->shortName.isEmpty())
+ optNameParts << "-"+opt->shortName;
+
+ if (!opt->longName.isEmpty())
+ optNameParts += "--"+opt->longName;
+
+ optName = optNameParts.join("|");
+
+ words << (opt->argName.isEmpty() ? optionTempl.arg(optName) : optionWithArgTempl.arg(optName).arg(opt->argName));
+ }
+
+ QString templ;
+ QString argName;
+ foreach (Argument* arg, arguments)
+ {
+ templ = (arg->mandatory ? mandatoryArgTempl : optionalArgTempl);
+ switch (arg->type)
+ {
+ case CliCommandSyntax::Argument::ALTERNATED:
+ argName = arg->names.join("|");
+ break;
+ case CliCommandSyntax::Argument::REGULAR:
+ argName = arg->names.first();
+ break;
+ case CliCommandSyntax::Argument::STRICT:
+ argName = arg->names.join("|");
+ templ = (arg->mandatory ? mandatoryStrictTempl : optionalStrictTempl);
+ break;
+ }
+ words << templ.arg(argName);
+ }
+
+ return words.join(" ");
+}
+
+bool CliCommandSyntax::isArgumentSet(int id) const
+{
+ if (!argumentMap.contains(id))
+ return false;
+
+ return argumentMap[id]->defined;
+}
+
+QString CliCommandSyntax::getArgument(int id) const
+{
+ if (!argumentMap.contains(id))
+ return QString::null;
+
+ return argumentMap[id]->value;
+}
+
+bool CliCommandSyntax::isOptionSet(int id) const
+{
+ if (!optionMap.contains(id))
+ return false;
+
+ return optionMap[id]->requested;
+}
+
+QString CliCommandSyntax::getOptionValue(int id) const
+{
+ if (!optionMap.contains(id))
+ return QString::null;
+
+ return optionMap[id]->value;
+}
+
+bool CliCommandSyntax::parseArg(const QString& arg)
+{
+ if (strictArgumentCount && arguments.size() < (argPosition + 1))
+ {
+ parsingErrorText = QObject::tr("Too many arguments.");
+ return false;
+ }
+
+ switch (arguments[argPosition]->type)
+ {
+ case CliCommandSyntax::Argument::ALTERNATED:
+ {
+ arguments[argPosition]->value = arg;
+ arguments[argPosition]->defined = true;
+ break;
+ }
+ case CliCommandSyntax::Argument::REGULAR:
+ {
+ arguments[argPosition]->value = arg;
+ arguments[argPosition]->defined = true;
+ break;
+ }
+ case CliCommandSyntax::Argument::STRICT:
+ {
+ if (!arguments[argPosition]->names.contains(arg))
+ {
+ parsingErrorText = QObject::tr("Invalid argument value: %1.\nExpected one of: %2").arg(arg)
+ .arg(arguments[argPosition]->names.join(", "));
+
+ return false;
+ }
+ arguments[argPosition]->value = arg;
+ arguments[argPosition]->defined = true;
+ break;
+ }
+ default:
+ qCritical() << "Invalid argument type in CliCommandSyntax:" << arguments[argPosition]->type;
+ return false;
+ }
+
+ argPosition++;
+ return true;
+}
+
+bool CliCommandSyntax::parseOpt(const QString& arg, const QStringList& args, int& argIdx)
+{
+ Option* opt = nullptr;
+ if (arg.startsWith("--"))
+ {
+ QString longName = arg.mid(2);
+ if (optionsLongNameMap.contains(longName))
+ opt = optionsLongNameMap.value(longName);
+ }
+ else
+ {
+ QString shortName = arg.mid(1);
+ if (optionsShortNameMap.contains(shortName))
+ opt = optionsShortNameMap.value(shortName);
+ }
+
+ if (!opt)
+ {
+ parsingErrorText = QObject::tr("Unknown option: %1", "CLI command syntax").arg(arg);
+ return false;
+ }
+
+ opt->requested = true;
+ lastParsedOption = opt;
+
+ if (!opt->argName.isEmpty())
+ {
+ if (args.size() <= (argIdx + 1))
+ {
+ parsingErrorText = QObject::tr("Option %1 requires an argument.", "CLI command syntax").arg(arg);
+ return false;
+ }
+
+ argIdx++;
+ opt->value = args[argIdx];
+ }
+ return true;
+}
+
+int CliCommandSyntax::requiredArguments() const
+{
+ int cnt = 0;
+ foreach (Argument* arg, arguments)
+ {
+ if (arg->mandatory)
+ cnt++;
+ }
+ return cnt;
+}
+
+void CliCommandSyntax::checkNewArgument(bool mandatory)
+{
+ if (arguments.size() > 0 && !arguments.last()->mandatory && mandatory)
+ {
+ qWarning() << "Adding mandatory CLI command argument after optional argument. This will result in invalid syntax definition. The command is:"
+ << this->name;
+ }
+}
+
+QString CliCommandSyntax::getName() const
+{
+ return name;
+}
+
+void CliCommandSyntax::setName(const QString& value)
+{
+ name = value;
+}
+
diff --git a/SQLiteStudio3/sqlitestudiocli/clicommandsyntax.h b/SQLiteStudio3/sqlitestudiocli/clicommandsyntax.h
new file mode 100644
index 0000000..adf88d2
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/clicommandsyntax.h
@@ -0,0 +1,97 @@
+#ifndef CLICOMMANDSYNTAX_H
+#define CLICOMMANDSYNTAX_H
+
+#include <QString>
+#include <QStringList>
+#include <QHash>
+
+class CliCommandSyntax
+{
+ public:
+ CliCommandSyntax();
+ ~CliCommandSyntax();
+
+ void addArgument(int id, const QString& name, bool mandatory = true);
+ void addStrictArgument(int id, const QStringList& values, bool mandatory = true);
+ void addAlternatedArgument(int id, const QStringList& names, bool mandatory = true);
+ void addOptionShort(int id, const QString& shortName);
+ void addOptionLong(int id, const QString& longName);
+ void addOption(int id, const QString& shortName, const QString& longName);
+ void addOptionWithArgShort(int id, const QString& shortName, const QString& argName);
+ void addOptionWithArgLong(int id, const QString& longName, const QString& argName);
+ void addOptionWithArg(int id, const QString& shortName, const QString& longName, const QString& argName);
+
+ bool parse(const QStringList& args);
+ QString getErrorText() const;
+ QStringList getStrictArgumentCandidates();
+ QList<int> getRegularArgumentCandidates();
+
+ void addAlias(const QString& alias);
+ QStringList getAliases() const;
+
+ QString getSyntaxDefinition() const;
+ QString getSyntaxDefinition(const QString& usedName) const;
+
+ bool isArgumentSet(int id) const;
+ QString getArgument(int id) const;
+
+ bool isOptionSet(int id) const;
+ QString getOptionValue(int id) const;
+
+ QString getName() const;
+ void setName(const QString& value);
+
+ bool getStrictArgumentCount() const;
+ void setStrictArgumentCount(bool value);
+
+ private:
+ struct Argument
+ {
+ enum Type
+ {
+ REGULAR,
+ STRICT,
+ ALTERNATED
+ };
+
+ int id;
+ QStringList names;
+ Type type = REGULAR;
+ bool mandatory = true;
+ bool defined = false;
+ QString value;
+ };
+
+ struct Option
+ {
+ int id;
+ QString shortName;
+ QString longName;
+ QString argName;
+ bool requested = false;
+ QString value;
+ };
+
+ bool parseArg(const QString& arg);
+ bool parseOpt(const QString& arg, const QStringList& args, int& argIdx);
+ int requiredArguments() const;
+ void checkNewArgument(bool mandatory);
+ Argument* addArgumentInternal(int id, const QStringList& names, bool mandatory, Argument::Type type);
+ Option* addOptionInternal(int id, const QString& shortName, const QString& longName, const QString& argName);
+
+ int argPosition = 0;
+ QString parsingErrorText;
+ bool strictArgumentCount = true;
+ bool pastOptions = false;
+ QString name;
+ QStringList aliases;
+ QList<Argument*> arguments;
+ QHash<int,Argument*> argumentMap;
+ QList<Option*> options;
+ Option* lastParsedOption = nullptr;
+ QHash<int,Option*> optionMap;
+ QHash<QString,Option*> optionsShortNameMap;
+ QHash<QString,Option*> optionsLongNameMap;
+};
+
+#endif // CLICOMMANDSYNTAX_H
diff --git a/SQLiteStudio3/sqlitestudiocli/clicompleter.cpp b/SQLiteStudio3/sqlitestudiocli/clicompleter.cpp
new file mode 100644
index 0000000..1c3c99c
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/clicompleter.cpp
@@ -0,0 +1,173 @@
+#include "clicompleter.h"
+#include "completionhelper.h"
+#include "cli.h"
+#include "cli_config.h"
+#include "common/unused.h"
+#include "commands/clicommand.h"
+#include "parser/lexer.h"
+#include "commands/clicommandfactory.h"
+#include <QString>
+#include <QDebug>
+
+#if defined(Q_OS_WIN32)
+#include "readline.h"
+#elif defined(Q_OS_UNIX)
+#include <readline/readline.h>
+#endif
+
+CliCompleter* CliCompleter::instance = nullptr;
+
+CliCompleter::CliCompleter()
+{
+}
+
+void CliCompleter::init(CLI* value)
+{
+ rl_attempted_completion_function = CliCompleter::complete;
+ cli = value;
+}
+
+CliCompleter* CliCompleter::getInstance()
+{
+ if (!instance)
+ instance = new CliCompleter();
+
+ return instance;
+}
+
+char** CliCompleter::complete(const char* text, int start, int end)
+{
+ UNUSED(start);
+
+#ifdef Q_OS_UNIX
+ // Unix readline needs this to disable the completion using rl_completion_entry_function
+ rl_attempted_completion_over = 1;
+#endif
+
+ return toCharArray(getInstance()->completeInternal(QString::fromLocal8Bit(text), QString::fromLocal8Bit(rl_line_buffer), end));
+}
+
+QStringList CliCompleter::completeInternal(const QString& toBeReplaced, const QString& text, int curPos)
+{
+ QString str;
+ if (!cli->getLine().isEmpty())
+ {
+ str += cli->getLine();
+ curPos += str.length();
+ }
+
+ str += text;
+
+ QStringList list;
+ if (str.startsWith(CFG_CLI.Console.CommandPrefixChar.get()))
+ list = completeCommand(str, curPos);
+ else
+ list = completeQuery(toBeReplaced, str, curPos);
+
+ list.removeDuplicates();
+
+#ifdef Q_OS_WIN
+ if (list.size() == 1)
+ list[0] += " ";
+#endif
+
+#ifdef Q_OS_UNIX
+ // Unix readline treats first element in the list as a common value of all elements
+ if (list.size() > 0)
+ list.prepend(longestCommonPart(list));
+#endif
+
+ return list;
+}
+
+QStringList CliCompleter::completeCommand(const QString& str, int curPos)
+{
+ QStringList results;
+ QString text = str.mid(0, curPos);
+
+ QStringList cmdWords = tokenizeArgs(text);
+ if (cmdWords.size() == 0)
+ return results;
+
+ if (text[text.length()-1].isSpace())
+ cmdWords << "";
+
+ QString cmdStr = cmdWords[0].mid(1);
+ if (cmdWords.size() > 1)
+ {
+ CliCommand* command = CliCommandFactory::getCommand(cmdStr);
+ if (!command)
+ return results;
+
+ command->setup(cli);
+ results = command->complete(cmdWords.mid(1)).filter(QRegExp("^"+cmdWords.last()+".*"));
+ }
+ else
+ {
+ QStringList cmdNames = CliCommandFactory::getCommandNames().filter(QRegExp("^"+cmdStr+".*"));
+ cmdNames.sort(Qt::CaseInsensitive);
+ foreach (const QString& cmdName, cmdNames)
+ results << CFG_CLI.Console.CommandPrefixChar.get() + cmdName;
+ }
+
+ return results;
+}
+
+QStringList CliCompleter::completeQuery(const QString& toBeReplaced, const QString& str, int curPos)
+{
+ QStringList list;
+ if (!cli->getCurrentDb())
+ return list;
+
+ bool keepOriginalStr = doKeepOriginalStr(str, curPos);
+
+ CompletionHelper completer(str, curPos, cli->getCurrentDb());
+ QList<ExpectedTokenPtr> expectedTokens = completer.getExpectedTokens().filtered();
+
+ foreach (const ExpectedTokenPtr& token, expectedTokens)
+ list << token->value;
+
+ list.removeAll("");
+ if (list.size() > 1)
+ list.removeOne(";"); // we don't want it together with other proposals, cause it introduces problems when proposed by completer
+
+ if (keepOriginalStr)
+ {
+ QMutableStringListIterator it(list);
+ while (it.hasNext())
+ it.next().prepend(toBeReplaced);
+ }
+ return list;
+}
+
+bool CliCompleter::doKeepOriginalStr(const QString& str, int curPos)
+{
+ Dialect dialect = Dialect::Sqlite3;
+ if (cli->getCurrentDb())
+ dialect = cli->getCurrentDb()->getDialect();
+
+ TokenList tokens = Lexer::tokenize(str.mid(0, curPos), dialect);
+ if (tokens.size() == 0)
+ return false;
+
+ return tokens.last()->isSeparating();
+}
+
+char** CliCompleter::toCharArray(const QStringList& list)
+{
+ if (list.size() == 0)
+ return nullptr;
+
+ char** array = (char**)malloc((list.size() + 1) * sizeof(char*));
+ array[list.size()] = nullptr;
+
+ int i = 0;
+ foreach (const QString& str, list)
+#if defined(Q_OS_WIN)
+ array[i++] = _strdup(str.toLocal8Bit().data());
+#elif defined(Q_OS_UNIX)
+ array[i++] = strdup(str.toLocal8Bit().data());
+#endif
+
+ return array;
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/clicompleter.h b/SQLiteStudio3/sqlitestudiocli/clicompleter.h
new file mode 100644
index 0000000..a43a98d
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/clicompleter.h
@@ -0,0 +1,30 @@
+#ifndef CLICOMPLETER_H
+#define CLICOMPLETER_H
+
+#include <QStringList>
+
+class CLI;
+
+class CliCompleter
+{
+ public:
+ static CliCompleter* getInstance();
+ static char** complete(const char* text, int start, int end);
+
+ void init(CLI* value);
+
+ private:
+ CliCompleter();
+ QStringList completeInternal(const QString& toBeReplaced, const QString& text, int curPos);
+ QStringList completeCommand(const QString& str, int curPos);
+ QStringList completeQuery(const QString& toBeReplaced, const QString& str, int curPos);
+ bool doKeepOriginalStr(const QString& str, int curPos);
+
+ static char** toCharArray(const QStringList& list);
+
+ static CliCompleter* instance;
+
+ CLI* cli = nullptr;
+};
+
+#endif // CLICOMPLETER_H
diff --git a/SQLiteStudio3/sqlitestudiocli/climsghandler.cpp b/SQLiteStudio3/sqlitestudiocli/climsghandler.cpp
new file mode 100644
index 0000000..d36f9d0
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/climsghandler.cpp
@@ -0,0 +1,38 @@
+#include "climsghandler.h"
+#include "qio.h"
+#include "cli_config.h"
+#include "common/unused.h"
+
+bool cliDebug = false;
+
+void cliMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
+{
+ if (!cliDebug)
+ return;
+
+ UNUSED(context);
+
+ QString txt;
+ switch (type) {
+ case QtDebugMsg:
+ txt = QString("Debug: %1").arg(msg);
+ break;
+ case QtWarningMsg:
+ txt = QString("Warning: %1").arg(msg);
+ break;
+ case QtCriticalMsg:
+ txt = QString("Critical: %1").arg(msg);
+ break;
+ case QtFatalMsg:
+ txt = QString("Fatal: %1").arg(msg);
+ abort();
+ }
+
+ qOut << txt << "\n";
+ qOut.flush();
+}
+
+void setCliDebug(bool enabled)
+{
+ cliDebug = enabled;
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/climsghandler.h b/SQLiteStudio3/sqlitestudiocli/climsghandler.h
new file mode 100644
index 0000000..e2f184e
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/climsghandler.h
@@ -0,0 +1,9 @@
+#ifndef CLIMSGHANDLER_H
+#define CLIMSGHANDLER_H
+
+#include <QtDebug>
+
+void cliMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg);
+void setCliDebug(bool enabled);
+
+#endif // CLIMSGHANDLER_H
diff --git a/SQLiteStudio3/sqlitestudiocli/cliutils.cpp b/SQLiteStudio3/sqlitestudiocli/cliutils.cpp
new file mode 100644
index 0000000..2b5c44d
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/cliutils.cpp
@@ -0,0 +1,99 @@
+#include "cliutils.h"
+#include <QtGlobal>
+
+#if defined(Q_OS_WIN32)
+#include <windows.h>
+#elif defined(Q_OS_UNIX)
+#include <sys/ioctl.h>
+#include <unistd.h>
+#endif
+
+#if defined(Q_OS_WIN32)
+
+int getCliColumns()
+{
+ CONSOLE_SCREEN_BUFFER_INFO data;
+ GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &data);
+ return data.dwSize.X;
+}
+
+int getCliRows()
+{
+ CONSOLE_SCREEN_BUFFER_INFO data;
+ GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &data);
+ return data.dwSize.Y;
+}
+
+#elif defined(Q_OS_UNIX)
+
+int getCliColumns()
+{
+ struct winsize w;
+ ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
+ return w.ws_col;
+}
+
+int getCliRows()
+{
+ struct winsize w;
+ ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
+ return w.ws_row;
+}
+
+#endif
+
+QStringList toAsciiTree(const AsciiTree& tree, const QList<bool>& indents, bool topLevel, bool lastNode)
+{
+ static const QString indentStr = " | ";
+ static const QString indentStrEmpty = " ";
+ static const QString branchStr = " +-";
+ static const QString branchStrLast = " `-";
+
+ QStringList lines;
+ QString line;
+
+ if (!topLevel)
+ {
+ // Draw indent before this node
+ foreach (bool indent, indents)
+ line += (indent ? indentStr : indentStrEmpty);
+
+ // Draw node prefix
+ line += (lastNode ? branchStrLast : branchStr);
+ }
+
+ // Draw label
+ line += tree.label;
+ lines << line;
+
+ if (tree.childs.size() == 0)
+ return lines;
+
+ // Draw childs
+ int i = 0;
+ int lastIdx = tree.childs.size() - 1;
+ QList<bool> subIndents = indents;
+
+ if (!topLevel)
+ subIndents << (lastNode ? false : true);
+
+ foreach (const AsciiTree& subTree, tree.childs)
+ {
+ lines += toAsciiTree(subTree, subIndents, false, i == lastIdx);
+ i++;
+ }
+
+ return lines;
+}
+
+QString toAsciiTree(const AsciiTree& tree)
+{
+ QList<bool> subIndents;
+ QStringList lines = toAsciiTree(tree, subIndents, true, true);
+ return lines.join("\n");
+}
+
+void initCliUtils()
+{
+ qRegisterMetaType<AsciiTree>();
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/cliutils.h b/SQLiteStudio3/sqlitestudiocli/cliutils.h
new file mode 100644
index 0000000..2130a65
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/cliutils.h
@@ -0,0 +1,22 @@
+#ifndef CLIUTILS_H
+#define CLIUTILS_H
+
+#include <QString>
+#include <QVariant>
+
+void initCliUtils();
+
+int getCliColumns();
+int getCliRows();
+
+struct AsciiTree
+{
+ QList<AsciiTree> childs;
+ QString label;
+};
+
+Q_DECLARE_METATYPE(AsciiTree)
+
+QString toAsciiTree(const AsciiTree& tree);
+
+#endif // CLIUTILS_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommand.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommand.cpp
new file mode 100644
index 0000000..3002ee5
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommand.cpp
@@ -0,0 +1,334 @@
+#include "clicommand.h"
+#include "qio.h"
+#include "cli_config.h"
+#include "cliutils.h"
+#include "common/utils.h"
+#include "common/utils_sql.h"
+#include "clicommandfactory.h"
+#include "services/dbmanager.h"
+#include "schemaresolver.h"
+#include "cli.h"
+
+#include <QDir>
+
+CliCommand::CliCommand()
+{
+}
+
+CliCommand::~CliCommand()
+{
+}
+
+void CliCommand::setup(CLI *cli)
+{
+ this->cli = cli;
+ defineSyntax();
+}
+
+bool CliCommand::isAsyncExecution() const
+{
+ return false;
+}
+
+bool CliCommand::parseArgs(const QStringList& args)
+{
+ bool res = syntax.parse(args);
+
+ if (!res && !syntax.getErrorText().isEmpty())
+ {
+ println(syntax.getErrorText());
+ println();
+ }
+
+ return res;
+}
+
+QString CliCommand::usage() const
+{
+ return syntax.getSyntaxDefinition();
+}
+
+QString CliCommand::usage(const QString& alias) const
+{
+ return syntax.getSyntaxDefinition(alias);
+}
+
+QString CliCommand::getName() const
+{
+ return syntax.getName();
+}
+
+QStringList CliCommand::complete(const QStringList& args)
+{
+ syntax.parse(args.mid(0, args.size() - 1));
+
+ QStringList results;
+ results += syntax.getStrictArgumentCandidates();
+ foreach (int id, syntax.getRegularArgumentCandidates())
+ results += getCompletionValuesFor(id, args.last());
+
+ return results;
+}
+
+QStringList CliCommand::aliases() const
+{
+ return syntax.getAliases();
+}
+
+void CliCommand::println(const QString &str)
+{
+ qOut << str << "\n";
+ qOut.flush();
+}
+
+void CliCommand::print(const QString& str)
+{
+ qOut << str;
+ qOut.flush();
+}
+
+void CliCommand::printBox(const QString& str)
+{
+ int cols = getCliColumns();
+
+ QStringList lines = str.split("\n");
+ println(".---------------------------");
+ foreach (const QString& line, lines)
+ {
+ foreach (const QString& lineWithMargin, applyMargin(line, cols - 3)) // 2 for "| " and 1 for final new line character
+ println("| " + lineWithMargin);
+ }
+
+ println("`---------------------------");
+}
+
+void CliCommand::printUsage()
+{
+ println(tr("Usage: %1%2").arg(CFG_CLI.Console.CommandPrefixChar.get()).arg(usage()));
+ println("");
+}
+
+QString CliCommand::getFilterAndFixDir(QDir& dir, const QString& path)
+{
+ if (path.isEmpty())
+ return "*";
+
+ QString filter;
+ QDir tempDir;
+ tempDir.setPath(path);
+ if (tempDir.exists() && path.endsWith("/"))
+ {
+ dir.cd(path);
+ filter = "*";
+ }
+ else if (tempDir.cdUp())
+ {
+ dir.setPath(path);
+ dir.cdUp();
+ filter = QFileInfo(path).fileName() + "*";
+ }
+ else
+ {
+ filter = path;
+ }
+ return filter;
+}
+
+QStringList CliCommand::getCompletionDbNames()
+{
+ QStringList results = DBLIST->getDbNames();
+ results.sort(Qt::CaseInsensitive);
+ return results;
+}
+
+QStringList CliCommand::getCompletionTables()
+{
+ QStringList results;
+ if (!cli->getCurrentDb())
+ return results;
+
+ Dialect dialect = Dialect::Sqlite3;
+
+ SchemaResolver resolver(cli->getCurrentDb());
+ resolver.setIgnoreSystemObjects(true);
+ results += wrapObjNamesIfNeeded(resolver.getTables(), dialect);
+ results += prefixEach("temp.", wrapObjNamesIfNeeded(resolver.getTables("temp"), dialect));
+ foreach (const QString& database, resolver.getDatabases())
+ results += prefixEach(wrapObjIfNeeded(database, Dialect::Sqlite3)+".", wrapObjNamesIfNeeded(resolver.getTables(database), dialect));
+
+ return results;
+}
+
+QStringList CliCommand::getCompletionIndexes()
+{
+ QStringList results;
+ if (!cli->getCurrentDb())
+ return results;
+
+ Dialect dialect = Dialect::Sqlite3;
+
+ SchemaResolver resolver(cli->getCurrentDb());
+ resolver.setIgnoreSystemObjects(true);
+ results += wrapObjNamesIfNeeded(resolver.getIndexes(), dialect);
+ results += prefixEach("temp.", wrapObjNamesIfNeeded(resolver.getIndexes("temp"), dialect));
+ foreach (const QString& database, resolver.getDatabases())
+ results += prefixEach(wrapObjIfNeeded(database, Dialect::Sqlite3)+".", wrapObjNamesIfNeeded(resolver.getIndexes(database), dialect));
+
+ return results;
+}
+
+QStringList CliCommand::getCompletionTriggers()
+{
+ QStringList results;
+ if (!cli->getCurrentDb())
+ return results;
+
+ Dialect dialect = Dialect::Sqlite3;
+
+ SchemaResolver resolver(cli->getCurrentDb());
+ resolver.setIgnoreSystemObjects(true);
+ results += wrapObjNamesIfNeeded(resolver.getTriggers(), dialect);
+ results += prefixEach("temp.", wrapObjNamesIfNeeded(resolver.getTriggers("temp"), dialect));
+ foreach (const QString& database, resolver.getDatabases())
+ results += prefixEach(wrapObjIfNeeded(database, Dialect::Sqlite3)+".", wrapObjNamesIfNeeded(resolver.getTriggers(database), dialect));
+
+ return results;
+}
+
+QStringList CliCommand::getCompletionViews()
+{
+ QStringList results;
+ if (!cli->getCurrentDb())
+ return results;
+
+ Dialect dialect = Dialect::Sqlite3;
+
+ SchemaResolver resolver(cli->getCurrentDb());
+ resolver.setIgnoreSystemObjects(true);
+ results += wrapObjNamesIfNeeded(resolver.getViews(), dialect);
+ results += prefixEach("temp.", wrapObjNamesIfNeeded(resolver.getViews("temp"), dialect));
+ foreach (const QString& database, resolver.getDatabases())
+ results += prefixEach(wrapObjIfNeeded(database, Dialect::Sqlite3)+".", wrapObjNamesIfNeeded(resolver.getViews(database), dialect));
+
+ return results;
+}
+
+QStringList CliCommand::getCompletionDbNamesOrFiles(const QString& partialValue)
+{
+ QStringList results = getCompletionDbNames();
+ results += getCompletionFiles(partialValue);
+ return results;
+}
+
+QStringList CliCommand::getCompletionFiles(const QString& partialValue)
+{
+ QDir dir;
+ QString filter = getFilterAndFixDir(dir, partialValue);
+ QFileInfoList entries = dir.entryInfoList({filter}, QDir::Files, QDir::LocaleAware|QDir::Name);
+
+ QStringList results;
+ QString name;
+ foreach (const QFileInfo& entry, entries)
+ {
+ name = entry.fileName();
+ if (dir != QDir::current())
+ name.prepend(dir.path() + "/");
+
+ results << name;
+ }
+
+ return results;
+}
+
+QStringList CliCommand::getCompletionDirs(const QString& partialValue)
+{
+ QDir dir;
+ QString filter = getFilterAndFixDir(dir, partialValue);
+ QFileInfoList entries = dir.entryInfoList({filter}, QDir::Dirs|QDir::NoDotAndDotDot, QDir::LocaleAware|QDir::Name);
+
+ QStringList results;
+ QString name;
+ foreach (const QFileInfo& entry, entries)
+ {
+ name = entry.fileName();
+ if (dir != QDir::current())
+ name.prepend(dir.path() + "/");
+
+ results << name;
+ }
+
+ return results;
+}
+
+QStringList CliCommand::getCompletionDirsOrFiles(const QString& partialValue)
+{
+ QStringList results = getCompletionDirs(partialValue);
+ results += getCompletionFiles(partialValue);
+ return results;
+}
+
+QStringList CliCommand::getCompletionInternalDbs()
+{
+ QStringList results;
+ if (!cli->getCurrentDb())
+ return results;
+
+ SchemaResolver resolver(cli->getCurrentDb());
+ results += resolver.getDatabases().toList();
+ results << "main" << "temp";
+ results.sort(Qt::CaseInsensitive);
+ return results;
+}
+
+QStringList CliCommand::getCompletionValuesFor(int id, const QString& partialValue)
+{
+ QStringList results;
+ if (id < 1000) // this base implementation is only for local enum values (>= 1000).
+ return results;
+
+ switch (static_cast<ArgIds>(id))
+ {
+ case CliCommand::DB_NAME:
+ results += getCompletionDbNames();
+ break;
+ case CliCommand::DB_NAME_OR_FILE:
+ results += getCompletionDbNamesOrFiles(partialValue);
+ break;
+ case CliCommand::FILE_PATH:
+ results += getCompletionFiles(partialValue);
+ break;
+ case CliCommand::DIR_PATH:
+ results += getCompletionDirs(partialValue);
+ break;
+ case CliCommand::CMD_NAME:
+ results += CliCommandFactory::getCommandNames();
+ break;
+ case CliCommand::DIR_OR_FILE:
+ results += getCompletionDirsOrFiles(partialValue);
+ break;
+ case CliCommand::INTRNAL_DB:
+ results += getCompletionInternalDbs();
+ break;
+ case CliCommand::TABLE:
+ results += getCompletionTables();
+ break;
+ case CliCommand::INDEX:
+ results += getCompletionIndexes();
+ break;
+ case CliCommand::TRIGGER:
+ results += getCompletionTriggers();
+ break;
+ case CliCommand::VIEW:
+ results += getCompletionViews();
+ break;
+ case CliCommand::STRING:
+ break;
+ }
+
+ return results;
+}
+
+QString CliCommand::cmdName(const QString& cmd)
+{
+ return CFG_CLI.Console.CommandPrefixChar.get()+cmd;
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommand.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommand.h
new file mode 100644
index 0000000..4fc8618
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommand.h
@@ -0,0 +1,97 @@
+#ifndef CLICOMMAND_H
+#define CLICOMMAND_H
+
+#include "clicommandsyntax.h"
+#include <QStringList>
+#include <QTextStream>
+#include <QObject>
+#include <QDir>
+
+class QFile;
+class SQLiteStudio;
+class DbManager;
+class CLI;
+class Config;
+
+class CliCommand : public QObject
+{
+ Q_OBJECT
+
+ public:
+ CliCommand();
+ virtual ~CliCommand();
+
+ void setup(CLI* cli);
+
+ /**
+ * @brief execute
+ */
+ virtual void execute() = 0;
+
+ /**
+ * @brief Short help displayed in commands index.
+ * @return Single line of command description.
+ */
+ virtual QString shortHelp() const = 0;
+
+ /**
+ * @brief Full help is displayed when help for specific command was requested.
+ * @return Multi line, detailed description, including syntax.
+ */
+ virtual QString fullHelp() const = 0;
+
+ virtual bool isAsyncExecution() const;
+
+ virtual void defineSyntax() = 0;
+
+ QStringList aliases() const;
+ bool parseArgs(const QStringList& args);
+ QString usage() const;
+ QString usage(const QString& alias) const;
+ QString getName() const;
+ QStringList complete(const QStringList& args);
+
+ protected:
+ enum ArgIds
+ {
+ DB_NAME = 1000,
+ DB_NAME_OR_FILE = 1001,
+ FILE_PATH = 1002,
+ DIR_OR_FILE = 1003,
+ DIR_PATH = 1004,
+ CMD_NAME = 1005,
+ INTRNAL_DB = 1006,
+ STRING = 1007,
+ TABLE = 1008,
+ INDEX = 1009,
+ TRIGGER = 1010,
+ VIEW = 1011
+ };
+
+ static void println(const QString& str = "");
+ static void print(const QString& str = "");
+ static void printBox(const QString& str);
+ static QString cmdName(const QString& cmd);
+
+ void printUsage();
+ QString getFilterAndFixDir(QDir& dir, const QString& path);
+ QStringList getCompletionDbNames();
+ QStringList getCompletionTables();
+ QStringList getCompletionIndexes();
+ QStringList getCompletionTriggers();
+ QStringList getCompletionViews();
+ QStringList getCompletionDbNamesOrFiles(const QString& partialValue);
+ QStringList getCompletionFiles(const QString& partialValue);
+ QStringList getCompletionDirs(const QString& partialValue);
+ QStringList getCompletionDirsOrFiles(const QString& partialValue);
+ QStringList getCompletionInternalDbs();
+ virtual QStringList getCompletionValuesFor(int id, const QString& partialValue);
+
+ CLI* cli = nullptr;
+ CliCommandSyntax syntax;
+
+ signals:
+ void execComplete();
+};
+
+#endif // CLICOMMAND_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandadd.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommandadd.cpp
new file mode 100644
index 0000000..e3c14bb
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandadd.cpp
@@ -0,0 +1,36 @@
+#include "clicommandadd.h"
+#include "cli.h"
+#include "services/dbmanager.h"
+
+void CliCommandAdd::execute()
+{
+ if (!DBLIST->addDb(syntax.getArgument(DB_NAME), syntax.getArgument(FILE_PATH)))
+ {
+ println(tr("Could not add database %1 to list.").arg(syntax.getArgument(FILE_PATH)));
+ return;
+ }
+
+ cli->setCurrentDb(DBLIST->getByName(syntax.getArgument(DB_NAME)));
+ println(tr("Database added: %1").arg(cli->getCurrentDb()->getName()));
+}
+
+QString CliCommandAdd::shortHelp() const
+{
+ return tr("adds new database to the list");
+}
+
+QString CliCommandAdd::fullHelp() const
+{
+ return tr(
+ "Adds given database pointed by <path> with given <name> to list the databases list. "
+ "The <name> is just a symbolic name that you can later refer to. Just pick any unique name. "
+ "For list of databases already on the list use %1 command."
+ ).arg(cmdName("dblist"));
+}
+
+void CliCommandAdd::defineSyntax()
+{
+ syntax.setName("add");
+ syntax.addArgument(DB_NAME, tr("name", "CLI command syntax"));
+ syntax.addArgument(FILE_PATH, tr("path", "CLI command syntax"));
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandadd.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommandadd.h
new file mode 100644
index 0000000..ae09ee1
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandadd.h
@@ -0,0 +1,15 @@
+#ifndef CLICOMMANDADD_H
+#define CLICOMMANDADD_H
+
+#include "clicommand.h"
+
+class CliCommandAdd : public CliCommand
+{
+ public:
+ void execute();
+ QString shortHelp() const;
+ QString fullHelp() const;
+ void defineSyntax();
+};
+
+#endif // CLICOMMANDADD_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandcd.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommandcd.cpp
new file mode 100644
index 0000000..0ca6cb6
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandcd.cpp
@@ -0,0 +1,34 @@
+#include "clicommandcd.h"
+#include <QCoreApplication>
+#include <QDir>
+
+void CliCommandCd::execute()
+{
+ QDir dir;
+ dir.cd(syntax.getArgument(DIR_PATH));
+ if (QDir::setCurrent(dir.absolutePath()))
+ println(tr("Changed directory to: %1").arg(QDir::currentPath()));
+ else
+ println(tr("Could not change directory to: %1").arg(QDir::currentPath()));
+}
+
+QString CliCommandCd::shortHelp() const
+{
+ return tr("changes current workind directory");
+}
+
+QString CliCommandCd::fullHelp() const
+{
+ return tr(
+ "Very similar command to 'cd' known from Unix systems and Windows. "
+ "It requires a <path> argument to be passed, therefore calling %1 will always cause a change of the directory. "
+ "To learn what's the current working directory use %2 command and to list contents of the current working directory "
+ "use %3 command."
+ );
+}
+
+void CliCommandCd::defineSyntax()
+{
+ syntax.setName("cd");
+ syntax.addArgument(DIR_PATH, tr("path", "CLI command syntax"));
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandcd.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommandcd.h
new file mode 100644
index 0000000..4cfabc9
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandcd.h
@@ -0,0 +1,15 @@
+#ifndef CLICOMMANDCD_H
+#define CLICOMMANDCD_H
+
+#include "clicommand.h"
+
+class CliCommandCd : public CliCommand
+{
+ public:
+ void execute();
+ QString shortHelp() const;
+ QString fullHelp() const;
+ void defineSyntax();
+};
+
+#endif // CLICOMMANDCD_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandclose.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommandclose.cpp
new file mode 100644
index 0000000..44fc72c
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandclose.cpp
@@ -0,0 +1,51 @@
+#include "clicommandclose.h"
+#include "cli.h"
+#include "db/db.h"
+#include "services/dbmanager.h"
+
+void CliCommandClose::execute()
+{
+ if (!syntax.isArgumentSet(DB_NAME) && !cli->getCurrentDb())
+ {
+ println(tr("Cannot call %1 when no database is set to be current. Specify current database with %2 command or pass database name to %3.")
+ .arg(cmdName("close")).arg(cmdName("use")).arg(cmdName("close")));
+ return;
+ }
+
+ if (syntax.isArgumentSet(DB_NAME))
+ {
+ Db* db = DBLIST->getByName(syntax.getArgument(DB_NAME));
+ if (db)
+ {
+ db->close();
+ println(tr("Connection to database %1 closed.").arg(db->getName()));
+ }
+ else
+ println(tr("No such database: %1. Use %2 to see list of known databases.").arg(syntax.getArgument(DB_NAME)).arg(cmdName("dblist")));
+ }
+ else if (cli->getCurrentDb())
+ {
+ cli->getCurrentDb()->close();
+ println(tr("Connection to database %1 closed.").arg(cli->getCurrentDb()->getName()));
+ }
+}
+
+QString CliCommandClose::shortHelp() const
+{
+ return tr("closes given (or current) database");
+}
+
+QString CliCommandClose::fullHelp() const
+{
+ return tr(
+ "Closes database connection. If the database was already closed, nothing happens. "
+ "If <name> is provided, it should be name of the database to close (as printed by %1 command). "
+ "The the <name> is not provided, then current working database is closed (see help for %2 for details)."
+ ).arg(cmdName("dblist")).arg(cmdName("use"));
+}
+
+void CliCommandClose::defineSyntax()
+{
+ syntax.setName("close");
+ syntax.addArgument(DB_NAME, tr("name", "CLI command syntax"), false);
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandclose.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommandclose.h
new file mode 100644
index 0000000..04fbbeb
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandclose.h
@@ -0,0 +1,15 @@
+#ifndef CLICOMMANDCLOSE_H
+#define CLICOMMANDCLOSE_H
+
+#include "clicommand.h"
+
+class CliCommandClose : public CliCommand
+{
+ public:
+ void execute();
+ QString shortHelp() const;
+ QString fullHelp() const;
+ void defineSyntax();
+};
+
+#endif // CLICOMMANDCLOSE_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommanddblist.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommanddblist.cpp
new file mode 100644
index 0000000..cbc16b6
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommanddblist.cpp
@@ -0,0 +1,86 @@
+#include "clicommanddblist.h"
+#include "cli.h"
+#include "services/dbmanager.h"
+#include "common/unused.h"
+#include "common/utils.h"
+#include <QList>
+
+void CliCommandDbList::execute()
+{
+ if (!cli->getCurrentDb())
+ {
+ println(tr("No current working database defined."));
+ return;
+ }
+
+ QString currentName = cli->getCurrentDb()->getName();
+
+ println(tr("Databases:"));
+ QList<Db*> dbList = DBLIST->getDbList();
+ QString path;
+ QString msg;
+
+ int maxNameLength = tr("Name", "CLI db name column").length();
+ int lgt = 0;
+ foreach (Db* db, dbList)
+ {
+ lgt = db->getName().length() + 1;
+ maxNameLength = qMax(maxNameLength, lgt);
+ }
+
+ int connStateLength = qMax(tr("Open", "CLI connection state column").length(), tr("Closed", "CLI connection state column").length());
+ connStateLength = qMax(connStateLength, tr("Connection", "CLI connection state column").length());
+
+ msg = pad(tr("Name", "CLI db name column"), maxNameLength, ' ');
+ msg += "|";
+ msg += pad(tr("Connection", "CLI connection state column"), connStateLength, ' ');
+ msg += "|";
+ msg += tr("Database file path");
+ println(msg);
+
+ msg = QString("-").repeated(maxNameLength);
+ msg += "+";
+ msg += QString("-").repeated(connStateLength);
+ msg += "+";
+ msg += QString("-").repeated(tr("Database file path").length() + 1);
+ println(msg);
+
+ QString name;
+ foreach (Db* db, dbList)
+ {
+ bool open = db->isOpen();
+ path = db->getPath();
+ name = db->getName();
+ if (name == currentName)
+ name.prepend("*");
+ else
+ name.prepend(" ");
+
+ msg = pad(name, maxNameLength, ' ');
+ msg += "|";
+ msg += pad((open ? tr("Open") : tr("Closed")), connStateLength, ' ');
+ msg += "|";
+ msg += path;
+ println(msg);
+ }
+}
+
+QString CliCommandDbList::shortHelp() const
+{
+ return tr("prints list of registered databases");
+}
+
+QString CliCommandDbList::fullHelp() const
+{
+ return tr(
+ "Prints list of databases registered in the SQLiteStudio. Each database on the list can be in open or closed state "
+ "and .dblist tells you that. The current working database (aka default database) is also marked on the list with '*' at the start of its name. "
+ "See help for %1 command to learn about the default database."
+ ).arg(cmdName("use"));
+}
+
+void CliCommandDbList::defineSyntax()
+{
+ syntax.setName("dblist");
+ syntax.addAlias("databases");
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommanddblist.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommanddblist.h
new file mode 100644
index 0000000..ecbaa6f
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommanddblist.h
@@ -0,0 +1,15 @@
+#ifndef CLICOMMANDDBLIST_H
+#define CLICOMMANDDBLIST_H
+
+#include "clicommand.h"
+
+class CliCommandDbList : public CliCommand
+{
+ public:
+ void execute();
+ QString shortHelp() const;
+ QString fullHelp() const;
+ void defineSyntax();
+};
+
+#endif // CLICOMMANDDBLIST_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommanddesc.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommanddesc.cpp
new file mode 100644
index 0000000..4865e23
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommanddesc.cpp
@@ -0,0 +1,26 @@
+#include "clicommanddesc.h"
+
+CliCommandDesc::CliCommandDesc()
+{
+}
+
+void CliCommandDesc::execute()
+{
+
+}
+
+QString CliCommandDesc::shortHelp() const
+{
+ return tr("shows details about the table");
+}
+
+QString CliCommandDesc::fullHelp() const
+{
+ return QString();
+}
+
+void CliCommandDesc::defineSyntax()
+{
+ syntax.setName("desc");
+ syntax.addArgument(TABLE, tr("table"));
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommanddesc.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommanddesc.h
new file mode 100644
index 0000000..ee52c30
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommanddesc.h
@@ -0,0 +1,16 @@
+#ifndef CLICOMMANDDESC_H
+#define CLICOMMANDDESC_H
+
+#include "clicommand.h"
+
+class CliCommandDesc : public CliCommand
+{
+ public:
+ CliCommandDesc();
+ void execute();
+ QString shortHelp() const;
+ QString fullHelp() const;
+ void defineSyntax();
+};
+
+#endif // CLICOMMANDDESC_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommanddir.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommanddir.cpp
new file mode 100644
index 0000000..c61f503
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommanddir.cpp
@@ -0,0 +1,50 @@
+#include "clicommanddir.h"
+#include <QDir>
+
+void CliCommandDir::execute()
+{
+ QDir dir;
+ QFileInfoList entries;
+
+ if (syntax.isArgumentSet(DIR_OR_FILE))
+ {
+ QString filter = getFilterAndFixDir(dir, syntax.getArgument(DIR_OR_FILE));
+ entries = dir.entryInfoList({filter}, QDir::AllEntries|QDir::NoDotAndDotDot, QDir::DirsFirst|QDir::LocaleAware|QDir::Name);
+ }
+ else
+ entries = dir.entryInfoList(QDir::AllEntries|QDir::NoDotAndDotDot, QDir::DirsFirst|QDir::LocaleAware|QDir::Name);
+
+ QString name;
+ foreach (const QFileInfo& entry, entries)
+ {
+ name = entry.fileName();
+ if (entry.isDir())
+ name += "/";
+
+ if (dir != QDir::current())
+ name.prepend(dir.path()+"/");
+
+ println(name);
+ }
+}
+
+QString CliCommandDir::shortHelp() const
+{
+ return tr("lists directories and files in current working directory");
+}
+
+QString CliCommandDir::fullHelp() const
+{
+ return tr(
+ "This is very similar to 'dir' command known from Windows and 'ls' command from Unix systems.\n"
+ "\n"
+ "You can pass <pattern> with wildcard characters to filter output."
+ );
+}
+
+void CliCommandDir::defineSyntax()
+{
+ syntax.setName("dir");
+ syntax.addAlias("ls");
+ syntax.addArgument(DIR_OR_FILE, tr("pattern"), false);
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommanddir.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommanddir.h
new file mode 100644
index 0000000..8aa5ccc
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommanddir.h
@@ -0,0 +1,15 @@
+#ifndef CLICOMMANDDIR_H
+#define CLICOMMANDDIR_H
+
+#include "clicommand.h"
+
+class CliCommandDir : public CliCommand
+{
+ public:
+ void execute();
+ QString shortHelp() const;
+ QString fullHelp() const;
+ void defineSyntax();
+};
+
+#endif // CLICOMMANDDIR_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandexit.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommandexit.cpp
new file mode 100644
index 0000000..2c45e1d
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandexit.cpp
@@ -0,0 +1,26 @@
+#include "clicommandexit.h"
+#include "cli.h"
+#include "common/unused.h"
+
+void CliCommandExit::execute()
+{
+ cli->exit();
+}
+
+QString CliCommandExit::shortHelp() const
+{
+ return tr("quits the application");
+}
+
+QString CliCommandExit::fullHelp() const
+{
+ return tr(
+ "Quits the application. Settings are stored in configuration file and will be restored on next startup."
+ );
+}
+
+void CliCommandExit::defineSyntax()
+{
+ syntax.setName("exit");
+ syntax.addAlias("quit");
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandexit.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommandexit.h
new file mode 100644
index 0000000..dc061bb
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandexit.h
@@ -0,0 +1,15 @@
+#ifndef CLICOMMANDEXIT_H
+#define CLICOMMANDEXIT_H
+
+#include "clicommand.h"
+
+class CliCommandExit : public CliCommand
+{
+ public:
+ void execute();
+ QString shortHelp() const;
+ QString fullHelp() const;
+ void defineSyntax();
+};
+
+#endif // CLICOMMANDEXIT_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandfactory.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommandfactory.cpp
new file mode 100644
index 0000000..742709b
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandfactory.cpp
@@ -0,0 +1,85 @@
+#include "clicommandfactory.h"
+#include "cli.h"
+#include "clicommandadd.h"
+#include "clicommandremove.h"
+#include "clicommandexit.h"
+#include "clicommanddblist.h"
+#include "clicommanduse.h"
+#include "clicommandopen.h"
+#include "clicommandclose.h"
+#include "clicommandsql.h"
+#include "clicommandhelp.h"
+#include "clicommandtables.h"
+#include "clicommandmode.h"
+#include "clicommandnullvalue.h"
+#include "clicommandhistory.h"
+#include "clicommanddir.h"
+#include "clicommandpwd.h"
+#include "clicommandcd.h"
+#include "clicommandtree.h"
+#include "clicommanddesc.h"
+#include <QDebug>
+
+QHash<QString,CliCommandFactory::CliCommandCreatorFunc> CliCommandFactory::mapping;
+
+#define REGISTER_CMD(Cmd) registerCommand([]() -> CliCommand* {return new Cmd();})
+
+void CliCommandFactory::init()
+{
+ REGISTER_CMD(CliCommandAdd);
+ REGISTER_CMD(CliCommandRemove);
+ REGISTER_CMD(CliCommandExit);
+ REGISTER_CMD(CliCommandDbList);
+ REGISTER_CMD(CliCommandUse);
+ REGISTER_CMD(CliCommandOpen);
+ REGISTER_CMD(CliCommandClose);
+ REGISTER_CMD(CliCommandSql);
+ REGISTER_CMD(CliCommandHelp);
+ REGISTER_CMD(CliCommandTables);
+ REGISTER_CMD(CliCommandMode);
+ REGISTER_CMD(CliCommandNullValue);
+ REGISTER_CMD(CliCommandHistory);
+ REGISTER_CMD(CliCommandDir);
+ REGISTER_CMD(CliCommandPwd);
+ REGISTER_CMD(CliCommandCd);
+ REGISTER_CMD(CliCommandTree);
+ REGISTER_CMD(CliCommandDesc);
+}
+
+CliCommand *CliCommandFactory::getCommand(const QString &cmdName)
+{
+ if (!mapping.contains(cmdName))
+ return nullptr;
+
+ return mapping[cmdName]();
+}
+
+QHash<QString,CliCommand*> CliCommandFactory::getAllCommands()
+{
+ QHash<QString,CliCommand*> results;
+ QHashIterator<QString,CliCommandFactory::CliCommandCreatorFunc> it(mapping);
+ while (it.hasNext())
+ {
+ it.next();
+ results[it.key()] = it.value()();
+ }
+
+ return results;
+}
+
+QStringList CliCommandFactory::getCommandNames()
+{
+ return mapping.keys();
+}
+
+void CliCommandFactory::registerCommand(CliCommandCreatorFunc func)
+{
+ CliCommand* cmd = func();
+ cmd->defineSyntax();
+
+ mapping[cmd->getName()] = func;
+ foreach (const QString& alias, cmd->aliases())
+ mapping[alias] = func;
+
+ delete cmd;
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandfactory.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommandfactory.h
new file mode 100644
index 0000000..4329463
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandfactory.h
@@ -0,0 +1,25 @@
+#ifndef CLICOMMANDFACTORY_H
+#define CLICOMMANDFACTORY_H
+
+#include <QString>
+#include <QHash>
+
+class CliCommand;
+
+class CliCommandFactory
+{
+ public:
+ static void init();
+ static CliCommand* getCommand(const QString& cmdName);
+ static QHash<QString,CliCommand*> getAllCommands();
+ static QStringList getCommandNames();
+
+ private:
+ typedef CliCommand* (*CliCommandCreatorFunc)();
+
+ static void registerCommand(CliCommandCreatorFunc func);
+
+ static QHash<QString,CliCommandCreatorFunc> mapping;
+};
+
+#endif // CLICOMMANDFACTORY_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandhelp.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommandhelp.cpp
new file mode 100644
index 0000000..7a957c3
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandhelp.cpp
@@ -0,0 +1,86 @@
+#include "clicommandhelp.h"
+#include "clicommandfactory.h"
+#include "common/utils.h"
+#include "cli_config.h"
+
+void CliCommandHelp::execute()
+{
+ if (syntax.isArgumentSet(CMD_NAME))
+ printHelp(syntax.getArgument(CMD_NAME));
+ else
+ printHelp();
+}
+
+QString CliCommandHelp::shortHelp() const
+{
+ return tr("shows this help message");
+}
+
+QString CliCommandHelp::fullHelp() const
+{
+ return tr(
+ "Use %1 to learn about certain commands supported by the command line interface (CLI) of the SQLiteStudio.\n"
+ "To see list of supported commands, type %2 without any arguments.\n\n"
+ "When passing <command> name, you can skip special prefix character ('%3').\n\n"
+ "You can always execute any command with exactly single '--help' option to see help for that command. "
+ "It's an alternative for typing: %1 <command>."
+ ).arg(cmdName("help")).arg(cmdName("help")).arg(CFG_CLI.Console.CommandPrefixChar.get()).arg(cmdName("help"));
+}
+
+void CliCommandHelp::defineSyntax()
+{
+ syntax.setName("help");
+ syntax.addArgument(CMD_NAME, tr("command", "CLI command syntax"), false);
+}
+
+void CliCommandHelp::printHelp(const QString& cmd)
+{
+ QString cmdStr = cmd.startsWith(".") ? cmd.mid(1) : cmd;
+ CliCommand* command = CliCommandFactory::getCommand(cmdStr);
+ if (!command)
+ {
+ println(tr("No such command: %1").arg(cmd));
+ println(tr("Type '%1' for list of available commands.").arg(cmdName("help")));
+ println("");
+ return;
+ }
+ command->defineSyntax();
+ QStringList aliases = command->aliases();
+ QString prefix = CFG_CLI.Console.CommandPrefixChar.get();
+
+ QString msg;
+ msg += tr("Usage: %1%2").arg(prefix).arg(command->usage(cmdStr));
+ msg += "\n";
+ if (aliases.size() > 0)
+ {
+ if (aliases.contains(cmdStr))
+ {
+ aliases.removeOne(cmdStr);
+ aliases << command->getName();
+ }
+
+ msg += tr("Aliases: %1").arg(prefix + aliases.join(", " + prefix));
+ msg += "\n";
+ }
+ msg += "\n";
+ msg += command->fullHelp();
+ delete command;
+
+ printBox(msg);
+}
+
+void CliCommandHelp::printHelp()
+{
+ QHash<QString, CliCommand*> allCommands = CliCommandFactory::getAllCommands();
+ QStringList names = allCommands.keys();
+ int width = longest(names).size();
+
+ names.sort();
+ QStringList msgList;
+ foreach (const QString& cmd, names)
+ {
+ msgList << (CFG_CLI.Console.CommandPrefixChar.get() + pad(cmd, width, ' ') + " - " + allCommands[cmd]->shortHelp());
+ delete allCommands[cmd];
+ }
+ printBox(msgList.join("\n"));
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandhelp.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommandhelp.h
new file mode 100644
index 0000000..c12c588
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandhelp.h
@@ -0,0 +1,19 @@
+#ifndef CLICOMMANDHELP_H
+#define CLICOMMANDHELP_H
+
+#include "clicommand.h"
+
+class CliCommandHelp : public CliCommand
+{
+ public:
+ void execute();
+ QString shortHelp() const;
+ QString fullHelp() const;
+ void defineSyntax();
+
+ private:
+ void printHelp(const QString& cmd);
+ void printHelp();
+};
+
+#endif // CLICOMMANDHELP_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandhistory.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommandhistory.cpp
new file mode 100644
index 0000000..fd2a494
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandhistory.cpp
@@ -0,0 +1,81 @@
+#include "clicommandhistory.h"
+#include "cli.h"
+#include "common/utils.h"
+#include "cliutils.h"
+#include "services/config.h"
+
+void CliCommandHistory::execute()
+{
+ if (syntax.isOptionSet(OPER_TYPE))
+ {
+ clear();
+ return;
+ }
+
+ if (syntax.isOptionSet(HIST_LIMIT))
+ {
+ setMax(syntax.getOptionValue(HIST_LIMIT));
+ return;
+ }
+
+ if (syntax.isOptionSet(SHOW_LIMIT))
+ {
+ println(tr("Current history limit is set to: %1").arg(CFG_CORE.Console.HistorySize.get()));
+ return;
+ }
+
+ int cols = getCliColumns();
+ QString hline = pad("", cols, '-');
+ foreach (const QString& line, cli->getHistory())
+ {
+ print(hline);
+ println(line);
+ }
+ println(hline);
+}
+
+QString CliCommandHistory::shortHelp() const
+{
+ return tr("prints history or erases it");
+}
+
+QString CliCommandHistory::fullHelp() const
+{
+ return tr(
+ "When no argument was passed, this command prints command line history. "
+ "Every history entry is separated with a horizontal line, so multiline entries are easier to read.\n"
+ "\n"
+ "When the -c or --clear option is passed, then the history gets erased.\n"
+ "When the -l or --limit option is passed, it sets the new history entries limit. It requires an additional argument"
+ "saying how many entries do you want the history to be limited to.\n"
+ "Use -ql or --querylimit option to see the current limit value."
+ );
+}
+
+void CliCommandHistory::defineSyntax()
+{
+ syntax.setName("history");
+ syntax.addOption(OPER_TYPE, "c", "clear");
+ syntax.addOptionWithArg(HIST_LIMIT, "l", "limit", tr("number"));
+ syntax.addOption(SHOW_LIMIT, "ql", "querylimit");
+}
+
+void CliCommandHistory::clear()
+{
+ cli->clearHistory();
+ println(tr("Console history erased."));
+}
+
+void CliCommandHistory::setMax(const QString& arg)
+{
+ bool ok;
+ int max = arg.toInt(&ok);
+ if (!ok)
+ {
+ println(tr("Invalid number: %1").arg(arg));
+ return;
+ }
+ CFG_CORE.Console.HistorySize.set(max);
+ cli->applyHistoryLimit();
+ println(tr("History limit set to %1").arg(max));
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandhistory.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommandhistory.h
new file mode 100644
index 0000000..f2e07d0
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandhistory.h
@@ -0,0 +1,26 @@
+#ifndef CLICOMMANDHISTORY_H
+#define CLICOMMANDHISTORY_H
+
+#include "clicommand.h"
+
+class CliCommandHistory : public CliCommand
+{
+ public:
+ void execute();
+ QString shortHelp() const;
+ QString fullHelp() const;
+ void defineSyntax();
+
+ private:
+ enum ArgIds
+ {
+ OPER_TYPE,
+ HIST_LIMIT,
+ SHOW_LIMIT
+ };
+
+ void clear();
+ void setMax(const QString& arg);
+};
+
+#endif // CLICOMMANDHISTORY_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandmode.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommandmode.cpp
new file mode 100644
index 0000000..b258045
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandmode.cpp
@@ -0,0 +1,65 @@
+#include "clicommandmode.h"
+#include "common/unused.h"
+#include "cli_config.h"
+
+void CliCommandMode::execute()
+{
+ if (!syntax.isArgumentSet(MODE))
+ {
+ println(tr("Current results printing mode: %1").arg(CliResultsDisplay::mode(CFG_CLI.Console.ResultsDisplayMode.get())));
+ return;
+ }
+
+ CliResultsDisplay::Mode mode = CliResultsDisplay::mode(syntax.getArgument(MODE).toUpper());
+ if (syntax.getArgument(MODE).toUpper() != CliResultsDisplay::mode(mode))
+ {
+ println(tr("Invalid results printing mode: %1").arg(syntax.getArgument(MODE).toUpper()));
+ return;
+ }
+
+ CFG_CLI.Console.ResultsDisplayMode.set(mode);
+ println(tr("New results printing mode: %1").arg(CliResultsDisplay::mode(mode)));
+}
+
+QString CliCommandMode::shortHelp() const
+{
+ return tr("tells or changes the query results format");
+}
+
+QString CliCommandMode::fullHelp() const
+{
+ return tr(
+ "When called without argument, tells the current output format for a query results. "
+ "When the <mode> is passed, the mode is changed to the given one. "
+ "Supported modes are:\n"
+ "- CLASSIC - columns are separated by a comma, not aligned,\n"
+ "- FIXED - columns have equal and fixed width, they always fit into terminal window width, but the data in columns can be cut off,\n"
+ "- COLUMNS - like FIXED, but smarter (do not use with huge result sets, see details below),\n"
+ "- ROW - each column from the row is displayed in new line, so the full data is displayed.\n"
+ "\n"
+ "The CLASSIC mode is recommended if you want to see all the data, but you don't want to waste lines for each column. "
+ "Each row will display full data for every column, but this also means, that columns will not be aligned to each other in next rows. "
+ "The CLASSIC mode also doesn't respect the width of your terminal (console) window, so if values in columns are wider than the window, "
+ "the row will be continued in next lines.\n"
+ "\n"
+ "The FIXED mode is recommended if you want a readable output and you don't care about long data values. "
+ "Columns will be aligned, making the output a nice table. The width of columns is calculated from width of the console window "
+ "and a number of columns.\n"
+ "\n"
+ "The COLUMNS mode is similar to FIXED mode, except it tries to be smart and make columns with shorter values more thin, "
+ "while columns with longer values get more space. First to shrink are columns with longest headers (so the header names are to be "
+ "cut off as first), then columns with the longest values are shrinked, up to the moment when all columns fit into terminal window.\n"
+ "ATTENTION! The COLUMNS mode reads all the results from the query at once in order to evaluate column widhts, therefore it is dangerous "
+ "to use this mode when working with huge result sets. Keep in mind that this mode will load entire result set into memory.\n"
+ "\n"
+ "The ROW mode is recommended if you need to see whole values and you don't expect many rows to be displayed, because this mode "
+ "displays a line of output per each column, so you'll get 10 lines for single row with 10 columns, then if you have 10 of such rows, "
+ "you will get 100 lines of output (+1 extra line per each row, to separate rows from each other)."
+ );
+}
+
+void CliCommandMode::defineSyntax()
+{
+ syntax.setName("mode");
+ syntax.addStrictArgument(MODE, {"classic", "fixed", "columns", "row"}, false);
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandmode.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommandmode.h
new file mode 100644
index 0000000..2b087d3
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandmode.h
@@ -0,0 +1,21 @@
+#ifndef CLICOMMANDMODE_H
+#define CLICOMMANDMODE_H
+
+#include "clicommand.h"
+
+class CliCommandMode : public CliCommand
+{
+ public:
+ void execute();
+ QString shortHelp() const;
+ QString fullHelp() const;
+ void defineSyntax();
+
+ private:
+ enum ArgIgs
+ {
+ MODE
+ };
+};
+
+#endif // CLICOMMANDMODE_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandnullvalue.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommandnullvalue.cpp
new file mode 100644
index 0000000..9616d9e
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandnullvalue.cpp
@@ -0,0 +1,32 @@
+#include "clicommandnullvalue.h"
+#include "cli_config.h"
+
+void CliCommandNullValue::execute()
+{
+ if (syntax.isArgumentSet(STRING))
+ CFG_CLI.Console.NullValue.set(syntax.getArgument(STRING));
+
+ println(tr("Current NULL representation string: %1").arg(CFG_CLI.Console.NullValue.get()));
+ return;
+}
+
+QString CliCommandNullValue::shortHelp() const
+{
+ return tr("tells or changes the NULL representation string");
+}
+
+QString CliCommandNullValue::fullHelp() const
+{
+ return tr(
+ "If no argument was passed, it tells what's the current NULL value representation "
+ "(that is - what is printed in place of NULL values in query results). "
+ "If the argument is given, then it's used as a new string to be used for NULL representation."
+ );
+}
+
+void CliCommandNullValue::defineSyntax()
+{
+ syntax.setName("null");
+ syntax.addAlias("nullvalue");
+ syntax.addArgument(STRING, QObject::tr("string", "CLI command syntax"), false);
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandnullvalue.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommandnullvalue.h
new file mode 100644
index 0000000..4ac4699
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandnullvalue.h
@@ -0,0 +1,15 @@
+#ifndef CLICOMMANDNULLVALUE_H
+#define CLICOMMANDNULLVALUE_H
+
+#include "clicommand.h"
+
+class CliCommandNullValue : public CliCommand
+{
+ public:
+ void execute();
+ QString shortHelp() const;
+ QString fullHelp() const;
+ void defineSyntax();
+};
+
+#endif // CLICOMMANDNULLVALUE_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandopen.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommandopen.cpp
new file mode 100644
index 0000000..fef1737
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandopen.cpp
@@ -0,0 +1,84 @@
+#include "clicommandopen.h"
+#include "cli.h"
+#include "services/dbmanager.h"
+#include <QFile>
+#include <QDir>
+#include <QDebug>
+
+void CliCommandOpen::execute()
+{
+ if (!syntax.isArgumentSet(DB_NAME_OR_FILE) && !cli->getCurrentDb())
+ {
+ println(tr("Cannot call %1 when no database is set to be current. Specify current database with %2 command or pass database name to %3.")
+ .arg(cmdName("open")).arg(cmdName("use")).arg(cmdName("open")));
+ return;
+ }
+
+ Db* db = nullptr;
+ if (syntax.isArgumentSet(DB_NAME_OR_FILE))
+ {
+ QString arg = syntax.getArgument(DB_NAME_OR_FILE);
+ db = DBLIST->getByName(arg);
+ if (!db)
+ {
+ if (QFile::exists(arg))
+ {
+ QString newName = DbManager::generateDbName(arg);
+ if (!DBLIST->addDb(newName, arg, false))
+ {
+ println(tr("Could not add database %1 to list.").arg(arg));
+ return;
+ }
+ db = DBLIST->getByName(arg);
+ Q_ASSERT(db != nullptr);
+ }
+ else
+ {
+ println(tr("File %1 doesn't exist in %2. Cannot open inexisting database with %3 command. "
+ "To create a new database, use %4 command.").arg(arg).arg(QDir::currentPath())
+ .arg(cmdName("open")).arg(cmdName("add")));
+ return;
+ }
+ }
+ }
+ else
+ {
+ db = cli->getCurrentDb();
+ if (!db)
+ {
+ qCritical() << "Default database is not in the list!";
+ return ;
+ }
+ }
+
+ if (!db->open())
+ {
+ println(db->getErrorText());
+ return;
+ }
+
+ cli->setCurrentDb(db);
+ println(tr("Database %1 has been open and set as the current working database.").arg(db->getName()));
+}
+
+QString CliCommandOpen::shortHelp() const
+{
+ return tr("opens database connection");
+}
+
+QString CliCommandOpen::fullHelp() const
+{
+ return tr(
+ "Opens connection to the database. If no additional argument was passed, then the connection is open to the "
+ "current default database (see help for %1 for details). However if an argument was passed, it can be either "
+ "<name> of the registered database to open, or it can be <path> to the database file to open. "
+ "In the second case, the <path> gets registered on the list with a generated name, but only for the period "
+ "of current application session. After restarting application such database is not restored on the list."
+ ).arg(cmdName("use"));
+}
+
+void CliCommandOpen::defineSyntax()
+{
+ syntax.setName("open");
+ syntax.addAlternatedArgument(DB_NAME_OR_FILE, {tr("name", "CLI command syntax"), tr("path", "CLI command syntax")}, false);
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandopen.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommandopen.h
new file mode 100644
index 0000000..e66f574
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandopen.h
@@ -0,0 +1,15 @@
+#ifndef CLICOMMANDOPEN_H
+#define CLICOMMANDOPEN_H
+
+#include "clicommand.h"
+
+class CliCommandOpen : public CliCommand
+{
+ public:
+ void execute();
+ QString shortHelp() const;
+ QString fullHelp() const;
+ void defineSyntax();
+};
+
+#endif // CLICOMMANDOPEN_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandpwd.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommandpwd.cpp
new file mode 100644
index 0000000..f96cae4
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandpwd.cpp
@@ -0,0 +1,28 @@
+#include "clicommandpwd.h"
+#include "common/unused.h"
+#include <QDir>
+
+void CliCommandPwd::execute()
+{
+ QDir dir;
+ println(dir.absolutePath());
+}
+
+QString CliCommandPwd::shortHelp() const
+{
+ return tr("prints the current working directory");
+}
+
+QString CliCommandPwd::fullHelp() const
+{
+ return tr(
+ "This is the same as 'pwd' command on Unix systems and 'cd' command without arguments on Windows. "
+ "It prints current working directory. You can change the current working directory with %1 command "
+ "and you can also list contents of the current working directory with %2 command."
+ ).arg(cmdName("cd")).arg(cmdName("dir"));
+}
+
+void CliCommandPwd::defineSyntax()
+{
+ syntax.setName("pwd");
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandpwd.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommandpwd.h
new file mode 100644
index 0000000..3568ce1
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandpwd.h
@@ -0,0 +1,15 @@
+#ifndef CLICOMMANDPWD_H
+#define CLICOMMANDPWD_H
+
+#include "clicommand.h"
+
+class CliCommandPwd : public CliCommand
+{
+ public:
+ void execute();
+ QString shortHelp() const;
+ QString fullHelp() const;
+ void defineSyntax();
+};
+
+#endif // CLICOMMANDPWD_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandremove.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommandremove.cpp
new file mode 100644
index 0000000..6474a78
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandremove.cpp
@@ -0,0 +1,51 @@
+#include "clicommandremove.h"
+#include "cli.h"
+#include "services/dbmanager.h"
+#include "db/db.h"
+
+void CliCommandRemove::execute()
+{
+ QString dbName = syntax.getArgument(DB_NAME);
+ Db* db = DBLIST->getByName(dbName);
+ if (!db)
+ {
+ println(tr("No such database: %1").arg(dbName));
+ return;
+ }
+
+ bool isCurrent = cli->getCurrentDb() == db;
+ QString name = db->getName();
+
+ DBLIST->removeDb(db);
+ println(tr("Database removed: %1").arg(name));
+
+ QList<Db*> dblist = DBLIST->getDbList();
+ if (isCurrent && dblist.size() > 0)
+ {
+ cli->setCurrentDb(dblist[0]);
+ println(tr("New current database set:"));
+ println(cli->getCurrentDb()->getName());
+ }
+ else
+ cli->setCurrentDb(nullptr);
+}
+
+QString CliCommandRemove::shortHelp() const
+{
+ return tr("removes database from the list");
+}
+
+QString CliCommandRemove::fullHelp() const
+{
+ return tr(
+ "Removes <name> database from the list of registered databases. "
+ "If the database was not on the list (see %1 command), then error message is printed "
+ "and nothing more happens."
+ ).arg(cmdName("dblist"));
+}
+
+void CliCommandRemove::defineSyntax()
+{
+ syntax.setName("remove");
+ syntax.addArgument(DB_NAME, tr("name", "CLI command syntax"));
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandremove.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommandremove.h
new file mode 100644
index 0000000..f4b5402
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandremove.h
@@ -0,0 +1,15 @@
+#ifndef CLICOMMANDREMOVE_H
+#define CLICOMMANDREMOVE_H
+
+#include "clicommand.h"
+
+class CliCommandRemove : public CliCommand
+{
+ public:
+ void execute();
+ QString shortHelp() const;
+ QString fullHelp() const;
+ void defineSyntax();
+};
+
+#endif // CLICOMMANDREMOVE_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandsql.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommandsql.cpp
new file mode 100644
index 0000000..3f2c4b3
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandsql.cpp
@@ -0,0 +1,506 @@
+#include "clicommandsql.h"
+#include "cli.h"
+#include "parser/ast/sqliteselect.h"
+#include "parser/parser.h"
+#include "parser/parsererror.h"
+#include "db/queryexecutor.h"
+#include "qio.h"
+#include "common/unused.h"
+#include "cli_config.h"
+#include "cliutils.h"
+#include <QList>
+#include <QDebug>
+
+void CliCommandSql::execute()
+{
+ if (!cli->getCurrentDb())
+ {
+ println(tr("No working database is set.\n"
+ "Call %1 command to set working database.\n"
+ "Call %2 to see list of all databases.")
+ .arg(cmdName("use")).arg(cmdName("dblist")));
+
+ return;
+ }
+
+ Db* db = cli->getCurrentDb();
+ if (!db || !db->isOpen())
+ {
+ println(tr("Database is not open."));
+ return;
+ }
+
+ // Executor deletes itself later when called with lambda.
+ QueryExecutor *executor = new QueryExecutor(db, syntax.getArgument(STRING));
+ connect(executor, SIGNAL(executionFinished(SqlQueryPtr)), this, SIGNAL(execComplete()));
+ connect(executor, SIGNAL(executionFailed(int,QString)), this, SLOT(executionFailed(int,QString)));
+ connect(executor, SIGNAL(executionFailed(int,QString)), this, SIGNAL(execComplete()));
+
+ executor->exec([=](SqlQueryPtr results)
+ {
+ if (results->isError())
+ return; // should not happen, since results handler function is called only for successful executions
+
+ switch (CFG_CLI.Console.ResultsDisplayMode.get())
+ {
+ case CliResultsDisplay::FIXED:
+ printResultsFixed(executor, results);
+ break;
+ case CliResultsDisplay::COLUMNS:
+ printResultsColumns(executor, results);
+ break;
+ case CliResultsDisplay::ROW:
+ printResultsRowByRow(executor, results);
+ break;
+ default:
+ printResultsClassic(executor, results);
+ break;
+ }
+ });
+}
+
+QString CliCommandSql::shortHelp() const
+{
+ return tr("executes SQL query");
+}
+
+QString CliCommandSql::fullHelp() const
+{
+ return tr(
+ "This command is executed every time you enter SQL query in command prompt. "
+ "It executes the query on the current working database (see help for %1 for details). "
+ "There's no sense in executing this command explicitly. Instead just type the SQL query "
+ "in the command prompt, without any command prefixed."
+ ).arg(cmdName("use"));
+}
+
+bool CliCommandSql::isAsyncExecution() const
+{
+ return true;
+}
+
+void CliCommandSql::defineSyntax()
+{
+ syntax.setName("query");
+ syntax.addArgument(STRING, tr("sql", "CLI command syntax"));
+ syntax.setStrictArgumentCount(false);
+}
+
+void CliCommandSql::printResultsClassic(QueryExecutor* executor, SqlQueryPtr results)
+{
+ int metaColumns = executor->getMetaColumnCount();
+ int resultColumnCount = executor->getResultColumns().size();
+
+ // Columns
+ foreach (const QueryExecutor::ResultColumnPtr& resCol, executor->getResultColumns())
+ qOut << resCol->displayName << "|";
+
+ qOut << "\n";
+
+ // Data
+ SqlResultsRowPtr row;
+ QList<QVariant> values;
+ int i;
+ while (results->hasNext())
+ {
+ row = results->next();
+ i = 0;
+ values = row->valueList().mid(metaColumns);
+ foreach (QVariant value, values)
+ {
+ qOut << getValueString(value);
+ if ((i + 1) < resultColumnCount)
+ qOut << "|";
+
+ i++;
+ }
+
+ qOut << "\n";
+ }
+ qOut.flush();
+}
+
+void CliCommandSql::printResultsFixed(QueryExecutor* executor, SqlQueryPtr results)
+{
+ QList<QueryExecutor::ResultColumnPtr> resultColumns = executor->getResultColumns();
+ int resultColumnsCount = resultColumns.size();
+ int metaColumns = executor->getMetaColumnCount();
+ int termCols = getCliColumns();
+ int baseColWidth = termCols / resultColumns.size() - 1;
+
+ if (resultColumnsCount == 0)
+ return;
+
+ if ((resultColumnsCount * 2 - 1) > termCols)
+ {
+ println(tr("Too many columns to display in %1 mode.").arg("FIXED"));
+ return;
+ }
+
+ int width;
+ QList<int> widths;
+ for (int i = 0; i < resultColumnsCount; i++)
+ {
+ width = baseColWidth;
+ if (i+1 == resultColumnsCount)
+ width += (termCols - resultColumnsCount * (baseColWidth + 1) + 1);
+
+ widths << width;
+ }
+
+ // Columns
+ QStringList columns;
+ foreach (const QueryExecutor::ResultColumnPtr& resCol, executor->getResultColumns())
+ columns << resCol->displayName;
+
+ printColumnHeader(widths, columns);
+
+ // Data
+ while (results->hasNext())
+ printColumnDataRow(widths, results->next(), metaColumns);
+
+ qOut.flush();
+}
+
+void CliCommandSql::printResultsColumns(QueryExecutor* executor, SqlQueryPtr results)
+{
+ // Check if we don't have more columns than we can display
+ QList<QueryExecutor::ResultColumnPtr> resultColumns = executor->getResultColumns();
+ int termCols = getCliColumns();
+ int resultColumnsCount = resultColumns.size();
+ QStringList headerNames;
+ if (resultColumnsCount == 0)
+ return;
+
+ // Every column requires at least 1 character width + column separators between them
+ if ((resultColumnsCount * 2 - 1) > termCols)
+ {
+ println(tr("Too many columns to display in %1 mode.").arg("COLUMNS"));
+ return;
+ }
+
+ // Preload data (we will calculate column widths basing on real values)
+ QList<SqlResultsRowPtr> allRows = results->getAll();
+ int metaColumns = executor->getMetaColumnCount();
+
+ // Get widths of each column in every data row, remember the longest ones
+ QList<SortedColumnWidth*> columnWidths;
+ SortedColumnWidth* colWidth = nullptr;
+ foreach (const QueryExecutor::ResultColumnPtr& resCol, resultColumns)
+ {
+ colWidth = new SortedColumnWidth();
+ colWidth->setHeaderWidth(resCol->displayName.length());
+ columnWidths << colWidth;
+ headerNames << resCol->displayName;
+ }
+
+ int dataLength;
+ foreach (const SqlResultsRowPtr& row, allRows)
+ {
+ for (int i = 0; i < resultColumnsCount; i++)
+ {
+ dataLength = row->value(metaColumns + i).toString().length();
+ columnWidths[i]->setMinDataWidth(dataLength);
+ }
+ }
+
+ // Calculate width as it would be required to display entire rows
+ int totalWidth = 0;
+ foreach (colWidth, columnWidths)
+ totalWidth += colWidth->getWidth();
+
+ totalWidth += (resultColumnsCount - 1); // column separators
+
+ // Adjust column sizes to fit into terminal window
+ if (totalWidth < termCols)
+ {
+ // Expanding last column
+ int diff = termCols - totalWidth;
+ columnWidths.last()->incrWidth(diff);
+ }
+ else if (totalWidth > termCols)
+ {
+ // Shrinking columns
+ shrinkColumns(columnWidths, termCols, resultColumnsCount, totalWidth);
+ }
+
+ // Printing
+ QList<int> finalWidths;
+ foreach (colWidth, columnWidths)
+ finalWidths << colWidth->getWidth();
+
+ printColumnHeader(finalWidths, headerNames);
+
+ foreach (SqlResultsRowPtr row, allRows)
+ printColumnDataRow(finalWidths, row, metaColumns);
+
+ qOut.flush();
+}
+
+void CliCommandSql::printResultsRowByRow(QueryExecutor* executor, SqlQueryPtr results)
+{
+ // Columns
+ int metaColumns = executor->getMetaColumnCount();
+ int colWidth = 0;
+ foreach (const QueryExecutor::ResultColumnPtr& resCol, executor->getResultColumns())
+ {
+ if (resCol->displayName.length() > colWidth)
+ colWidth = resCol->displayName.length();
+ }
+
+ QStringList columns;
+ foreach (const QueryExecutor::ResultColumnPtr& resCol, executor->getResultColumns())
+ columns << pad(resCol->displayName, -colWidth, ' ');
+
+ // Data
+ static const QString rowCntTemplate = tr("Row %1");
+ int termWidth = getCliColumns();
+ QString rowCntString;
+ int i;
+ int rowCnt = 1;
+ SqlResultsRowPtr row;
+ while (results->hasNext())
+ {
+ row = results->next();
+ i = 0;
+ rowCntString = " " + rowCntTemplate.arg(rowCnt) + " ";
+ qOut << center(rowCntString, termWidth - 1, '-') << "\n";
+ foreach (QVariant value, row->valueList().mid(metaColumns))
+ {
+ qOut << columns[i] + ": " + getValueString(value) << "\n";
+ i++;
+ }
+ rowCnt++;
+ }
+ qOut.flush();
+}
+
+void CliCommandSql::shrinkColumns(QList<CliCommandSql::SortedColumnWidth*>& columnWidths, int termCols, int resultColumnsCount, int totalWidth)
+{
+ // This implements quite a smart shrinking algorithm:
+ // All columns are sorted by their current total width (data and header width)
+ // and then longest headers are shrinked first, then if headers are no longer a problem,
+ // but the data is - then longest data values are shrinked.
+ // If either the hader or the data value is huge (way more than fits into terminal),
+ // then such column is shrinked in one step to a reasonable width, so it can be later
+ // shrinked more precisely.
+ int maxSingleColumnWidth = (termCols - (resultColumnsCount - 1) * 2 );
+ bool shrinkData;
+ int previousTotalWidth = -1;
+ while (totalWidth > termCols && totalWidth != previousTotalWidth)
+ {
+ shrinkData = true;
+ previousTotalWidth = totalWidth;
+
+ // Sort columns by current widths
+ qSort(columnWidths);
+
+ // See if we can shrink headers only, or we already need to shrink the data
+ foreach (SortedColumnWidth* colWidth, columnWidths)
+ {
+ if (colWidth->isHeaderLonger())
+ {
+ shrinkData = false;
+ break;
+ }
+ }
+
+ // Do the shrinking
+ if (shrinkData)
+ {
+ for (int i = resultColumnsCount - 1; i >= 0; i--)
+ {
+ // If the data is way larger then the terminal, shrink it to reasonable length in one step.
+ // We also make sure that after data shrinking, the header didn't become longer than the data,
+ // cause at this moment, we were finished with headers and we enforce shrinking data
+ // and so do with headers.
+ if (columnWidths[i]->getDataWidth() > maxSingleColumnWidth)
+ {
+ totalWidth -= (columnWidths[i]->getDataWidth() - maxSingleColumnWidth);
+ columnWidths[i]->setDataWidth(maxSingleColumnWidth);
+ columnWidths[i]->setMaxHeaderWidth(maxSingleColumnWidth);
+ break;
+ }
+ else if (columnWidths[i]->getDataWidth() > 1) // just shrink it by 1
+ {
+ totalWidth -= 1;
+ columnWidths[i]->decrDataWidth();
+ columnWidths[i]->setMaxHeaderWidth(columnWidths[i]->getDataWidth());
+ break;
+ }
+ }
+ }
+ else // shrinking headers
+ {
+ for (int i = resultColumnsCount - 1; i >= 0; i--)
+ {
+ // We will shrink only the header that
+ if (!columnWidths[i]->isHeaderLonger())
+ continue;
+
+ // If the header is way larger then the terminal, shrink it to reasonable length in one step
+ if (columnWidths[i]->getHeaderWidth() > maxSingleColumnWidth)
+ {
+ totalWidth -= (columnWidths[i]->getHeaderWidth() - maxSingleColumnWidth);
+ columnWidths[i]->setHeaderWidth(maxSingleColumnWidth);
+ break;
+ }
+ else if (columnWidths[i]->getHeaderWidth() > 1) // otherwise just shrink it by 1
+ {
+ totalWidth -= 1;
+ columnWidths[i]->decrHeaderWidth();
+ break;
+ }
+ }
+ }
+ }
+
+ if (totalWidth == previousTotalWidth && totalWidth > termCols)
+ qWarning() << "The shrinking algorithm in printResultsColumns() failed, it could not shrink columns enough.";
+}
+
+void CliCommandSql::printColumnHeader(const QList<int>& widths, const QStringList& columns)
+{
+ QStringList line;
+ int i = 0;
+ foreach (const QString& col, columns)
+ {
+ line << pad(col.left(widths[i]), widths[i], ' ');
+ i++;
+ }
+
+ qOut << line.join("|");
+
+ line.clear();
+ QString hline("-");
+ for (i = 0; i < columns.count(); i++)
+ line << hline.repeated(widths[i]);
+
+ qOut << line.join("+");
+}
+
+void CliCommandSql::printColumnDataRow(const QList<int>& widths, const SqlResultsRowPtr& row, int rowIdOffset)
+{
+ int i = 0;
+ QStringList line;
+ foreach (const QVariant& value, row->valueList().mid(rowIdOffset))
+ {
+ line << pad(getValueString(value).left(widths[i]), widths[i], ' ');
+ i++;
+ }
+
+ qOut << line.join("|");
+}
+
+QString CliCommandSql::getValueString(const QVariant& value)
+{
+ if (value.isValid() && !value.isNull())
+ return value.toString();
+
+ return CFG_CLI.Console.NullValue.get();
+}
+
+void CliCommandSql::executionFailed(int code, const QString& msg)
+{
+ UNUSED(code);
+ qOut << tr("Query execution error: %1").arg(msg) << "\n\n";
+ qOut.flush();
+}
+
+CliCommandSql::SortedColumnWidth::SortedColumnWidth()
+{
+ dataWidth = 0;
+ headerWidth = 0;
+ width = 0;
+}
+
+bool CliCommandSql::SortedColumnWidth::operator<(const CliCommandSql::SortedColumnWidth& other)
+{
+ return width < other.width;
+}
+
+int CliCommandSql::SortedColumnWidth::getHeaderWidth() const
+{
+ return headerWidth;
+}
+
+void CliCommandSql::SortedColumnWidth::setHeaderWidth(int value)
+{
+ headerWidth = value;
+ updateWidth();
+}
+
+void CliCommandSql::SortedColumnWidth::setMaxHeaderWidth(int value)
+{
+ if (headerWidth > value)
+ {
+ headerWidth = value;
+ updateWidth();
+ }
+}
+
+void CliCommandSql::SortedColumnWidth::incrHeaderWidth(int value)
+{
+ headerWidth += value;
+ updateWidth();
+}
+
+void CliCommandSql::SortedColumnWidth::decrHeaderWidth(int value)
+{
+ headerWidth -= value;
+ updateWidth();
+}
+
+int CliCommandSql::SortedColumnWidth::getDataWidth() const
+{
+ return dataWidth;
+}
+
+void CliCommandSql::SortedColumnWidth::setDataWidth(int value)
+{
+ dataWidth = value;
+ updateWidth();
+}
+
+void CliCommandSql::SortedColumnWidth::setMinDataWidth(int value)
+{
+ if (dataWidth < value)
+ {
+ dataWidth = value;
+ updateWidth();
+ }
+}
+
+void CliCommandSql::SortedColumnWidth::incrDataWidth(int value)
+{
+ dataWidth += value;
+ updateWidth();
+}
+
+void CliCommandSql::SortedColumnWidth::decrDataWidth(int value)
+{
+ dataWidth -= value;
+ updateWidth();
+}
+
+void CliCommandSql::SortedColumnWidth::incrWidth(int value)
+{
+ width += value;
+ dataWidth = width;
+ headerWidth = width;
+}
+
+int CliCommandSql::SortedColumnWidth::getWidth() const
+{
+ return width;
+}
+
+bool CliCommandSql::SortedColumnWidth::isHeaderLonger() const
+{
+ return headerWidth > dataWidth;
+}
+
+void CliCommandSql::SortedColumnWidth::updateWidth()
+{
+ width = qMax(headerWidth, dataWidth);
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandsql.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommandsql.h
new file mode 100644
index 0000000..5423a85
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandsql.h
@@ -0,0 +1,66 @@
+#ifndef CLICOMMANDSQL_H
+#define CLICOMMANDSQL_H
+
+#include "clicommand.h"
+#include "db/sqlquery.h"
+
+class QueryExecutor;
+
+class CliCommandSql : public CliCommand
+{
+ Q_OBJECT
+
+ public:
+ void execute();
+ QString shortHelp() const;
+ QString fullHelp() const;
+ bool isAsyncExecution() const;
+ void defineSyntax();
+
+ private:
+ class SortedColumnWidth
+ {
+ public:
+ SortedColumnWidth();
+
+ bool operator<(const SortedColumnWidth& other);
+
+ int getHeaderWidth() const;
+ void setHeaderWidth(int value);
+ void setMaxHeaderWidth(int value);
+ void incrHeaderWidth(int value = 1);
+ void decrHeaderWidth(int value = 1);
+
+ int getDataWidth() const;
+ void setDataWidth(int value);
+ void setMinDataWidth(int value);
+ void incrDataWidth(int value = 1);
+ void decrDataWidth(int value = 1);
+
+ void incrWidth(int value = 0);
+ int getWidth() const;
+ bool isHeaderLonger() const;
+
+ private:
+ void updateWidth();
+
+ int width;
+ int headerWidth;
+ int dataWidth;
+ };
+
+ void printResultsClassic(QueryExecutor *executor, SqlQueryPtr results);
+ void printResultsFixed(QueryExecutor *executor, SqlQueryPtr results);
+ void printResultsColumns(QueryExecutor *executor, SqlQueryPtr results);
+ void printResultsRowByRow(QueryExecutor *executor, SqlQueryPtr results);
+ void shrinkColumns(QList<SortedColumnWidth*>& columnWidths, int termCols, int resultColumnsCount, int totalWidth);
+ void printColumnHeader(const QList<int>& widths, const QStringList& columns);
+ void printColumnDataRow(const QList<int>& widths, const SqlResultsRowPtr& row, int rowIdOffset);
+
+ QString getValueString(const QVariant& value);
+
+ private slots:
+ void executionFailed(int code, const QString& msg);
+};
+
+#endif // CLICOMMANDSQL_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandtables.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommandtables.cpp
new file mode 100644
index 0000000..9a6b325
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandtables.cpp
@@ -0,0 +1,79 @@
+#include "clicommandtables.h"
+#include "cli.h"
+#include "schemaresolver.h"
+#include "services/dbmanager.h"
+#include "common/utils.h"
+
+void CliCommandTables::execute()
+{
+ Db* db = nullptr;
+ if (syntax.isArgumentSet(DB_NAME))
+ {
+ db = DBLIST->getByName(syntax.getArgument(DB_NAME));
+ if (!db)
+ {
+ println(tr("No such database: %1. Use .dblist to see list of known databases.").arg(syntax.getArgument(DB_NAME)));
+ return;
+ }
+ }
+ else if (cli->getCurrentDb())
+ {
+ db = cli->getCurrentDb();
+ }
+ else
+ {
+ println(tr("Cannot call %1 when no database is set to be current. Specify current database with %2 command or pass database name to %3.")
+ .arg(cmdName("tables")).arg(cmdName("use")).arg(cmdName("tables")));
+ return;
+ }
+
+ if (!db->isOpen())
+ {
+ println(tr("Database %1 is closed.").arg(db->getName()));
+ return;
+ }
+
+ println();
+
+ SchemaResolver resolver(db);
+ resolver.setIgnoreSystemObjects(!syntax.isOptionSet(SHOW_SYSTEM_TABLES));
+ QSet<QString> dbList;
+ dbList << "main" << "temp";
+ dbList += resolver.getDatabases();
+
+ int width = longest(dbList.toList()).length();
+ width = qMax(width, tr("Database").length());
+
+ println(pad(tr("Database"), width, ' ') + " " + tr("Table"));
+ println(pad("", width, '-') + "-------------------");
+
+ foreach (const QString& dbName, dbList)
+ {
+ foreach (const QString& table, resolver.getTables(dbName))
+ println(pad(dbName, width, ' ') + " " + table);
+ }
+
+ println();
+}
+
+QString CliCommandTables::shortHelp() const
+{
+ return tr("prints list of tables in the database");
+}
+
+QString CliCommandTables::fullHelp() const
+{
+ return tr(
+ "Prints list of tables in given <database> or in the current working database. "
+ "Note, that the <database> should be the name of the registered database (see %1). "
+ "The output list includes all tables from any other databases attached to the queried database.\n"
+ "When the -s option is given, then system tables are also listed."
+ ).arg(cmdName("use"));
+}
+
+void CliCommandTables::defineSyntax()
+{
+ syntax.setName("tables");
+ syntax.addArgument(DB_NAME, tr("database", "CLI command syntax"), false);
+ syntax.addOptionShort(SHOW_SYSTEM_TABLES, "s");
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandtables.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommandtables.h
new file mode 100644
index 0000000..68ae314
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandtables.h
@@ -0,0 +1,21 @@
+#ifndef CLICOMMANDTABLES_H
+#define CLICOMMANDTABLES_H
+
+#include "clicommand.h"
+
+class CliCommandTables : public CliCommand
+{
+ public:
+ void execute();
+ QString shortHelp() const;
+ QString fullHelp() const;
+ void defineSyntax();
+
+ private:
+ enum ArgIds
+ {
+ SHOW_SYSTEM_TABLES
+ };
+};
+
+#endif // CLICOMMANDTABLES_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandtree.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommandtree.cpp
new file mode 100644
index 0000000..cbd57c6
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandtree.cpp
@@ -0,0 +1,154 @@
+#include "clicommandtree.h"
+#include "cli.h"
+#include "common/sortedhash.h"
+#include "common/unused.h"
+
+const QString CliCommandTree::metaNodeNameTemplate = "<%1>";
+
+void CliCommandTree::execute()
+{
+ if (!cli->getCurrentDb())
+ {
+ println(tr("No current working database is selected. Use %1 to define one and then run %2.").arg(cmdName("use")).arg(cmdName("tree")));
+ return;
+ }
+
+ bool printColumns = syntax.isOptionSet(COLUMNS);
+ bool printSystemObjects = syntax.isOptionSet(SYSTEM_OBJECTS);
+
+ SchemaResolver resolver(cli->getCurrentDb());
+ resolver.setIgnoreSystemObjects(!printSystemObjects);
+
+ QStringList databases;
+ if (syntax.isArgumentSet(INTRNAL_DB))
+ {
+ databases << syntax.getArgument(INTRNAL_DB);
+ }
+ else
+ {
+ databases << "main" << "temp";
+ databases += resolver.getDatabases().toList();
+ }
+
+ AsciiTree tree;
+ tree.label = cli->getCurrentDb()->getName();
+ foreach (const QString& database, databases)
+ {
+ tree.childs << getDatabaseTree(database, resolver, printColumns);
+ }
+
+ println();
+ println(toAsciiTree(tree));
+ println();
+}
+
+AsciiTree CliCommandTree::getDatabaseTree(const QString& database, SchemaResolver& resolver, bool printColumns)
+{
+ QStringList tables = resolver.getTables(database);
+ QStringList views = resolver.getViews(database);
+
+ AsciiTree tree;
+ AsciiTree tablesTree;
+ AsciiTree viewsTree;
+
+ tablesTree.label = metaNodeNameTemplate.arg(tr("Tables"));
+ foreach (const QString& table, tables)
+ tablesTree.childs << getTableTree(database, table, resolver, printColumns);
+
+ viewsTree.label = metaNodeNameTemplate.arg(tr("Views"));
+ foreach (const QString& view, views)
+ viewsTree.childs << getViewTree(database, view, resolver);
+
+ tree.label = database;
+ tree.childs << tablesTree << viewsTree;
+ return tree;
+}
+
+AsciiTree CliCommandTree::getTableTree(const QString& database, const QString& table, SchemaResolver& resolver, bool printColumns)
+{
+ QStringList columns;
+ if (printColumns)
+ columns = resolver.getTableColumns(database, table);
+
+ QStringList indexes = resolver.getIndexesForTable(database, table);
+ QStringList triggers = resolver.getTriggersForTable(database, table);
+
+ AsciiTree tree;
+ AsciiTree columnsTree;
+ AsciiTree indexesTree;
+ AsciiTree triggersTree;
+
+ if (printColumns)
+ {
+ columnsTree.label = metaNodeNameTemplate.arg(tr("Columns"));
+ foreach (const QString& column, columns)
+ columnsTree.childs << getTreeLeaf(column);
+ }
+
+ indexesTree.label = metaNodeNameTemplate.arg(tr("Indexes"));
+ foreach (const QString& index, indexes)
+ indexesTree.childs << getTreeLeaf(index);
+
+ triggersTree.label = metaNodeNameTemplate.arg(tr("Triggers"));
+ foreach (const QString& trig, triggers)
+ triggersTree.childs << getTreeLeaf(trig);
+
+ if (printColumns)
+ tree.childs << columnsTree;
+
+ tree.label = table;
+ tree.childs << indexesTree;
+ tree.childs << triggersTree;
+
+ return tree;
+}
+
+AsciiTree CliCommandTree::getViewTree(const QString& database, const QString& view, SchemaResolver& resolver)
+{
+ QStringList triggers = resolver.getTriggersForView(database, view);
+
+ AsciiTree tree;
+ AsciiTree triggersTree;
+
+ triggersTree.label = metaNodeNameTemplate.arg(tr("Triggers"));
+ foreach (const QString& trig, triggers)
+ triggersTree.childs << getTreeLeaf(trig);
+
+ tree.label = view;
+ tree.childs << triggersTree;
+
+ return tree;
+}
+
+AsciiTree CliCommandTree::getTreeLeaf(const QString& column)
+{
+ AsciiTree tree;
+ tree.label = column;
+ return tree;
+}
+
+QString CliCommandTree::shortHelp() const
+{
+ return tr("prints all objects in the database as a tree");
+}
+
+QString CliCommandTree::fullHelp() const
+{
+ return tr(
+ "Prints all objects (tables, indexes, triggers and views) that are in the database as a tree. "
+ "The tree is very similar to the one that you can see in GUI client of the SQLiteStudio.\n"
+ "When -c option is given, then also columns will be listed under each table.\n"
+ "When -s option is given, then also system objects will be printed (sqlite_* tables, autoincrement indexes, etc).\n"
+ "The database argument is optional and if provided, then only given database will be printed. This is not a registered database name, "
+ "but instead it's an internal SQLite database name, like 'main', 'temp', or any attached database name. To print tree for other "
+ "registered database, call %1 first to switch the working database, and then use %2 command."
+ ).arg(cmdName("use")).arg(cmdName("tree"));
+}
+
+void CliCommandTree::defineSyntax()
+{
+ syntax.setName("tree");
+ syntax.addOptionShort(COLUMNS, "c");
+ syntax.addOptionShort(SYSTEM_OBJECTS, "s");
+ syntax.addArgument(INTRNAL_DB, "database", false);
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommandtree.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommandtree.h
new file mode 100644
index 0000000..815d781
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommandtree.h
@@ -0,0 +1,31 @@
+#ifndef CLICOMMANDTREE_H
+#define CLICOMMANDTREE_H
+
+#include "schemaresolver.h"
+#include "cliutils.h"
+#include "clicommand.h"
+
+class CliCommandTree : public CliCommand
+{
+ public:
+ void execute();
+ QString shortHelp() const;
+ QString fullHelp() const;
+ void defineSyntax();
+
+ private:
+ enum Opts
+ {
+ COLUMNS,
+ SYSTEM_OBJECTS
+ };
+
+ AsciiTree getDatabaseTree(const QString& database, SchemaResolver& resolver, bool printColumns);
+ AsciiTree getTableTree(const QString& database, const QString& table, SchemaResolver& resolver, bool printColumns);
+ AsciiTree getViewTree(const QString& database, const QString& view, SchemaResolver& resolver);
+ AsciiTree getTreeLeaf(const QString& column);
+
+ static const QString metaNodeNameTemplate;
+};
+
+#endif // CLICOMMANDTREE_H
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommanduse.cpp b/SQLiteStudio3/sqlitestudiocli/commands/clicommanduse.cpp
new file mode 100644
index 0000000..ef0f641
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommanduse.cpp
@@ -0,0 +1,64 @@
+#include "clicommanduse.h"
+#include "cli.h"
+#include "services/config.h"
+#include "../cli_config.h"
+#include "services/dbmanager.h"
+
+void CliCommandUse::execute()
+{
+ if (syntax.isArgumentSet(DB_NAME))
+ {
+ if (!cli->getCurrentDb())
+ {
+ println(tr("No current database selected."));
+ return;
+ }
+ println(tr("Current database: %1").arg(cli->getCurrentDb()->getName()));
+ return;
+ }
+
+ Db* db = DBLIST->getByName(syntax.getArgument(DB_NAME));
+ if (!db)
+ {
+ println(tr("No such database: %1").arg(syntax.getArgument(DB_NAME)));
+ return;
+ }
+
+ cli->setCurrentDb(db);
+ CFG_CLI.Console.DefaultDatabase.set(db->getName());
+
+ println(tr("Current database: %1").arg(db->getName()));
+}
+
+QString CliCommandUse::shortHelp() const
+{
+ return tr("changes default working database");
+}
+
+QString CliCommandUse::fullHelp() const
+{
+ return tr(
+ "Changes current working database to <name>. If the <name> database is not registered in the application, "
+ "then the error message is printed and no change is made.\n"
+ "\n"
+ "What is current working database?\n"
+ "When you type a SQL query to be executed, it is executed on the default database, which is also known as "
+ "the current working database. Most of database-related commands can also work using default database, if no database was "
+ "provided in their arguments. The current database is always identified by command line prompt. "
+ "The default database is always defined (unless there is no database on the list at all).\n"
+ "\n"
+ "The default database can be selected in various ways:\n"
+ "- using %1 command,\n"
+ "- by passing database file name to the application startup parameters,\n"
+ "- by passing registered database name to the application startup parameters,\n"
+ "- by restoring previously selected default database from saved configuration,\n"
+ "- or when default database was not selected by any of the above, then first database from the registered databases list "
+ "becomes the default one."
+ ).arg(cmdName("use"));
+}
+
+void CliCommandUse::defineSyntax()
+{
+ syntax.setName("use");
+ syntax.addArgument(DB_NAME, tr("name", "CLI command syntax"), false);
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/commands/clicommanduse.h b/SQLiteStudio3/sqlitestudiocli/commands/clicommanduse.h
new file mode 100644
index 0000000..9a8f280
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/commands/clicommanduse.h
@@ -0,0 +1,15 @@
+#ifndef CLICOMMANDUSE_H
+#define CLICOMMANDUSE_H
+
+#include "clicommand.h"
+
+class CliCommandUse : public CliCommand
+{
+ public:
+ void execute();
+ QString shortHelp() const;
+ QString fullHelp() const;
+ void defineSyntax();
+};
+
+#endif // CLICOMMANDUSE_H
diff --git a/SQLiteStudio3/sqlitestudiocli/main.cpp b/SQLiteStudio3/sqlitestudiocli/main.cpp
new file mode 100644
index 0000000..118e3f6
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/main.cpp
@@ -0,0 +1,92 @@
+#include "cli.h"
+#include "clicommandexecutor.h"
+#include "sqlitestudio.h"
+#include "commands/clicommand.h"
+#include "cli_config.h"
+#include "cliutils.h"
+#include "qio.h"
+#include "climsghandler.h"
+#include "completionhelper.h"
+#include "services/updatemanager.h"
+#include "services/pluginmanager.h"
+#include <QCoreApplication>
+#include <QtGlobal>
+#include <QCommandLineParser>
+#include <QCommandLineOption>
+
+bool listPlugins = false;
+
+QString cliHandleCmdLineArgs()
+{
+ QCommandLineParser parser;
+ parser.setApplicationDescription(QObject::tr("Command line interface to SQLiteStudio, a SQLite manager."));
+ parser.addHelpOption();
+ parser.addVersionOption();
+
+ QCommandLineOption debugOption({"d", "debug"}, QObject::tr("Enables debug messages on standard error output."));
+ QCommandLineOption lemonDebugOption("debug-lemon", QObject::tr("Enables Lemon parser debug messages for SQL code assistant."));
+ QCommandLineOption listPluginsOption("list-plugins", QObject::tr("Lists plugins installed in the SQLiteStudio end exists."));
+ parser.addOption(debugOption);
+ parser.addOption(lemonDebugOption);
+ parser.addOption(listPluginsOption);
+
+ parser.addPositionalArgument(QObject::tr("file"), QObject::tr("Database file to open"));
+
+ parser.process(qApp->arguments());
+
+ if (parser.isSet(debugOption))
+ setCliDebug(true);
+
+ if (parser.isSet(listPluginsOption))
+ listPlugins = true;
+
+ CompletionHelper::enableLemonDebug = parser.isSet(lemonDebugOption);
+
+ QStringList args = parser.positionalArguments();
+ if (args.size() > 0)
+ return args[0];
+
+ return QString::null;
+}
+
+int main(int argc, char *argv[])
+{
+ QCoreApplication a(argc, argv);
+
+ int retCode = 1;
+ if (UpdateManager::handleUpdateOptions(a.arguments(), retCode))
+ return retCode;
+
+ QCoreApplication::setApplicationName("SQLiteStudio");
+ QCoreApplication::setApplicationVersion(SQLITESTUDIO->getVersionString());
+
+ qInstallMessageHandler(cliMessageHandler);
+
+ QString dbToOpen = cliHandleCmdLineArgs();
+
+ CliResultsDisplay::staticInit();
+ initCliUtils();
+
+ SQLITESTUDIO->init(a.arguments(), false);
+ SQLITESTUDIO->initPlugins();
+
+ if (listPlugins)
+ {
+ for (const PluginManager::PluginDetails& details : PLUGINS->getAllPluginDetails())
+ qOut << details.name << " " << details.versionString << "\n";
+
+ return 0;
+ }
+
+ CliCommandExecutor executor;
+
+ QObject::connect(CLI::getInstance(), &CLI::execCommand, &executor, &CliCommandExecutor::execCommand);
+ QObject::connect(&executor, &CliCommandExecutor::executionComplete, CLI::getInstance(), &CLI::executionComplete);
+
+ if (!dbToOpen.isEmpty())
+ CLI::getInstance()->openDbFile(dbToOpen);
+
+ CLI::getInstance()->start();
+
+ return a.exec();
+}
diff --git a/SQLiteStudio3/sqlitestudiocli/sqlitestudiocli.pro b/SQLiteStudio3/sqlitestudiocli/sqlitestudiocli.pro
new file mode 100644
index 0000000..1b11aa2
--- /dev/null
+++ b/SQLiteStudio3/sqlitestudiocli/sqlitestudiocli.pro
@@ -0,0 +1,95 @@
+#-------------------------------------------------
+#
+# Project created by QtCreator 2013-02-28T23:21:43
+#
+#-------------------------------------------------
+
+include($$PWD/../dirs.pri)
+
+OBJECTS_DIR = $$OBJECTS_DIR/sqlitestudiocli
+MOC_DIR = $$MOC_DIR/sqlitestudiocli
+UI_DIR = $$UI_DIR/sqlitestudiocli
+
+QT += core
+QT -= gui
+
+TARGET = sqlitestudiocli
+CONFIG += console
+CONFIG -= app_bundle
+
+TEMPLATE = app
+
+CONFIG += c++11
+QMAKE_CXXFLAGS += -pedantic
+linux|portable {
+ QMAKE_LFLAGS += -Wl,-rpath,./lib
+}
+
+SOURCES += main.cpp \
+ cli.cpp \
+ commands/clicommand.cpp \
+ commands/clicommandfactory.cpp \
+ commands/clicommandadd.cpp \
+ commands/clicommandremove.cpp \
+ commands/clicommandexit.cpp \
+ commands/clicommanddblist.cpp \
+ commands/clicommanduse.cpp \
+ commands/clicommandopen.cpp \
+ commands/clicommandclose.cpp \
+ commands/clicommandsql.cpp \
+ clicommandexecutor.cpp \
+ cli_config.cpp \
+ commands/clicommandhelp.cpp \
+ cliutils.cpp \
+ commands/clicommandtables.cpp \
+ climsghandler.cpp \
+ commands/clicommandmode.cpp \
+ commands/clicommandnullvalue.cpp \
+ commands/clicommandhistory.cpp \
+ commands/clicommanddir.cpp \
+ commands/clicommandpwd.cpp \
+ commands/clicommandcd.cpp \
+ clicommandsyntax.cpp \
+ commands/clicommandtree.cpp \
+ clicompleter.cpp \
+ commands/clicommanddesc.cpp
+
+LIBS += -lcoreSQLiteStudio
+
+win32: {
+ INCLUDEPATH += $$PWD/../../../include
+ LIBS += -L$$PWD/../../../lib -ledit_static
+}
+
+unix: {
+ LIBS += -lreadline -ltermcap
+}
+
+HEADERS += \
+ cli.h \
+ commands/clicommand.h \
+ commands/clicommandfactory.h \
+ commands/clicommandadd.h \
+ commands/clicommandremove.h \
+ commands/clicommandexit.h \
+ commands/clicommanddblist.h \
+ commands/clicommanduse.h \
+ commands/clicommandopen.h \
+ commands/clicommandclose.h \
+ commands/clicommandsql.h \
+ cli_config.h \
+ clicommandexecutor.h \
+ commands/clicommandhelp.h \
+ cliutils.h \
+ commands/clicommandtables.h \
+ climsghandler.h \
+ commands/clicommandmode.h \
+ commands/clicommandnullvalue.h \
+ commands/clicommandhistory.h \
+ commands/clicommanddir.h \
+ commands/clicommandpwd.h \
+ commands/clicommandcd.h \
+ clicommandsyntax.h \
+ commands/clicommandtree.h \
+ clicompleter.h \
+ commands/clicommanddesc.h