shell-cmd v1.2
Recursively find files matching a regex and execute a shell command for each match.
Loading...
Searching...
No Matches
cmd.cpp
Go to the documentation of this file.
1/**
2 * @file cmd.cpp
3 * @brief shell-cmd v1.3 — Recursively find files matching a regex and execute a shell command for each match.
4 * @details Walks a directory tree using std::filesystem, applies metadata filters (size, time,
5 * permissions, ownership, type), substitutes placeholders in a command template, and
6 * executes the resulting command for every matched entry. Supports parallel execution,
7 * exclude patterns (regex or glob via @c -i / @c --glob-exclude), confirm mode,
8 * stop-on-error, list-all mode (@c -l / @c --list-all) which collects all matched
9 * paths and runs the command once with @c %%0 expanded to the full list, and expression
10 * filters (@c -f / @c --expr) which allow combining @c glob(), @c regex(), and
11 * @c regex_match() predicates with boolean operators @c and, @c or, and @c not.
12 * @see https://lostsidedead.biz
13 * @license GNU GPL v3
14 */
15
16#include "argz.hpp"
17#include <chrono>
18#include <cstdlib>
19#include <filesystem>
20#include <format>
21#include <grp.h>
22#include <iostream>
23#include <pwd.h>
24#include <regex>
25#include <signal.h>
26#include <span>
27#include <string>
28#include <sys/stat.h>
29#include <sys/wait.h>
30#include <unistd.h>
31#include <vector>
32
33namespace fs = std::filesystem;
34
35/**
36 * @brief Check whether to use color output on the given file descriptor.
37 * @details Respects the NO_COLOR environment variable convention (https://no-color.org/).
38 * @param fd File descriptor to check (1 = stdout, 2 = stderr).
39 * @return true if the fd is a terminal and NO_COLOR is not set.
40 */
41static bool use_color(int fd) {
42 if (std::getenv("NO_COLOR") != nullptr)
43 return false;
44 return isatty(fd) != 0;
45}
46
47/**
48 * @brief Print a colored error message to stderr.
49 * @details Prefixes the message with "Error: " (bold red when color is enabled).
50 * @param msg The error message to print.
51 */
52static void print_error(const std::string &msg) {
53 if (use_color(2))
54 std::cerr << std::format("\x1b[1;31mError:\x1b[0m {}\n", msg);
55 else
56 std::cerr << std::format("Error: {}\n", msg);
57}
58
59/// @brief Comparison operator for size and time filters.
60enum class CmpOp {
64};
65
66/// @brief Holds a parsed size filter with comparison operator and byte threshold.
67struct SizeFilter {
68 bool active = false; ///< Whether this filter is enabled.
69 CmpOp op = CmpOp::EQ; ///< Comparison direction (equal, less-than, greater-than).
70 uintmax_t bytes = 0; ///< Size threshold in bytes.
71};
72
73/// @brief Holds a parsed modification-time filter with comparison operator and day count.
74struct TimeFilter {
75 bool active = false; ///< Whether this filter is enabled.
76 CmpOp op = CmpOp::EQ; ///< Comparison direction.
77 int days = 0; ///< Age threshold in days.
78};
79
84
85/// @brief Aggregates all runtime options parsed from the command line.
86struct Options {
87 bool dry_run = false; ///< Print commands without executing.
88 bool verbose = false; ///< Print commands before executing.
89 bool hidden = false; ///< Include hidden (dot) files/directories.
90 int max_depth = -1; ///< Max recursion depth (-1 = unlimited).
91 SizeFilter size_filter; ///< Optional size filter.
92 TimeFilter mtime_filter; ///< Optional modification-time filter.
93 std::string perm_filter; ///< Octal permission string, e.g. "755".
94 std::string user_filter; ///< Owner username filter.
95 std::string group_filter; ///< Group name filter.
96 char type_filter = 0; ///< Type filter: 'f' file, 'd' directory, 'l' symlink.
97 std::string exclude_pattern; ///< Regex (or glob, see glob_exclude) pattern to exclude files/dirs.
98 bool stop_on_error = false; ///< Halt on first command failure.
99 bool confirm = false; ///< Prompt for confirmation before each command.
100 int jobs = 1; ///< Number of parallel jobs (1 = sequential).
101 std::string shell = "/bin/bash"; ///< Shell to use for command execution.
102 std::string shell_name = "bash"; ///< Shell argv[0] name.
103 bool collect_all = false; ///< If true (via -l/--list-all), collect all matched file paths and run one command with a combined argument list.
105 bool glob = false; ///< If true, treat search pattern as a glob instead of regex.
106 bool glob_exclude = false; ///< If true (via -i/--glob-exclude), treat exclude pattern as a glob instead of regex.
107 std::string expr_str; ///< Expression filter string from --expr.
108};
109
110static Options opts; ///< Global runtime options.
111
112/// @brief Tracks execution statistics printed in the summary.
113struct Stats {
114 int files_matched = 0; ///< Number of entries that matched all filters.
115 int commands_run = 0; ///< Number of commands executed (or printed in dry-run).
116 int commands_failed = 0; ///< Number of commands that returned non-zero.
117};
118
119static Stats stats; ///< Global execution statistics.
120static std::vector<pid_t> child_pids; ///< PIDs of outstanding child processes (parallel mode).
121static bool stop_requested = false; ///< Set to true when stop-on-error triggers.
122static volatile sig_atomic_t interrupted = 0; ///< Set to 1 by SIGINT handler (Ctrl+C).
123
124/**
125 * @brief Signal handler for SIGINT (Ctrl+C).
126 * @details Sets the interrupted flag so the main loop exits cleanly.
127 */
128static void sigint_handler(int /*sig*/) {
129 interrupted = 1;
130}
131
132std::string glob_to_regex(const std::string &glob);
133SizeFilter parse_size_filter(const std::string &s);
134TimeFilter parse_time_filter(const std::string &s);
135bool matches_filters(const fs::directory_entry &entry);
136bool proc_cmd(const std::string &cmd, std::span<const std::string> text, std::string file_string = "");
137void wait_for_slot();
138void wait_all();
139std::string replace_string(std::string orig, const std::string &with, const std::string &rep);
140void fill_list(const fs::path &path, const std::string &cmd, const std::string &regex_str, std::vector<std::string> &args, std::vector<std::string> &files, int depth);
141std::string join(std::vector<std::string> &v);
142void add_directory(const fs::path &path, const std::string &cmd, const std::string &regex_str, std::vector<std::string> &args, int depth);
143int System(const std::string &command);
144
145/**
146 * @brief Convert a glob pattern to an equivalent regex string.
147 * @details Escapes regex-special characters and translates glob wildcards:
148 * '*' becomes '.*', '?' becomes '.', character class brackets '[...]'
149 * are passed through with '!' or '^' mapped to '^' for negation,
150 * and all other special characters are escaped with a backslash.
151 * The result is anchored with '^' and '$'.
152 *
153 * Used by @c --glob to convert the search pattern to regex, and by
154 * @c --glob-exclude (@c -i) to convert the exclude pattern to regex.
155 * @param glob The glob pattern string, e.g. "*.cpp", "test?", "[!a-z]*".
156 * @return The equivalent anchored regex string, e.g. "^.*\.cpp$".
157 */
158std::string glob_to_regex(const std::string &glob) {
159 std::string result;
160 result += '^';
161
162 bool in_class = false;
163
164 for (size_t i = 0; i < glob.size(); ++i) {
165 char c = glob[i];
166
167 if (in_class) {
168 if (c == ']') {
169 in_class = false;
170 result += ']';
171 } else if (c == '\\') {
172 result += "\\\\";
173 } else {
174 result += c;
175 }
176 continue;
177 }
178
179 switch (c) {
180 case '*':
181 result += ".*";
182 break;
183 case '?':
184 result += '.';
185 break;
186 case '[':
187 in_class = true;
188 result += '[';
189 if (i + 1 < glob.size() && (glob[i + 1] == '!' || glob[i + 1] == '^')) {
190 result += '^';
191 ++i;
192 }
193 break;
194 case '.':
195 case '\\':
196 case '+':
197 case '^':
198 case '$':
199 case '|':
200 case '(':
201 case ')':
202 case '{':
203 case '}':
204 result += '\\';
205 result += c;
206 break;
207 default:
208 result += c;
209 break;
210 }
211 }
212
213 if (in_class)
214 result += '\\';
215
216 result += '$';
217 return result;
218}
219
220// --- Expression filter (--expr) ------------------------------------------------
221
222/// @brief Node types for the expression filter AST.
224
225/// @brief AST node for expression-based file matching.
226struct ExprNode {
228 std::regex compiled; ///< Pre-compiled regex (leaf nodes only).
229 std::unique_ptr<ExprNode> left; ///< Left child (AND/OR) or sole child (NOT).
230 std::unique_ptr<ExprNode> right; ///< Right child (AND/OR only).
231
232 /// @brief Evaluate this expression node against a file path.
233 bool evaluate(const std::string &path) const {
234 switch (type) {
235 case ExprType::GLOB:
236 return std::regex_search(path, compiled);
238 return std::regex_search(path, compiled);
240 return std::regex_match(path, compiled);
241 case ExprType::AND:
242 return left->evaluate(path) && right->evaluate(path);
243 case ExprType::OR:
244 return left->evaluate(path) || right->evaluate(path);
245 case ExprType::NOT:
246 return !left->evaluate(path);
247 }
248 return false;
249 }
250};
251
252/// @brief Token produced by the expression tokenizer.
253struct ExprToken {
255 std::string value;
256};
257
258/// @brief Tokenizer for expression filter strings.
260 const std::string &src;
261 size_t pos = 0;
262 void skip_ws() {
263 while (pos < src.size() && std::isspace(static_cast<unsigned char>(src[pos])))
264 ++pos;
265 }
266public:
267 explicit ExprTokenizer(const std::string &s) : src(s) {}
269 skip_ws();
270 if (pos >= src.size())
271 return {ExprToken::END, ""};
272 char c = src[pos];
273 if (c == '(') { ++pos; return {ExprToken::LPAREN, "("}; }
274 if (c == ')') { ++pos; return {ExprToken::RPAREN, ")"}; }
275 if (c == '"' || c == '\'') {
276 char q = c;
277 ++pos;
278 std::string val;
279 while (pos < src.size() && src[pos] != q) {
280 if (src[pos] == '\\' && pos + 1 < src.size()) {
281 ++pos;
282 val += src[pos];
283 } else {
284 val += src[pos];
285 }
286 ++pos;
287 }
288 if (pos < src.size())
289 ++pos;
290 return {ExprToken::STRING, val};
291 }
292 if (std::isalpha(static_cast<unsigned char>(c)) || c == '_') {
293 std::string val;
294 while (pos < src.size() &&
295 (std::isalnum(static_cast<unsigned char>(src[pos])) || src[pos] == '_'))
296 val += src[pos++];
297 return {ExprToken::IDENT, val};
298 }
299 throw std::runtime_error(
300 std::format("unexpected character '{}' in expression at position {}", c, pos));
301 }
302};
303
304/// @brief Recursive-descent parser for expression filter strings.
305///
306/// Grammar:
307/// expr := or_expr
308/// or_expr := and_expr ("or" and_expr)*
309/// and_expr := not_expr ("and" not_expr)*
310/// not_expr := "not" not_expr | primary
311/// primary := function "(" STRING ")" | "(" expr ")"
312/// function := "glob" | "regex" | "regex_search" | "regex_match"
316 void advance() { cur = tok.next(); }
317 void expect(ExprToken::Type t, const std::string &desc) {
318 if (cur.type != t)
319 throw std::runtime_error(std::format(
320 "expected {} in expression, got '{}'", desc,
321 cur.value.empty() ? "end" : cur.value));
322 advance();
323 }
324 std::unique_ptr<ExprNode> parse_primary() {
325 if (cur.type == ExprToken::LPAREN) {
326 advance();
327 auto node = parse_or();
329 return node;
330 }
331 if (cur.type != ExprToken::IDENT)
332 throw std::runtime_error(std::format(
333 "unexpected token '{}' in expression",
334 cur.value.empty() ? "end" : cur.value));
335 std::string name = cur.value;
336 ExprType ft;
337 if (name == "glob")
338 ft = ExprType::GLOB;
339 else if (name == "regex" || name == "regex_search")
341 else if (name == "regex_match")
343 else
344 throw std::runtime_error(
345 std::format("unknown function '{}' in expression", name));
346 advance();
347 expect(ExprToken::LPAREN, "'(' after function name");
348 if (cur.type != ExprToken::STRING)
349 throw std::runtime_error(
350 "expected quoted string as function argument");
351 std::string pattern = cur.value;
352 advance();
354 auto node = std::make_unique<ExprNode>();
355 node->type = ft;
356 node->compiled = std::regex(
357 ft == ExprType::GLOB ? glob_to_regex(pattern) : pattern,
358 std::regex::ECMAScript);
359 return node;
360 }
361 std::unique_ptr<ExprNode> parse_not() {
362 if (cur.type == ExprToken::IDENT && cur.value == "not") {
363 advance();
364 auto child = parse_not();
365 auto node = std::make_unique<ExprNode>();
366 node->type = ExprType::NOT;
367 node->left = std::move(child);
368 return node;
369 }
370 return parse_primary();
371 }
372 std::unique_ptr<ExprNode> parse_and() {
373 auto left = parse_not();
374 while (cur.type == ExprToken::IDENT && cur.value == "and") {
375 advance();
376 auto right = parse_not();
377 auto node = std::make_unique<ExprNode>();
378 node->type = ExprType::AND;
379 node->left = std::move(left);
380 node->right = std::move(right);
381 left = std::move(node);
382 }
383 return left;
384 }
385 std::unique_ptr<ExprNode> parse_or() {
386 auto left = parse_and();
387 while (cur.type == ExprToken::IDENT && cur.value == "or") {
388 advance();
389 auto right = parse_and();
390 auto node = std::make_unique<ExprNode>();
391 node->type = ExprType::OR;
392 node->left = std::move(left);
393 node->right = std::move(right);
394 left = std::move(node);
395 }
396 return left;
397 }
398public:
399 explicit ExprParser(const std::string &s) : tok(s) {}
400 std::unique_ptr<ExprNode> parse() {
401 advance();
402 auto root = parse_or();
403 if (cur.type != ExprToken::END)
404 throw std::runtime_error(
405 "unexpected content after expression");
406 return root;
407 }
408};
409
410static std::unique_ptr<ExprNode> expr_root; ///< Parsed expression tree (set when --expr is used).
411
412/// @brief Check whether a path matches the active search pattern or expression.
413static bool entry_matches_path(const std::string &fullpath, const std::string &regex_str) {
414 if (expr_root)
415 return expr_root->evaluate(fullpath);
416 std::regex ex(regex_str, std::regex::ECMAScript);
417 if (opts.mode == RegExMode::REGEX_SEARCH)
418 return std::regex_search(fullpath, ex);
419 return std::regex_match(fullpath, ex);
420}
421
422/**
423 * @brief Parse a size filter string into a SizeFilter.
424 * @param s Filter string, e.g. "+10M", "-1K", "4096".
425 * Prefix '+' means greater-than, '-' means less-than, no prefix means exact.
426 * Suffix K/M/G multiplies by 1024/1048576/1073741824.
427 * @return A populated SizeFilter with active=true.
428 */
429SizeFilter parse_size_filter(const std::string &s) {
430 SizeFilter f;
431 f.active = true;
432 std::string val = s;
433 if (val[0] == '+') {
434 f.op = CmpOp::GT;
435 val = val.substr(1);
436 } else if (val[0] == '-') {
437 f.op = CmpOp::LT;
438 val = val.substr(1);
439 } else {
440 f.op = CmpOp::EQ;
441 }
442 uintmax_t multiplier = 1;
443 char suffix = val.back();
444 if (suffix == 'K' || suffix == 'k') {
445 multiplier = 1024;
446 val.pop_back();
447 } else if (suffix == 'M' || suffix == 'm') {
448 multiplier = 1024 * 1024;
449 val.pop_back();
450 } else if (suffix == 'G' || suffix == 'g') {
451 multiplier = 1024ULL * 1024 * 1024;
452 val.pop_back();
453 }
454 f.bytes = std::stoull(val) * multiplier;
455 return f;
456}
457
458/**
459 * @brief Parse a time filter string into a TimeFilter.
460 * @param s Filter string, e.g. "+7", "-1", "3".
461 * Prefix '+' means older-than, '-' means newer-than, no prefix means exact.
462 * @return A populated TimeFilter with active=true.
463 */
464TimeFilter parse_time_filter(const std::string &s) {
465 TimeFilter f;
466 f.active = true;
467 std::string val = s;
468 if (val[0] == '+') {
469 f.op = CmpOp::GT;
470 val = val.substr(1);
471 } else if (val[0] == '-') {
472 f.op = CmpOp::LT;
473 val = val.substr(1);
474 } else {
475 f.op = CmpOp::EQ;
476 }
477 f.days = std::stoi(val);
478 return f;
479}
480
481/**
482 * @brief Test a directory entry against all active metadata filters.
483 * @param entry The filesystem directory entry to check.
484 * @return true if the entry passes all active filters, false otherwise.
485 */
486bool matches_filters(const fs::directory_entry &entry) {
487 std::error_code ec;
488
489 // Type filter
490 if (opts.type_filter != 0) {
491 switch (opts.type_filter) {
492 case 'f':
493 if (!entry.is_regular_file(ec))
494 return false;
495 break;
496 case 'd':
497 if (!entry.is_directory(ec))
498 return false;
499 break;
500 case 'l':
501 if (!entry.is_symlink(ec))
502 return false;
503 break;
504 }
505 }
506
507 // Size filter (only meaningful for regular files)
508 if (opts.size_filter.active) {
509 if (!entry.is_regular_file(ec))
510 return false;
511 auto sz = entry.file_size(ec);
512 if (ec)
513 return false;
514 switch (opts.size_filter.op) {
515 case CmpOp::GT:
516 if (sz <= opts.size_filter.bytes)
517 return false;
518 break;
519 case CmpOp::LT:
520 if (sz >= opts.size_filter.bytes)
521 return false;
522 break;
523 case CmpOp::EQ:
524 if (sz != opts.size_filter.bytes)
525 return false;
526 break;
527 }
528 }
529
530 // Modification time filter
531 if (opts.mtime_filter.active) {
532 auto ftime = entry.last_write_time(ec);
533 if (ec)
534 return false;
535 auto sctp = std::chrono::clock_cast<std::chrono::system_clock>(ftime);
536 auto now = std::chrono::system_clock::now();
537 auto age = std::chrono::duration_cast<std::chrono::hours>(now - sctp).count() / 24;
538 switch (opts.mtime_filter.op) {
539 case CmpOp::GT:
540 if (age <= opts.mtime_filter.days)
541 return false;
542 break;
543 case CmpOp::LT:
544 if (age >= opts.mtime_filter.days)
545 return false;
546 break;
547 case CmpOp::EQ:
548 if (age != opts.mtime_filter.days)
549 return false;
550 break;
551 }
552 }
553
554 // Permission filter (octal comparison)
555 if (!opts.perm_filter.empty()) {
556 struct stat st;
557 if (stat(entry.path().c_str(), &st) != 0)
558 return false;
559 auto mode = st.st_mode & 07777;
560 auto target = static_cast<mode_t>(std::stoul(opts.perm_filter, nullptr, 8));
561 if (mode != target)
562 return false;
563 }
564
565 // User filter
566 if (!opts.user_filter.empty()) {
567 struct stat st;
568 if (stat(entry.path().c_str(), &st) != 0)
569 return false;
570 struct passwd *pw = getpwuid(st.st_uid);
571 if (!pw || opts.user_filter != pw->pw_name)
572 return false;
573 }
574
575 // Group filter
576 if (!opts.group_filter.empty()) {
577 struct stat st;
578 if (stat(entry.path().c_str(), &st) != 0)
579 return false;
580 struct group *gr = getgrgid(st.st_gid);
581 if (!gr || opts.group_filter != gr->gr_name)
582 return false;
583 }
584
585 return true;
586}
587
588/**
589 * @brief Replace all occurrences of a substring within a string.
590 * @param orig The original string.
591 * @param with The substring to search for.
592 * @param rep The replacement text.
593 * @return A new string with all occurrences replaced.
594 */
595std::string replace_string(std::string orig, const std::string &with, const std::string &rep) {
596 size_t pos = 0;
597 while ((pos = orig.find(with, pos)) != std::string::npos) {
598 orig.replace(pos, with.length(), rep);
599 pos += rep.length();
600 }
601 return orig;
602}
603
604/**
605 * @brief Recursively collect all file paths matching a regex and metadata filters.
606 * @details Used by the @c -l / @c --list-all mode. Walks the directory tree in the
607 * same way as add_directory(), but instead of executing a command for each
608 * match it appends the full path to @p files. After the traversal the caller
609 * joins the collected paths and passes them to proc_cmd() as the @c file_string
610 * argument so that @c %%0 expands to the entire list.
611 * @param path The directory to scan.
612 * @param cmd The command template (unused during collection; kept for signature symmetry).
613 * @param regex_str ECMAScript regex matched against each entry's full path.
614 * @param args Mutable argument vector forwarded from main().
615 * @param files [out] Vector that accumulates the full paths of all matched entries.
616 * @param depth Current recursion depth (0 at the root call).
617 */
618void fill_list(const fs::path &path, const std::string &cmd, const std::string &regex_str, std::vector<std::string> &args, std::vector<std::string> &files, int depth) {
619 if (opts.max_depth >= 0 && depth > opts.max_depth)
620 return;
622 return;
623
624 std::error_code ec;
625 auto dir = fs::directory_iterator(path, fs::directory_options::skip_permission_denied, ec);
626 if (ec) {
627 print_error(std::format("could not open directory: {}", path.string()));
628 exit(EXIT_FAILURE);
629 }
630
631 for (const auto &entry : dir) {
633 return;
634 auto filename = entry.path().filename().string();
635 if (!opts.hidden && filename.starts_with('.'))
636 continue;
637
638 // Exclude pattern check
639 if (!opts.exclude_pattern.empty()) {
640 std::regex excl(opts.exclude_pattern, std::regex::ECMAScript);
641 if(opts.mode == RegExMode::REGEX_SEARCH) {
642 if (std::regex_search(filename, excl))
643 continue;
644 } else if(opts.mode == RegExMode::REGEX_MATCH) {
645 if(std::regex_match(filename,excl))
646 continue;
647 }
648 }
649
650 if (entry.is_directory(ec)) {
651 if (opts.type_filter == 'd') {
652 auto fullpath = entry.path().string();
653 if (entry_matches_path(fullpath, regex_str) && matches_filters(entry)) {
654 stats.files_matched++;
655 return;
656 }
657 }
658 fill_list(entry.path(), cmd, regex_str, args, files, depth + 1);
659 } else if (entry.is_symlink(ec) && opts.type_filter == 'l') {
660 auto fullpath = entry.path().string();
661 if (entry_matches_path(fullpath, regex_str) && matches_filters(entry)) {
662 stats.files_matched++;
663 files.push_back(fullpath);
664 }
665 } else if (entry.is_regular_file(ec) || (entry.is_symlink(ec) && opts.type_filter == 0)) {
666 auto fullpath = entry.path().string();
667 if (entry_matches_path(fullpath, regex_str) && matches_filters(entry)) {
668 stats.files_matched++;
669 files.push_back(fullpath);
670 }
671 }
672 }
673}
674
675/**
676 * @brief Recursively walk a directory, match entries against a regex and filters, and run commands.
677 * @param path The directory to scan.
678 * @param cmd The command template containing placeholders.
679 * @param regex_str ECMAScript regex matched against each entry's full path.
680 * @param args Mutable argument vector; args[0] is overwritten with the matched path.
681 * @param depth Current recursion depth (0 at the root call).
682 */
683void add_directory(const fs::path &path, const std::string &cmd, const std::string &regex_str, std::vector<std::string> &args, int depth) {
684 if (opts.max_depth >= 0 && depth > opts.max_depth)
685 return;
687 return;
688
689 std::error_code ec;
690 auto dir = fs::directory_iterator(path, fs::directory_options::skip_permission_denied, ec);
691 if (ec) {
692 print_error(std::format("could not open directory: {}", path.string()));
693 exit(EXIT_FAILURE);
694 }
695
696 for (const auto &entry : dir) {
698 return;
699 auto filename = entry.path().filename().string();
700 if (!opts.hidden && filename.starts_with('.'))
701 continue;
702
703 // Exclude pattern check
704 if (!opts.exclude_pattern.empty()) {
705 std::regex excl(opts.exclude_pattern, std::regex::ECMAScript);
706 if(opts.mode == RegExMode::REGEX_SEARCH) {
707 if (std::regex_search(filename, excl))
708 continue;
709 } else if(opts.mode == RegExMode::REGEX_MATCH) {
710 if (std::regex_match(filename, excl))
711 continue;
712 }
713 }
714
715 if (entry.is_directory(ec)) {
716 if (opts.type_filter == 'd') {
717 auto fullpath = entry.path().string();
718 if (entry_matches_path(fullpath, regex_str) && matches_filters(entry)) {
719 stats.files_matched++;
720 args[0] = fullpath;
721 if (!proc_cmd(cmd, args))
722 return;
723 }
724 }
725 add_directory(entry.path(), cmd, regex_str, args, depth + 1);
726 } else if (entry.is_symlink(ec) && opts.type_filter == 'l') {
727 auto fullpath = entry.path().string();
728 if (entry_matches_path(fullpath, regex_str) && matches_filters(entry)) {
729 stats.files_matched++;
730 args[0] = fullpath;
731 if (!proc_cmd(cmd, args))
732 return;
733 }
734 } else if (entry.is_regular_file(ec) || (entry.is_symlink(ec) && opts.type_filter == 0)) {
735 auto fullpath = entry.path().string();
736 if (entry_matches_path(fullpath, regex_str) && matches_filters(entry)) {
737 stats.files_matched++;
738 args[0] = fullpath;
739 if (!proc_cmd(cmd, args))
740 return;
741 }
742 }
743 }
744}
745
746/**
747 * @brief Execute a shell command using fork/exec with proper signal handling.
748 * @details Blocks SIGCHLD and ignores SIGINT/SIGQUIT in the parent process
749 * to prevent interrupted batch operations. Restores all signal masks
750 * after the child exits.
751 * @param command The shell command string to execute via the configured shell.
752 * @return The child's exit status from waitpid, or -1 on fork failure.
753 */
754int System(const std::string &command) {
755 sigset_t bmask, omask;
756 struct sigaction sa_ignore, sa_oquit, sa_origint, sa_default;
757 pid_t id;
758 int status, serrno;
759
760 if (command.empty())
761 return System(":") == 0;
762
763 sigemptyset(&bmask);
764 sigaddset(&bmask, SIGCHLD);
765 sigprocmask(SIG_BLOCK, &bmask, &omask);
766 sa_ignore.sa_handler = SIG_IGN;
767 sa_ignore.sa_flags = 0;
768 sigemptyset(&sa_ignore.sa_mask);
769 // Use our sigint_handler instead of SIG_IGN so Ctrl+C is recorded
770 struct sigaction sa_int;
771 sa_int.sa_handler = sigint_handler;
772 sa_int.sa_flags = 0;
773 sigemptyset(&sa_int.sa_mask);
774 sigaction(SIGINT, &sa_int, &sa_origint);
775 sigaction(SIGQUIT, &sa_ignore, &sa_oquit);
776
777 switch ((id = fork())) {
778 case -1:
779 status = -1;
780 break;
781 case 0:
782 sa_default.sa_handler = SIG_DFL;
783 sa_default.sa_flags = 0;
784 sigemptyset(&sa_default.sa_mask);
785 if (sa_origint.sa_handler != SIG_IGN)
786 sigaction(SIGINT, &sa_default, NULL);
787 if (sa_oquit.sa_handler != SIG_IGN)
788 sigaction(SIGQUIT, &sa_default, NULL);
789
790 execl(opts.shell.c_str(), opts.shell_name.c_str(), "-c", command.c_str(), static_cast<char *>(nullptr));
791 _exit(127);
792 break;
793 default:
794 while (waitpid(id, &status, 0) == -1) {
795 if (errno != EINTR) {
796 status = -1;
797 break;
798 }
799 }
800 break;
801 }
802 serrno = errno;
803 sigprocmask(SIG_SETMASK, &omask, NULL);
804 sigaction(SIGINT, &sa_origint, NULL);
805 sigaction(SIGQUIT, &sa_oquit, NULL);
806 // If the child was killed by SIGINT (or the shell caught it and exited 130),
807 // flag the parent to exit cleanly
808 if ((WIFSIGNALED(status) && WTERMSIG(status) == SIGINT) ||
809 (WIFEXITED(status) && WEXITSTATUS(status) == 130))
810 interrupted = 1;
811 errno = serrno;
812 return status;
813}
814
815/**
816 * @brief Block until a parallel execution slot is available.
817 * @details Waits for any outstanding child process to finish, updates stats,
818 * and sets stop_requested if stop-on-error is enabled and a child failed.
819 */
821 while (static_cast<int>(child_pids.size()) >= opts.jobs) {
822 int status;
823 pid_t pid = waitpid(-1, &status, 0);
824 if (pid > 0) {
825 std::erase(child_pids, pid);
826 stats.commands_run++;
827 if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
828 stats.commands_failed++;
829 if (opts.stop_on_error)
830 stop_requested = true;
831 }
832 }
833 }
834}
835
836/**
837 * @brief Wait for all outstanding child processes to finish.
838 * @details Called at the end of execution in parallel mode to drain the process pool.
839 */
840void wait_all() {
841 while (!child_pids.empty()) {
842 int status;
843 pid_t pid = waitpid(-1, &status, 0);
844 if (pid > 0) {
845 std::erase(child_pids, pid);
846 stats.commands_run++;
847 if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
848 stats.commands_failed++;
849 }
850 }
851}
852
853/**
854 * @brief Join a vector of strings into a single space-delimited string.
855 * @details Used by the @c --list-all mode to combine all matched file paths into
856 * one string that replaces @c %%0 in the command template.
857 * @param v The vector of strings to join.
858 * @return A string with each element separated by a space (trailing space included).
859 */
860std::string join(std::vector<std::string> &v) {
861 std::string temp;
862 for (auto &i : v) {
863 temp += i + " ";
864 }
865 return temp;
866}
867
868/**
869 * @brief Substitute placeholders in a command template and execute the result.
870 * @details
871 * - Default mode (one invocation per match): %0=basename, %1=full path, %2+ extra args.
872 * - --list-all mode (-l): collects all matching file paths into a single space-delimited
873 * string and passes this as file_string. In this mode %0 is replaced with the full
874 * list of matches, not individual filenames.
875 * - Supports confirm mode, dry-run, parallel forking, and stop-on-error.
876 * @param cmd The command template string.
877 * @param text Span of strings: text[0] is the matched file path, text[1+] are extras.
878 * @param file_string When --list-all is set, contains all matched paths joined by spaces.
879 * @return true to continue processing, false to stop (stop-on-error triggered).
880 */
881bool proc_cmd(const std::string &cmd, std::span<const std::string> text, std::string file_string) {
882 std::string r = cmd;
883 if (!text.empty()) {
884 if (file_string.empty()) {
885 auto fpath = fs::path(text[0]);
886 auto fname = fpath.filename().string();
887 r = replace_string(r, "%0", fname);
888 r = replace_string(r, "%b", fpath.stem().string());
889 r = replace_string(r, "%e", fpath.extension().string());
890 }
891 }
892 if (file_string.empty() && !text.empty()) {
893 for (size_t i = 0; i < text.size(); ++i) {
894 auto placeholder = std::format("%{}", i + 1);
895 if (i == 0 && text[i].find(' ') != std::string::npos)
896 r = replace_string(r, placeholder, std::format("\"{}\"", text[i]));
897 else
898 r = replace_string(r, placeholder, std::string{text[i]});
899 }
900 } else {
901
902 r = replace_string(r, "%0", file_string);
903 for (size_t i = 0; i < text.size(); ++i) {
904 std::string placeholder = std::format("%{}", i + 1);
905 if (text[i].find(' ') != std::string::npos) {
906 r = replace_string(r, placeholder, std::format("\"{}\"", text[i]));
907 } else
908 r = replace_string(r, placeholder, std::string{text[i]});
909 }
910 }
911
912 if (opts.confirm) {
913 if (use_color(1))
914 std::cout << std::format("\x1b[1;33mExecute:\x1b[0m {} \x1b[1m[y/N]\x1b[0m ", r);
915 else
916 std::cout << std::format("Execute: {} ? [y/N] ", r);
917 std::string answer;
918 std::getline(std::cin, answer);
919 if (answer != "y" && answer != "Y")
920 return true;
921 }
922
923 if (opts.verbose || opts.dry_run) {
924 if (use_color(1))
925 std::cout << std::format("\x1b[36m{}\x1b[0m\n", r);
926 else
927 std::cout << r << "\n";
928 }
929
930 if (opts.dry_run) {
931 stats.commands_run++;
932 return true;
933 }
934
935 if (opts.jobs > 1) {
937 if (stop_requested)
938 return false;
939 pid_t pid = fork();
940 if (pid == 0) {
941 execl(opts.shell.c_str(), opts.shell_name.c_str(), "-c", r.c_str(), static_cast<char *>(nullptr));
942 _exit(127);
943 } else if (pid > 0) {
944 child_pids.push_back(pid);
945 } else {
946 perror("fork");
947 stats.commands_failed++;
948 return !opts.stop_on_error;
949 }
950 return true;
951 }
952
953 int ret = System(r);
954 stats.commands_run++;
955 if (interrupted)
956 return false;
957 if (ret != 0) {
958 stats.commands_failed++;
959 if (opts.stop_on_error) {
960 print_error(std::format("command failed (exit {}), stopping.", WEXITSTATUS(ret)));
961 stop_requested = true;
962 return false;
963 }
964 }
965 return true;
966}
967
968/**
969 * @brief Print usage information to stdout.
970 * @param prog The program name (argv[0]).
971 */
972void print_help(const char *prog) {
973 bool co = use_color(1);
974 std::string b = co ? "\x1b[1m" : "";
975 std::string bw = co ? "\x1b[1;37m" : "";
976 std::string by = co ? "\x1b[1;33m" : "";
977 std::string g = co ? "\x1b[32m" : "";
978 std::string r = co ? "\x1b[0m" : "";
979 std::cout
980 << b << "usage:" << r << " " << bw << prog << r << " [options] path \"command %1 [%2 %3..]\" regex [extra_args..]\n\n"
981 << bw << "Recursively find files matching regex and run command for each." << r << "\n\n"
982 << by << "placeholders:" << r << "\n"
983 << " " << g << "%0" << r << " filename only (no path, per-match mode)\n"
984 << " " << g << "%1" << r << " full path to matched file\n"
985 << " " << g << "%2+" << r << " extra arguments from command line\n"
986 << " " << g << "%b" << r << " basename without extension\n"
987 << " " << g << "%e" << r << " file extension (including dot)\n\n"
988 << " (with -l/--list-all) %0 expands to all matched paths joined by spaces\n\n"
989 << by << "options:" << r << "\n"
990 << " " << g << "-z, --regex-match" << r << " Use regex_match instead of search\n"
991 << " " << g << "-b, --glob" << r << " Treat pattern as a glob (*, ?) instead of regex\n"
992 << " " << g << "-n, --dry-run" << r << " dry-run, print commands without executing\n"
993 << " " << g << "-v, --verbose" << r << " verbose, print each command before running\n"
994 << " " << g << "-a, --all" << r << " include hidden files/directories\n"
995 << " " << g << "-l, --list-all" << r << " collect all matches and invoke command once with %0=all-matches\n"
996 << " " << g << "-d, --depth N" << r << " max recursion depth (0 = current dir only)\n"
997 << " " << g << "-s, --size SIZE" << r << " filter by size: +10M (>10MB), -1K (<1KB),\n"
998 << " 4096 (exactly 4096 bytes). Suffixes: K, M, G\n"
999 << " " << g << "-m, --mtime DAYS" << r << " filter by modification time: +7 (older than 7 days),\n"
1000 << " -1 (modified within last day), 3 (exactly 3 days)\n"
1001 << " " << g << "-p, --perm MODE" << r << " filter by permissions (octal), e.g. 755\n"
1002 << " " << g << "-u, --user USER" << r << " filter by owner username\n"
1003 << " " << g << "-g, --group GROUP" << r << " filter by group name\n"
1004 << " " << g << "-t, --type TYPE" << r << " filter by type: f (file), d (directory), l (symlink)\n"
1005 << " " << g << "-x, --exclude REGEX" << r << " exclude files/directories matching REGEX\n"
1006 << " " << g << "-i, --glob-exclude" << r << " treat exclude pattern as a glob instead of regex\n"
1007 << " " << g << "-f, --expr EXPR" << r << " filter expression: glob(), regex(), regex_match(),\n"
1008 << " combined with and/or/not and parentheses\n"
1009 << " " << g << "-e, --stop-on-error" << r << " stop on first command failure\n"
1010 << " " << g << "-c, --confirm" << r << " prompt for confirmation before each command\n"
1011 << " " << g << "-j, --jobs N" << r << " run N commands in parallel (default: 1)\n"
1012 << " " << g << "-w, --shell SHELL" << r << " shell to use for execution (default: /bin/bash)\n"
1013 << " " << g << "-h, --help" << r << " show this help\n";
1014}
1015
1016/**
1017 * @brief Program entry point.
1018 * @details Parses command-line arguments via argz, configures options, validates
1019 * positional arguments and placeholder consistency, runs directory traversal,
1020 * waits for parallel children, and prints a summary.
1021 * @param argc Argument count.
1022 * @param argv Argument vector.
1023 * @return EXIT_SUCCESS on success, EXIT_FAILURE if any command failed or input was invalid.
1024 */
1025int main(int argc, char **argv) {
1026 // Install SIGINT handler for clean Ctrl+C exit
1027 struct sigaction sa_int;
1028 sa_int.sa_handler = sigint_handler;
1029 sa_int.sa_flags = 0;
1030 sigemptyset(&sa_int.sa_mask);
1031 sigaction(SIGINT, &sa_int, nullptr);
1032
1033 Argz<std::string> argz(argc, argv);
1034 argz.addOptionSingle('n', "dry-run mode")
1035 .addOptionDouble('N', "dry-run", "dry-run mode")
1036 .addOptionSingle('v', "verbose output")
1037 .addOptionDouble('V', "verbose", "verbose output")
1038 .addOptionSingle('a', "include hidden files")
1039 .addOptionDouble('A', "all", "include hidden files")
1040 .addOptionSingleValue('d', "max depth")
1041 .addOptionDoubleValue('D', "depth", "max depth")
1042 .addOptionSingleValue('s', "size filter")
1043 .addOptionDoubleValue('S', "size", "size filter")
1044 .addOptionSingleValue('m', "modification time filter")
1045 .addOptionDoubleValue('M', "mtime", "modification time filter")
1046 .addOptionSingleValue('p', "permission filter")
1047 .addOptionDoubleValue('P', "perm", "permission filter")
1048 .addOptionSingleValue('u', "user filter")
1049 .addOptionDoubleValue('U', "user", "user filter")
1050 .addOptionSingleValue('g', "group filter")
1051 .addOptionDoubleValue('G', "group", "group filter")
1052 .addOptionSingleValue('t', "type filter")
1053 .addOptionDoubleValue('T', "type", "type filter")
1054 .addOptionSingleValue('x', "exclude pattern")
1055 .addOptionDoubleValue('X', "exclude", "exclude pattern")
1056 .addOptionSingle('e', "stop on error")
1057 .addOptionDouble('E', "stop-on-error", "stop on error")
1058 .addOptionSingle('c', "confirm mode")
1059 .addOptionDouble('C', "confirm", "confirm mode")
1060 .addOptionSingleValue('j', "parallel jobs")
1061 .addOptionDoubleValue('J', "jobs", "parallel jobs")
1062 .addOptionSingleValue('w', "shell path")
1063 .addOptionDoubleValue('W', "shell", "shell path")
1064 .addOptionSingle('l', "list all matches")
1065 .addOptionDouble('L', "list-all", "list all matches")
1066 .addOptionSingle('h', "show help")
1067 .addOptionSingle('z', "regex match mode")
1068 .addOptionDouble('Z', "regex-match", "Regex mode match")
1069 .addOptionSingle('b', "glob mode")
1070 .addOptionDouble('B', "glob", "glob mode")
1071 .addOptionSingle('i', "glob exclude mode")
1072 .addOptionDouble('I', "glob-exclude", "glob exclude mode")
1073 .addOptionSingleValue('f', "filter expression")
1074 .addOptionDoubleValue('F', "expr", "filter expression")
1075 .addOptionDouble('H', "help", "show help");
1076
1077 std::vector<std::string> positional;
1078
1079 try {
1081 int ret;
1082 while ((ret = argz.proc(arg)) != -1) {
1083 switch (ret) {
1084 case 'n':
1085 case 'N':
1086 opts.dry_run = true;
1087 break;
1088 case 'v':
1089 case 'V':
1090 opts.verbose = true;
1091 break;
1092 case 'a':
1093 case 'A':
1094 opts.hidden = true;
1095 break;
1096 case 'd':
1097 case 'D':
1098 opts.max_depth = std::stoi(arg.arg_value);
1099 break;
1100 case 's':
1101 case 'S':
1102 opts.size_filter = parse_size_filter(arg.arg_value);
1103 break;
1104 case 'm':
1105 case 'M':
1106 opts.mtime_filter = parse_time_filter(arg.arg_value);
1107 break;
1108 case 'p':
1109 case 'P':
1110 opts.perm_filter = arg.arg_value;
1111 break;
1112 case 'u':
1113 case 'U':
1114 opts.user_filter = arg.arg_value;
1115 break;
1116 case 'g':
1117 case 'G':
1118 opts.group_filter = arg.arg_value;
1119 break;
1120 case 't':
1121 case 'T':
1122 if (arg.arg_value == "f" || arg.arg_value == "d" || arg.arg_value == "l") {
1123 opts.type_filter = arg.arg_value[0];
1124 } else {
1125 print_error(std::format("invalid type '{}'. Use f (file), d (directory), or l (symlink).", arg.arg_value));
1126 return EXIT_FAILURE;
1127 }
1128 break;
1129 case 'x':
1130 case 'X':
1131 opts.exclude_pattern = arg.arg_value;
1132 break;
1133 case 'e':
1134 case 'E':
1135 opts.stop_on_error = true;
1136 break;
1137 case 'c':
1138 case 'C':
1139 opts.confirm = true;
1140 break;
1141 case 'j':
1142 case 'J':
1143 opts.jobs = std::stoi(arg.arg_value);
1144 if (opts.jobs < 1)
1145 opts.jobs = 1;
1146 break;
1147 case 'w':
1148 case 'W': {
1149 opts.shell = arg.arg_value;
1150 auto slash = opts.shell.rfind('/');
1151 opts.shell_name = (slash != std::string::npos) ? opts.shell.substr(slash + 1) : opts.shell;
1152 break;
1153 }
1154 case 'l':
1155 case 'L':
1156 opts.collect_all = true;
1157 break;
1158 case 'h':
1159 case 'H':
1160 print_help(argv[0]);
1161 return 0;
1162 case 'Z':
1163 case 'z':
1165 break;
1166 case 'b':
1167 case 'B':
1168 opts.glob = true;
1169 break;
1170 case 'i':
1171 case 'I':
1172 opts.glob_exclude = true;
1173 break;
1174 case 'f':
1175 case 'F':
1176 opts.expr_str = arg.arg_value;
1177 break;
1178 case '-':
1179 positional.push_back(arg.arg_value);
1180 break;
1181 }
1182 }
1183 } catch (const ArgException<std::string> &e) {
1184 print_error(e.text());
1185 return EXIT_FAILURE;
1186 }
1187
1188 size_t min_pos = opts.expr_str.empty() ? 3 : 2;
1189 if (positional.size() < min_pos) {
1190 print_error(opts.expr_str.empty()
1191 ? "at least three positional arguments required (or use --expr)."
1192 : "at least two positional arguments required with --expr.");
1193 print_help(argv[0]);
1194 return EXIT_FAILURE;
1195 }
1196
1197 try {
1198 const auto &path = positional[0];
1199 const auto &input = positional[1];
1200 std::string regex_str;
1201 if (!opts.expr_str.empty()) {
1202 expr_root = ExprParser(opts.expr_str).parse();
1203 } else {
1204 regex_str = opts.glob ? glob_to_regex(positional[2]) : positional[2];
1205 }
1206 if (opts.glob_exclude && !opts.exclude_pattern.empty())
1207 opts.exclude_pattern = glob_to_regex(opts.exclude_pattern);
1208 size_t index = (opts.collect_all) ? 1 : 2;
1209 std::vector<std::string> args;
1210 if (!opts.collect_all)
1211 args.push_back("filename");
1212 size_t extra_start = opts.expr_str.empty() ? 3 : 2;
1213 for (size_t i = extra_start; i < positional.size(); ++i) {
1214 if (input.find(std::format("%{}", index)) == std::string::npos) {
1215 print_error(std::format("command has no placeholder %{} for extra argument \"{}\"", index, positional[i]));
1216 return EXIT_FAILURE;
1217 }
1218 args.push_back(positional[i]);
1219 ++index;
1220 }
1221 if (opts.collect_all) {
1222 std::vector<std::string> files;
1223 fill_list(path, input, regex_str, args, files, 0);
1224 std::string all_files = join(files);
1225 if (proc_cmd(input, args, all_files)) {
1226 if (opts.verbose) {
1227 std::cout << std::format("Success command file list: {} .\n", all_files);
1228 }
1229 return EXIT_SUCCESS;
1230 } else {
1231 std::cout << "List all command failed.\n";
1232 return EXIT_FAILURE;
1233 }
1234 return EXIT_SUCCESS;
1235 }
1236
1237 add_directory(path, input, regex_str, args, 0);
1238 if (opts.jobs > 1)
1239 wait_all();
1240
1241 if (interrupted) {
1242 // Kill outstanding child processes
1243 for (pid_t pid : child_pids)
1244 kill(pid, SIGTERM);
1245 for (pid_t pid : child_pids)
1246 waitpid(pid, nullptr, 0);
1247 child_pids.clear();
1248 std::cerr << "\nInterrupted.\n";
1249 if (opts.verbose || opts.dry_run || stats.commands_failed > 0 || stats.commands_run > 0) {
1250 if (use_color(2)) {
1251 std::cerr << std::format("\x1b[1mSummary:\x1b[0m \x1b[1;32m{}\x1b[0m matched, \x1b[1;33m{}\x1b[0m run, {}{}\x1b[0m failed\n",
1252 stats.files_matched, stats.commands_run,
1253 stats.commands_failed > 0 ? "\x1b[1;31m" : "\x1b[1;32m",
1254 stats.commands_failed);
1255 } else {
1256 std::cerr << std::format("Summary: {} matched, {} run, {} failed\n",
1257 stats.files_matched, stats.commands_run, stats.commands_failed);
1258 }
1259 }
1260 return 130;
1261 }
1262
1263 if (opts.verbose || opts.dry_run || stats.commands_failed > 0) {
1264 if (use_color(2)) {
1265 std::cerr << std::format("\n\x1b[1mSummary:\x1b[0m \x1b[1;32m{}\x1b[0m matched, \x1b[1;33m{}\x1b[0m run, {}{}\x1b[0m failed\n",
1266 stats.files_matched, stats.commands_run,
1267 stats.commands_failed > 0 ? "\x1b[1;31m" : "\x1b[1;32m",
1268 stats.commands_failed);
1269 } else {
1270 std::cerr << std::format("\nSummary: {} matched, {} run, {} failed\n",
1271 stats.files_matched, stats.commands_run, stats.commands_failed);
1272 }
1273 }
1274 } catch (const std::exception &e) {
1275 std::cerr << "Exception: " << e.what() << "\n";
1276 return EXIT_FAILURE;
1277 }
1278
1279 return stats.commands_failed > 0 ? EXIT_FAILURE : 0;
1280}
String text() const
Definition argz.hpp:84
Definition argz.hpp:91
int proc(Argument< String > &a)
Definition argz.hpp:198
Argz< String > & addOptionSingle(const int &c, const String &description)
Definition argz.hpp:151
Recursive-descent parser for expression filter strings.
Definition cmd.cpp:313
std::unique_ptr< ExprNode > parse()
Definition cmd.cpp:400
void advance()
Definition cmd.cpp:316
ExprTokenizer tok
Definition cmd.cpp:314
std::unique_ptr< ExprNode > parse_not()
Definition cmd.cpp:361
std::unique_ptr< ExprNode > parse_and()
Definition cmd.cpp:372
std::unique_ptr< ExprNode > parse_or()
Definition cmd.cpp:385
ExprToken cur
Definition cmd.cpp:315
ExprParser(const std::string &s)
Definition cmd.cpp:399
void expect(ExprToken::Type t, const std::string &desc)
Definition cmd.cpp:317
std::unique_ptr< ExprNode > parse_primary()
Definition cmd.cpp:324
Tokenizer for expression filter strings.
Definition cmd.cpp:259
ExprToken next()
Definition cmd.cpp:268
const std::string & src
Definition cmd.cpp:260
void skip_ws()
Definition cmd.cpp:262
ExprTokenizer(const std::string &s)
Definition cmd.cpp:267
size_t pos
Definition cmd.cpp:261
void print_help(const char *prog)
Print usage information to stdout.
Definition cmd.cpp:972
void wait_all()
Wait for all outstanding child processes to finish.
Definition cmd.cpp:840
int main(int argc, char **argv)
Program entry point.
Definition cmd.cpp:1025
ExprType
Node types for the expression filter AST.
Definition cmd.cpp:223
@ GLOB
Definition cmd.cpp:223
@ REGEX_SEARCH
Definition cmd.cpp:223
@ NOT
Definition cmd.cpp:223
@ AND
Definition cmd.cpp:223
@ REGEX_MATCH
Definition cmd.cpp:223
static volatile sig_atomic_t interrupted
Set to 1 by SIGINT handler (Ctrl+C).
Definition cmd.cpp:122
void fill_list(const fs::path &path, const std::string &cmd, const std::string &regex_str, std::vector< std::string > &args, std::vector< std::string > &files, int depth)
Recursively collect all file paths matching a regex and metadata filters.
Definition cmd.cpp:618
static void print_error(const std::string &msg)
Print a colored error message to stderr.
Definition cmd.cpp:52
static Stats stats
Global execution statistics.
Definition cmd.cpp:119
CmpOp
Comparison operator for size and time filters.
Definition cmd.cpp:60
@ EQ
Definition cmd.cpp:61
@ LT
Definition cmd.cpp:62
@ GT
Definition cmd.cpp:63
std::string replace_string(std::string orig, const std::string &with, const std::string &rep)
Replace all occurrences of a substring within a string.
Definition cmd.cpp:595
static bool stop_requested
Set to true when stop-on-error triggers.
Definition cmd.cpp:121
void wait_for_slot()
Block until a parallel execution slot is available.
Definition cmd.cpp:820
static Options opts
Global runtime options.
Definition cmd.cpp:110
int System(const std::string &command)
Execute a shell command using fork/exec with proper signal handling.
Definition cmd.cpp:754
RegExMode
Definition cmd.cpp:80
@ REGEX_SEARCH
Definition cmd.cpp:81
@ REGEX_MATCH
Definition cmd.cpp:82
static bool use_color(int fd)
Check whether to use color output on the given file descriptor.
Definition cmd.cpp:41
std::string join(std::vector< std::string > &v)
Join a vector of strings into a single space-delimited string.
Definition cmd.cpp:860
TimeFilter parse_time_filter(const std::string &s)
Parse a time filter string into a TimeFilter.
Definition cmd.cpp:464
SizeFilter parse_size_filter(const std::string &s)
Parse a size filter string into a SizeFilter.
Definition cmd.cpp:429
bool matches_filters(const fs::directory_entry &entry)
Test a directory entry against all active metadata filters.
Definition cmd.cpp:486
void add_directory(const fs::path &path, const std::string &cmd, const std::string &regex_str, std::vector< std::string > &args, int depth)
Recursively walk a directory, match entries against a regex and filters, and run commands.
Definition cmd.cpp:683
bool proc_cmd(const std::string &cmd, std::span< const std::string > text, std::string file_string="")
Substitute placeholders in a command template and execute the result.
Definition cmd.cpp:881
static bool entry_matches_path(const std::string &fullpath, const std::string &regex_str)
Check whether a path matches the active search pattern or expression.
Definition cmd.cpp:413
static std::unique_ptr< ExprNode > expr_root
Parsed expression tree (set when –expr is used).
Definition cmd.cpp:410
static std::vector< pid_t > child_pids
PIDs of outstanding child processes (parallel mode).
Definition cmd.cpp:120
static void sigint_handler(int)
Signal handler for SIGINT (Ctrl+C).
Definition cmd.cpp:128
std::string glob_to_regex(const std::string &glob)
Convert a glob pattern to an equivalent regex string.
Definition cmd.cpp:158
String arg_value
Definition argz.hpp:41
AST node for expression-based file matching.
Definition cmd.cpp:226
std::regex compiled
Pre-compiled regex (leaf nodes only).
Definition cmd.cpp:228
std::unique_ptr< ExprNode > left
Left child (AND/OR) or sole child (NOT).
Definition cmd.cpp:229
std::unique_ptr< ExprNode > right
Right child (AND/OR only).
Definition cmd.cpp:230
ExprType type
Definition cmd.cpp:227
bool evaluate(const std::string &path) const
Evaluate this expression node against a file path.
Definition cmd.cpp:233
Token produced by the expression tokenizer.
Definition cmd.cpp:253
@ RPAREN
Definition cmd.cpp:254
@ STRING
Definition cmd.cpp:254
@ IDENT
Definition cmd.cpp:254
@ LPAREN
Definition cmd.cpp:254
std::string value
Definition cmd.cpp:255
enum ExprToken::Type type
Aggregates all runtime options parsed from the command line.
Definition cmd.cpp:86
bool confirm
Prompt for confirmation before each command.
Definition cmd.cpp:99
bool hidden
Include hidden (dot) files/directories.
Definition cmd.cpp:89
std::string group_filter
Group name filter.
Definition cmd.cpp:95
std::string shell_name
Shell argv[0] name.
Definition cmd.cpp:102
std::string perm_filter
Octal permission string, e.g. "755".
Definition cmd.cpp:93
bool stop_on_error
Halt on first command failure.
Definition cmd.cpp:98
char type_filter
Type filter: 'f' file, 'd' directory, 'l' symlink.
Definition cmd.cpp:96
std::string exclude_pattern
Regex (or glob, see glob_exclude) pattern to exclude files/dirs.
Definition cmd.cpp:97
std::string expr_str
Expression filter string from –expr.
Definition cmd.cpp:107
TimeFilter mtime_filter
Optional modification-time filter.
Definition cmd.cpp:92
int max_depth
Max recursion depth (-1 = unlimited).
Definition cmd.cpp:90
std::string user_filter
Owner username filter.
Definition cmd.cpp:94
SizeFilter size_filter
Optional size filter.
Definition cmd.cpp:91
std::string shell
Shell to use for command execution.
Definition cmd.cpp:101
bool glob_exclude
If true (via -i/–glob-exclude), treat exclude pattern as a glob instead of regex.
Definition cmd.cpp:106
bool dry_run
Print commands without executing.
Definition cmd.cpp:87
bool verbose
Print commands before executing.
Definition cmd.cpp:88
bool collect_all
If true (via -l/–list-all), collect all matched file paths and run one command with a combined argume...
Definition cmd.cpp:103
bool glob
If true, treat search pattern as a glob instead of regex.
Definition cmd.cpp:105
RegExMode mode
RegEx mode.
Definition cmd.cpp:104
int jobs
Number of parallel jobs (1 = sequential).
Definition cmd.cpp:100
Holds a parsed size filter with comparison operator and byte threshold.
Definition cmd.cpp:67
CmpOp op
Comparison direction (equal, less-than, greater-than).
Definition cmd.cpp:69
bool active
Whether this filter is enabled.
Definition cmd.cpp:68
uintmax_t bytes
Size threshold in bytes.
Definition cmd.cpp:70
Tracks execution statistics printed in the summary.
Definition cmd.cpp:113
int files_matched
Number of entries that matched all filters.
Definition cmd.cpp:114
int commands_failed
Number of commands that returned non-zero.
Definition cmd.cpp:116
int commands_run
Number of commands executed (or printed in dry-run).
Definition cmd.cpp:115
Holds a parsed modification-time filter with comparison operator and day count.
Definition cmd.cpp:74
CmpOp op
Comparison direction.
Definition cmd.cpp:76
int days
Age threshold in days.
Definition cmd.cpp:77
bool active
Whether this filter is enabled.
Definition cmd.cpp:75