33namespace fs = std::filesystem;
42 if (std::getenv(
"NO_COLOR") !=
nullptr)
44 return isatty(fd) != 0;
54 std::cerr << std::format(
"\x1b[1;31mError:\x1b[0m {}\n", msg);
56 std::cerr << std::format(
"Error: {}\n", msg);
136bool proc_cmd(
const std::string &cmd, std::span<const std::string> text, std::string file_string =
"");
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 ®ex_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 ®ex_str, std::vector<std::string> &args,
int depth);
143int System(
const std::string &command);
162 bool in_class =
false;
164 for (
size_t i = 0; i < glob.size(); ++i) {
171 }
else if (c ==
'\\') {
189 if (i + 1 < glob.size() && (glob[i + 1] ==
'!' || glob[i + 1] ==
'^')) {
229 std::unique_ptr<ExprNode>
left;
236 return std::regex_search(path,
compiled);
238 return std::regex_search(path,
compiled);
240 return std::regex_match(path,
compiled);
242 return left->evaluate(path) &&
right->evaluate(path);
244 return left->evaluate(path) ||
right->evaluate(path);
246 return !
left->evaluate(path);
263 while (
pos <
src.size() && std::isspace(
static_cast<unsigned char>(
src[
pos])))
275 if (c ==
'"' || c ==
'\'') {
292 if (std::isalpha(
static_cast<unsigned char>(c)) || c ==
'_') {
294 while (
pos <
src.size() &&
295 (std::isalnum(
static_cast<unsigned char>(
src[
pos])) ||
src[
pos] ==
'_'))
299 throw std::runtime_error(
300 std::format(
"unexpected character '{}' in expression at position {}", c,
pos));
319 throw std::runtime_error(std::format(
320 "expected {} in expression, got '{}'", desc,
321 cur.value.empty() ?
"end" :
cur.value));
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;
339 else if (name ==
"regex" || name ==
"regex_search")
341 else if (name ==
"regex_match")
344 throw std::runtime_error(
345 std::format(
"unknown function '{}' in expression", name));
349 throw std::runtime_error(
350 "expected quoted string as function argument");
351 std::string pattern =
cur.value;
354 auto node = std::make_unique<ExprNode>();
356 node->compiled = std::regex(
358 std::regex::ECMAScript);
365 auto node = std::make_unique<ExprNode>();
367 node->left = std::move(child);
377 auto node = std::make_unique<ExprNode>();
379 node->left = std::move(left);
380 node->right = std::move(right);
381 left = std::move(node);
390 auto node = std::make_unique<ExprNode>();
392 node->left = std::move(left);
393 node->right = std::move(right);
394 left = std::move(node);
404 throw std::runtime_error(
405 "unexpected content after expression");
416 std::regex ex(regex_str, std::regex::ECMAScript);
418 return std::regex_search(fullpath, ex);
419 return std::regex_match(fullpath, ex);
436 }
else if (val[0] ==
'-') {
442 uintmax_t multiplier = 1;
443 char suffix = val.back();
444 if (suffix ==
'K' || suffix ==
'k') {
447 }
else if (suffix ==
'M' || suffix ==
'm') {
448 multiplier = 1024 * 1024;
450 }
else if (suffix ==
'G' || suffix ==
'g') {
451 multiplier = 1024ULL * 1024 * 1024;
454 f.
bytes = std::stoull(val) * multiplier;
471 }
else if (val[0] ==
'-') {
477 f.
days = std::stoi(val);
490 if (
opts.type_filter != 0) {
491 switch (
opts.type_filter) {
493 if (!entry.is_regular_file(ec))
497 if (!entry.is_directory(ec))
501 if (!entry.is_symlink(ec))
508 if (
opts.size_filter.active) {
509 if (!entry.is_regular_file(ec))
511 auto sz = entry.file_size(ec);
514 switch (
opts.size_filter.op) {
516 if (sz <=
opts.size_filter.bytes)
520 if (sz >=
opts.size_filter.bytes)
524 if (sz !=
opts.size_filter.bytes)
531 if (
opts.mtime_filter.active) {
532 auto ftime = entry.last_write_time(ec);
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) {
540 if (age <=
opts.mtime_filter.days)
544 if (age >=
opts.mtime_filter.days)
548 if (age !=
opts.mtime_filter.days)
555 if (!
opts.perm_filter.empty()) {
557 if (stat(entry.path().c_str(), &st) != 0)
559 auto mode = st.st_mode & 07777;
560 auto target =
static_cast<mode_t
>(std::stoul(
opts.perm_filter,
nullptr, 8));
566 if (!
opts.user_filter.empty()) {
568 if (stat(entry.path().c_str(), &st) != 0)
570 struct passwd *pw = getpwuid(st.st_uid);
571 if (!pw ||
opts.user_filter != pw->pw_name)
576 if (!
opts.group_filter.empty()) {
578 if (stat(entry.path().c_str(), &st) != 0)
580 struct group *gr = getgrgid(st.st_gid);
581 if (!gr ||
opts.group_filter != gr->gr_name)
595std::string
replace_string(std::string orig,
const std::string &with,
const std::string &rep) {
597 while ((pos = orig.find(with, pos)) != std::string::npos) {
598 orig.replace(pos, with.length(), rep);
618void fill_list(
const fs::path &path,
const std::string &cmd,
const std::string ®ex_str, std::vector<std::string> &args, std::vector<std::string> &files,
int depth) {
619 if (
opts.max_depth >= 0 && depth >
opts.max_depth)
625 auto dir = fs::directory_iterator(path, fs::directory_options::skip_permission_denied, ec);
627 print_error(std::format(
"could not open directory: {}", path.string()));
631 for (
const auto &entry : dir) {
634 auto filename = entry.path().filename().string();
635 if (!
opts.hidden && filename.starts_with(
'.'))
639 if (!
opts.exclude_pattern.empty()) {
640 std::regex excl(
opts.exclude_pattern, std::regex::ECMAScript);
642 if (std::regex_search(filename, excl))
645 if(std::regex_match(filename,excl))
650 if (entry.is_directory(ec)) {
651 if (
opts.type_filter ==
'd') {
652 auto fullpath = entry.path().string();
654 stats.files_matched++;
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();
662 stats.files_matched++;
663 files.push_back(fullpath);
665 }
else if (entry.is_regular_file(ec) || (entry.is_symlink(ec) &&
opts.type_filter == 0)) {
666 auto fullpath = entry.path().string();
668 stats.files_matched++;
669 files.push_back(fullpath);
683void add_directory(
const fs::path &path,
const std::string &cmd,
const std::string ®ex_str, std::vector<std::string> &args,
int depth) {
684 if (
opts.max_depth >= 0 && depth >
opts.max_depth)
690 auto dir = fs::directory_iterator(path, fs::directory_options::skip_permission_denied, ec);
692 print_error(std::format(
"could not open directory: {}", path.string()));
696 for (
const auto &entry : dir) {
699 auto filename = entry.path().filename().string();
700 if (!
opts.hidden && filename.starts_with(
'.'))
704 if (!
opts.exclude_pattern.empty()) {
705 std::regex excl(
opts.exclude_pattern, std::regex::ECMAScript);
707 if (std::regex_search(filename, excl))
710 if (std::regex_match(filename, excl))
715 if (entry.is_directory(ec)) {
716 if (
opts.type_filter ==
'd') {
717 auto fullpath = entry.path().string();
719 stats.files_matched++;
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();
729 stats.files_matched++;
734 }
else if (entry.is_regular_file(ec) || (entry.is_symlink(ec) &&
opts.type_filter == 0)) {
735 auto fullpath = entry.path().string();
737 stats.files_matched++;
755 sigset_t bmask, omask;
756 struct sigaction sa_ignore, sa_oquit, sa_origint, sa_default;
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);
770 struct sigaction sa_int;
773 sigemptyset(&sa_int.sa_mask);
774 sigaction(SIGINT, &sa_int, &sa_origint);
775 sigaction(SIGQUIT, &sa_ignore, &sa_oquit);
777 switch ((
id = fork())) {
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);
790 execl(
opts.shell.c_str(),
opts.shell_name.c_str(),
"-c", command.c_str(),
static_cast<char *
>(
nullptr));
794 while (waitpid(
id, &status, 0) == -1) {
795 if (errno != EINTR) {
803 sigprocmask(SIG_SETMASK, &omask, NULL);
804 sigaction(SIGINT, &sa_origint, NULL);
805 sigaction(SIGQUIT, &sa_oquit, NULL);
808 if ((WIFSIGNALED(status) && WTERMSIG(status) == SIGINT) ||
809 (WIFEXITED(status) && WEXITSTATUS(status) == 130))
823 pid_t pid = waitpid(-1, &status, 0);
826 stats.commands_run++;
827 if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
828 stats.commands_failed++;
829 if (
opts.stop_on_error)
843 pid_t pid = waitpid(-1, &status, 0);
846 stats.commands_run++;
847 if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
848 stats.commands_failed++;
860std::string
join(std::vector<std::string> &v) {
881bool proc_cmd(
const std::string &cmd, std::span<const std::string> text, std::string file_string) {
884 if (file_string.empty()) {
885 auto fpath = fs::path(text[0]);
886 auto fname = fpath.filename().string();
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]));
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]));
914 std::cout << std::format(
"\x1b[1;33mExecute:\x1b[0m {} \x1b[1m[y/N]\x1b[0m ", r);
916 std::cout << std::format(
"Execute: {} ? [y/N] ", r);
918 std::getline(std::cin, answer);
919 if (answer !=
"y" && answer !=
"Y")
925 std::cout << std::format(
"\x1b[36m{}\x1b[0m\n", r);
927 std::cout << r <<
"\n";
931 stats.commands_run++;
941 execl(
opts.shell.c_str(),
opts.shell_name.c_str(),
"-c", r.c_str(),
static_cast<char *
>(
nullptr));
943 }
else if (pid > 0) {
947 stats.commands_failed++;
948 return !
opts.stop_on_error;
954 stats.commands_run++;
958 stats.commands_failed++;
959 if (
opts.stop_on_error) {
960 print_error(std::format(
"command failed (exit {}), stopping.", WEXITSTATUS(ret)));
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" :
"";
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";
1027 struct sigaction sa_int;
1029 sa_int.sa_flags = 0;
1030 sigemptyset(&sa_int.sa_mask);
1031 sigaction(SIGINT, &sa_int,
nullptr);
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");
1077 std::vector<std::string> positional;
1082 while ((ret = argz.
proc(arg)) != -1) {
1086 opts.dry_run =
true;
1090 opts.verbose =
true;
1125 print_error(std::format(
"invalid type '{}'. Use f (file), d (directory), or l (symlink).", arg.
arg_value));
1126 return EXIT_FAILURE;
1135 opts.stop_on_error =
true;
1139 opts.confirm =
true;
1150 auto slash =
opts.shell.rfind(
'/');
1151 opts.shell_name = (slash != std::string::npos) ?
opts.shell.substr(slash + 1) :
opts.shell;
1156 opts.collect_all =
true;
1172 opts.glob_exclude =
true;
1185 return EXIT_FAILURE;
1188 size_t min_pos =
opts.expr_str.empty() ? 3 : 2;
1189 if (positional.size() < min_pos) {
1191 ?
"at least three positional arguments required (or use --expr)."
1192 :
"at least two positional arguments required with --expr.");
1194 return EXIT_FAILURE;
1198 const auto &path = positional[0];
1199 const auto &input = positional[1];
1200 std::string regex_str;
1201 if (!
opts.expr_str.empty()) {
1206 if (
opts.glob_exclude && !
opts.exclude_pattern.empty())
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;
1218 args.push_back(positional[i]);
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)) {
1227 std::cout << std::format(
"Success command file list: {} .\n", all_files);
1229 return EXIT_SUCCESS;
1231 std::cout <<
"List all command failed.\n";
1232 return EXIT_FAILURE;
1234 return EXIT_SUCCESS;
1246 waitpid(pid,
nullptr, 0);
1248 std::cerr <<
"\nInterrupted.\n";
1249 if (
opts.verbose ||
opts.dry_run ||
stats.commands_failed > 0 ||
stats.commands_run > 0) {
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",
1253 stats.commands_failed > 0 ?
"\x1b[1;31m" :
"\x1b[1;32m",
1254 stats.commands_failed);
1256 std::cerr << std::format(
"Summary: {} matched, {} run, {} failed\n",
1263 if (
opts.verbose ||
opts.dry_run ||
stats.commands_failed > 0) {
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",
1267 stats.commands_failed > 0 ?
"\x1b[1;31m" :
"\x1b[1;32m",
1268 stats.commands_failed);
1270 std::cerr << std::format(
"\nSummary: {} matched, {} run, {} failed\n",
1274 }
catch (
const std::exception &e) {
1275 std::cerr <<
"Exception: " << e.what() <<
"\n";
1276 return EXIT_FAILURE;
1279 return stats.commands_failed > 0 ? EXIT_FAILURE : 0;
int proc(Argument< String > &a)
Argz< String > & addOptionSingle(const int &c, const String &description)
Recursive-descent parser for expression filter strings.
std::unique_ptr< ExprNode > parse()
std::unique_ptr< ExprNode > parse_not()
std::unique_ptr< ExprNode > parse_and()
std::unique_ptr< ExprNode > parse_or()
ExprParser(const std::string &s)
void expect(ExprToken::Type t, const std::string &desc)
std::unique_ptr< ExprNode > parse_primary()
Tokenizer for expression filter strings.
ExprTokenizer(const std::string &s)
void print_help(const char *prog)
Print usage information to stdout.
void wait_all()
Wait for all outstanding child processes to finish.
int main(int argc, char **argv)
Program entry point.
ExprType
Node types for the expression filter AST.
static volatile sig_atomic_t interrupted
Set to 1 by SIGINT handler (Ctrl+C).
void fill_list(const fs::path &path, const std::string &cmd, const std::string ®ex_str, std::vector< std::string > &args, std::vector< std::string > &files, int depth)
Recursively collect all file paths matching a regex and metadata filters.
static void print_error(const std::string &msg)
Print a colored error message to stderr.
static Stats stats
Global execution statistics.
CmpOp
Comparison operator for size and time filters.
std::string replace_string(std::string orig, const std::string &with, const std::string &rep)
Replace all occurrences of a substring within a string.
static bool stop_requested
Set to true when stop-on-error triggers.
void wait_for_slot()
Block until a parallel execution slot is available.
static Options opts
Global runtime options.
int System(const std::string &command)
Execute a shell command using fork/exec with proper signal handling.
static bool use_color(int fd)
Check whether to use color output on the given file descriptor.
std::string join(std::vector< std::string > &v)
Join a vector of strings into a single space-delimited string.
TimeFilter parse_time_filter(const std::string &s)
Parse a time filter string into a TimeFilter.
SizeFilter parse_size_filter(const std::string &s)
Parse a size filter string into a SizeFilter.
bool matches_filters(const fs::directory_entry &entry)
Test a directory entry against all active metadata filters.
void add_directory(const fs::path &path, const std::string &cmd, const std::string ®ex_str, std::vector< std::string > &args, int depth)
Recursively walk a directory, match entries against a regex and filters, and run commands.
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.
static bool entry_matches_path(const std::string &fullpath, const std::string ®ex_str)
Check whether a path matches the active search pattern or expression.
static std::unique_ptr< ExprNode > expr_root
Parsed expression tree (set when –expr is used).
static std::vector< pid_t > child_pids
PIDs of outstanding child processes (parallel mode).
static void sigint_handler(int)
Signal handler for SIGINT (Ctrl+C).
std::string glob_to_regex(const std::string &glob)
Convert a glob pattern to an equivalent regex string.
AST node for expression-based file matching.
std::regex compiled
Pre-compiled regex (leaf nodes only).
std::unique_ptr< ExprNode > left
Left child (AND/OR) or sole child (NOT).
std::unique_ptr< ExprNode > right
Right child (AND/OR only).
bool evaluate(const std::string &path) const
Evaluate this expression node against a file path.
Token produced by the expression tokenizer.
enum ExprToken::Type type
Aggregates all runtime options parsed from the command line.
bool confirm
Prompt for confirmation before each command.
bool hidden
Include hidden (dot) files/directories.
std::string group_filter
Group name filter.
std::string shell_name
Shell argv[0] name.
std::string perm_filter
Octal permission string, e.g. "755".
bool stop_on_error
Halt on first command failure.
char type_filter
Type filter: 'f' file, 'd' directory, 'l' symlink.
std::string exclude_pattern
Regex (or glob, see glob_exclude) pattern to exclude files/dirs.
std::string expr_str
Expression filter string from –expr.
TimeFilter mtime_filter
Optional modification-time filter.
int max_depth
Max recursion depth (-1 = unlimited).
std::string user_filter
Owner username filter.
SizeFilter size_filter
Optional size filter.
std::string shell
Shell to use for command execution.
bool glob_exclude
If true (via -i/–glob-exclude), treat exclude pattern as a glob instead of regex.
bool dry_run
Print commands without executing.
bool verbose
Print commands before executing.
bool collect_all
If true (via -l/–list-all), collect all matched file paths and run one command with a combined argume...
bool glob
If true, treat search pattern as a glob instead of regex.
RegExMode mode
RegEx mode.
int jobs
Number of parallel jobs (1 = sequential).
Holds a parsed size filter with comparison operator and byte threshold.
CmpOp op
Comparison direction (equal, less-than, greater-than).
bool active
Whether this filter is enabled.
uintmax_t bytes
Size threshold in bytes.
Tracks execution statistics printed in the summary.
int files_matched
Number of entries that matched all filters.
int commands_failed
Number of commands that returned non-zero.
int commands_run
Number of commands executed (or printed in dry-run).
Holds a parsed modification-time filter with comparison operator and day count.
CmpOp op
Comparison direction.
int days
Age threshold in days.
bool active
Whether this filter is enabled.