#ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #define NOMINMAX #include #include #include #endif #include #include #include "argv.h" #include "eval_python.h" #include "file.h" #include "log.h" #include "machine.h" using namespace std::string_literals; fs::path construct_command_pathname(char* command) { // The pseudo source file for command-line string input (-s, kdiag's // input): a file IN THE CWD named after the command, so that a relative // @read in string input resolves against the directory the user ran the // command from -- the string-input analog of "a document's relative // names resolve against the document". The basename matters: the // former "/" string concatenation anchored resolution at // the BINARY's directory whenever argv[0] was absolute // (/usr/local/bin/ktext, a wrapper script), so every relative @read in // -s input searched bin/ instead of the cwd. 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); // A console decodes what a program writes in the CONSOLE's output code // page, which on a localized Windows is an OEM page (850 on a German one) // and never UTF-8, whatever the process's own code page (the manifest // makes that UTF-8). The commands write UTF-8 -- kdesc --katoms's // ⟨name⟩, an error report's ⟨katom⟩ -- so a console showed three cp850 // characters per symbol while a file or a pipe received the right bytes // (which is why no suite saw it; Andy did, 2026-09-25). Switch the // console to UTF-8 while a command runs, when a stream is a console, and // put it back at exit: the page belongs to the console, not the process, // and would outlive it. static UINT console_output_cp = 0; static UINT console_output_cp_now = 0; if (_isatty(_fileno(stdout)) || _isatty(_fileno(stderr))) { console_output_cp = GetConsoleOutputCP(); if (console_output_cp != 0 && console_output_cp != CP_UTF8) { SetConsoleOutputCP(CP_UTF8); console_output_cp_now = GetConsoleOutputCP(); std::atexit([] { SetConsoleOutputCP(console_output_cp); }); } } #endif //command_name = absolute_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 {}; args.opt("v", "'verbosity'", "level", "0", "'verbosity'"); args.parse(argc, argv, false); verbose_level = args.as_verbosity("v"); // A klammer set's Python speaks through the log (eval_python.h): the // commands only -- the binding leaves the host's sys.stdout alone. set_python_output_to_log(true); #ifdef _WIN32 if (console_output_cp != 0 && console_output_cp != CP_UTF8) { K::log(1, "Console output code page set to UTF-8 for this run (was " + std::to_string(console_output_cp) + ", now " + std::to_string(console_output_cp_now) + ")"); } #endif } bool show_usage(int argc, char* argv[]) { return argc == 1 || (argc == 3 && std::string(argv[1]) == "-v"s); } std::string construct_output_filename( const std::string& output_dir, const std::string& output_basename, const std::string& target) { std::string result {}; if (target == "html") { result = output_dir + "/" + output_basename + "/index.html"; } else { result = output_dir + "/" + output_basename + "." + target; } return result; } bool only_definitions(std::vector filenames) { if (filenames.empty()) return false; bool result = true; for (auto f : filenames) { if (fs::path(f).extension() != ".k") { result = false; break; } } return result; } std::tuple parse_args( const std::vector& input_filenames, std::string target, std::string output_basename, bool display_only) { // output_dir std::string output_target = target; std::string output_dir = ""; std::string ext = extension(output_basename); // std::string output_filename = ""; bool write_files = true; if (target.empty() && output_basename.empty()) { output_target = "any"; } if (output_basename == "-") { write_files = false; } else if (!output_basename.empty()) { output_dir = file_directory(output_basename); output_basename = file_basename(output_basename); } else if (!input_filenames.empty()) { // No -o: output goes next to the input file ("" -- an input with no // directory component -- resolves to cwd below). output_dir = file_directory(input_filenames[0]); output_basename = file_basename(input_filenames[0]); } output_dir = absolute_pathname(output_dir); if (output_target.empty()) { target = ext; } std::string output_filename = construct_output_filename(output_dir, output_basename, target); if (output_basename.empty() && input_filenames.empty()) { display_only = true; } else if (only_definitions(input_filenames)) { display_only = true; } std::vector> vars = { {"Output target", output_target}, {"Output directory", output_dir}, {"Output basename", output_basename}, {"Output filename", output_filename}, {"Write file", write_files ? "true" : "false"}}; int verbose = 1; for (auto [label, value] : vars) { if (label == "Output filename") { //verbose = 2; } (void)K::log(int(verbose), label + ":", value); } return {output_target, output_dir, output_basename, output_filename, write_files, display_only}; } void set_command_state( Machine& machine, const std::string& frame_name, const std::string& target, const std::vector& input_filenames, const std::string& output_dir, const std::string& output_basename, bool display_only) { State& S = machine.m_state; S.open_frame(frame_name); S.set("K_target", target); S.set("K_output_dir", output_dir); S.set("K_output_basename", output_basename); S.set("K_stdout_only", display_only ? "true" : "false"); // The list form uses the standalone-"/" separator (filenames may // contain spaces); K_input_filename is the single root input file. S.set("K_input_filenames", join(input_filenames, " / ")); S.set("K_input_filename", input_filenames.empty() ? "" : input_filenames[0]); S.set("K_verbose_level", std::to_string(verbose_level)); 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().generic_string()); } } void load_klammersets(Machine& machine, const strings_t& symbols) { // "none" is the absence of a klammerset, not the name of one, so it says // nothing about what should be loaded ALONGSIDE it: written first it would // silently discard the rest of the list, written last it would be looked up // as a symbol and reported missing. Both readings are wrong, so neither is // guessed at -- "none" is accepted only alone. if (is_in("none"s, symbols) && symbols.size() > 1) { throw Klammerset_error( "The klammerset \"none\" cannot be combined with other klammersets: " "it means that none is loaded. Give \"none\" alone, or name only the " "klammersets to load.", Locator::none()); } if (symbols.empty()) { // Which klammerset was loaded, and from where, is a DERIVED value: the // user wrote no --klammersets at all and got the SKS from // $KLAMMERTEXT_HOME. Only the symbol branch below reported its // resolution, so the commonest case -- the default -- said nothing. std::string sks = machine.m_state.value("KLAMMERTEXT_HOME") + "/sks/sks.k"; (void)K::log(1, "Klammerset (default): " + sks); machine.read(fs::path(sks)); } else if (symbols[0] == "none") { (void)K::log(1, "Klammersets: none"); } else { // The whitespace-delimited words in symbols must be klammerset symbols: for (const auto& symbol : symbols) { // Same guard as ":requires" (Machine::load_klammerset_files): a set // named twice on the command line is loaded once. if (machine.m_klammersets.has(symbol)) { (void)K::log(1, "Klammerset \"" + symbol + "\": already loaded, skipped"); continue; } std::string klammerset_filename = resolve_klammerset_symbol( 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". (void)K::log(1, "Klammerset \"" + symbol + "\": " + klammerset_filename); machine.read(fs::path(klammerset_filename)); } } }