diff --git a/src/common.cc b/src/common.cc index 0461ef471..d220be57f 100644 --- a/src/common.cc +++ b/src/common.cc @@ -39,9 +39,12 @@ #include #include #include +#include #include #include #include +#include +#include #include #include "config.h" @@ -141,31 +144,12 @@ double get_time() { return tv.tv_sec + (tv.tv_nsec * 1e-9); } -#if defined(_POSIX_C_SOURCE) && !defined(__OpenBSD__) && !defined(__HAIKU__) -std::filesystem::path to_real_path(const std::string &source) { - wordexp_t p; - char **w; - int i; - std::string checked = "\"" + source + "\""; - const char *csource = source.c_str(); - if (wordexp(checked.c_str(), &p, 0) != 0) { return std::string(); } - w = p.we_wordv; - const char *resolved_path = strdup(w[0]); - wordfree(&p); - return std::filesystem::weakly_canonical(resolved_path); -} -#else -// TODO: Use this implementation once it's finished. -// `wordexp` calls shell which is inconsistent across different environments. -std::filesystem::path to_real_path(const std::string &source) { +std::filesystem::path to_real_path(const std::string &source, bool sensitive) { /* Wordexp (via default shell) does: - [x] tilde substitution `~` - [x] variable substitution (via `variable_substitute`) - - [ ] command substitution `$(command)` - - exec.cc does execution already; missing recursive descent parser for - $(...) because they can be nested and mixed with self & other expressions - from this list + - [x] command substitution `$(command)` - [ ] [arithmetic expansion](https://www.gnu.org/software/bash/manual/html_node/Arithmetic-Expansion.html) `$((10 + 2))` @@ -174,12 +158,17 @@ std::filesystem::path to_real_path(const std::string &source) { - [ ] [field splitting](https://www.gnu.org/software/bash/manual/html_node/Word-Splitting.html) - [ ] wildcard expansion - - [ ] quote removal Extra: - - canonicalization added + - [-] quote removal (no need) + + Extra: + - canonicalization to mimic `realpath` */ try { - std::string input = tilde_expand(source); - std::string expanded = variable_substitute(input); + std::string expanded = tilde_expand(source); + expanded = variable_substitute(expanded); + expanded = command_substitute(expanded); + auto w_expanded = wildcard_expand_path(expanded); + if (w_expanded.size() > 0) { expanded = w_expanded.front(); } std::filesystem::path absolute = std::filesystem::absolute(expanded); return std::filesystem::weakly_canonical(absolute); } catch (const std::filesystem::filesystem_error &e) { @@ -188,7 +177,6 @@ std::filesystem::path to_real_path(const std::string &source) { return source; } } -#endif int open_fifo(const char *file, int *reported) { int fd = 0; @@ -325,42 +313,147 @@ std::string variable_substitute(std::string s) { if (pos + 1 >= s.size()) { break; } if (s[pos + 1] == '$') { + // handle escaped $$ s.erase(pos, 1); ++pos; + continue; + } + + std::string var; + std::string::size_type l = 0; + + if (isalpha(static_cast(s[pos + 1])) != 0) { + l = 1; + while (pos + l < s.size() && + (isalnum(static_cast(s[pos + l])) != 0)) { + ++l; + } + var = s.substr(pos + 1, l - 1); + } else if (s[pos + 1] == '{') { + l = s.find('}', pos); + if (l == std::string::npos) { break; } + l -= pos - 1; + var = s.substr(pos + 2, l - 3); } else { - std::string var; - std::string::size_type l = 0; - - if (isalpha(static_cast(s[pos + 1])) != 0) { - l = 1; - while (pos + l < s.size() && - (isalnum(static_cast(s[pos + l])) != 0)) { - ++l; - } - var = s.substr(pos + 1, l - 1); - } else if (s[pos + 1] == '{') { - l = s.find('}', pos); - if (l == std::string::npos) { break; } - l -= pos - 1; - var = s.substr(pos + 2, l - 3); - } else { - ++pos; + ++pos; + } + + if (l != 0u) { + s.erase(pos, l); + const char *val = getenv(var.c_str()); + if (val != nullptr) { + s.insert(pos, val); + pos += strlen(val); } + } + } + + return s; +} + +std::string command_substitute(std::string s) { + std::string::size_type pos = 0; + while ((pos = s.find('$', pos)) != std::string::npos) { + if (s.length() - pos - 1 < 2) { break; } + + if (s[pos + 1] == '$') { + // handle escaped $$ + s.erase(pos, 1); + ++pos; + continue; + } - if (l != 0u) { - s.erase(pos, l); - const char *val = getenv(var.c_str()); - if (val != nullptr) { - s.insert(pos, val); - pos += strlen(val); + // must start with "$(", but not with "$((" + if (s[pos + 1] != '(' || s[pos + 2] == '(') { continue; } + + auto start = pos + 2; // exclusive (will end on closing brace) + auto end = start; // exclusive (will end on closing brace) + size_t open_braces = 0; + + // handle nested commands and math braces properly + while (end < s.size()) { + if (s[end] == '$' && (end + 1) < s.size() && s[end + 1] == '$') { + end += 2; + continue; + } + if (s[end] == '$' && (end + 1) < s.size() && s[end + 1] == '(') { + open_braces++; + end += 2; + if (end < s.size() && s[end] == '(') { + open_braces++; + end += 1; } + continue; } + if (s[end] == ')') { + if (open_braces > 0) { + open_braces -= 1; + } else { + break; + } + } + end += 1; + } + if (end >= s.size()) { + // unclosed command expression + break; + } else { + pos = end + 1; } - } + auto command = s.substr(start + 2, end - start - 2); + + std::string substitution = ""; + // TODO: Much like previously used wordexp, this can hang. Ideally, + // pid_popen should be used to allow terminating to_real_path after some + // time. + FILE *pipe = popen(command.c_str(), "r"); + if (pipe) { + char buffer[128]; + while (fgets(buffer, sizeof(buffer), pipe) != nullptr) { + substitution += buffer; + } + pclose(pipe); + } else { + NORM_ERR("unable to pipe command: '%s'", command.c_str()); + } + std::int32_t substitution_delta = substitution.length() - end - start + 1; + s.replace(start, end - start + 1, substitution); + pos += substitution_delta; + } return s; } +std::vector wildcard_expand_path( + const std::filesystem::path &path) { + std::vector result; + + struct path_segment_info { + bool special; + void *segment; + }; + + std::vector current_path; + for (auto segment : split(path, std::filesystem::path::preferred_separator)) { + bool special_segment = false; + for (std::string_view::size_type i = 0; i < segment.length(); i++) { + if (segment[i] == '\\') { + i++; + } else if (segment[i] == '?' || segment[i] == '*') { + special_segment = true; + break; + } + } + + if (!current_path.empty()) { + current_path.push_back(std::filesystem::path::preferred_separator); + } + current_path.append(segment); + } + + return result; +} + void format_seconds(char *buf, unsigned int n, long seconds) { long days; int hours, minutes; diff --git a/src/common.h b/src/common.h index 506407751..48a7825a3 100644 --- a/src/common.h +++ b/src/common.h @@ -22,6 +22,10 @@ * */ +/// This file contains functions that are useful to most target platforms and +/// behave similarly across them. +/// It also has some non-specific functions that extend C++ standard library. + #ifndef _COMMON_H #define _COMMON_H @@ -30,11 +34,16 @@ #include #include #include +#include #include #include +#include +#include +#include -#include "lua/setting.hh" #include "content/text_object.h" +#include "logging.h" +#include "lua/setting.hh" char *readfile(const char *filename, int *total_read, char showerror); @@ -61,13 +70,133 @@ struct process *get_first_process(void); void get_cpu_count(void); double get_time(void); +/// @brief Iterator that handles interation over `D` delimited segments of a +/// string. +/// +/// Iterated sequence can be any type that can be turned into std::string_view. +/// `D` can be one of many delimiter elements - a char or a substring. +/// +/// If `Yield` is `std::string_view` string will not be copied, but +/// `std::string_view` isn't null terminated. If `Yield` is `std::string` then +/// the string will be copied and will be null terminated (compatible with C +/// APIs). +template +class SplitIterator { + std::string_view base; + D delimiter; + std::string_view::size_type delimiter_length; + std::string_view::size_type pos; + std::string_view item; + + public: + using iterator_category = std::forward_iterator_tag; + using value_type = Yield; + + SplitIterator(std::string_view str, D delimiter, size_t start = 0) + : base(str), delimiter(delimiter), pos(start) { + // Precompute delimiter length based on `D` type. + if constexpr (std::is_same_v) { + delimiter_length = 1; + } else { + delimiter_length = + static_cast(delimiter.size()); + if (delimiter_length == 0) { + CRIT_ERR("SplitIterator provided with an empty delimiter for string %s", + base); + } + } + ++(*this); // Load first entry + } + + Yield operator*() const { + if constexpr (std::is_same_v) { + return item; + } else { + return Yield(item); + } + } + + inline SplitIterator &operator++() { return next(); } + inline SplitIterator &operator++(int) { return next(); } + + bool operator==(const SplitIterator &other) const { + return base == other.base && pos == other.pos; + } + + bool operator!=(const SplitIterator &other) const { + return base != other.base || pos != other.pos; + } + + private: + SplitIterator &next() { + if (pos == std::string_view::npos) { return *this; } + + size_t next = base.find(delimiter, pos); + if (next == std::string::npos) { + item = base.substr(pos); + pos = std::string_view::npos; + } else { + item = base.substr(pos, next - pos); + pos = next + delimiter_length; + } + return *this; + } +}; + +/// SplitIterator helper class, see SplitIterator for details. +template +class SplitIterable { + std::string_view base; + D delimiter; + + public: + SplitIterable(std::string_view base, D delim) + : base(base), delimiter(delim) {} + + auto begin() const { return SplitIterator(base, delimiter); } + auto end() const { + return SplitIterator(base, delimiter, std::string_view::npos); + } +}; + +/// Splits a string like type `S` with a delimiter `D`, producing entries of +/// type `Yield` (`std::string_view` by default). +template +inline SplitIterable split(const S &string, const D &delimiter) { + return SplitIterable(std::string_view(string), delimiter); +} + +/// Splits a string like type `S` with a delimiter `D`, allocating a +/// `std::vector` with entries of type `Yield`. +template +inline std::vector split_to_vec(const S &string, const D &delimiter) { + std::vector result; + for (const auto yield : split(string, delimiter)) { + result.push_back(yield); + } + return result; +} + /// @brief Handles environment variable expansion in paths and canonicalization. /// +/// This is a simplified reimplementation of `wordexp` function that's cross +/// platform. +/// Path expansion is done in following stages: +/// - Tilde expansion (e.g. `~/file` -> `/home/user/file`) +/// - Variable substitution (e.g. `$XDG_CONFIG_DIRS/file` -> `/etc/xdg/file`) +/// - Command substitution (e.g. `path/$(echo "part")` -> `path/part`) +/// - Wildcard expansion (e.g. `/some/**/path` -> `/some/deeply/nested/path`) +/// +/// In case wildcard expansion produces multiple matches, first one will be +/// returned, but ordering depends on directory listing order (of filesystem +// and/or OS). +/// /// Examples: /// - `~/conky` -> `/home/conky_user/conky` /// - `$HOME/conky` -> `/home/conky_user/conky` /// - `$HOME/a/b/../c/../../conky` -> `/home/conky_user/conky` std::filesystem::path to_real_path(const std::string &source); + FILE *open_file(const char *file, int *reported); int open_fifo(const char *file, int *reported); @@ -81,8 +210,27 @@ std::optional user_home(const std::string &username); std::optional user_home(); /// Performs tilde expansion on a path-like string and returns the result. std::string tilde_expand(const std::string &unexpanded); -/// Performs variable substitution on a string and returns the result. +/// @brief Performs variable substitution on a string and returns the result. +/// +/// Example: +/// - `$HOME/.config/conky` -> `/home/user/.config/conky` std::string variable_substitute(std::string s); +/// @brief Performs command substitution on a string and returns the result. +/// +/// Examples: +/// - `/some/$(echo "path")` -> `/some/path` +/// - `/nested/$(echo $((2 + 3)))` -> `/nested/5` +std::string command_substitute(std::string s); +/// @brief Performs wildcard expansion of a path. +/// +/// This function will +/// +/// Following characters are supported: +/// - ? (question mark) - a single character +/// - * (asterisk) - zero or more characters +/// - ** (double asterisk) - zero or more subpaths +std::vector wildcard_expand_path( + const std::filesystem::path &path); void format_seconds(char *buf, unsigned int n, long seconds); void format_seconds_short(char *buf, unsigned int n, long seconds); diff --git a/src/conky.cc b/src/conky.cc index 5a5397808..8708543c0 100644 --- a/src/conky.cc +++ b/src/conky.cc @@ -1680,24 +1680,11 @@ void update_text() { int inotify_fd = -1; #endif -template -void split(const std::string &s, char delim, Out result) { - std::stringstream ss(s); - std::string item; - while (std::getline(ss, item, delim)) { *(result++) = item; } -} -std::vector split(const std::string &s, char delim) { - std::vector elems; - split(s, delim, std::back_inserter(elems)); - return elems; -} - bool is_on_battery() { // checks if at least one battery specified in // "detect_battery" is discharging char buf[64]; - std::vector b_items = split(detect_battery.get(*state), ','); - - for (auto const &value : b_items) { + for (auto const &value : + split(detect_battery.get(*state), ',')) { get_battery_short_status(buf, 64, value.c_str()); if (buf[0] == 'D') { return true; } }