Filenames with spaces; output beside the input file; warning fixes (from dev c08eb1bbfa4c)

- Filenames may contain spaces: quote on the command line; filename
  lists use a standalone "/" separator; unseparated names that do not
  exist are rejoined into names that do (announced); leading ~ expands.
- Without -o, output is written to the input file's directory; -o fully
  specifies the output directory and basename.
- "Word not parsed" warnings now appear without -v, only for text that
  survives removal, with correct line numbers.
- New tst/filename_test.sh regression suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 23:22:59 +02:00
parent d9c98ac86d
commit fd9a370af7
21 changed files with 430 additions and 34 deletions

View File

@@ -115,6 +115,120 @@ bool file_exists(const std::string& pathname, bool error_if_not, bool is_directo
}
// Filename lists (see file.h): standalone-"/" separation, existence-rescue
// grouping, and tilde expansion.
std::string expand_tilde(const std::string& path)
{
if (path == "~" || (path.size() > 1 && path[0] == '~' && path[1] == '/')) {
std::string home = get_env_var("HOME");
if (!home.empty()) {
return home + path.substr(1);
}
}
return path;
}
static bool filename_exists(const std::string& name, const std::string& base_dir)
{
std::string resolved = expand_tilde(name);
if (!base_dir.empty() && !fs::path(resolved).is_absolute()) {
resolved = base_dir + "/" + resolved;
}
return fs::is_regular_file(resolved);
}
strings_t group_filename_tokens(const strings_t& tokens, const std::string& base_dir)
{
strings_t result {};
if (std::find(tokens.begin(), tokens.end(), "/") != tokens.end()) {
// Deterministic form: standalone "/" separates the filenames; the
// tokens between separators form one name. No existence checks.
strings_t group {};
for (const std::string& t : tokens) {
if (t == "/") {
if (!group.empty()) result.push_back(join(group));
group.clear();
} else {
group.push_back(t);
}
}
if (!group.empty()) result.push_back(join(group));
} else {
// Rescue: a token naming an existing file stands alone; one that
// does not is joined with following tokens until the accumulated
// name exists. A name that never resolves is kept as given, so the
// missing-file error downstream reports what the user wrote.
size_t i = 0;
while (i < tokens.size()) {
if (filename_exists(tokens[i], base_dir)) {
result.push_back(tokens[i]);
++i;
continue;
}
std::string acc = tokens[i];
size_t j = i + 1;
bool found = false;
while (j < tokens.size()) {
acc += " " + tokens[j];
++j;
if (filename_exists(acc, base_dir)) {
std::cerr << command_name << ": interpreting \""
<< acc << "\" as one filename\n";
result.push_back(acc);
i = j;
found = true;
break;
}
}
if (!found) {
result.push_back(tokens[i]);
++i;
}
}
}
for (std::string& name : result) {
name = expand_tilde(name);
}
return result;
}
strings_t resolve_filename_list(const std::string& text, const std::string& base_dir)
{
// Split on standalone "/" at the string level first, so a name's inner
// spacing survives exactly; without a separator, fall back to
// whitespace tokens and the rescue in group_filename_tokens().
auto standalone_slash = [&](size_t i) {
return text[i] == '/'
&& (i == 0 || std::isspace(static_cast<unsigned char>(text[i-1])))
&& (i + 1 == text.size() || std::isspace(static_cast<unsigned char>(text[i+1])));
};
bool has_separator = false;
for (size_t i = 0; i < text.size(); ++i) {
if (standalone_slash(i)) {
has_separator = true;
break;
}
}
if (has_separator) {
strings_t result {};
std::string part {};
for (size_t i = 0; i < text.size(); ++i) {
if (standalone_slash(i)) {
part = trim(part);
if (!part.empty()) result.push_back(expand_tilde(part));
part.clear();
} else {
part += text[i];
}
}
part = trim(part);
if (!part.empty()) result.push_back(expand_tilde(part));
return result;
}
return group_filename_tokens(word_split(text), base_dir);
}
std::string string_from_file(const std::string& pathname, bool strip_surrounding_whitespace)
{
std::regex klammertext_filename_re { R"(.*\.kt?)" };