├── .gitignore ├── .travis.yml ├── Makefile ├── README.md ├── ff.1 ├── ff.c └── fuzzy-find.el /.gitignore: -------------------------------------------------------------------------------- 1 | ff 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | 3 | compiler: 4 | - clang 5 | - gcc 6 | 7 | install: make 8 | 9 | script: ./ff -t 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ff: ff.c 2 | 3 | clean: 4 | rm -f ff *.o 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fuzzy-find: fuzzy completion for finding files # 2 | 3 | `ff` searches a directory tree with basic [fuzzy-completion][fc]. I wrote it 4 | because `find -name "blah"` only scans filenames (not their parent 5 | directories), and regular expressions for fuzzy completion are 6 | cumbersome. 7 | 8 | [fc]: http://common-lisp.net/project/slime/doc/html/Fuzzy-Completion.html 9 | 10 | Searching for "aeiou" will print any paths that match the RE 11 | `.*a.*e.*i.*o.*u.*`. 12 | 13 | By default, `ff` searches recursively from the current directory, but its 14 | search root can be set with the `-r` option. 15 | 16 | `ff` query strings are not regular expressions - characters such as 17 | '.' and '-' match literally. Any sections enclosed in '/'s 18 | will be required to match within the same path element, and the 19 | consecutive match character (default '=') toggles exact matching. 20 | (I chose '=' because it doesn't mean anything in basic REs and it's 21 | unshifted on most keyboards.) 22 | 23 | See also: [compound-completion][cc] 24 | 25 | [cc]: http://common-lisp.net/project/slime/doc/html/Compound-Completion.html 26 | 27 | 28 | ## Installation ## 29 | 30 | Just run make. I mean, it's one C file. The makefile is just `ff: ff.c`. 31 | Copy ff somewhere in your path. 32 | 33 | 34 | ## Example ## 35 | 36 | `ff aeiou` matches both `~/and/the/first/one/used.txt` 37 | and `~/after_the_furious_ultimatum.txt`, because the characters 'a', 'e', 38 | 'i', 'o', and 'u' appear sequentially. 39 | 40 | `ff a/e/i/o/u` only matches `~/and/the/first/one/used.txt`, since the `/`s force 41 | each vowel to appear in its own directory element. 42 | 43 | `ff ae=iou=` would only match `~/after_the_furious_ultimatum.txt`, since 44 | it matches an 'a', then an 'e', then the `=`s specify a *consecutive* "iou" string. 45 | 46 | 47 | ## Usage ## 48 | 49 | ff [-diltR] [-c char] [-n count] [-r root] query 50 | -c CHAR char to toggle Consecutive match (default: '=') 51 | -d show Dotfiles 52 | -D only show directories 53 | -h print this Help 54 | -i case-Insensitive search 55 | -l follow Links (warning: no cycle detection) 56 | -t run Tests and exit 57 | -r ROOT set search Root (default: .) 58 | -R don't recurse subdirectories 59 | 60 | # Build Status 61 | 62 | [![Build Status](https://travis-ci.org/silentbicycle/ff.png)](http://travis-ci.org/silentbicycle/ff) 63 | -------------------------------------------------------------------------------- /ff.1: -------------------------------------------------------------------------------- 1 | .TH FF 1 2 | .CT 1 files prog_other 3 | .SH NAME 4 | ff \- fuzzy-find: fuzzy completion for finding files 5 | .SH SYNOPSIS 6 | .B ff 7 | [ 8 | .BI \-dDhilRt 9 | ] 10 | [ 11 | .BI \-c 12 | .I char 13 | ] 14 | [ 15 | .BI \-n 16 | .I count 17 | ] 18 | [ 19 | .BI \-r 20 | .I root 21 | ] 22 | .I pattern 23 | .SH DESCRIPTION 24 | .I ff 25 | searches a directory tree with basic 26 | .I fuzzy-completion. 27 | I wrote it because 28 | .BI "find \-name \'blah\'" 29 | only scans filenames (not their parent directories), and 30 | regular expressions for fuzzy completion are cumbersome. 31 | 32 | Searching for 33 | .I "aeiou" 34 | will print any paths that match the re 35 | .BI '.*a.*e.*i.*o.*u.*'. 36 | 37 | By default, 38 | .I ff 39 | searches recursively from the current directory, but its 40 | search root can be set with the 41 | .I '-r' 42 | option. 43 | 44 | .BI ff 45 | query query strings are not regular expressions - characters such as 46 | .I '.' 47 | and 48 | .I '-' 49 | match literally. Any sections enclosed in 50 | .I '/'s 51 | will be required to match within the same path element, and the 52 | consecutive match character (default 53 | .I '=' 54 | ) toggles exact matching. 55 | (The default is '=' because it doesn't mean anything in 56 | basic REs and it's usually unshifted on keyboards.) 57 | 58 | .SH OPTIONS 59 | .TP 60 | .BI \-c " CHAR" 61 | char to toggle 62 | .I Consecutive 63 | match (default: 64 | .I '=' 65 | ) 66 | .TP 67 | .BI \-d 68 | show 69 | .I Dotfiles 70 | .TP 71 | .BI \-D 72 | only show 73 | .I Directories 74 | .TP 75 | .BI \-h 76 | show this 77 | .I Help 78 | .TP 79 | .BI \-i 80 | .I case-Insensitive 81 | search 82 | .TP 83 | .BI \-l 84 | follow 85 | .I Links 86 | (warning: no cycle detection) 87 | .TP 88 | .BI \-r " ROOT" 89 | set search 90 | .I Root 91 | (default: .) 92 | .TP 93 | .BI \-R 94 | don't recurse subdirectories 95 | .TP 96 | .BI \-t 97 | run 98 | .I Tests 99 | and exit 100 | 101 | .SH EXAMPLES 102 | .TP 103 | .EX 104 | ff aeiou 105 | .EE 106 | This matches both 107 | .BI '~/and/the/first/one/used.txt' 108 | and 109 | .BI '~/after_the_furious_ultimatum.txt', 110 | because the characters 'a', 'e', 'i', 'o', and 'u' appear sequentially. 111 | 112 | .TP 113 | .EX 114 | ff a/e/i/o/u 115 | .EE 116 | This only matches 117 | .BI '~/and/the/first/one/used.txt', 118 | since the 119 | .I '/'s 120 | require each vowel to appear in distinct directory elements. 121 | 122 | .TP 123 | .EX 124 | ff ae=iou= 125 | .EE 126 | This would only match 127 | .I '~/after_the_furious_ultimatum.txt', 128 | since it matches an 'a', then an 'e', then the '='s specify a 129 | .B consecutive 130 | "iou" string. (The second '=' toggles off the consecutive match, 131 | but is optional here since it is at the end of the pattern.) 132 | 133 | .SH SEE ALSO 134 | .IR find (1) 135 | .SH BUGS 136 | Not a web framework. 137 | -------------------------------------------------------------------------------- /ff.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Scott Vokes 3 | * 4 | * Permission to use, copy, modify, and/or distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #define FF_VERSION "0.6.0" 29 | 30 | static void bail(char *msg) { 31 | fprintf(stderr, "%s", msg); 32 | exit(EXIT_FAILURE); 33 | } 34 | 35 | static int dotfiles = 0; /* show dotfiles? */ 36 | static int only_dirs = 0; /* only show directories */ 37 | static char conseq_char = '='; /* consecutive match toggle char */ 38 | static int nocase = 0; /* case insensitive? */ 39 | static int links = 0; /* follow links? */ 40 | static int recurse = 1; /* search file tree recursively? */ 41 | 42 | static char rootbuf[FILENAME_MAX]; 43 | static char pathbuf[FILENAME_MAX]; 44 | 45 | static char *query = NULL; 46 | static int query_len = 0; 47 | 48 | static void usage() { 49 | fprintf(stderr, 50 | "fuzzy-finder v. %s, by Scott Vokes \n" 51 | "usage: ff [-dhiltR] [-c char] [-n count] [-r root] query\n" 52 | "-c CHAR char to toggle Consecutive match (default: '=')\n" 53 | "-d show Dotfiles\n" 54 | "-D only show directories\n" 55 | "-h print this Help\n" 56 | "-i case-Insensitive search\n" 57 | "-l follow Links\n" 58 | "-t run Tests and exit\n" 59 | "-r ROOT set search Root (default: .)\n" 60 | "-R don't recurse subdirectories\n", FF_VERSION); 61 | exit(EXIT_FAILURE); 62 | } 63 | 64 | /* Append a name element to the path buffer. */ 65 | static uint put_path(uint offset, const char *elt, int dir) { 66 | uint sz = FILENAME_MAX - offset; 67 | uint res = snprintf(pathbuf + offset, sz, "%s%s", 68 | elt, dir ? "/" : ""); 69 | if (FILENAME_MAX < res) { 70 | fprintf(stderr, "snprintf error\n"); 71 | exit(EXIT_FAILURE); 72 | } 73 | 74 | /* If it ends with "//" then drop the second '/'. */ 75 | if (offset + res >= 2 && pathbuf[offset + res - 2] == '/') { 76 | res--; 77 | pathbuf[offset + res] = '\0'; 78 | } 79 | 80 | return res; 81 | } 82 | 83 | #define IS_DIR(d) (d->d_type == DT_DIR) 84 | #define IS_LINK(d) (d->d_type == DT_LNK) 85 | 86 | /* Try to sequentially match the next characters of the query against 87 | * a filename, returning the endpoint in the query. If the query 88 | * contains the consecutive-match toggle character (def. '='), then 89 | * the following characters (until another '=') need to be matched 90 | * as a conecutive group. 91 | * For example, "aeiou" matches "abefijopuv", but "a=eio=u" does not.*/ 92 | static uint match_chars(const char *name, uint nlen, uint qo) { 93 | uint i = 0; 94 | int exact = 0; 95 | char c = '\0'; 96 | 97 | while (i < nlen) { 98 | if (query[qo] != conseq_char) { /* look for indiv. chars */ 99 | c = nocase ? tolower(name[i]) : name[i]; 100 | i++; 101 | if (query[qo] == c) { 102 | qo++; 103 | if (qo == query_len) break; 104 | } 105 | } else { /* look for consecutive chars */ 106 | int old_qo = qo; 107 | advance: 108 | qo++; 109 | if (qo == query_len) return qo; /* done */ 110 | if (query[qo] == conseq_char) { /* done w/ consecutive match */ 111 | qo++; 112 | continue; 113 | } 114 | c = nocase ? tolower(name[i]) : name[i]; 115 | i++; 116 | if (c != query[qo]) { 117 | qo = old_qo; 118 | continue; 119 | } 120 | goto advance; 121 | } 122 | } 123 | return qo; 124 | } 125 | 126 | /* Incrementally match the query string against the file tree. 127 | * Sections of the query surrounded by '/'s must all match within 128 | * the same path element: "d/ex/" matches "dev/example/foo", but 129 | * not "dev/eta/text". */ 130 | static void walk(const char *path, uint po, 131 | uint qo) { 132 | DIR *dir = opendir(path); 133 | struct dirent *dp = NULL; 134 | int i = 0; 135 | int npo = 0, nqo = 0; /* new path and query offsets */ 136 | int expects_dir = 0; 137 | int is_dir = 0; 138 | 139 | if (dir == NULL) { 140 | perror(path); 141 | errno = 0; 142 | return; 143 | } 144 | 145 | /* If the rest of the query has any '/'s, then the preceding portion 146 | * must be completely matched by the next directory name. */ 147 | for (i=qo; id_name; 153 | uint nlen = strlen(name); 154 | 155 | /* Skip empty names, dotfiles, and links. */ 156 | if (nlen == 0) continue; 157 | if (name[0] == '.') { 158 | if (!dotfiles) continue; 159 | if (name[1] == '\0' || 0 == strcmp(name, "..")) continue; 160 | } 161 | if (!links && IS_LINK(dp)) continue; 162 | 163 | nqo = match_chars(name, nlen, qo); 164 | is_dir = IS_DIR(dp); 165 | npo = put_path(po, name, is_dir) + po; 166 | 167 | /* If this is a directory that doesn't completely match 168 | * to the next '/', skip, unless at start of the query. */ 169 | if (expects_dir && nqo > 0 && query[nqo] != '/' && is_dir) 170 | continue; 171 | 172 | /* Check for trailing / in pattern. Do this *before* 173 | * printing the path, in case the pattern ends in '/'. */ 174 | if (is_dir && query[nqo] == '/') nqo++; 175 | 176 | /* Print complete matches. */ 177 | if (nqo == query_len) { 178 | if (!only_dirs || is_dir) { 179 | printf("%s\n", pathbuf); 180 | } 181 | } 182 | 183 | /* Walk subdirectories, checking from new query offset. */ 184 | if (is_dir && recurse) walk(pathbuf, npo, nqo); 185 | } 186 | 187 | if (closedir(dir) == -1) { 188 | perror("Closedir failure"); 189 | exit(EXIT_FAILURE); 190 | } 191 | } 192 | 193 | 194 | /********************** 195 | * Arguments and main * 196 | **********************/ 197 | 198 | static void run_tests(); 199 | 200 | static void set_root(const char *path) { 201 | if (path == NULL) bail("Bad root path\n"); 202 | if (path[0] == '~') { /* properly expand ~ */ 203 | const char *home = getenv("HOME"); 204 | if (home == NULL) bail("Failed to get $HOME\n"); 205 | 206 | if (FILENAME_MAX < snprintf(rootbuf, FILENAME_MAX, 207 | "%s/%s", home, path + 1)) { 208 | bail("error: path longer than FILENAME_MAX\n"); 209 | } 210 | } else { 211 | strncpy(rootbuf, path, FILENAME_MAX); 212 | rootbuf[FILENAME_MAX-1] = '\0'; 213 | } 214 | } 215 | 216 | /* Process args, return root path (or NULL). */ 217 | static void proc_args(int argc, char **argv) { 218 | uint i = 0; 219 | int a = 0; 220 | 221 | while ((a = getopt(argc, argv, "c:dDhilr:tR")) != -1) { 222 | switch (a) { 223 | case 'c': /* set consecutive match char */ 224 | conseq_char = optarg[0]; break; 225 | case 'd': /* show dotfiles */ 226 | dotfiles = 1; break; 227 | case 'D': /* only print directories */ 228 | only_dirs = 1; break; 229 | case 'h': /* help */ 230 | usage(); break; 231 | case 'i': /* case-insensitive */ 232 | nocase = 1; break; 233 | case 'l': /* follow links */ 234 | links = 1; break; 235 | case 'r': /* set search root */ 236 | set_root(optarg); 237 | break; 238 | case 't': /* run tests and exit */ 239 | run_tests(); break; 240 | case 'R': 241 | recurse = 0; break; 242 | default: 243 | fprintf(stderr, "ff: illegal option: -- %c\n", a); 244 | usage(); 245 | } 246 | } 247 | 248 | argc -= optind; 249 | argv += optind; 250 | if (argc < 1) usage(); 251 | query = argv[0]; 252 | } 253 | 254 | int main(int argc, char **argv) { 255 | char *root = rootbuf; 256 | 257 | proc_args(argc, argv); 258 | if (rootbuf[0] == '\0') { 259 | root = getcwd(rootbuf, FILENAME_MAX); 260 | if (root == NULL) bail("Could not get current working directory.\n"); 261 | } 262 | 263 | if (query == NULL) bail("Bad query\n"); 264 | 265 | query_len = strlen(query); 266 | 267 | if (nocase) { 268 | int i; 269 | for (i=0; i 4 | ;; 5 | ;; This file is not part of Emacs. In fact, it's ISC licensed. 6 | ;; 7 | ;; Permission to use, copy, modify, and/or distribute this software for 8 | ;; any purpose with or without fee is hereby granted, provided that the 9 | ;; above copyright notice and this permission notice appear in all 10 | ;; copies. 11 | ;; 12 | ;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 13 | ;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 14 | ;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 15 | ;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 16 | ;; DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR 17 | ;; PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 18 | ;; TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 19 | ;; PERFORMANCE OF THIS SOFTWARE. 20 | ;; 21 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 22 | ;; 23 | ;; Simple Emacs wrapper for fuzzy-find. 24 | ;; 25 | ;; Usage: 26 | ;; 27 | ;; M-x find-file-fuzzily 28 | ;; M-x find-file-fuzzily-with-root-path (to specify search root) 29 | ;; 30 | ;; Those will start an asynchronous fuzzy-find process and begin 31 | ;; populating a results buffer. Pressing enter on a filename will 32 | ;; find-file ('o' to find-file-other-window), and 'q' kills the buffer 33 | ;; and process. 34 | ;; 35 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 36 | 37 | (defvar fuzzy-find-program-name "ff" 38 | "Name of the fuzzy-find executable.") 39 | 40 | (defvar fuzzy-find-result-buffer-name "*fuzzy-find-results*") 41 | 42 | (defvar fuzzy-find-consecutive-match-char ?= 43 | "Character used to toggle the consecutive-match flag.") 44 | 45 | (defun fuzzy-find-kill-process () 46 | "Kill the currently running fuzzy-find process and result buffer, if any." 47 | (kill-buffer fuzzy-find-result-buffer-name) 48 | (kill-process "fuzzy-find")) 49 | 50 | (defun fuzzy-find (query root) 51 | "Search with fuzzy-find. 52 | For interactive use, use find-file-fuzzily instead." 53 | (let ((buf (get-buffer fuzzy-find-result-buffer-name))) 54 | (when buf (kill-buffer buf)) 55 | (let ((proc (start-process "fuzzy-find" 56 | fuzzy-find-result-buffer-name 57 | fuzzy-find-program-name 58 | "-c" (char-to-string 59 | fuzzy-find-consecutive-match-char) 60 | "-r" root 61 | query)) 62 | (buf (get-buffer fuzzy-find-result-buffer-name))) 63 | (with-current-buffer buf 64 | (local-set-key (kbd "RET") 'ffap) 65 | (local-set-key (kbd "o") 'ffap-other-window) 66 | (local-set-key (kbd "q") 'fuzzy-find-kill-process) 67 | (toggle-read-only 1)) 68 | (switch-to-buffer-other-window buf)))) 69 | 70 | (defun find-file-fuzzily (query) 71 | "Search with fuzzy-find." 72 | (interactive "sQuery: ") 73 | (fuzzy-find query ".")) 74 | 75 | (defun find-file-fuzzily-with-root-path (root query) 76 | "Search with fuzzy-find, with an explicit root directory." 77 | (interactive "GRoot: \nsQuery: ") 78 | (fuzzy-find query root)) 79 | 80 | (provide 'fuzzy-find) 81 | --------------------------------------------------------------------------------