A snapshot of the development tree. The substantial changes since the last one:
COMMAND OUTPUT POLICY. The three commands display text in exactly three cases,
and each owns a stream: LOGGING under "-v" greater than 0 and an ERROR before
termination go to STDERR; OUTPUT THE USER ASKED FOR goes to STDOUT. For ktext
that output is a document, so "ktext doc.kt -d | ..." is now safe -- logging
used to share the stream and land inside the document. A bare command prints
its usage and succeeds rather than failing. Colour is emitted only to a
terminal, per stream, and NO_COLOR is honoured.
"-v 1" reports every decision whose outcome you could not have read off your own
input: the klammerset that was loaded and from which file, a font's directory, a
":files" name's file, how "-o" was expanded. Higher levels are the trace.
The commands no longer warn and continue: an anomaly is an error, described with
its location. Two exceptions remain, each for a stated reason -- a condition
that is expected and temporary by design, and a judgment that is a heuristic
rather than exact.
@cond IS NOW A TRUE SPECIAL FORM, resolved at APPLICATION time rather than when
the file is read. Two consequences for a writer:
* a state variable reaches the predicate. "@@@state Flag :value true @@@
@cond *Flag* | T | F @" renders "T"; it used to see the literal "*Flag*" and
silently take the false branch. The document now behaves like a klammer
body, whose arguments are bound before its conditionals are decided.
* nothing in a discarded branch happens -- it is not read, not evaluated, not
expanded. An @eval in the branch not taken used to run anyway.
Its predicate relation is total and strict: true, True, 1; false, False, 0, and
empty; anything else is an error at the @cond rather than silently false.
@eval REACHING OUTSIDE. ":shell" and ":haskell" now keep the command's standard
error out of the document (it appears under "-v 1") and treat a nonzero exit as
an error naming what the command reported. A command that exits nonzero on
purpose -- "grep" finding no match -- says so with "|| true".
KLAMMER SETS. Several combine: "--klammersets a b c" loads all three in the
order given, sharing one namespace, with the definition modes deciding
collisions. "none" means none and may not be combined with other symbols. A
klammerset with symbol X is declared in a file X/X.k, which is what lets two
sets require the same third set without loading it twice.
TESTS. Four new suites: the kdiag command's interface, the @eval primitive's
contract with the outside world, and verbosity at both tiers. Three suites
that could not run on macOS at all now do.
Assembled from dev commit 6c8ee6c22fca.
561 lines
16 KiB
C++
561 lines
16 KiB
C++
#include <stdlib.h>
|
|
#include <cstdio>
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <iterator>
|
|
#include <set>
|
|
#include <sstream>
|
|
#include <utility>
|
|
#include <regex>
|
|
|
|
#include "util.h"
|
|
#include "show.h"
|
|
|
|
std::string trim_left(const std::string& s)
|
|
{
|
|
std::string result = s;
|
|
result.erase(result.begin(), std::find_if(result.begin(), result.end(), [](unsigned char ch) {
|
|
return !std::isspace(ch);
|
|
}));
|
|
return result;
|
|
}
|
|
|
|
std::string trim_right(const std::string& s)
|
|
{
|
|
std::string result = s;
|
|
result.erase(std::find_if(result.rbegin(), result.rend(), [](unsigned char ch) {
|
|
return !std::isspace(ch);
|
|
}).base(), result.end());
|
|
return result;
|
|
}
|
|
|
|
std::string trim(std::string s)
|
|
{
|
|
return trim_left(trim_right(std::move(s)));
|
|
}
|
|
|
|
std::string trim_char_left(std::string s, char remove)
|
|
{
|
|
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [&](char c) { return c != remove; }));
|
|
return s;
|
|
}
|
|
|
|
std::string trim_char_right(std::string s, char remove)
|
|
{
|
|
s.erase(std::find_if(s.rbegin(), s.rend(), [&](char c) { return c != remove; }).base(), s.end());
|
|
return s;
|
|
}
|
|
|
|
std::string trim_char(std::string s, char remove)
|
|
{
|
|
return trim_char_left(trim_char_right(std::move(s), remove), remove);
|
|
}
|
|
|
|
|
|
std::string escape_regex(const std::string& input)
|
|
{
|
|
std::string result;
|
|
result.reserve(input.length() * 2); // Reserve space for potential escapes
|
|
|
|
for (char c : input) {
|
|
// Escape special regex characters
|
|
if (std::string("\\^$.|?*+()[{}]").find(c) != std::string::npos) {
|
|
result += '\\';
|
|
}
|
|
result += c;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string string_replace(const std::string& source, const std::string& old_str, const std::string& new_str)
|
|
{
|
|
return std::regex_replace(source, std::regex(escape_regex(old_str)), new_str);
|
|
/*
|
|
std::string result { source };
|
|
auto pos = result.find(old_str);
|
|
std::string old_str_e = old_str; // escape_regex(old_str);
|
|
while (pos != std::string::npos) {
|
|
// result = result.replace(pos, old_str.size(), new_str);
|
|
result = result.replace(pos, old_str_e.size(), new_str);
|
|
// pos = result.find(old_str);
|
|
pos = result.find(old_str_e);
|
|
// std::cout << " " << result << "\n";
|
|
}
|
|
return result;
|
|
*/
|
|
}
|
|
|
|
bool contains(const std::string& str, const std::string& substr)
|
|
{
|
|
return str.find(substr) != std::string::npos;
|
|
}
|
|
|
|
bool contains(const std::vector<std::string>& strings, const std::string& element)
|
|
{
|
|
return std::find(strings.begin(), strings.end(), element) != strings.end();
|
|
}
|
|
|
|
std::string regex_escape(const std::string& s)
|
|
{
|
|
/*
|
|
regex special { R"([\$.|?*+(){})" }; // ^ is reserved
|
|
return regex_replace(s, special, "\\[&$]");
|
|
*/
|
|
std::set chars { '\\', '|', '(', ')', '{', '}', '[', ']', '$', '^' };
|
|
std::string result {};
|
|
for (char c : s) {
|
|
if (chars.find(c) != chars.end())
|
|
result += "\\";
|
|
result += c;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
strings_t regex_split(const std::string& s, const std::regex& re, bool trim_parts)
|
|
{
|
|
strings_t result = {};
|
|
if (s.empty()) {
|
|
return result;
|
|
}
|
|
auto it = std::sregex_token_iterator(s.begin(), s.end(), re, -1);
|
|
while (it != std::sregex_token_iterator()) {
|
|
std::string part { *it };
|
|
if (trim_parts)
|
|
part = trim(part);
|
|
result.push_back(part);
|
|
it++;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
strings_t word_split(const std::string& s)
|
|
{
|
|
return regex_split(s, std::regex("\\s+"));
|
|
}
|
|
|
|
std::string collapse_whitespace(const std::string& s)
|
|
{
|
|
std::string result {};
|
|
bool in_space = true; // leading whitespace is dropped
|
|
for (char c : s) {
|
|
if (std::isspace(static_cast<unsigned char>(c)) != 0) {
|
|
in_space = true;
|
|
} else {
|
|
if (in_space && !result.empty()) {
|
|
result += ' ';
|
|
}
|
|
in_space = false;
|
|
result += c;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool contains_fold(const std::string& haystack, const std::string& needle)
|
|
{
|
|
auto fold = [](const std::string& s) {
|
|
std::string result {};
|
|
for (char c : s) {
|
|
result += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
}
|
|
return result;
|
|
};
|
|
return fold(haystack).find(fold(needle)) != std::string::npos;
|
|
}
|
|
|
|
bool is_in(const std::string& s, const strings_t& v)
|
|
{
|
|
return find(v.begin(), v.end(), s) != v.end();
|
|
}
|
|
|
|
bool is_not_in(const std::string& s, const strings_t& v)
|
|
{
|
|
return find(v.begin(), v.end(), s) == v.end();
|
|
}
|
|
|
|
strings_t find_all(const std::string& str, const std::regex& pattern, int match_group)
|
|
{
|
|
std::sregex_iterator end {};
|
|
strings_t result;
|
|
for (std::sregex_iterator p {str.begin(), str.end(), pattern}; p!= end; ++p)
|
|
result.push_back((*p)[match_group]);
|
|
return result;
|
|
}
|
|
|
|
strings_t find_all(const std::string& str, const std::string& pattern, int match_group)
|
|
{
|
|
return find_all(str, std::regex(pattern), match_group);
|
|
}
|
|
|
|
strings_t split_into_paragraphs(const std::string& s)
|
|
{
|
|
std::string t {trim(s)};
|
|
std::string marker { "_PAR_" };
|
|
t = trim(std::regex_replace(t, std::regex(R"(\n *(\n *)+)"), marker)) + marker;
|
|
//return find_all(t, regex(R"(((\s|.)*?)" + marker + ")"), 1);
|
|
return find_all(t, std::regex(R"(((\s|.)*?)_PAR_)"), 1);
|
|
|
|
}
|
|
|
|
std::string add_margin(const std::string& s, unsigned int margin_size)
|
|
{
|
|
auto margin = std::string(margin_size, ' ');
|
|
return trim_right(
|
|
margin + std::regex_replace(s, std::regex(R"(\n)"), '\n' + margin));
|
|
}
|
|
|
|
std::string justify_string(const std::string& s, unsigned int width=80, bool french_spacing=false)
|
|
{
|
|
strings_t words = find_all(s, R"([^\s]+)");
|
|
std::stringstream ss {};
|
|
std::stringstream line {};
|
|
for (std::string w : words) {
|
|
if (line.str().size() + w.size() + 1 > width) {
|
|
ss << line.str() << '\n';
|
|
line.str("");
|
|
}
|
|
if (not french_spacing and w[w.size()-1] == '.')
|
|
w += " ";
|
|
line << w << " ";
|
|
}
|
|
if (!line.str().empty())
|
|
ss << line.str();
|
|
std::string result = trim(ss.str());
|
|
result = std::regex_replace(result, std::regex("~"), " ");
|
|
return result;
|
|
}
|
|
|
|
std::string justify(
|
|
const std::string& input_text, unsigned int text_width, unsigned int margin_width)
|
|
{
|
|
std::string result {};
|
|
text_width -= margin_width;
|
|
for (const std::string& par : split_into_paragraphs(trim(input_text))) {
|
|
result += justify_string(par, text_width) + "\n\n";
|
|
}
|
|
if (margin_width > 0) {
|
|
result = add_margin(result, margin_width);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string join(const strings_t& ss, const std::string& separator)
|
|
{
|
|
if (ss.empty()) {
|
|
return std::string();
|
|
} else if (ss.size() == 1) {
|
|
return ss[0];
|
|
} else {
|
|
std::stringstream strm {};
|
|
std::copy(ss.begin(), ss.end() - 1,
|
|
std::ostream_iterator<std::string>(strm, separator.c_str()));
|
|
strm << ss.back();
|
|
return strm.str();
|
|
}
|
|
}
|
|
|
|
std::string join(int argc, char* argv[], const std::string& separator)
|
|
{
|
|
std::string result {};
|
|
for (int i = 0; i < argc; ++i) {
|
|
// Append directly (see argv_to_string): avoids a temporary and the same
|
|
// GCC 12 -Wrestrict false positive.
|
|
result += argv[i];
|
|
result += separator;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string argv_to_string(int argc, char* argv[])
|
|
{
|
|
if (argc == 0) {
|
|
return "";
|
|
}
|
|
std::string result = argv[0];
|
|
for (int i = 1; i < argc; ++i) {
|
|
// Append the pieces directly rather than building a `" " + string(...)`
|
|
// temporary: identical result, avoids an allocation, and sidesteps a
|
|
// GCC 12 -Wrestrict false positive on the temporary's memcpy.
|
|
result += ' ';
|
|
result += argv[i];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
std::string plural(const std::string& word, int count)
|
|
{
|
|
std::string result {word};
|
|
if (count != 1) {
|
|
if (*(word.end()-1) == 'y')
|
|
result = word.substr(0, word.size()-2) + "ies";
|
|
else
|
|
result = word + "s";
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string plural(const std::string& word, const strings_t& things)
|
|
{
|
|
std::string result { word };
|
|
if (things.size() != 1) {
|
|
if (*(word.end()-1) == 'y')
|
|
result = word.substr(0, word.size()-2) + "ies";
|
|
else
|
|
result = word + "s";
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string to_be(int count, bool present)
|
|
{
|
|
std::string result {};
|
|
if (count > 1) {
|
|
if (present) {
|
|
result = "are";
|
|
} else {
|
|
result = "were";
|
|
}
|
|
} else {
|
|
if (present) {
|
|
result = "is";
|
|
} else {
|
|
result = "was";
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
int max_length(const strings_t& ss)
|
|
{
|
|
size_t result = 0;
|
|
for_each(ss.begin(), ss.end(),
|
|
[&result](const std::string& s) { result = std::max(result, s.size()); });
|
|
return result;
|
|
}
|
|
|
|
|
|
/*
|
|
std::vector<std::string> map_key_lengths(std::map<std::string, auto> map)
|
|
{
|
|
int result = 0;
|
|
for (auto const& item: map) {
|
|
result = std::max(result, item.first.size());
|
|
}
|
|
}
|
|
|
|
|
|
int
|
|
std::map<int, int> m;
|
|
std::vector<int> key, value;
|
|
for(std::map<int,int>::iterator it = m.begin(); it != m.end(); ++it) {
|
|
key.push_back(it->first);
|
|
value.push_back(it->second);
|
|
std::cout << "Key: " << it->first << std::endl;
|
|
std::cout << "Value: " << it->second << std::endl;
|
|
}
|
|
*/
|
|
|
|
|
|
std::vector<std::pair<std::string, std::string>> environment_variables(bool allow_empty_definitions)
|
|
{
|
|
// std::cout << "read_environment:\n";
|
|
std::vector<std::pair<std::string, std::string>> result {};
|
|
extern char **environ;
|
|
for (int i = 0; environ[i]; i++) {
|
|
auto parts = regex_split(environ[i], std::regex("="), true);
|
|
if (!allow_empty_definitions && parts.size() < 2) {
|
|
throw Internal_error(
|
|
"Incorrect environment variable format:\n" + std::string(environ[i]),
|
|
Locator(), false);
|
|
}
|
|
std::string name = parts[0];
|
|
parts.erase(parts.begin());
|
|
std::string value = join(parts, "=");
|
|
// std::cout << name << sp_arrow << value << "\n";
|
|
result.push_back({name, value});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string replace_environment_variables(const std::string& str)
|
|
{
|
|
if (str.find('{') == std::string::npos || str.find('}') == std::string::npos) {
|
|
return str;
|
|
}
|
|
if (str.find('{') == std::string::npos || str.find('\n') != std::string::npos) {
|
|
return str;
|
|
}
|
|
|
|
if (str.size() < 3) {
|
|
return str;
|
|
}
|
|
std::regex variable_re(R"((.*?)\{([A-Z_]+)\})");
|
|
std::sregex_iterator end {};
|
|
std::string result {};
|
|
//sregex_iterator q {};
|
|
size_t endpos = 0;
|
|
for (std::sregex_iterator p {str.begin(), str.end(), variable_re}; p!= end; ++p) {
|
|
std::smatch m = *p;
|
|
std::string prefix = m[1];
|
|
std::string var = m[2];
|
|
endpos = m.position() + m.length();
|
|
std::string envvar = get_env_var(var);
|
|
result += prefix + envvar;
|
|
}
|
|
if (endpos < str.size() - 1) {
|
|
result += str.substr(endpos);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string abbrev(const std::string& s, unsigned int max_length, bool remove_newlines)
|
|
{
|
|
std::string result {s};
|
|
if (s.size() > max_length) {
|
|
if (remove_newlines) {
|
|
result = trim(result);
|
|
result = std::regex_replace(result, std::regex("\n"), broken_bar);
|
|
}
|
|
int suffix_size = 8;
|
|
// Plain, deliberately. This string is spliced into another and shown
|
|
// later, on a stream this function cannot know -- so a colour baked in
|
|
// here could reach a pipe or a file. It also fixes an arithmetic
|
|
// error that the colours caused: ellipsis.size() counted the 14
|
|
// characters of the two escape sequences as visible width, so the
|
|
// abbreviation was cut 14 characters shorter than max_length asked.
|
|
std::string ellipsis { "[...]" };
|
|
int prefix_end = max_length - suffix_size - static_cast<int>(ellipsis.size());
|
|
result = result.replace(result.begin() + prefix_end,
|
|
result.end() - suffix_size,
|
|
ellipsis);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void remove_element(std::vector<std::string>& ss, const std::string& removed)
|
|
{
|
|
ss.erase(std::remove_if(ss.begin(), ss.end(),
|
|
[&removed](const std::string& s) { return s == removed; }),
|
|
ss.end());
|
|
}
|
|
|
|
void remove_duplicates(strings_t& ss)
|
|
{
|
|
// https://en.cppreference.com/w/cpp/algorithm/unique
|
|
std::sort(ss.begin(), ss.end());
|
|
auto last = std::unique(ss.begin(), ss.end());
|
|
ss.erase(last, ss.end());
|
|
}
|
|
|
|
std::string display_string(const std::string& s, unsigned int width, bool replace_newlines)
|
|
{
|
|
std::string suffix { "..." };
|
|
std::string result { s };
|
|
if (replace_newlines)
|
|
result = std::regex_replace(result, std::regex("\n"), "/");
|
|
auto rlen = result.length();
|
|
auto slen = suffix.length();
|
|
if ((rlen > slen) and (rlen - slen) > width) {
|
|
result = result.replace(result.begin()+width, result.end(), suffix); //substr(0, width) + suffix;
|
|
}
|
|
//result = '"' + result + '"';
|
|
return result;
|
|
}
|
|
|
|
std::pair<std::string,std::string> extract_parameter_type(const std::string& parameter_name)
|
|
{
|
|
std::regex name_pat { R"((\w+)\.(\w+))" };
|
|
std::smatch match {};
|
|
std::string type_name = "string";
|
|
std::string name = parameter_name;
|
|
if (std::regex_match(parameter_name, match, name_pat)) {
|
|
name = match[1];
|
|
type_name = match[2];
|
|
}
|
|
return { name, type_name };
|
|
}
|
|
|
|
std::tuple<std::string, std::string, bool> regex_split_prefix(const std::regex& pattern, const std::string& text)
|
|
{
|
|
std::smatch match;
|
|
if (std::regex_search(text, match, pattern) && match.position() == 0) {
|
|
// Match found at the beginning of the string
|
|
std::string matched = match.str();
|
|
std::string remainder = text.substr(matched.length());
|
|
return { matched, remainder, true };
|
|
} else {
|
|
// No match at the beginning
|
|
return { "", text, false };
|
|
}
|
|
}
|
|
|
|
std::vector<std::string> dlist_split(const std::string& s)
|
|
{
|
|
// If a [^\w] character surrounded by spaces exists in s, it is the delimiter.
|
|
// If not, the first space-delimited word is the delimiter.
|
|
if (s.find(" ") == std::string::npos) { // Only one element.
|
|
//std::vector<std::string> result {s};
|
|
//return result;
|
|
return {s};
|
|
} else {
|
|
std::smatch match{};
|
|
std::string elements_str{s};
|
|
std::string delimiter {};
|
|
if (std::regex_search(s, match, std::regex(R"(\s+([^\w])\s+)"))) {
|
|
delimiter = match[1];
|
|
} else {
|
|
std::string::const_iterator iter =
|
|
std::find_if(s.cbegin(), s.cend(), [](char c) { return c == ' '; });
|
|
delimiter = std::string(s.cbegin(), iter);
|
|
elements_str = std::string(iter, s.cend());
|
|
}
|
|
std::vector<std::string> elements {
|
|
regex_split(elements_str, std::regex(delimiter), true) };
|
|
return elements;
|
|
}
|
|
}
|
|
|
|
std::string get_env_var(const std::string& var) {
|
|
std::lock_guard<std::mutex> lock(env_mutex);
|
|
const char* val = getenv(var.c_str());
|
|
return val ? std::string(val) : "";
|
|
}
|
|
|
|
std::string freplace(const std::string& src, const std::regex& pattern,
|
|
const std::function<std::string(std::smatch)>& func)
|
|
{
|
|
std::string result {};
|
|
std::smatch match;
|
|
|
|
bool found = std::regex_search(src.begin(), src.end(), match, pattern);
|
|
auto pos = src.begin();
|
|
// int n = 0;
|
|
while (found) {
|
|
std::string part(pos, pos + match.position());
|
|
result += part;
|
|
result += func(match);
|
|
pos += match.position(0) + match.length(0);
|
|
found = std::regex_search(pos, src.end(), match, pattern);
|
|
// n++;
|
|
}
|
|
std::string part(pos, pos + match.position());
|
|
result += part;
|
|
// std::cout << "Found " << n << " matches\n";
|
|
return result;
|
|
}
|
|
|
|
std::string exec(const char* cmd)
|
|
{
|
|
std::array<char, 128> buffer;
|
|
std::string result;
|
|
FILE* pipe = popen(cmd, "r");
|
|
if (!pipe) throw std::runtime_error("popen() failed");
|
|
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
|
|
result += buffer.data();
|
|
}
|
|
pclose(pipe);
|
|
return result;
|
|
}
|