max_size = 10GB
-------------------------------------------------------------------------------
+==== Multi-line values
+
+Values can span multiple lines using indentation-based continuation. Lines that
+start with whitespace (spaces or tabs) are treated as continuation lines and are
+joined to the previous value with a single space. Comments and blank lines
+within a multi-line value are skipped. For example:
+
+-------------------------------------------------------------------------------
+ignore_options =
+ -Wall
+ -Wextra
+ # This is a comment within the value
+ -pedantic
+# This ends the multi-line value
+compiler = gcc
+-------------------------------------------------------------------------------
+
+This is equivalent to:
+
+-------------------------------------------------------------------------------
+ignore_options = -Wall -Wextra -pedantic
+compiler = gcc
+-------------------------------------------------------------------------------
+
+Multi-line values are particularly useful for options that accept multiple
+items.
+
=== Boolean values
Some configuration options are boolean values (i.e. truth values). In a
[#config_remote_storage]
*remote_storage* (*CCACHE_REMOTE_STORAGE*)::
- This option specifies one or several storage backends (separated by space)
- to query after checking the local cache (unless
+ This option specifies one or several storage backends (separated by
+ whitespace) to query after checking the local cache (unless
<<config_remote_only,*remote_only*>> is true). See
_<<Remote storage backends>>_ for documentation of syntax and available
backends.
+
Examples:
+
-* `+file:/shared/nfs/directory+`
-* `+file:///shared/nfs/one|read-only file:///shared/nfs/two+`
-* `+file:///Z:/example/windows/folder+`
-* `+http://example.com/cache+`
-* `+redis://example.com+`
+----
+remote_storage = file:/shared/nfs/directory+`
+remote_storage =
+ file:///shared/nfs/one|read-only
+ file:///shared/nfs/two
+remote_storage = file:///Z:/example/windows/folder
+remote_storage = http://example.com/cache
+remote_storage = redis://example.com
+----
+
NOTE: In previous ccache versions this option was called *secondary_storage*
(*CCACHE_SECONDARY_STORAGE*), which can still be used as an alias.
#include <ccache/core/exceptions.hpp>
#include <ccache/core/sloppiness.hpp>
#include <ccache/util/assertions.hpp>
+#include <ccache/util/configreader.hpp>
#include <ccache/util/direntry.hpp>
#include <ccache/util/environment.hpp>
#include <ccache/util/expected.hpp>
struct ConfigKeyTableEntry
{
ConfigItem item;
- std::optional<std::string> alias = std::nullopt;
+ std::optional<std::string_view> alias = std::nullopt;
};
-const std::unordered_map<std::string, ConfigKeyTableEntry> k_config_key_table =
- {
+const std::unordered_map<std::string_view, ConfigKeyTableEntry>
+ k_config_key_table = {
{"absolute_paths_in_stderr", {ConfigItem::absolute_paths_in_stderr} },
{"base_dir", {ConfigItem::base_dir} },
{"cache_dir", {ConfigItem::cache_dir} },
{"umask", {ConfigItem::umask} },
};
-const std::unordered_map<std::string, std::string> k_env_variable_table = {
- {"ABSSTDERR", "absolute_paths_in_stderr" },
- {"BASEDIR", "base_dir" },
- {"CC", "compiler" }, // Alias for CCACHE_COMPILER
- {"COMMENTS", "keep_comments_cpp" },
- {"COMPILER", "compiler" },
- {"COMPILERCHECK", "compiler_check" },
- {"COMPILERTYPE", "compiler_type" },
- {"COMPRESS", "compression" },
- {"COMPRESSLEVEL", "compression_level" },
- {"DEBUG", "debug" },
- {"DEBUGDIR", "debug_dir" },
- {"DEBUGLEVEL", "debug_level" },
- {"DEPEND", "depend_mode" },
- {"DIR", "cache_dir" },
- {"DIRECT", "direct_mode" },
- {"DISABLE", "disable" },
- {"EXTENSION", "cpp_extension" },
- {"EXTRAFILES", "extra_files_to_hash" },
- {"FILECLONE", "file_clone" },
- {"HARDLINK", "hard_link" },
- {"HASHDIR", "hash_dir" },
- {"IGNOREHEADERS", "ignore_headers_in_manifest"},
- {"IGNOREOPTIONS", "ignore_options" },
- {"INODECACHE", "inode_cache" },
- {"LOGFILE", "log_file" },
- {"MAXFILES", "max_files" },
- {"MAXSIZE", "max_size" },
- {"MSVC_DEP_PREFIX", "msvc_dep_prefix" },
- {"NAMESPACE", "namespace" },
- {"PATH", "path" },
- {"PCH_EXTSUM", "pch_external_checksum" },
- {"PREFIX", "prefix_command" },
- {"PREFIX_CPP", "prefix_command_cpp" },
- {"READONLY", "read_only" },
- {"READONLY_DIRECT", "read_only_direct" },
- {"RECACHE", "recache" },
- {"REMOTE_ONLY", "remote_only" },
- {"REMOTE_STORAGE", "remote_storage" },
- {"RESHARE", "reshare" },
- {"RESPONSE_FILE_FORMAT", "response_file_format" },
- {"SECONDARY_STORAGE", "remote_storage" }, // Alias for CCACHE_REMOTE_STORAGE
- {"SLOPPINESS", "sloppiness" },
- {"STATS", "stats" },
- {"STATSLOG", "stats_log" },
- {"TEMPDIR", "temporary_dir" },
- {"UMASK", "umask" },
+const std::unordered_map<std::string_view, std::string_view>
+ k_env_variable_table = {
+ {"ABSSTDERR", "absolute_paths_in_stderr" },
+ {"BASEDIR", "base_dir" },
+ {"CC", "compiler" }, // Alias for CCACHE_COMPILER
+ {"COMMENTS", "keep_comments_cpp" },
+ {"COMPILER", "compiler" },
+ {"COMPILERCHECK", "compiler_check" },
+ {"COMPILERTYPE", "compiler_type" },
+ {"COMPRESS", "compression" },
+ {"COMPRESSLEVEL", "compression_level" },
+ {"DEBUG", "debug" },
+ {"DEBUGDIR", "debug_dir" },
+ {"DEBUGLEVEL", "debug_level" },
+ {"DEPEND", "depend_mode" },
+ {"DIR", "cache_dir" },
+ {"DIRECT", "direct_mode" },
+ {"DISABLE", "disable" },
+ {"EXTENSION", "cpp_extension" },
+ {"EXTRAFILES", "extra_files_to_hash" },
+ {"FILECLONE", "file_clone" },
+ {"HARDLINK", "hard_link" },
+ {"HASHDIR", "hash_dir" },
+ {"IGNOREHEADERS", "ignore_headers_in_manifest"},
+ {"IGNOREOPTIONS", "ignore_options" },
+ {"INODECACHE", "inode_cache" },
+ {"LOGFILE", "log_file" },
+ {"MAXFILES", "max_files" },
+ {"MAXSIZE", "max_size" },
+ {"MSVC_DEP_PREFIX", "msvc_dep_prefix" },
+ {"NAMESPACE", "namespace" },
+ {"PATH", "path" },
+ {"PCH_EXTSUM", "pch_external_checksum" },
+ {"PREFIX", "prefix_command" },
+ {"PREFIX_CPP", "prefix_command_cpp" },
+ {"READONLY", "read_only" },
+ {"READONLY_DIRECT", "read_only_direct" },
+ {"RECACHE", "recache" },
+ {"REMOTE_ONLY", "remote_only" },
+ {"REMOTE_STORAGE", "remote_storage" },
+ {"RESHARE", "reshare" },
+ {"RESPONSE_FILE_FORMAT", "response_file_format" },
+ {"SECONDARY_STORAGE", "remote_storage" }, // Alias for CCACHE_REMOTE_STORAGE
+ {"SLOPPINESS", "sloppiness" },
+ {"STATS", "stats" },
+ {"STATSLOG", "stats_log" },
+ {"TEMPDIR", "temporary_dir" },
+ {"UMASK", "umask" },
};
util::Args::ResponseFileFormat
return true;
}
-// `line` is the full configuration line excluding newline. `key` will be empty
-// for comments and blank lines. `value` does not include newline.
-using ConfigLineHandler = std::function<void(
- const std::string& line, const std::string& key, const std::string& value)>;
-
-// Call `config_line_handler` for each line in `path`.
-bool
-parse_config_file(const fs::path& path,
- const ConfigLineHandler& config_line_handler)
-{
- std::ifstream file(util::pstr(path).c_str());
- if (!file) {
- return false;
- }
-
- std::string line;
-
- size_t line_number = 0;
- while (std::getline(file, line)) {
- ++line_number;
-
- try {
- std::string key;
- std::string value;
- std::string error_message;
- if (!parse_line(line, &key, &value, &error_message)) {
- throw core::Error(error_message);
- }
- config_line_handler(line, key, value);
- } catch (const core::Error& e) {
- throw core::Error(FMT("{}:{}: {}", path, line_number, e.what()));
- }
- }
- return true;
-}
-
std::unordered_map<std::string, std::string>
create_cmdline_settings_map(const std::vector<std::string>& settings)
{
bool
Config::update_from_file(const fs::path& path)
{
- return parse_config_file(
- path, [&](const auto& /*line*/, const auto& key, const auto& value) {
- if (!key.empty()) {
- set_item(key, value, std::nullopt, false, util::pstr(path));
- }
- });
+ auto config_content = util::read_file<std::string>(path);
+ if (!config_content) {
+ return false;
+ }
+
+ util::ConfigReader reader(*config_content);
+
+ while (true) {
+ auto item_result = reader.read_next_item();
+ if (!item_result) {
+ throw core::Error(FMT("{}:{}: {}",
+ path,
+ item_result.error().line_number,
+ item_result.error().message));
+ }
+
+ auto& item = *item_result;
+ if (!item) {
+ break; // EOF
+ }
+
+ try {
+ set_item(item->key, item->value, std::nullopt, false, util::pstr(path));
+ } catch (const core::Error& e) {
+ throw core::Error(FMT("{}:{}: {}", path, item->line_number, e.what()));
+ }
+ }
+
+ return true;
}
void
FMT("failed to write to {}: ", resolved_path));
}
- core::AtomicFile output(resolved_path, core::AtomicFile::Mode::text);
- bool found = false;
-
- if (!parse_config_file(
- path,
- [&](const auto& c_line, const auto& c_key, const auto& /*c_value*/) {
- if (c_key == key) {
- output.write(FMT("{} = {}\n", key, value));
- found = true;
- } else {
- output.write(FMT("{}\n", c_line));
- }
- })) {
- throw core::Error(FMT("failed to open {}: {}", path, strerror(errno)));
+ auto config_content = util::read_file<std::string>(path);
+ if (!config_content) {
+ throw core::Error(FMT("failed to read {}: {}", path, strerror(errno)));
}
- if (!found) {
- output.write(FMT("{} = {}\n", key, value));
+ std::string result;
+ result.reserve(config_content->size() + key.size() + 3 + value.size() + 1);
+ bool key_found = false;
+
+ util::ConfigReader reader(*config_content);
+ while (true) {
+ auto item_result = reader.read_next_raw_item();
+ if (!item_result) {
+ throw core::Error(FMT("failed to read {}: {}:{}",
+ path,
+ item_result.error().line_number,
+ item_result.error().message));
+ }
+
+ auto& item = *item_result;
+ if (!item) {
+ break; // EOF
+ }
+
+ if (item->key == key) {
+ // Copy everything before this entry
+ result.append(*config_content, 0, item->value_start_pos);
+ result.append(value);
+ result.append(*config_content,
+ item->value_start_pos + item->value_length);
+ key_found = true;
+ break;
+ }
}
+ if (!key_found) {
+ result = std::move(*config_content);
+ if (!result.empty() && result.back() != '\n') {
+ result += '\n';
+ }
+ result += FMT("{} = {}\n", key, value);
+ }
+
+ core::AtomicFile output(resolved_path, core::AtomicFile::Mode::text);
+ output.write(result);
output.commit();
}
}
void
-Config::set_item(const std::string& key,
+Config::set_item(const std::string_view& key,
const std::string& unexpanded_value,
const std::optional<std::string>& env_var_key,
bool negate,
break;
}
- const std::string& canonical_key = it->second.alias.value_or(key);
+ const std::string_view& canonical_key = it->second.alias.value_or(key);
const auto& [element, inserted] = m_origins.emplace(canonical_key, origin);
if (!inserted) {
element->second = origin;
#include <functional>
#include <optional>
#include <string>
+#include <string_view>
#include <unordered_map>
#include <vector>
std::unordered_map<std::string /*key*/, std::string /*origin*/> m_origins;
- void set_item(const std::string& key,
+ void set_item(const std::string_view& key,
const std::string& unexpanded_value,
const std::optional<std::string>& env_var_key,
bool negate,
assertions.cpp
bytes.cpp
clang.cpp
+ configreader.cpp
cpu.cpp
direntry.cpp
environment.cpp
--- /dev/null
+// Copyright (C) 2025 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+#include "configreader.hpp"
+
+#include <ccache/util/string.hpp>
+
+#include <cctype>
+
+namespace util {
+
+namespace {
+
+bool
+is_comment_or_blank(std::string_view line)
+{
+ std::string stripped = util::strip_whitespace(line);
+ return stripped.empty() || stripped[0] == '#';
+}
+
+} // namespace
+
+ConfigReader::ConfigReader(std::string_view config)
+ : m_config(config),
+ m_lines(split_into_views(config, "\n", Tokenizer::Mode::include_empty))
+{
+ // First pass: find all keys
+ for (size_t i = 0; i < m_lines.size(); ++i) {
+ std::string_view line = m_lines[i];
+
+ if (!is_comment_or_blank(line)) {
+ // Non-comment, non-blank line found
+ if (!line.empty() && util::is_space(line[0])) {
+ // Indented non-comment line without a preceding key
+ if (m_items.empty()) {
+ m_error = Error{i + 1, "indented key"};
+ return;
+ }
+ } else {
+ // This should be a key=value line
+ size_t eq_pos = line.find('=');
+ if (eq_pos == std::string_view::npos) {
+ // Non-indented line without "=" is an error
+ m_error = Error{i + 1, "missing equal sign"};
+ return;
+ }
+
+ std::string_view key = line.substr(0, eq_pos);
+ while (!key.empty() && util::is_space(key.back())) {
+ key.remove_suffix(1);
+ }
+
+ size_t line_start = static_cast<size_t>(line.data() - m_config.data());
+ size_t value_start = line_start + eq_pos + 1;
+ while (value_start < m_config.size()
+ && util::is_space(m_config[value_start])) {
+ ++value_start;
+ }
+
+ m_items.push_back(RawItem{i + 1, key, value_start, 0});
+ }
+ }
+ }
+
+ // Second pass: compute value lengths for each key
+ for (size_t i = 0; i < m_items.size(); ++i) {
+ auto& item = m_items[i];
+ size_t line_index = item.line_number - 1; // Convert to 0-based
+
+ // Determine the end line for this item's value
+ size_t search_end_line = (i + 1 < m_items.size())
+ ? (m_items[i + 1].line_number - 1)
+ : m_lines.size();
+
+ // Backtrack from search_end_line to skip trailing comments and blank lines
+ size_t value_end_line = search_end_line;
+ while (value_end_line > line_index + 1) {
+ if (!is_comment_or_blank(m_lines[value_end_line - 1])) {
+ // Found the last content line
+ break;
+ }
+ --value_end_line;
+ }
+
+ std::string_view last_line = m_lines[value_end_line - 1];
+ size_t last_line_start =
+ static_cast<size_t>(last_line.data() - m_config.data());
+ // If the line ends with CR (as in CRLF files split on '\n'), treat the
+ // trailing CR as part of the newline and exclude it from the value end.
+ size_t last_line_size = last_line.size();
+ if (last_line_size > 0 && last_line.back() == '\r') {
+ --last_line_size;
+ }
+ size_t value_end = last_line_start + last_line_size;
+
+ item.value_length = value_end - item.value_start_pos;
+ }
+}
+
+tl::expected<std::optional<ConfigReader::RawItem>, ConfigReader::Error>
+ConfigReader::read_next_raw_item()
+{
+ if (m_error) {
+ return tl::make_unexpected(*m_error);
+ }
+
+ if (m_current_item >= m_items.size()) {
+ return std::nullopt; // EOF
+ }
+
+ return m_items[m_current_item++];
+}
+
+tl::expected<std::optional<ConfigReader::Item>, ConfigReader::Error>
+ConfigReader::read_next_item()
+{
+ auto raw_item_result = read_next_raw_item();
+ if (!raw_item_result) {
+ return tl::make_unexpected(raw_item_result.error());
+ }
+
+ auto& raw_item = *raw_item_result;
+ if (!raw_item) {
+ return std::nullopt;
+ }
+
+ std::string_view raw_value =
+ m_config.substr(raw_item->value_start_pos, raw_item->value_length);
+
+ // Normalize the value: skip comments/blanks, join continuation lines
+ std::string normalized_value;
+ auto value_lines =
+ split_into_views(raw_value, "\n", Tokenizer::Mode::include_empty);
+
+ for (auto line : value_lines) {
+ std::string stripped = util::strip_whitespace(line);
+ if (!stripped.empty() && stripped[0] != '#') {
+ if (!normalized_value.empty()) {
+ normalized_value += ' ';
+ }
+ normalized_value += stripped;
+ }
+ }
+
+ return Item{raw_item->line_number, raw_item->key, normalized_value};
+}
+
+} // namespace util
--- /dev/null
+// Copyright (C) 2025 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+#pragma once
+
+#include <tl/expected.hpp>
+
+#include <cstddef>
+#include <functional>
+#include <map>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace util {
+
+// Reader for configuration files with support for indentation-based line
+// continuation.
+//
+// Continuation rules:
+//
+// 1. Indented lines (start with whitespace) continue the previous value.
+// 2. Comments (start with #) and blank lines are skipped during continuation.
+// 3. Non-indented non-comment lines start new entries.
+// 4. Continuation lines are stripped and joined with single spaces.
+class ConfigReader
+{
+public:
+ struct Error
+ {
+ size_t line_number;
+ std::string message;
+ };
+
+ struct RawItem
+ {
+ size_t line_number;
+ std::string_view key;
+ size_t value_start_pos; // Position in m_config where value starts
+ size_t value_length;
+ };
+
+ struct Item
+ {
+ size_t line_number;
+ std::string_view key;
+ std::string value; // Normalized: comments/blanks removed, lines joined
+ };
+
+ explicit ConfigReader(std::string_view config);
+
+ // Read the next configuration item in raw form. Returns std::nullopt at EOF.
+ tl::expected<std::optional<RawItem>, Error> read_next_raw_item();
+
+ // Read the next configuration item with normalized value. Returns
+ // std::nullopt at EOF.
+ tl::expected<std::optional<Item>, Error> read_next_item();
+
+private:
+ std::string_view m_config;
+ std::vector<std::string_view> m_lines;
+ std::vector<RawItem> m_items;
+ std::optional<Error> m_error;
+ size_t m_current_item = 0;
+};
+
+} // namespace util
test_util_bitset.cpp
test_util_bytes.cpp
test_util_clang.cpp
+ test_util_configreader.cpp
test_util_conversion.cpp
test_util_direntry.cpp
test_util_environment.cpp
"cache_dir = $USER$/${USER}/.ccache\n"
"\n"
"\n"
- " #A comment\n"
- "\t compiler = foo\n"
+ "#A comment\n"
+ "compiler = foo\n"
"compiler_check = none\n"
"compiler_type = nvcc\n"
"compression=false\n"
}
}
+TEST_CASE("Config::update_from_file, multi-line values")
+{
+ TestContext test_context;
+
+ Config config;
+
+ SUBCASE("basic continuation")
+ {
+ REQUIRE(util::write_file("ccache.conf", "path = a\n b\n c"));
+ CHECK(config.update_from_file("ccache.conf"));
+ CHECK(config.path() == "a b c");
+ }
+
+ SUBCASE("indented continuation with empty value on first line")
+ {
+ REQUIRE(util::write_file("ccache.conf", "path =\n b\n c"));
+ CHECK(config.update_from_file("ccache.conf"));
+ CHECK(config.path() == "b c");
+ }
+
+ SUBCASE("comments are not part of continuation")
+ {
+ REQUIRE(util::write_file("ccache.conf",
+ "path = a\n"
+ " b\n"
+ "# comment\n"
+ "compiler = c"));
+ CHECK(config.update_from_file("ccache.conf"));
+ CHECK(config.path() == "a b");
+ CHECK(config.compiler() == "c");
+ }
+
+ SUBCASE("comments and empty lines don't break continuation")
+ {
+ REQUIRE(util::write_file("ccache.conf",
+ "path = a\n"
+ " b\n"
+ "\n"
+ "# comment\n"
+ " compiler = c"));
+ CHECK(config.update_from_file("ccache.conf"));
+ CHECK(config.path() == "a b compiler = c");
+ CHECK(config.compiler() == "");
+ }
+
+ SUBCASE("blank lines are not part of continuation")
+ {
+ REQUIRE(util::write_file("ccache.conf",
+ "path = a\n"
+ " b\n"
+ "\n"
+ "compiler = c"));
+ CHECK(config.update_from_file("ccache.conf"));
+ CHECK(config.path() == "a b");
+ CHECK(config.compiler() == "c");
+ }
+
+ SUBCASE("hash after value does not mean comment")
+ {
+ REQUIRE(util::write_file("ccache.conf",
+ "ignore_options =\n"
+ " -Wall\n"
+ " zzz = b\n"
+ " b # not a comment\n"
+ " c"));
+ CHECK(config.update_from_file("ccache.conf"));
+ CHECK(config.ignore_options() == "-Wall zzz = b b # not a comment c");
+ }
+
+ SUBCASE("multiple indented multi-line values")
+ {
+ REQUIRE(util::write_file("ccache.conf",
+ "path = /usr/bin\n"
+ " /usr/local/bin\n"
+ "compiler = gcc\n"
+ "ignore_options = -Wall\n"
+ " -Wextra\n"
+ " -pedantic"));
+ CHECK(config.update_from_file("ccache.conf"));
+ CHECK(config.path() == "/usr/bin /usr/local/bin");
+ CHECK(config.compiler() == "gcc");
+ CHECK(config.ignore_options() == "-Wall -Wextra -pedantic");
+ }
+
+ SUBCASE("both indented and non-indented comments are skipped")
+ {
+ REQUIRE(util::write_file("ccache.conf",
+ "path =\n"
+ " a\n"
+ " b # not a comment\n"
+ "\n"
+ "# nonindented comment\n"
+ " # indented comment\n"
+ " c = d"));
+ CHECK(config.update_from_file("ccache.conf"));
+ CHECK(config.path() == "a b # not a comment c = d");
+ }
+
+ SUBCASE("tab indentation also works")
+ {
+ REQUIRE(util::write_file("ccache.conf", "path = a\n\tb\n\tc"));
+ CHECK(config.update_from_file("ccache.conf"));
+ CHECK(config.path() == "a b c");
+ }
+
+ SUBCASE("mixed spaces and tabs")
+ {
+ REQUIRE(util::write_file("ccache.conf", "path = a\n b\n\tc"));
+ CHECK(config.update_from_file("ccache.conf"));
+ CHECK(config.path() == "a b c");
+ }
+}
+
TEST_CASE("Config::update_from_environment")
{
Config config;
std::string content = *util::read_file<std::string>("ccache.conf");
CHECK(content == "# c1\npath = vanilla\n#c2\ncompiler = chocolate\n");
}
+
+ SUBCASE("comments in multi-line values are kept")
+ {
+ REQUIRE(util::write_file("ccache.conf",
+ "ignore_options = -Wall\n"
+ " -Wextra\n"
+ "# A comment\n"
+ " -pedantic\n"
+ "compiler = gcc\n"));
+ config.set_value_in_file("ccache.conf", "compiler", "clang");
+ std::string content = *util::read_file<std::string>("ccache.conf");
+ CHECK(content == "ignore_options = -Wall\n"
+ " -Wextra\n"
+ "# A comment\n"
+ " -pedantic\n"
+ "compiler = clang\n");
+ }
+
+ SUBCASE("possible to replace multi-line value")
+ {
+ REQUIRE(util::write_file("ccache.conf",
+ "ignore_options = -Wall\n"
+ " -Wextra\n"
+ "# A comment\n"
+ " -pedantic\n"
+ "compiler = gcc\n"));
+ config.set_value_in_file("ccache.conf", "ignore_options", "-Weverything");
+ std::string content = *util::read_file<std::string>("ccache.conf");
+ CHECK(content == "ignore_options = -Weverything\n"
+ "compiler = gcc\n");
+ }
}
TEST_CASE("Config::get_string_value")
--- /dev/null
+// Copyright (C) 2025 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+#include <ccache/util/configreader.hpp>
+
+#include <doctest/doctest.h>
+
+#include <ostream> // https://github.com/doctest/doctest/issues/618
+
+using util::ConfigReader;
+
+TEST_SUITE("util::ConfigReader")
+{
+ TEST_CASE("missing equal sign")
+ {
+ ConfigReader reader("key");
+ auto result = reader.read_next_item();
+ REQUIRE(!result);
+ CHECK(result.error().line_number == 1);
+ CHECK(result.error().message == "missing equal sign");
+ }
+
+ TEST_CASE("indented key")
+ {
+ ConfigReader reader(" key = value");
+ auto result = reader.read_next_item();
+ REQUIRE(!result);
+ CHECK(result.error().line_number == 1);
+ CHECK(result.error().message == "indented key");
+ }
+
+ TEST_CASE("read_next_item: simple key=value")
+ {
+ ConfigReader reader("key = value");
+ auto result = reader.read_next_item();
+ REQUIRE(result);
+ auto& item = **result;
+ CHECK(item.line_number == 1);
+ CHECK(item.key == "key");
+ CHECK(item.value == "value");
+ auto eof = reader.read_next_item();
+ REQUIRE(eof);
+ CHECK(!*eof);
+ }
+
+ TEST_CASE("read_next_item: multiple items")
+ {
+ ConfigReader reader("key1 = value1 \nkey2=value2\n");
+ auto result1 = reader.read_next_item();
+ REQUIRE(result1);
+ auto& item1 = **result1;
+ CHECK(item1.key == "key1");
+ CHECK(item1.value == "value1");
+
+ auto result2 = reader.read_next_item();
+ REQUIRE(result2);
+ auto& item2 = **result2;
+ CHECK(item2.key == "key2");
+ CHECK(item2.value == "value2");
+
+ auto eof = reader.read_next_item();
+ REQUIRE(eof);
+ CHECK(!*eof);
+ }
+
+ TEST_CASE("read_next_item: indented continuation")
+ {
+ ConfigReader reader("key = a\n b\n c");
+ auto result = reader.read_next_item();
+ REQUIRE(result);
+ auto& item = **result;
+ CHECK(item.key == "key");
+ CHECK(item.value == "a b c");
+ }
+
+ TEST_CASE("read_next_item: empty value on first line")
+ {
+ ConfigReader reader("key =\n b\n c");
+ auto result = reader.read_next_item();
+ REQUIRE(result);
+ auto& item = **result;
+ CHECK(item.key == "key");
+ CHECK(item.value == "b c");
+ }
+
+ TEST_CASE("read_next_item: comments are skipped")
+ {
+ ConfigReader reader("key = a\n b\n# comment\n c");
+ auto result = reader.read_next_item();
+ REQUIRE(result);
+ auto& item = **result;
+ CHECK(item.value == "a b c");
+ }
+
+ TEST_CASE("read_next_item: blank lines are skipped")
+ {
+ ConfigReader reader("key = a\n b\n\n c");
+ auto result = reader.read_next_item();
+ REQUIRE(result);
+ auto& item = **result;
+ CHECK(item.value == "a b c");
+ }
+
+ TEST_CASE("read_next_item: inline comments preserved")
+ {
+ ConfigReader reader("key = a # not a comment");
+ auto result = reader.read_next_item();
+ REQUIRE(result);
+ auto& item = **result;
+ CHECK(item.value == "a # not a comment");
+ }
+
+ TEST_CASE("read_next_item: different comments")
+ {
+ ConfigReader reader(
+ "world =\n"
+ " a\n"
+ " b # not a comment\n"
+ "\n"
+ "# nonindented comment\n"
+ " # indented comment\n"
+ " c = d");
+ auto result = reader.read_next_item();
+ REQUIRE(result);
+ auto& item = **result;
+ CHECK(item.key == "world");
+ CHECK(item.value == "a b # not a comment c = d");
+ }
+
+ TEST_CASE("read_next_item: leading comments are skipped")
+ {
+ ConfigReader reader("# comment\nkey = value");
+ auto result = reader.read_next_item();
+ REQUIRE(result);
+ auto& item = **result;
+ CHECK(item.line_number == 2);
+ CHECK(item.key == "key");
+ }
+
+ TEST_CASE("read_next_item: tab indentation")
+ {
+ ConfigReader reader("key = a\n\tb\n\tc");
+ auto result = reader.read_next_item();
+ REQUIRE(result);
+ auto& item = **result;
+ CHECK(item.value == "a b c");
+ }
+
+ TEST_CASE("read_next_item: empty config")
+ {
+ ConfigReader reader("");
+ auto result = reader.read_next_item();
+ REQUIRE(result);
+ CHECK(!*result);
+ }
+
+ TEST_CASE("read_next_item: only comments")
+ {
+ ConfigReader reader("# comment1\n# comment2");
+ auto result = reader.read_next_item();
+ REQUIRE(result);
+ CHECK(!*result);
+ }
+
+ TEST_CASE("read_next_item: CRLF line endings")
+ {
+ // Use CRLF line endings as might appear on Windows files
+ ConfigReader reader("key = a\r\n b\r\n# comment\r\n c\r\nother = x\r\n");
+ auto result1 = reader.read_next_item();
+ REQUIRE(result1);
+ auto& item1 = **result1;
+ CHECK(item1.key == "key");
+ CHECK(item1.value == "a b c");
+
+ auto result2 = reader.read_next_item();
+ REQUIRE(result2);
+ auto& item2 = **result2;
+ CHECK(item2.key == "other");
+ CHECK(item2.value == "x");
+ }
+
+ TEST_CASE("read_next_raw_item: simple key=value")
+ {
+ ConfigReader reader("key = value");
+ auto result = reader.read_next_raw_item();
+ REQUIRE(result);
+ auto& item = **result;
+ CHECK(item.key == "key");
+ CHECK(item.value_start_pos == 6); // After "key = "
+ CHECK(item.value_length == 5); // Length of "value"
+ }
+
+ TEST_CASE("read_next_raw_item: preserves embedded comments and blank lines")
+ {
+ std::string config = "key = a\n b\n\n# comment\n \n c\nother = x";
+ ConfigReader reader(config);
+
+ auto result1 = reader.read_next_raw_item();
+ REQUIRE(result1);
+ auto& item1 = **result1;
+ CHECK(item1.key == "key");
+ // Raw value should include "a\n b\n\n# comment\n \n c" (no trailing
+ // newline)
+ std::string raw_value =
+ config.substr(item1.value_start_pos, item1.value_length);
+ CHECK(raw_value == "a\n b\n\n# comment\n \n c");
+
+ auto result2 = reader.read_next_raw_item();
+ REQUIRE(result2);
+ auto& item2 = **result2;
+ CHECK(item2.key == "other");
+ }
+
+ TEST_CASE("read_next_raw_item: multiple items")
+ {
+ std::string config = "key1 = value1\nkey2 = value2";
+ ConfigReader reader(config);
+
+ auto result1 = reader.read_next_raw_item();
+ REQUIRE(result1);
+ auto& item1 = **result1;
+ CHECK(item1.key == "key1");
+ std::string value1 =
+ config.substr(item1.value_start_pos, item1.value_length);
+ CHECK(value1 == "value1");
+
+ auto result2 = reader.read_next_raw_item();
+ REQUIRE(result2);
+ auto& item2 = **result2;
+ CHECK(item2.key == "key2");
+ std::string value2 =
+ config.substr(item2.value_start_pos, item2.value_length);
+ CHECK(value2 == "value2");
+ }
+
+ TEST_CASE("read_next_raw_item: EOF handling")
+ {
+ ConfigReader reader("key = value");
+ auto result = reader.read_next_raw_item();
+ REQUIRE(result);
+ REQUIRE(*result);
+ auto eof = reader.read_next_raw_item();
+ REQUIRE(eof);
+ CHECK(!*eof);
+ }
+
+ TEST_CASE("read_next_raw_item: CRLF preserves raw blocks")
+ {
+ std::string config =
+ "key = a\r\n"
+ " b\r\n"
+ "\r\n"
+ "# comment\r\n"
+ " c\r\n"
+ "other = x\r\n";
+
+ ConfigReader reader(config);
+ auto res = reader.read_next_raw_item();
+ REQUIRE(res);
+ auto& raw_item = **res;
+ CHECK(raw_item.key == "key");
+
+ // Extract raw substring and verify it preserves embedded blank/comment
+ // lines (without the trailing newline)
+ std::string raw_value =
+ config.substr(raw_item.value_start_pos, raw_item.value_length);
+
+ CHECK(raw_value == "a\r\n b\r\n\r\n# comment\r\n c");
+
+ auto res2 = reader.read_next_raw_item();
+ REQUIRE(res2);
+ auto& raw_item2 = **res2;
+ CHECK(raw_item2.key == "other");
+ }
+}