Skip to content

Commit 8569f77

Browse files
committed
Replace wordexp with custom implementation on all platforms
Signed-off-by: Tin Švagelj <[email protected]>
1 parent 97b4ec7 commit 8569f77

File tree

3 files changed

+295
-72
lines changed

3 files changed

+295
-72
lines changed

src/common.cc

Lines changed: 143 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,12 @@
3939
#include <unistd.h>
4040
#include <cctype>
4141
#include <cerrno>
42+
#include <cstdint>
4243
#include <cstdio>
4344
#include <cstdlib>
4445
#include <ctime>
46+
#include <filesystem>
47+
#include <string>
4548
#include <vector>
4649

4750
#include "config.h"
@@ -141,36 +144,12 @@ double get_time() {
141144
return tv.tv_sec + (tv.tv_nsec * 1e-9);
142145
}
143146

144-
#if defined(_POSIX_C_SOURCE) && !defined(__OpenBSD__) && !defined(__HAIKU__)
145-
std::filesystem::path to_real_path(const std::string &source) {
146-
wordexp_t p;
147-
char **w;
148-
int i;
149-
std::string checked = std::string(source);
150-
std::string::size_type n = 0;
151-
while ((n = checked.find(" ", n)) != std::string::npos) {
152-
checked.replace(n, 1, "\\ ");
153-
n += 2;
154-
}
155-
const char *csource = source.c_str();
156-
if (wordexp(checked.c_str(), &p, 0) != 0) { return std::string(); }
157-
w = p.we_wordv;
158-
const char *resolved_path = strdup(w[0]);
159-
wordfree(&p);
160-
return std::filesystem::weakly_canonical(resolved_path);
161-
}
162-
#else
163-
// TODO: Use this implementation once it's finished.
164-
// `wordexp` calls shell which is inconsistent across different environments.
165-
std::filesystem::path to_real_path(const std::string &source) {
147+
std::filesystem::path to_real_path(const std::string &source, bool sensitive) {
166148
/*
167149
Wordexp (via default shell) does:
168150
- [x] tilde substitution `~`
169151
- [x] variable substitution (via `variable_substitute`)
170-
- [ ] command substitution `$(command)`
171-
- exec.cc does execution already; missing recursive descent parser for
172-
$(...) because they can be nested and mixed with self & other expressions
173-
from this list
152+
- [x] command substitution `$(command)`
174153
- [ ] [arithmetic
175154
expansion](https://www.gnu.org/software/bash/manual/html_node/Arithmetic-Expansion.html)
176155
`$((10 + 2))`
@@ -179,12 +158,17 @@ std::filesystem::path to_real_path(const std::string &source) {
179158
- [ ] [field
180159
splitting](https://www.gnu.org/software/bash/manual/html_node/Word-Splitting.html)
181160
- [ ] wildcard expansion
182-
- [ ] quote removal Extra:
183-
- canonicalization added
161+
- [-] quote removal (no need)
162+
163+
Extra:
164+
- canonicalization to mimic `realpath`
184165
*/
185166
try {
186-
std::string input = tilde_expand(source);
187-
std::string expanded = variable_substitute(input);
167+
std::string expanded = tilde_expand(source);
168+
expanded = variable_substitute(expanded);
169+
expanded = command_substitute(expanded);
170+
auto w_expanded = wildcard_expand_path(expanded);
171+
if (w_expanded.size() > 0) { expanded = w_expanded.front(); }
188172
std::filesystem::path absolute = std::filesystem::absolute(expanded);
189173
return std::filesystem::weakly_canonical(absolute);
190174
} catch (const std::filesystem::filesystem_error &e) {
@@ -193,7 +177,6 @@ std::filesystem::path to_real_path(const std::string &source) {
193177
return source;
194178
}
195179
}
196-
#endif
197180

198181
int open_fifo(const char *file, int *reported) {
199182
int fd = 0;
@@ -330,42 +313,147 @@ std::string variable_substitute(std::string s) {
330313
if (pos + 1 >= s.size()) { break; }
331314

332315
if (s[pos + 1] == '$') {
316+
// handle escaped $$
333317
s.erase(pos, 1);
334318
++pos;
319+
continue;
320+
}
321+
322+
std::string var;
323+
std::string::size_type l = 0;
324+
325+
if (isalpha(static_cast<unsigned char>(s[pos + 1])) != 0) {
326+
l = 1;
327+
while (pos + l < s.size() &&
328+
(isalnum(static_cast<unsigned char>(s[pos + l])) != 0)) {
329+
++l;
330+
}
331+
var = s.substr(pos + 1, l - 1);
332+
} else if (s[pos + 1] == '{') {
333+
l = s.find('}', pos);
334+
if (l == std::string::npos) { break; }
335+
l -= pos - 1;
336+
var = s.substr(pos + 2, l - 3);
335337
} else {
336-
std::string var;
337-
std::string::size_type l = 0;
338-
339-
if (isalpha(static_cast<unsigned char>(s[pos + 1])) != 0) {
340-
l = 1;
341-
while (pos + l < s.size() &&
342-
(isalnum(static_cast<unsigned char>(s[pos + l])) != 0)) {
343-
++l;
344-
}
345-
var = s.substr(pos + 1, l - 1);
346-
} else if (s[pos + 1] == '{') {
347-
l = s.find('}', pos);
348-
if (l == std::string::npos) { break; }
349-
l -= pos - 1;
350-
var = s.substr(pos + 2, l - 3);
351-
} else {
352-
++pos;
338+
++pos;
339+
}
340+
341+
if (l != 0u) {
342+
s.erase(pos, l);
343+
const char *val = getenv(var.c_str());
344+
if (val != nullptr) {
345+
s.insert(pos, val);
346+
pos += strlen(val);
353347
}
348+
}
349+
}
350+
351+
return s;
352+
}
353+
354+
std::string command_substitute(std::string s) {
355+
std::string::size_type pos = 0;
356+
while ((pos = s.find('$', pos)) != std::string::npos) {
357+
if (s.length() - pos - 1 < 2) { break; }
358+
359+
if (s[pos + 1] == '$') {
360+
// handle escaped $$
361+
s.erase(pos, 1);
362+
++pos;
363+
continue;
364+
}
354365

355-
if (l != 0u) {
356-
s.erase(pos, l);
357-
const char *val = getenv(var.c_str());
358-
if (val != nullptr) {
359-
s.insert(pos, val);
360-
pos += strlen(val);
366+
// must start with "$(", but not with "$(("
367+
if (s[pos + 1] != '(' || s[pos + 2] == '(') { continue; }
368+
369+
auto start = pos + 2; // exclusive (will end on closing brace)
370+
auto end = start; // exclusive (will end on closing brace)
371+
size_t open_braces = 0;
372+
373+
// handle nested commands and math braces properly
374+
while (end < s.size()) {
375+
if (s[end] == '$' && (end + 1) < s.size() && s[end + 1] == '$') {
376+
end += 2;
377+
continue;
378+
}
379+
if (s[end] == '$' && (end + 1) < s.size() && s[end + 1] == '(') {
380+
open_braces++;
381+
end += 2;
382+
if (end < s.size() && s[end] == '(') {
383+
open_braces++;
384+
end += 1;
361385
}
386+
continue;
362387
}
388+
if (s[end] == ')') {
389+
if (open_braces > 0) {
390+
open_braces -= 1;
391+
} else {
392+
break;
393+
}
394+
}
395+
end += 1;
396+
}
397+
if (end >= s.size()) {
398+
// unclosed command expression
399+
break;
400+
} else {
401+
pos = end + 1;
363402
}
364-
}
365403

404+
auto command = s.substr(start + 2, end - start - 2);
405+
406+
std::string substitution = "";
407+
// TODO: Much like previously used wordexp, this can hang. Ideally,
408+
// pid_popen should be used to allow terminating to_real_path after some
409+
// time.
410+
FILE *pipe = popen(command.c_str(), "r");
411+
if (pipe) {
412+
char buffer[128];
413+
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
414+
substitution += buffer;
415+
}
416+
pclose(pipe);
417+
} else {
418+
NORM_ERR("unable to pipe command: '%s'", command.c_str());
419+
}
420+
std::int32_t substitution_delta = substitution.length() - end - start + 1;
421+
s.replace(start, end - start + 1, substitution);
422+
pos += substitution_delta;
423+
}
366424
return s;
367425
}
368426

427+
std::vector<std::filesystem::path> wildcard_expand_path(
428+
const std::filesystem::path &path) {
429+
std::vector<std::filesystem::path> result;
430+
431+
struct path_segment_info {
432+
bool special;
433+
void *segment;
434+
};
435+
436+
std::vector<path_segment_info> current_path;
437+
for (auto segment : split(path, std::filesystem::path::preferred_separator)) {
438+
bool special_segment = false;
439+
for (std::string_view::size_type i = 0; i < segment.length(); i++) {
440+
if (segment[i] == '\\') {
441+
i++;
442+
} else if (segment[i] == '?' || segment[i] == '*') {
443+
special_segment = true;
444+
break;
445+
}
446+
}
447+
448+
if (!current_path.empty()) {
449+
current_path.push_back(std::filesystem::path::preferred_separator);
450+
}
451+
current_path.append(segment);
452+
}
453+
454+
return result;
455+
}
456+
369457
void format_seconds(char *buf, unsigned int n, long seconds) {
370458
long days;
371459
int hours, minutes;

0 commit comments

Comments
 (0)