Download
133 lines 4.3 KiBCModified 2026-01-25 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
#define _GNU_SOURCE
#include <dirent.h>
#include <fnmatch.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "argparse.h"

const char *program_name = "wheres";
static const char *USAGE =
    "Usage:\n"
    "   patterns or paths...: patterns to search for or paths to search for them in\n"
    "   --help,-h: print this help message\n"
    "   --dir,-d <dir>: search in a given directory\n"
    "   --ignorecase,-i: search case-insensitively\n"
    "   --type,-t <type>: search for (f)iles, (d)irectories, (l)inks, (p)ipes, (s)ockets, (b)lock "
    "devices, (c)haracter devices\n"
    "   --nul,-0: print a nul byte instead of a newline after each match\n";

#ifndef streq
#define streq(str, target) (strcmp(str, target) == 0)
#endif

static int pat_flags = FNM_PATHNAME;
static const char *type = NULL;
static char print_separator = '\n';

static inline bool matches_any_pattern(const char *name, int patc, const char *patv[patc]) {
    if (patc == 0) return true;
    for (int i = 0; i < patc; i++) {
        if (fnmatch(patv[i], name, pat_flags) == 0) {
            return true;
        }
    }
    return false;
}

static inline bool is_valid_type(struct dirent *entry) {
    if (type == NULL) return true;
    switch (entry->d_type) {
    case DT_FIFO: return strchr(type, 'p') != NULL;
    case DT_CHR: return strchr(type, 'c') != NULL;
    case DT_DIR: return strchr(type, 'd') != NULL;
    case DT_BLK: return strchr(type, 'b') != NULL;
    case DT_REG: return strchr(type, 'f') != NULL;
    case DT_LNK: return strchr(type, 'l') != NULL;
    case DT_SOCK: return strchr(type, 's') != NULL;
    case DT_WHT: return strchr(type, 'w') != NULL;
    default: return false;
    }
}

void find(const char *path, int patc, const char *patv[patc]) {
    DIR *dir = opendir(path);
    if (!dir) return;

    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) {
        const char *name = entry->d_name;
        if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) {
            // Skip '.' and '..'
            continue;
        }

        if (is_valid_type(entry) && matches_any_pattern(name, patc, patv)) {
            dprintf(STDOUT_FILENO, "%s/%s%c", path, name, print_separator);
        }

        if (entry->d_type == DT_DIR) {
            char new_path[PATH_MAX];
            snprintf(new_path, sizeof(new_path), "%s/%s", streq(path, "/") ? "" : path, name);
            find(new_path, patc, patv);
        }
    }
    closedir(dir);
}

int main(int argc, char *argv[]) {
    program_name = argv[0];
    int num_dirs = 0;
    // This array is guaranteed to be big enough, since argc >= 1, and each argument
    // adds at most one search dir.
    const char *dirs[argc] = {};
    int num_pats = 0;
    const char *pats[argc] = {};
    for (int i = 1; i < argc;) {
        const char *dir;
        const char *new_type;
        if (streq(argv[i], "--")) {
            break;
        } else if ((dir = pop_str_arg(program_name, USAGE, "--dir", 'd', &i, argc, argv))) {
            dirs[num_dirs++] = dir;
        } else if (pop_bool_arg("--ignorecase", 'i', &i, argc, argv)) {
            pat_flags |= FNM_CASEFOLD;
        } else if (pop_bool_arg("--help", 'h', &i, argc, argv)) {
            printf("%s - find files and directories\n\n%s", program_name, USAGE);
            return 0;
        } else if ((new_type = pop_str_arg(program_name, USAGE, "--type", 't', &i, argc, argv)) != NULL) {
            type = new_type;
            continue;
        } else if (pop_bool_arg("--nul", '0', &i, argc, argv)) {
            print_separator = '\0';
        } else if (strchr(argv[i], '/') || streq(argv[i], "..") || streq(argv[i], ".") || streq(argv[i], "~")) {
            dirs[num_dirs++] = argv[i];
            i += 1;
        } else if (argv[i][0] == '-') {
            USAGE_ERROR(program_name, USAGE, "Unrecognized argument: %s", argv[i]);
        } else {
            pats[num_pats++] = argv[i];
            i += 1;
        }
    }

    if (num_dirs == 0) {
        dirs[num_dirs++] = ".";
    }

    for (int i = 0; i < num_dirs; i++) {
        static char real[PATH_MAX];
        char *path = realpath(dirs[i], real);
        if (!path) {
            fprintf(stderr, "Not a valid path: %s", dirs[i]);
            exit(1);
        }
        find(path, num_pats, pats);
        if (path != real) free(path);
    }

    return 0;
}