Klammertext runs natively on Windows; a Windows source-install guide; the bundled sh for @eval :shell

Assembled from klammertext-dev f344a37d772c. The commands, the library,
the Standard Klammer Set, the reflection klammerset and the Python
binding build and run natively on Windows (MSYS2 UCRT64 gcc, python.org
Python), and both test tiers pass there. doc/install/windows_source_install.md
is the fifth install guide: a source install parallel to the Linux and
macOS ones, verified by following it on a pristine Windows 11.

Behaviour on every system: paths Klammertext prints use forward slashes;
output is LF; the command line is UTF-8 on Windows; @eval :shell is a
POSIX shell command everywhere -- on Windows it runs the sh Klammertext
ships, BusyBox for Windows (win/busybox/, GPLv2, with its licence and the
source pointer). KLAMMERTEXT_FONTS and KLAMMERTEXT_KLAMMERSETS are lists in
the system's PATH convention (';' on Windows). On Linux and macOS nothing
observable changes.
This commit is contained in:
ack committed 2026-09-25 00:33:26 +02:00
1 parent f9af1ff869
commit 830b19394d
55 files changed
+1182 -153

No files matched your search

+1 -1
View File
@@ -134,5 +134,5 @@ public:
// these, so a shell-quoted filename containing spaces stays one
// element; the space-joined m_value remains only for get()/describe().
std::map<std::string, std::vector<std::string>> m_vectors {};
long unsigned int m_syntax_size = 0;
std::size_t m_syntax_size = 0;
};
+21 -4
View File
@@ -1,3 +1,7 @@
#ifdef _WIN32
#include <fcntl.h>
#include <io.h>
#endif
#include <tuple>
#include "argv.h"
@@ -21,11 +25,24 @@ fs::path construct_command_pathname(char* command)
return fs::current_path() / fs::path(command).filename();
}
void set_command_identity(const std::string& name, const std::string& pathname)
{
command_name = name;
command_pathname = pathname;
}
void set_verbose_level(int argc, char* argv[])
{
#ifdef _WIN32
// Klammertext writes LF on every system: a document a Windows user pipes
// or redirects must not gain CR, and the suites read the streams byte for
// byte. The C runtime's text mode would append it to every line.
_setmode(_fileno(stdout), _O_BINARY);
_setmode(_fileno(stderr), _O_BINARY);
#endif
//command_name = absolute_pathname(argv[0]);
command_name = std::string(argv[0]);
command_pathname = construct_command_pathname(argv[0]);
command_name = fs::path(argv[0]).generic_string();
command_pathname = construct_command_pathname(argv[0]).generic_string();
//std::cout << "set_verbose_level: " << command_pathname << "\n";
Argv args {};
@@ -140,7 +157,7 @@ void set_command_state(
if (!input_filenames.empty()) {
S.set("K_input_dir", absolute_pathname(file_directory(input_filenames[0])));
} else {
S.set("K_input_dir", fs::current_path().string());
S.set("K_input_dir", fs::current_path().generic_string());
}
}
@@ -178,7 +195,7 @@ void load_klammersets(Machine& machine, const strings_t& symbols)
continue;
}
std::string klammerset_filename = resolve_klammerset_symbol(
symbol, machine.m_state.value("K_input_dir"), Locator::none()).string();
symbol, machine.m_state.value("K_input_dir"), Locator::none()).generic_string();
// Symbol -> file is the search path's answer, and the search
// path has three stages with shadowing: the file it landed on is
// exactly what a user cannot read off "--klammersets x".
+7
View File
@@ -10,6 +10,13 @@
class Machine;
void set_verbose_level(int argc, char* argv[]);
// What an error report and the -s pseudo source name as the command, set
// from OUTSIDE the library (the Python binding). It is a function, not an
// assignment to the header's inline variables, because on Windows every
// module -- the DLL, an extension module -- has its OWN copy of an inline
// variable: an assignment made in the binding never reached the copy the
// library's error reports read. On Linux the two are one variable.
void set_command_identity(const std::string& name, const std::string& pathname);
bool show_usage(int argc, char* argv[]);
fs::path construct_command_pathname(char* command);
+4 -2
View File
@@ -139,8 +139,10 @@ std::string short_path(const std::string& path)
std::error_code ec {};
fs::path home_real = fs::weakly_canonical(fs::path(home), ec);
fs::path path_real = fs::weakly_canonical(fs::path(path), ec);
std::string prefix = (ec ? std::string(home) : home_real.string()) + "/";
const std::string& subject = ec ? path : path_real.string();
// Both in the generic spelling ('/'): on Windows weakly_canonical
// answers with backslashes while the report's paths use '/'.
std::string prefix = (ec ? fs::path(home).generic_string() : home_real.generic_string()) + "/";
const std::string subject = ec ? fs::path(path).generic_string() : path_real.generic_string();
if (subject.size() > prefix.size()
&& subject.compare(0, prefix.size(), prefix) == 0) {
return subject.substr(prefix.size());
+1 -1
View File
@@ -11,7 +11,7 @@ bool display_source(const std::string& filename_arg)
return false;
} else {
fs::path filename = fs::path(filename_arg).filename();
return sks_commands.count(filename) == 0;
return sks_commands.count(filename.string()) == 0;
}
}
+14 -2
View File
@@ -10,7 +10,16 @@
#include <algorithm>
#include <optional>
#include <unistd.h>
#ifdef _WIN32
// mingw-w64 has no <sys/wait.h>: there pclose() returns the child's exit code
// itself, not a wait status, and no signal can have killed it.
#define WIFEXITED(s) ((s) != -1)
#define WEXITSTATUS(s) (s)
#define WIFSIGNALED(s) 0
#define WTERMSIG(s) 0
#else
#include <sys/wait.h>
#endif
// Run a shell command and return its STANDARD OUTPUT, which becomes document
// text. Two things the first version did not do, both of them the same defect
@@ -51,7 +60,8 @@ std::string shell(State state, std::string command, Locator loc)
// The braces keep the redirection outside the writer's command, so a
// command containing its own pipeline or redirection still works.
std::string wrapped = "{ " + command + " ; } 2>" + q_(err_path);
FILE* pipe = popen(wrapped.c_str(), "r");
Shell_command sh_cmd(wrapped, loc);
FILE* pipe = popen(sh_cmd.line().c_str(), "r");
if (!pipe) {
fs::remove(err_path);
throw Environment_error(
@@ -129,7 +139,9 @@ std::string run_haskell(const std::string& hsfile, Locator loc)
err_path = err_template.data();
std::string command = "runghc " + q_(hsfile) + " 2>" + q_(err_path);
FILE* pipe = popen(command.c_str(), "r");
// Through the same sh as :shell: the quoting is sh quoting.
Shell_command sh_cmd(command, loc);
FILE* pipe = popen(sh_cmd.line().c_str(), "r");
if (!pipe) {
fs::remove(err_path);
throw Environment_error("Could not run runghc.", loc);
+71 -6
View File
@@ -33,7 +33,7 @@ std::string absolute_pathname(const std::string& filename, const std::string& ba
{
fs::path p(filename);
if (filename.empty()) {
return fs::current_path().string();
return fs::current_path().generic_string();
}
if (!base.empty()) {
// Resolve filename relative to base's directory
@@ -41,7 +41,7 @@ std::string absolute_pathname(const std::string& filename, const std::string& ba
fs::path base_dir = fs::is_directory(b) ? b : b.parent_path();
p = base_dir / p;
}
return fs::absolute(p).string();
return fs::absolute(p).generic_string();
}
std::string relative_pathname(const std::string& filename)
@@ -70,6 +70,66 @@ size_t count_substrings(const std::string& text, const std::string& substring) {
return count;
}
std::string home_directory()
{
if (const char* home = std::getenv("HOME"); home && *home) {
return home;
}
#ifdef _WIN32
if (const char* profile = std::getenv("USERPROFILE"); profile && *profile) {
return profile;
}
#endif
return {};
}
std::string find_in_path(const std::string& program)
{
const char* path = std::getenv("PATH");
if (path == nullptr) {
return {};
}
std::stringstream ss(path);
std::string dir;
while (std::getline(ss, dir, path_list_separator)) {
if (dir.empty()) {
continue;
}
fs::path candidate = fs::path(dir) / program;
std::error_code ec {};
if (fs::is_regular_file(candidate, ec)) {
return candidate.generic_string();
}
#ifdef _WIN32
fs::path exe = candidate;
exe += ".exe";
if (fs::is_regular_file(exe, ec)) {
return exe.generic_string();
}
#endif
}
return {};
}
std::vector<std::string> directory_list(const char* variable, const std::string& default_subdir)
{
std::string paths {};
if (const char* env = std::getenv(variable); env && *env) {
paths = env;
} else if (std::string home = home_directory(); !home.empty()) {
paths = home + "/" + default_subdir;
}
std::vector<std::string> result {};
std::stringstream ss(paths);
std::string dir;
while (std::getline(ss, dir, path_list_separator)) {
if (!dir.empty()) {
result.push_back(dir);
}
}
return result;
}
fs::path relative_to_cwd(const fs::path& input)
{
const auto base = fs::current_path();
@@ -289,8 +349,10 @@ std::string string_from_file(const std::string& pathname, bool strip_surrounding
void string_to_file(const std::string& pathname, std::string contents)
{
fs::create_directories(file_directory(fs::absolute(pathname)));
std::ofstream out(pathname);
fs::create_directories(file_directory(fs::absolute(pathname).string()));
// Binary: the text Klammertext writes ends its lines with LF on every
// system; Windows' text mode would turn each into CRLF.
std::ofstream out(pathname, std::ios::binary);
if (!out) {
throw File_error("Could not write file " + pathname);
}
@@ -319,7 +381,7 @@ strings_t get_files_in_directory(const std::string& dir)
for (const auto& entry : fs::directory_iterator(dir)) {
if (entry.is_regular_file()) {
// std::cout << entry.path().filename() << std::endl;
result.push_back(entry.path().filename());
result.push_back(entry.path().filename().string());
}
}
} catch (const fs::filesystem_error& ex) {
@@ -456,8 +518,11 @@ strings_t get_sks_directories(const std::string& s, bool include_argument)
std::string symbolic_klammertext_filename(std::string filename)
{
// Both in the generic spelling: a Windows KLAMMERTEXT_HOME may be written
// with either separator, and a path Klammertext prints uses '/'.
return string_replace(
filename, get_env_var("KLAMMERTEXT_HOME"), "$KLAMMERTEXT_HOME");
fs::path(filename).generic_string(),
fs::path(get_env_var("KLAMMERTEXT_HOME")).generic_string(), "$KLAMMERTEXT_HOME");
}
+22
View File
@@ -16,6 +16,28 @@ std::string relative_pathname(const std::string& filename, const std::string& ba
fs::path relative_to_cwd(const fs::path& input);
// The separator of a directory list in an environment variable
// (KLAMMERTEXT_FONTS, KLAMMERTEXT_KLAMMERSETS): ';' on Windows, where a drive
// letter puts a colon inside every absolute path, ':' everywhere else -- the
// convention of PATH on each system.
#ifdef _WIN32
constexpr char path_list_separator = ';';
#else
constexpr char path_list_separator = ':';
#endif
// The user's home directory as the system names it: HOME, and on Windows
// USERPROFILE when HOME is unset (a plain PowerShell or cmd sets no HOME;
// an MSYS2 shell does). Empty when neither is set.
std::string home_directory();
// The first PROGRAM on PATH (split on path_list_separator; on Windows the
// name also tried with ".exe"), or "" when none: what "which" answers, without
// spawning a shell -- on Windows popen()'s cmd.exe has no "which".
std::string find_in_path(const std::string& program);
// The directories listed in an environment variable, split on
// path_list_separator, empty entries dropped; DEFAULT_SUBDIR under the home
// directory when the variable is unset or empty.
std::vector<std::string> directory_list(const char* variable, const std::string& default_subdir);
bool file_exists(const std::string& pathname, bool error_if_not=false, bool is_directory=false);
// Text to its lines, and a file to (its whole text, its lines). These
+25 -27
View File
@@ -23,12 +23,20 @@
namespace fs = std::filesystem;
// The interpreter spawned for the font-metric script: python.org's Windows
// build installs "python" only; Linux and macOS name it "python3".
#ifdef _WIN32
static const char* const python_interpreter = "python";
#else
static const char* const python_interpreter = "python3";
#endif
// A filesystem extension lowercased for case-insensitive matching, so a
// font named Foo.TTF or Foo.OTF is recognized like Foo.ttf / Foo.otf.
static std::string lower_extension(const fs::path& path)
{
std::string ext = path.extension();
std::string ext = path.extension().string();
for (char& c : ext) {
c = std::tolower(static_cast<unsigned char>(c));
}
@@ -49,29 +57,14 @@ std::string name_to_dirname(const std::string& name)
}
// The directories searched for installed fonts: the colon-separated
// KLAMMERTEXT_FONTS environment variable, or ~/.klammertext/fonts when it
// is not set. Directories are searched in listed order, and the default
// fonts in $KLAMMERTEXT_HOME/fnt are searched last, so an installed font
// can deliberately shadow a default one.
// The directories searched for installed fonts: the KLAMMERTEXT_FONTS
// environment variable (a list in the system's PATH convention: ':' or, on
// Windows, ';'), or ~/.klammertext/fonts when it is not set. Directories are
// searched in listed order, and the default fonts in $KLAMMERTEXT_HOME/fnt are
// searched last, so an installed font can deliberately shadow a default one.
strings_t installed_font_dirs()
{
strings_t result {};
std::string paths {};
const char* env = std::getenv("KLAMMERTEXT_FONTS");
if (env && *env) {
paths = env;
} else if (const char* home = std::getenv("HOME"); home && *home) {
paths = std::string(home) + "/.klammertext/fonts";
}
std::stringstream ss(paths);
std::string dir;
while (std::getline(ss, dir, ':')) {
if (!dir.empty()) {
result.push_back(dir);
}
}
return result;
return directory_list("KLAMMERTEXT_FONTS", ".klammertext/fonts");
}
std::string default_font_dir()
@@ -151,7 +144,7 @@ std::vector<Resolved_font> fonts_in_directory(const std::string& base)
for (auto& entry : fs::directory_iterator(base)) {
if (entry.path().extension() == ".css" &&
fs::is_directory(base + "/" + entry.path().stem().string())) {
names.push_back(entry.path().stem());
names.push_back(entry.path().stem().string());
}
}
std::sort(names.begin(), names.end());
@@ -199,10 +192,12 @@ static void extract_font_metrics(Resolved_font& font)
if (!fs::exists(ttf_path))
return;
// Extract both x-height and cap-height ratios from OS/2 table
// The path goes inside a Python literal: forward slashes, which Python
// opens on every system, where a Windows backslash would be an escape.
std::string script =
"python3 -c \""
std::string(python_interpreter) + " -c \""
"import struct; "
"f = open('" + ttf_path + "', 'rb'); "
"f = open('" + fs::path(ttf_path).generic_string() + "', 'rb'); "
"_, n = struct.unpack('>IH', f.read(6)); "
"f.read(6); "
"t = {};\n"
@@ -219,6 +214,9 @@ static void extract_font_metrics(Resolved_font& font)
"print(f'{xh/upm:.4f} {ch/upm:.4f}') if ver >= 2 else None; "
"f.close()\"";
std::string result = trim(exec(script.c_str()));
// A DERIVED value a designer cannot read off the font: the two ratios
// (or nothing, when the script failed) decide the html size normalization.
(void)K::log(3, "Font metrics of " + ttf_path + ": " + (result.empty() ? std::string("(none)") : result));
if (!result.empty()) {
try {
auto pos = result.find(' ');
@@ -418,7 +416,7 @@ std::vector<Font_file> classify_font_files(const std::string& directory)
for (auto& entry : fs::recursive_directory_iterator(directory)) {
std::string ext = lower_extension(entry.path());
if (entry.is_regular_file() && (ext == ".ttf" || ext == ".otf")) {
result.push_back(classify_font_file(entry.path()));
result.push_back(classify_font_file(entry.path().string()));
}
}
return result;
@@ -521,7 +519,7 @@ std::string write_font_samples(const std::string& output_dir, const std::string&
install_fonts(source_dir, output_dir + "/fonts");
for (auto& entry : fs::directory_iterator(output_dir + "/fonts")) {
if (entry.path().extension() == ".css") {
std::string css = string_from_file(entry.path());
std::string css = string_from_file(entry.path().string());
std::smatch match {};
if (std::regex_search(css, match,
std::regex(R"(font-family:\s*'([^']+)')"))) {
+5
View File
@@ -160,3 +160,8 @@ void warn_unparsed_katoms(katom_list& katoms, bool warn)
}
}
}
void set_show_rewrite_rules(bool on)
{
show_rewrite_rules = on;
}
+4
View File
@@ -82,6 +82,10 @@ katom_list katomize_lines(const strings_t& lines, const std::string& source,
// expand_compound_katom() and attributed to whichever rule the loop
// happened to be holding, so it always read as "bar precedence".
inline bool show_rewrite_rules = false;
// Set it through this, never by assignment from a command: on Windows the
// command's copy of an inline variable is not the library's (see
// set_command_identity in command.h).
void set_show_rewrite_rules(bool on);
// Warn about text that matched no katom type and that no rule could divide
// -- but only where it survives processing: text removed by #, ##, or
+7 -19
View File
@@ -151,7 +151,7 @@ std::vector<std::string> klammerset_search_dirs(const std::string& local_dir)
// A repeated directory (e.g. the local stage already IS
// $KLAMMERTEXT_HOME) adds nothing and clutters error messages.
auto push_unique = [&result](const std::string& dir) {
std::string canonical = fs::weakly_canonical(dir).string();
std::string canonical = fs::weakly_canonical(dir).generic_string();
if (!is_in(canonical, result)) {
result.push_back(canonical);
}
@@ -171,22 +171,10 @@ std::vector<std::string> klammerset_search_dirs(const std::string& local_dir)
// a document could extend its own search path between one declaration and
// the next. Nothing intended that -- it fell out of a fresh getenv and an
// in-process interpreter.
static const std::string paths = [] {
const char* env = std::getenv("KLAMMERTEXT_KLAMMERSETS");
if (env && *env) {
return std::string(env);
}
if (const char* home = std::getenv("HOME"); home && *home) {
return std::string(home) + "/.klammertext/klammersets";
}
return std::string();
}();
std::stringstream ss(paths);
std::string dir;
while (std::getline(ss, dir, ':')) {
if (!dir.empty()) {
push_unique(dir);
}
static const strings_t installed =
directory_list("KLAMMERTEXT_KLAMMERSETS", ".klammertext/klammersets");
for (const std::string& dir : installed) {
push_unique(dir);
}
if (const char* kthome = std::getenv(klammertext_home_var.c_str()); kthome && *kthome) {
push_unique(kthome);
@@ -280,11 +268,11 @@ std::string describe_klammerset_search(const std::string& local_dir, int margin)
if (!is_klammerset_symbol(symbol) || !fs::is_regular_file(declaration)) {
continue;
}
std::string line = tab + symbol + sp_arrow + declaration.string();
std::string line = tab + symbol + sp_arrow + declaration.generic_string();
if (first_hit.count(symbol)) {
line += " (shadowed by " + first_hit[symbol] + ")";
} else {
first_hit[symbol] = declaration.string();
first_hit[symbol] = declaration.generic_string();
}
// What a set IS, under where it lives: the symbol and the path
// say which file would load, and nothing at all about whether
+6 -6
View File
@@ -12,14 +12,14 @@ std::string abbreviate_location(
{
std::string result = location;
fs::path basename = fs::path(location).filename();
if (sks_commands.contains(basename)) {
result = basename;
if (sks_commands.contains(basename.string())) {
result = basename.string();
} else {
if (make_map) {
if (relpath.count(location)) {
result = relpath[location];
} else {
result = fs::relative(location, fs::current_path());
result = fs::relative(location, fs::current_path()).generic_string();
relpath[location] = result;
}
}
@@ -29,7 +29,7 @@ std::string abbreviate_location(
Locator::Locator(const fs::path& filename, int line, int chr)
: m_filename(filename)
: m_filename(filename.generic_string())
, m_line(line)
, m_chr(chr)
{
@@ -49,7 +49,7 @@ std::string Locator::str(bool relative) const
// relative_pathname(m_filename, fs::current_path().string()) << ":"
std::string filename = m_filename;
if (relative) {
filename = relative_to_cwd(m_filename);
filename = relative_to_cwd(m_filename).generic_string();
}
std::stringstream ss {};
ss << "[" << filename << ":"
@@ -63,7 +63,7 @@ std::string Locator::desc(bool relative) const
// relative_pathname(m_filename, fs::current_path().string()) << ":"
std::string filename = m_filename;
if (relative) {
filename = relative_to_cwd(m_filename);
filename = relative_to_cwd(m_filename).generic_string();
}
std::stringstream ss {};
ss << filename << ", line " << m_line << ", character " << m_chr + 1;
+1 -1
View File
@@ -19,7 +19,7 @@ public:
// toolchains (e.g. Homebrew GCC on macOS); guard against
// fs::path(nullptr) -> strlen(NULL).
: m_filename(location.file_name()
? fs::absolute(location.file_name()) : fs::path{})
? fs::absolute(location.file_name()).generic_string() : std::string{})
, m_line(int(location.line()))
, m_chr(int(location.column()))
{};
+8 -8
View File
@@ -491,7 +491,7 @@ void Machine::read(const fs::path& pathname)
// :cpp libraries live next to the file that uses them).
m_state.add_search_dir(fs::absolute(pathname).parent_path().string());
m_state.open_frame("Machine state: " + pathname.string());
input(string_from_file(pathname), pathname.string());
input(string_from_file(pathname.string()), pathname.generic_string());
m_sources.push_back(pathname);
}
@@ -519,7 +519,7 @@ void Machine::expand_read_katoms(
bool klammers, bool eval, bool cond, bool read)
{
(void)K::log(3);
current_filename = resolve_relative_to(current_filename);
current_filename = resolve_relative_to(current_filename).generic_string();
// msg() << "current_filename: " << current_filename << "\n";
if (std::find_if(katoms.begin(), katoms.end(), begin_read) != katoms.end()) {
(void)K::log(3);
@@ -553,11 +553,11 @@ void Machine::expand_read_katoms(
input_filename = fs::canonical(input_filename);
m_state.add_search_dir(input_filename.parent_path().string());
std::string text = trim_right(string_from_file(input_filename.string()));
katom_list ks = katomize_lines(line_split(text), input_filename);
katom_list ks = katomize_lines(line_split(text), input_filename.generic_string());
// ks =
process_katoms(
// ks, command_pathname,
ks, input_filename,
ks, input_filename.generic_string(),
nonascii, literal, ignore, whitespace, klammers, eval, cond, read);
katoms.insert(end, ks.begin(), ks.end());
}
@@ -640,7 +640,7 @@ void Machine::load_klammerset_files(const Klammerset& klammerset, katom_iter ins
std::vector<std::string> filenames {};
for (const auto& required : klammerset.m_requires) {
std::string path = is_klammerset_symbol(required)
? resolve_klammerset_symbol(required, base_dir, klammerset.m_loc).string()
? resolve_klammerset_symbol(required, base_dir, klammerset.m_loc).generic_string()
: required;
// ALREADY LOADED? Decided BEFORE the file is opened. The guard used
// to sit in Klammerset_registry::add, which only runs once the file has
@@ -682,11 +682,11 @@ void Machine::load_klammerset_files(const Klammerset& klammerset, katom_iter ins
klammerset.m_loc);
}
pathname = fs::canonical(pathname);
loaded_files.push_back(pathname.string());
loaded_files.push_back(pathname.generic_string());
m_state.add_search_dir(pathname.parent_path().string());
std::string text = trim_right(string_from_file(pathname.string()));
katom_list ks = katomize_lines(line_split(text), pathname);
process_katoms(ks, pathname);
katom_list ks = katomize_lines(line_split(text), pathname.generic_string());
process_katoms(ks, pathname.generic_string());
loaded.insert(loaded.end(), ks.begin(), ks.end());
}
m_katoms.insert(insert_at, loaded.begin(), loaded.end());
+3 -1
View File
@@ -357,7 +357,9 @@ std::string State::python_code()
m_search_dirs.begin(), m_search_dirs.end());
for (const auto& d : python_dirs) {
if (directory_has_python_file(d)) {
ss << "sys.path.append('" << d << "')\n";
// A Python string literal, not the raw text: a Windows directory
// holds backslashes, and C:\Users reads as a \U unicode escape.
ss << "sys.path.append(" << pyformat_string({d}) << ")\n";
}
}
if (!m_frames.empty()) {
+46 -1
View File
@@ -1,3 +1,8 @@
#include <unistd.h>
#include <fstream>
#include "error.h"
#include "file.h"
#include "locator.h"
#include <stdlib.h>
#include <cstdio>
#include <algorithm>
@@ -621,11 +626,51 @@ std::string freplace(const std::string& src, const std::regex& pattern,
return result;
}
// Shell_command -- see util.h. On Windows the script goes to a file under
// the temp directory (mkstemp, so concurrent runs do not collide), and the
// line is ""<busybox>" sh "<script>"" -- the outer pair of quotes because
// cmd /c strips the FIRST and LAST quote of a line holding more than two.
Shell_command::Shell_command(const std::string& sh_command, [[maybe_unused]] const Locator& loc)
{
#ifdef _WIN32
fs::path busybox = fs::path(get_env_var("KLAMMERTEXT_HOME")) / "win" / "busybox" / "busybox64u.exe";
if (!fs::exists(busybox)) {
throw Environment_error(
"The shell Klammertext runs sh commands with on Windows is missing: "
+ busybox.generic_string(), loc);
}
std::string path = (fs::temp_directory_path() / "ktext_sh_XXXXXX").string();
std::vector<char> tmpl(path.begin(), path.end());
tmpl.push_back('\0');
int fd = mkstemp(tmpl.data());
if (fd == -1) {
throw Environment_error("Could not create a temporary file for a shell command", loc);
}
close(fd);
m_script = tmpl.data();
std::ofstream out(m_script, std::ios::binary);
out << sh_command << "\n";
out.close();
m_line = "\"\"" + busybox.string() + "\" sh \"" + m_script + "\"\"";
#else
m_line = sh_command;
#endif
}
Shell_command::~Shell_command()
{
if (!m_script.empty()) {
std::error_code ec {};
fs::remove(m_script, ec);
}
}
std::string exec(const char* cmd)
{
std::array<char, 128> buffer;
std::string result;
FILE* pipe = popen(cmd, "r");
Shell_command sh_cmd(cmd, Locator::none());
FILE* pipe = popen(sh_cmd.line().c_str(), "r");
if (!pipe) throw std::runtime_error("popen() failed");
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
result += buffer.data();
+22
View File
@@ -7,6 +7,8 @@
#include <memory>
#include <functional>
#include <tuple>
class Locator; // locator.h; a full include would cycle through util.h
#include <utility>
#include <algorithm>
@@ -85,6 +87,26 @@ std::pair<std::string,std::string> extract_parameter_type(const std::string& par
std::tuple<std::string, std::string, bool> regex_split_prefix(const std::regex& pattern, const std::string& text);
std::vector<std::string> dlist_split(const std::string& s);
std::string get_env_var(const std::string& var);
// A POSIX sh command as popen()/system() must receive it. On Linux and
// macOS line() is the text itself. On Windows popen() runs cmd.exe, which
// cannot carry a quoted string with an embedded newline, so the command is
// written to a script file and line() runs the sh Klammertext ships
// (win/busybox/) on that file; the file lives as long as this object. exec()
// uses it, so every command Klammertext composes runs under sh on every
// system; code that calls popen()/system() itself (eval.cpp,
// tst/klammer_test) uses it too.
class Shell_command
{
public:
Shell_command(const std::string& sh_command, const Locator& loc);
~Shell_command();
Shell_command(const Shell_command&) = delete;
Shell_command& operator=(const Shell_command&) = delete;
const std::string& line() const { return m_line; }
private:
std::string m_line {};
std::string m_script {}; // the script file, Windows only
};
std::string freplace(const std::string& src, const std::regex& pattern,
const std::function<std::string(std::smatch)>& func);
std::string exec(const char* cmd);