mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-09-20 08:40:50 +00:00
Merge pull request #41730 from azat/client-interactive-history-search
Add interactive history search with fzf-like utility (fzf/sk) to clickhouse-client/local
This commit is contained in:
commit
dfffe157f6
@ -1,6 +1,7 @@
|
|||||||
#include <base/ReplxxLineReader.h>
|
#include <base/ReplxxLineReader.h>
|
||||||
#include <base/errnoToString.h>
|
#include <base/errnoToString.h>
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@ -13,8 +14,10 @@
|
|||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <filesystem>
|
||||||
#include <fmt/format.h>
|
#include <fmt/format.h>
|
||||||
|
#include <boost/algorithm/string/split.hpp>
|
||||||
|
#include <boost/algorithm/string/classification.hpp> /// is_any_of
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@ -35,6 +38,166 @@ std::string getEditor()
|
|||||||
return editor;
|
return editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string getFuzzyFinder()
|
||||||
|
{
|
||||||
|
const char * env_path = std::getenv("PATH"); // NOLINT(concurrency-mt-unsafe)
|
||||||
|
|
||||||
|
if (!env_path || !*env_path)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
std::vector<std::string> paths;
|
||||||
|
boost::split(paths, env_path, boost::is_any_of(":"));
|
||||||
|
for (const auto & path_str : paths)
|
||||||
|
{
|
||||||
|
std::filesystem::path path(path_str);
|
||||||
|
std::filesystem::path sk_bin_path = path / "sk";
|
||||||
|
if (!access(sk_bin_path.c_str(), X_OK))
|
||||||
|
return sk_bin_path;
|
||||||
|
|
||||||
|
std::filesystem::path fzf_bin_path = path / "fzf";
|
||||||
|
if (!access(fzf_bin_path.c_str(), X_OK))
|
||||||
|
return fzf_bin_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See comments in ShellCommand::executeImpl()
|
||||||
|
/// (for the vfork via dlsym())
|
||||||
|
int executeCommand(char * const argv[])
|
||||||
|
{
|
||||||
|
#if !defined(USE_MUSL)
|
||||||
|
/** Here it is written that with a normal call `vfork`, there is a chance of deadlock in multithreaded programs,
|
||||||
|
* because of the resolving of symbols in the shared library
|
||||||
|
* http://www.oracle.com/technetwork/server-storage/solaris10/subprocess-136439.html
|
||||||
|
* Therefore, separate the resolving of the symbol from the call.
|
||||||
|
*/
|
||||||
|
static void * real_vfork = dlsym(RTLD_DEFAULT, "vfork");
|
||||||
|
#else
|
||||||
|
/// If we use Musl with static linking, there is no dlsym and no issue with vfork.
|
||||||
|
static void * real_vfork = reinterpret_cast<void *>(&vfork);
|
||||||
|
#endif
|
||||||
|
if (!real_vfork)
|
||||||
|
throw std::runtime_error("Cannot find vfork symbol");
|
||||||
|
|
||||||
|
pid_t pid = reinterpret_cast<pid_t (*)()>(real_vfork)();
|
||||||
|
|
||||||
|
if (-1 == pid)
|
||||||
|
throw std::runtime_error(fmt::format("Cannot vfork {}: {}", argv[0], errnoToString()));
|
||||||
|
|
||||||
|
/// Child
|
||||||
|
if (0 == pid)
|
||||||
|
{
|
||||||
|
sigset_t mask;
|
||||||
|
sigemptyset(&mask);
|
||||||
|
sigprocmask(0, nullptr, &mask); // NOLINT(concurrency-mt-unsafe) // ok in newly created process
|
||||||
|
sigprocmask(SIG_UNBLOCK, &mask, nullptr); // NOLINT(concurrency-mt-unsafe) // ok in newly created process
|
||||||
|
|
||||||
|
execvp(argv[0], argv);
|
||||||
|
_exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int status = 0;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
int exited_pid = waitpid(pid, &status, 0);
|
||||||
|
if (exited_pid != -1)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (errno == EINTR)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
throw std::runtime_error(fmt::format("Cannot waitpid {}: {}", pid, errnoToString()));
|
||||||
|
} while (true);
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeRetry(int fd, const std::string & data)
|
||||||
|
{
|
||||||
|
size_t bytes_written = 0;
|
||||||
|
const char * begin = data.c_str();
|
||||||
|
size_t offset = data.size();
|
||||||
|
|
||||||
|
while (bytes_written != offset)
|
||||||
|
{
|
||||||
|
ssize_t res = ::write(fd, begin + bytes_written, offset - bytes_written);
|
||||||
|
if ((-1 == res || 0 == res) && errno != EINTR)
|
||||||
|
throw std::runtime_error(fmt::format("Cannot write to {}: {}", fd, errnoToString()));
|
||||||
|
bytes_written += res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::string readFile(const std::string & path)
|
||||||
|
{
|
||||||
|
std::ifstream t(path);
|
||||||
|
std::string str;
|
||||||
|
t.seekg(0, std::ios::end);
|
||||||
|
str.reserve(t.tellg());
|
||||||
|
t.seekg(0, std::ios::beg);
|
||||||
|
str.assign((std::istreambuf_iterator<char>(t)), std::istreambuf_iterator<char>());
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple wrapper for temporary files.
|
||||||
|
class TemporaryFile
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
std::string path;
|
||||||
|
int fd = -1;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit TemporaryFile(const char * pattern)
|
||||||
|
: path(pattern)
|
||||||
|
{
|
||||||
|
size_t dot_pos = path.rfind('.');
|
||||||
|
if (dot_pos != std::string::npos)
|
||||||
|
fd = ::mkstemps(path.data(), path.size() - dot_pos);
|
||||||
|
else
|
||||||
|
fd = ::mkstemp(path.data());
|
||||||
|
|
||||||
|
if (-1 == fd)
|
||||||
|
throw std::runtime_error(fmt::format("Cannot create temporary file {}: {}", path, errnoToString()));
|
||||||
|
}
|
||||||
|
~TemporaryFile()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
close();
|
||||||
|
unlink();
|
||||||
|
}
|
||||||
|
catch (const std::runtime_error & e)
|
||||||
|
{
|
||||||
|
fmt::print(stderr, "{}", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void close()
|
||||||
|
{
|
||||||
|
if (fd == -1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (0 != ::close(fd))
|
||||||
|
throw std::runtime_error(fmt::format("Cannot close temporary file {}: {}", path, errnoToString()));
|
||||||
|
fd = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void write(const std::string & data)
|
||||||
|
{
|
||||||
|
if (fd == -1)
|
||||||
|
throw std::runtime_error(fmt::format("Cannot write to uninitialized file {}", path));
|
||||||
|
|
||||||
|
writeRetry(fd, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void unlink()
|
||||||
|
{
|
||||||
|
if (0 != ::unlink(path.c_str()))
|
||||||
|
throw std::runtime_error(fmt::format("Cannot remove temporary file {}: {}", path, errnoToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string & getPath() { return path; }
|
||||||
|
};
|
||||||
|
|
||||||
/// Copied from replxx::src/util.cxx::now_ms_str() under the terms of 3-clause BSD license of Replxx.
|
/// Copied from replxx::src/util.cxx::now_ms_str() under the terms of 3-clause BSD license of Replxx.
|
||||||
/// Copyright (c) 2017-2018, Marcin Konarski (amok at codestation.org)
|
/// Copyright (c) 2017-2018, Marcin Konarski (amok at codestation.org)
|
||||||
/// Copyright (c) 2010, Salvatore Sanfilippo (antirez at gmail dot com)
|
/// Copyright (c) 2010, Salvatore Sanfilippo (antirez at gmail dot com)
|
||||||
@ -142,6 +305,7 @@ ReplxxLineReader::ReplxxLineReader(
|
|||||||
replxx::Replxx::highlighter_callback_t highlighter_)
|
replxx::Replxx::highlighter_callback_t highlighter_)
|
||||||
: LineReader(history_file_path_, multiline_, std::move(extenders_), std::move(delimiters_)), highlighter(std::move(highlighter_))
|
: LineReader(history_file_path_, multiline_, std::move(extenders_), std::move(delimiters_)), highlighter(std::move(highlighter_))
|
||||||
, editor(getEditor())
|
, editor(getEditor())
|
||||||
|
, fuzzy_finder(getFuzzyFinder())
|
||||||
{
|
{
|
||||||
using namespace std::placeholders;
|
using namespace std::placeholders;
|
||||||
using Replxx = replxx::Replxx;
|
using Replxx = replxx::Replxx;
|
||||||
@ -249,6 +413,17 @@ ReplxxLineReader::ReplxxLineReader(
|
|||||||
return rx.invoke(Replxx::ACTION::COMMIT_LINE, code);
|
return rx.invoke(Replxx::ACTION::COMMIT_LINE, code);
|
||||||
};
|
};
|
||||||
rx.bind_key(Replxx::KEY::meta('#'), insert_comment_action);
|
rx.bind_key(Replxx::KEY::meta('#'), insert_comment_action);
|
||||||
|
|
||||||
|
/// interactive search in history (requires fzf/sk)
|
||||||
|
if (!fuzzy_finder.empty())
|
||||||
|
{
|
||||||
|
auto interactive_history_search = [this](char32_t code)
|
||||||
|
{
|
||||||
|
openInteractiveHistorySearch();
|
||||||
|
return rx.invoke(Replxx::ACTION::REPAINT, code);
|
||||||
|
};
|
||||||
|
rx.bind_key(Replxx::KEY::control('R'), interactive_history_search);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ReplxxLineReader::~ReplxxLineReader()
|
ReplxxLineReader::~ReplxxLineReader()
|
||||||
@ -293,116 +468,70 @@ void ReplxxLineReader::addToHistory(const String & line)
|
|||||||
rx.print("Unlock of history file failed: %s\n", errnoToString().c_str());
|
rx.print("Unlock of history file failed: %s\n", errnoToString().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See comments in ShellCommand::executeImpl()
|
|
||||||
/// (for the vfork via dlsym())
|
|
||||||
int ReplxxLineReader::executeEditor(const std::string & path)
|
|
||||||
{
|
|
||||||
std::vector<char> argv0(editor.data(), editor.data() + editor.size() + 1);
|
|
||||||
std::vector<char> argv1(path.data(), path.data() + path.size() + 1);
|
|
||||||
char * const argv[] = {argv0.data(), argv1.data(), nullptr};
|
|
||||||
|
|
||||||
static void * real_vfork = dlsym(RTLD_DEFAULT, "vfork");
|
|
||||||
if (!real_vfork)
|
|
||||||
{
|
|
||||||
rx.print("Cannot find symbol vfork in myself: %s\n", errnoToString().c_str());
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pid_t pid = reinterpret_cast<pid_t (*)()>(real_vfork)();
|
|
||||||
|
|
||||||
if (-1 == pid)
|
|
||||||
{
|
|
||||||
rx.print("Cannot vfork: %s\n", errnoToString().c_str());
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Child
|
|
||||||
if (0 == pid)
|
|
||||||
{
|
|
||||||
sigset_t mask;
|
|
||||||
sigemptyset(&mask);
|
|
||||||
sigprocmask(0, nullptr, &mask); // NOLINT(concurrency-mt-unsafe) // ok in newly created process
|
|
||||||
sigprocmask(SIG_UNBLOCK, &mask, nullptr); // NOLINT(concurrency-mt-unsafe) // ok in newly created process
|
|
||||||
|
|
||||||
execvp(editor.c_str(), argv);
|
|
||||||
rx.print("Cannot execute %s: %s\n", editor.c_str(), errnoToString().c_str());
|
|
||||||
_exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
int status = 0;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
int exited_pid = waitpid(pid, &status, 0);
|
|
||||||
if (exited_pid == -1)
|
|
||||||
{
|
|
||||||
if (errno == EINTR)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
rx.print("Cannot waitpid: %s\n", errnoToString().c_str());
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
break;
|
|
||||||
} while (true);
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReplxxLineReader::openEditor()
|
void ReplxxLineReader::openEditor()
|
||||||
{
|
{
|
||||||
char filename[] = "clickhouse_replxx_XXXXXX.sql";
|
TemporaryFile editor_file("clickhouse_client_editor_XXXXXX.sql");
|
||||||
int fd = ::mkstemps(filename, 4);
|
editor_file.write(rx.get_state().text());
|
||||||
if (-1 == fd)
|
editor_file.close();
|
||||||
{
|
|
||||||
rx.print("Cannot create temporary file to edit query: %s\n", errnoToString().c_str());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
replxx::Replxx::State state(rx.get_state());
|
char * const argv[] = {editor.data(), editor_file.getPath().data(), nullptr};
|
||||||
|
try
|
||||||
size_t bytes_written = 0;
|
|
||||||
const char * begin = state.text();
|
|
||||||
size_t offset = strlen(state.text());
|
|
||||||
while (bytes_written != offset)
|
|
||||||
{
|
{
|
||||||
ssize_t res = ::write(fd, begin + bytes_written, offset - bytes_written);
|
if (executeCommand(argv) == 0)
|
||||||
if ((-1 == res || 0 == res) && errno != EINTR)
|
|
||||||
{
|
{
|
||||||
rx.print("Cannot write to temporary query file %s: %s\n", filename, errnoToString().c_str());
|
const std::string & new_query = readFile(editor_file.getPath());
|
||||||
break;
|
rx.set_state(replxx::Replxx::State(new_query.c_str(), new_query.size()));
|
||||||
}
|
}
|
||||||
bytes_written += res;
|
|
||||||
}
|
}
|
||||||
|
catch (const std::runtime_error & e)
|
||||||
if (0 != ::close(fd))
|
|
||||||
{
|
{
|
||||||
rx.print("Cannot close temporary query file %s: %s\n", filename, errnoToString().c_str());
|
rx.print(e.what());
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (0 == executeEditor(filename))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
std::ifstream t(filename);
|
|
||||||
std::string str;
|
|
||||||
t.seekg(0, std::ios::end);
|
|
||||||
str.reserve(t.tellg());
|
|
||||||
t.seekg(0, std::ios::beg);
|
|
||||||
str.assign((std::istreambuf_iterator<char>(t)), std::istreambuf_iterator<char>());
|
|
||||||
rx.set_state(replxx::Replxx::State(str.c_str(), str.size()));
|
|
||||||
}
|
|
||||||
catch (...)
|
|
||||||
{
|
|
||||||
rx.print("Cannot read from temporary query file %s: %s\n", filename, errnoToString().c_str());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bracketed_paste_enabled)
|
if (bracketed_paste_enabled)
|
||||||
enableBracketedPaste();
|
enableBracketedPaste();
|
||||||
|
}
|
||||||
|
|
||||||
if (0 != ::unlink(filename))
|
void ReplxxLineReader::openInteractiveHistorySearch()
|
||||||
rx.print("Cannot remove temporary query file %s: %s\n", filename, errnoToString().c_str());
|
{
|
||||||
|
assert(!fuzzy_finder.empty());
|
||||||
|
TemporaryFile history_file("clickhouse_client_history_in_XXXXXX.bin");
|
||||||
|
auto hs(rx.history_scan());
|
||||||
|
while (hs.next())
|
||||||
|
{
|
||||||
|
history_file.write(hs.get().text());
|
||||||
|
history_file.write(std::string(1, '\0'));
|
||||||
|
}
|
||||||
|
history_file.close();
|
||||||
|
|
||||||
|
TemporaryFile output_file("clickhouse_client_history_out_XXXXXX.sql");
|
||||||
|
output_file.close();
|
||||||
|
|
||||||
|
char sh[] = "sh";
|
||||||
|
char sh_c[] = "-c";
|
||||||
|
/// NOTE: You can use one of the following to configure the behaviour additionally:
|
||||||
|
/// - SKIM_DEFAULT_OPTIONS
|
||||||
|
/// - FZF_DEFAULT_OPTS
|
||||||
|
std::string fuzzy_finder_command = fmt::format(
|
||||||
|
"{} --read0 --tac --no-sort --tiebreak=index --bind=ctrl-r:toggle-sort --height=30% < {} > {}",
|
||||||
|
fuzzy_finder, history_file.getPath(), output_file.getPath());
|
||||||
|
char * const argv[] = {sh, sh_c, fuzzy_finder_command.data(), nullptr};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (executeCommand(argv) == 0)
|
||||||
|
{
|
||||||
|
const std::string & new_query = readFile(output_file.getPath());
|
||||||
|
rx.set_state(replxx::Replxx::State(new_query.c_str(), new_query.size()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const std::runtime_error & e)
|
||||||
|
{
|
||||||
|
rx.print(e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bracketed_paste_enabled)
|
||||||
|
enableBracketedPaste();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReplxxLineReader::enableBracketedPaste()
|
void ReplxxLineReader::enableBracketedPaste()
|
||||||
|
@ -27,6 +27,7 @@ private:
|
|||||||
void addToHistory(const String & line) override;
|
void addToHistory(const String & line) override;
|
||||||
int executeEditor(const std::string & path);
|
int executeEditor(const std::string & path);
|
||||||
void openEditor();
|
void openEditor();
|
||||||
|
void openInteractiveHistorySearch();
|
||||||
|
|
||||||
replxx::Replxx rx;
|
replxx::Replxx rx;
|
||||||
replxx::Replxx::highlighter_callback_t highlighter;
|
replxx::Replxx::highlighter_callback_t highlighter;
|
||||||
@ -36,4 +37,5 @@ private:
|
|||||||
bool bracketed_paste_enabled = false;
|
bool bracketed_paste_enabled = false;
|
||||||
|
|
||||||
std::string editor;
|
std::string editor;
|
||||||
|
std::string fuzzy_finder;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user