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
198181int 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+
369457void format_seconds (char *buf, unsigned int n, long seconds) {
370458 long days;
371459 int hours, minutes;
0 commit comments