3 * selector is a simple command line utility for selection of strings
4 * with a dynamic pattern-matching.
6 * Copyright (c) 2009, 2010 Francois Fleuret
7 * Written by Francois Fleuret <francois@fleuret.org>
9 * This file is part of selector.
11 * selector is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License version 3 as
13 * published by the Free Software Foundation.
15 * selector is distributed in the hope that it will be useful, but
16 * WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 * General Public License for more details.
20 * You should have received a copy of the GNU General Public License
21 * along with selector. If not, see <http://www.gnu.org/licenses/>.
27 To use it as a super-history-search for bash:
28 selector -q -b -i -d -v -w -l ${HISTSIZE} <(history)
41 #include <sys/ioctl.h>
49 #define BUFFER_SIZE 4096
51 /* Yeah, global variables! */
53 int nb_lines_max = 1000;
54 char pattern_separator = ';';
55 char label_separator = '\0';
56 int output_to_vt_buffer = 0;
57 int add_control_qs = 0;
61 int inverse_order = 0;
62 int remove_duplicates = 0;
64 int case_sensitive = 0;
68 int attr_modeline, attr_focus_line, attr_error;
70 /********************************************************************/
72 /* malloc with error checking. */
74 void *safe_malloc(size_t n) {
77 printf("Can not allocate memory: %s\n", strerror(errno));
83 /*********************************************************************/
85 void inject_into_tty_buffer(char *string, int add_control_qs) {
86 struct termios oldtio, newtio;
88 const char control_q = '\021';
89 tcgetattr(STDIN_FILENO, &oldtio);
90 memset(&newtio, 0, sizeof(newtio));
91 /* Set input mode (non-canonical, *no echo*,...) */
92 tcsetattr(STDIN_FILENO, TCSANOW, &newtio);
93 /* Put the selected string in the tty input buffer */
94 for(k = string; *k; k++) {
95 if(add_control_qs && !(*k >= ' ' && *k <= '~')) {
96 /* Add ^Q to quote control characters */
97 ioctl(STDIN_FILENO, TIOCSTI, &control_q);
99 ioctl(STDIN_FILENO, TIOCSTI, k);
101 /* Restore the old settings */
102 tcsetattr(STDIN_FILENO, TCSANOW, &oldtio);
105 /*********************************************************************/
107 void check_opt(int argc, char **argv, int n_opt, int n, const char *help) {
108 if(n_opt + n >= argc) {
109 fprintf(stderr, "Selector: Missing argument for %s, expecting %s.\n",
115 int string_to_positive_integer(char *string) {
121 for(s = string; *s && *s != ','; s++) {
122 if(*s >= '0' && *s <= '9') {
123 result = result * 10 + (int) (*s - '0');
130 "Selector: Value `%s' is not a positive integer.\n",
138 void error_feedback() {
146 void print_help(FILE *out) {
148 fprintf(out, "Selector version %s (%s)\n", VERSION, UNAME);
149 fprintf(out, "Written by Francois Fleuret <francois@fleuret.org>.\n");
151 fprintf(out, "Usage: selector [options] [<filename1> [<filename2> ...]]\n");
153 fprintf(out, " -h show this help\n");
154 fprintf(out, " -v inject the selected line in the tty\n");
155 fprintf(out, " -w quote control characters with ^Qs when using -v\n");
156 fprintf(out, " -d remove duplicated lines\n");
157 fprintf(out, " -b remove the bash history line prefix\n");
158 fprintf(out, " -z remove the zsh history line prefix\n");
159 fprintf(out, " -i invert the order of lines\n");
160 fprintf(out, " -e start in regexp mode\n");
161 fprintf(out, " -a start in case sensitive mode\n");
162 fprintf(out, " -m monochrome mode\n");
163 fprintf(out, " -q make a flash instead of a beep on an edition error\n");
164 fprintf(out, " -- all following arguments are filenames\n");
165 fprintf(out, " -t <title>\n");
166 fprintf(out, " add a title in the modeline\n");
167 fprintf(out, " -c <fg modeline> <bg modeline> <fg highlight> <bg highlight>\n");
168 fprintf(out, " set the display colors\n");
169 fprintf(out, " -o <output filename>\n");
170 fprintf(out, " set a file to write the selected line to\n");
171 fprintf(out, " -s <pattern separator>\n");
172 fprintf(out, " set the symbol to separate substrings in the pattern\n");
173 fprintf(out, " -x <label separator>\n");
174 fprintf(out, " set the symbol to terminate the label\n");
175 fprintf(out, " -l <max number of lines>\n");
176 fprintf(out, " set the maximum number of lines to take into account\n");
180 /*********************************************************************/
182 /* A quick and dirty hash table */
184 /* The table itself stores indexes of the strings taken in a char**
185 table. When a string is added, if it was already in the table, the
186 new index replaces the previous one. */
188 struct hash_table_t {
193 struct hash_table_t *new_hash_table(int size) {
195 struct hash_table_t *hash_table;
197 hash_table = safe_malloc(sizeof(struct hash_table_t));
199 hash_table->size = size;
200 hash_table->entries = safe_malloc(hash_table->size * sizeof(int));
202 for(k = 0; k < hash_table->size; k++) {
203 hash_table->entries[k] = -1;
209 void free_hash_table(struct hash_table_t *hash_table) {
210 free(hash_table->entries);
214 /* Adds new_string in the table, associated to new_index. If this
215 string was not already in the table, returns -1. Otherwise, returns
216 the previous index it had. */
218 int add_and_get_previous_index(struct hash_table_t *hash_table,
219 const char *new_string, int new_index,
222 unsigned int code = 0;
225 /* This is my recipe. I checked, it seems to work (as long as
226 hash_table->size is not a multiple of 387433 that should be
229 for(k = 0; new_string[k]; k++) {
230 code = code * 387433 + (unsigned int) (new_string[k]);
233 code = code % hash_table->size;
235 while(hash_table->entries[code] >= 0) {
236 /* There is a string with that code */
237 if(strcmp(new_string, strings[hash_table->entries[code]]) == 0) {
238 /* It is the same string, we keep a copy of the stored index */
239 int result = hash_table->entries[code];
240 /* Put the new one */
241 hash_table->entries[code] = new_index;
242 /* And return the previous one */
245 /* This collision was not the same string, let's move to the next
247 code = (code + 1) % hash_table->size;
250 /* This string was not already in there, store the index in the
251 table and return -1 */
253 hash_table->entries[code] = new_index;
257 /*********************************************************************
258 A matcher matches either with a collection of substrings, or with a
266 char *splitted_patterns, **patterns;
269 int match(char *string, matcher_t *matcher) {
271 if(matcher->nb_patterns >= 0) {
272 if(matcher->case_sensitive) {
273 for(n = 0; n < matcher->nb_patterns; n++) {
274 if(strstr(string, matcher->patterns[n]) == 0) return 0;
277 for(n = 0; n < matcher->nb_patterns; n++) {
278 if(strcasestr(string, matcher->patterns[n]) == 0) return 0;
283 return regexec(&matcher->preg, string, 0, 0, 0) == 0;
287 void free_matcher(matcher_t *matcher) {
288 if(matcher->nb_patterns < 0) {
289 if(!matcher->regexp_error) regfree(&matcher->preg);
291 free(matcher->splitted_patterns);
292 free(matcher->patterns);
296 void initialize_matcher(int use_regexp, int case_sensitive,
297 matcher_t *matcher, const char *pattern) {
299 char *t, *last_pattern_start;
303 matcher->nb_patterns = -1;
304 matcher->regexp_error = regcomp(&matcher->preg, pattern,
305 case_sensitive ? 0 : REG_ICASE);
307 matcher->regexp_error = 0;
308 matcher->nb_patterns = 1;
309 matcher->case_sensitive = case_sensitive;
311 for(s = pattern; *s; s++) {
312 if(*s == pattern_separator) {
313 matcher->nb_patterns++;
317 matcher->splitted_patterns =
318 safe_malloc((strlen(pattern) + 1) * sizeof(char));
321 safe_malloc(matcher->nb_patterns * sizeof(char *));
323 strcpy(matcher->splitted_patterns, pattern);
326 last_pattern_start = matcher->splitted_patterns;
327 for(t = matcher->splitted_patterns; n < matcher->nb_patterns; t++) {
328 if(*t == pattern_separator || *t == '\0') {
330 matcher->patterns[n++] = last_pattern_start;
331 last_pattern_start = t + 1;
337 /*********************************************************************
340 void delete_char(char *buffer, int *position) {
341 if(buffer[*position]) {
343 while(c < BUFFER_SIZE && buffer[c]) {
344 buffer[c] = buffer[c+1];
347 } else error_feedback();
350 void backspace_char(char *buffer, int *position) {
352 if(buffer[*position]) {
353 int c = *position - 1;
355 buffer[c] = buffer[c+1];
359 buffer[*position - 1] = '\0';
363 } else error_feedback();
366 void insert_char(char *buffer, int *position, char character) {
367 if(strlen(buffer) < BUFFER_SIZE - 1) {
369 char t = buffer[c], u;
378 buffer[(*position)++] = character;
379 } else error_feedback();
382 void kill_before_cursor(char *buffer, int *position) {
384 while(buffer[*position + s]) {
385 buffer[s] = buffer[*position + s];
392 void kill_after_cursor(char *buffer, int *position) {
393 buffer[*position] = '\0';
396 /*********************************************************************/
398 int previous_visible(int current_line, int nb_lines, char **lines,
399 matcher_t *matcher) {
400 int line = current_line - 1;
401 while(line >= 0 && !match(lines[line], matcher)) line--;
405 int next_visible(int current_line, int nb_lines, char **lines,
406 matcher_t *matcher) {
407 int line = current_line + 1;
408 while(line < nb_lines && !match(lines[line], matcher)) line++;
416 /*********************************************************************/
418 /* The line highlighted is the first one matching the matcher in that
419 order: (1) current_focus_line after motion, if it does not match,
420 then (2) the first with a greater index, if none matches, then (3)
421 the first with a lesser index.
423 The index of the line actually shown highlighted is written in
424 displayed_focus_line (it can be -1 if no line at all matches the
427 If there is a motion and a line is actually shown highlighted, its
428 value is written in current_focus_line. */
430 void update_screen(int *current_focus_line, int *displayed_focus_line,
432 int nb_lines, char **lines,
436 char buffer[BUFFER_SIZE];
439 int console_width, console_height;
440 int nb_printed_lines = 0;
443 initialize_matcher(use_regexp, case_sensitive, &matcher, pattern);
445 console_width = getmaxx(stdscr);
446 console_height = getmaxy(stdscr);
448 use_default_colors();
450 /* Add an empty line where we will print the modeline at the end */
454 /* If the regexp is erroneous, print a message saying so */
456 if(matcher.regexp_error) {
458 addnstr("Regexp syntax error", console_width);
462 /* Else, and we do have lines to select from, find a visible line. */
464 else if(nb_lines > 0) {
466 if(match(lines[*current_focus_line], &matcher)) {
467 new_focus_line = *current_focus_line;
469 new_focus_line = next_visible(*current_focus_line, nb_lines, lines,
471 if(new_focus_line < 0) {
472 new_focus_line = previous_visible(*current_focus_line, nb_lines, lines,
477 /* If we found a visible line and we should move, let's move */
479 if(new_focus_line >= 0 && motion != 0) {
480 int l = new_focus_line;
482 /* We want to go down, let's find the first visible line below */
483 for(m = 0; l >= 0 && m < motion; m++) {
484 l = next_visible(l, nb_lines, lines, &matcher);
490 /* We want to go up, let's find the first visible line above */
491 for(m = 0; l >= 0 && m < -motion; m++) {
492 l = previous_visible(l, nb_lines, lines, &matcher);
500 /* Here new_focus_line is either a line number matching the
503 if(new_focus_line >= 0) {
505 int first_line = new_focus_line, last_line = new_focus_line;
508 /* We find the first and last line to show, so that the total of
509 visible lines between them (them included) is
512 while(nb_match < console_height-1 &&
513 (first_line > 0 || last_line < nb_lines - 1)) {
517 while(first_line > 0 && !match(lines[first_line], &matcher)) {
520 if(match(lines[first_line], &matcher)) {
525 if(nb_match < console_height - 1 && last_line < nb_lines - 1) {
527 while(last_line < nb_lines - 1 && !match(lines[last_line], &matcher)) {
531 if(match(lines[last_line], &matcher)) {
537 /* Now we display them */
539 for(l = first_line; l <= last_line; l++) {
540 if(match(lines[l], &matcher)) {
543 while(lines[l][k] && k < BUFFER_SIZE - 2 && k < console_width - 2) {
544 buffer[k] = lines[l][k];
548 /* We fill the rest of the line with blanks if this is the
551 if(l == new_focus_line) {
552 while(k < console_width) {
562 /* Highlight the highlighted line ... */
564 if(l == new_focus_line) {
565 attron(attr_focus_line);
566 addnstr(buffer, console_width);
567 attroff(attr_focus_line);
569 addnstr(buffer, console_width);
576 /* If we are on a focused line and we moved, this become the new
580 *current_focus_line = new_focus_line;
584 *displayed_focus_line = new_focus_line;
586 if(nb_printed_lines == 0) {
588 addnstr("No selection", console_width);
593 /* Else, print a message saying that there are no lines to select from */
597 addnstr("Empty choice", console_width);
603 /* Draw the modeline */
607 attron(attr_modeline);
609 for(k = 0; k < console_width; k++) buffer[k] = ' ';
610 buffer[console_width] = '\0';
611 addnstr(buffer, console_width);
615 /* There must be a more elegant way of moving the cursor at a
616 location met during display */
623 cursor_x += strlen(title) + 1;
626 sprintf(buffer, "%d/%d ", nb_printed_lines, nb_lines);
628 cursor_x += strlen(buffer);
630 addnstr(pattern, cursor_position);
631 cursor_x += cursor_position;
633 if(pattern[cursor_position]) {
634 addstr(pattern + cursor_position);
639 /* Add a few info about the mode we are in (regexp and/or case
642 if(use_regexp || case_sensitive) {
659 attroff(attr_modeline);
664 free_matcher(&matcher);
667 /*********************************************************************/
669 void store_line(struct hash_table_t *hash_table,
670 const char *new_line,
671 int *nb_lines, char **lines) {
674 /* Remove the zsh history prefix */
676 if(zsh_history && *new_line == ':') {
677 while(*new_line && *new_line != ';') new_line++;
678 if(*new_line == ';') new_line++;
681 /* Remove the bash history prefix */
684 while(*new_line == ' ') new_line++;
685 while(*new_line >= '0' && *new_line <= '9') new_line++;
686 while(*new_line == ' ') new_line++;
689 /* Check for duplicates with the hash table and insert the line in
690 the list if necessary */
693 dup = add_and_get_previous_index(hash_table,
694 new_line, *nb_lines, lines);
700 lines[*nb_lines] = safe_malloc((strlen(new_line) + 1) * sizeof(char));
701 strcpy(lines[*nb_lines], new_line);
703 /* The string was already in there, so we do not allocate a new
704 string but use the pointer to the first occurence of it */
705 lines[*nb_lines] = lines[dup];
712 void read_file(struct hash_table_t *hash_table,
713 const char *input_filename,
714 int nb_lines_max, int *nb_lines, char **lines) {
716 char raw_line[BUFFER_SIZE];
717 int start, end, eol, k;
720 file = fopen(input_filename, "r");
723 fprintf(stderr, "Selector: Can not open `%s'.\n", input_filename);
730 while(*nb_lines < nb_lines_max && (end > start || !feof(file))) {
733 /* Look for the end of a line in what is already in the buffer */
734 while(eol < end && raw_line[eol] != '\n') eol++;
736 /* if we did not find the of a line, move what has not been
737 processed and is in the buffer to the beginning of the buffer,
738 fill the buffer with new data from the file, and look for the
741 for(k = 0; k < end - start; k++) {
742 raw_line[k] = raw_line[k + start];
747 end += fread(raw_line + end, sizeof(char), BUFFER_SIZE - end, file);
748 while(eol < end && raw_line[eol] != '\n') eol++;
751 /* The end of the line is the buffer size, which means the line is
754 if(eol == BUFFER_SIZE) {
755 raw_line[BUFFER_SIZE - 1] = '\0';
756 fprintf(stderr, "Selector: Line too long (max is %d characters):\n",
758 fprintf(stderr, raw_line);
759 fprintf(stderr, "\n");
763 /* If we got a line, we replace the carriage return by a \0 to
766 raw_line[eol] = '\0';
768 store_line(hash_table, raw_line + start,
777 /*********************************************************************/
779 static struct option long_options[] = {
780 { "output-file", 1, 0, 'o' },
781 { "pattern-separator", 1, 0, 's' },
782 { "label-separator", 1, 0, 'x' },
783 { "inject-in-tty", no_argument, 0, 'v' },
784 { "add-control-qs", no_argument, 0, 'w' },
785 { "monochrome", no_argument, 0, 'm' },
786 { "no-beep", no_argument, 0, 'q' },
787 { "revert-order", no_argument, 0, 'i' },
788 { "remove-bash-prefix", no_argument, 0, 'b' },
789 { "remove-zsh-prefix", no_argument, 0, 'z' },
790 { "remove-duplicates", no_argument, 0, 'd' },
791 { "regexp", no_argument, 0, 'e' },
792 { "case-sensitive", no_argument, 0, 'a' },
793 { "title", 1, 0, 't' },
794 { "number-of-lines", 1, 0, 'l' },
795 { "colors", 1, 0, 'c' },
796 { "rest-are-files", no_argument, 0, '-' },
797 { "help", no_argument, 0, 'h' },
801 int main(int argc, char **argv) {
803 char output_filename[BUFFER_SIZE];
805 char pattern[BUFFER_SIZE];
808 int error = 0, show_help = 0;
809 int rest_are_files = 0;
811 int current_focus_line, displayed_focus_line;
813 int color_fg_modeline, color_bg_modeline;
814 int color_fg_highlight, color_bg_highlight;
816 char **lines, **labels;
818 struct hash_table_t *hash_table;
820 if(!isatty(STDIN_FILENO)) {
821 fprintf(stderr, "Selector: The standard input is not a tty.\n");
825 color_fg_modeline = COLOR_WHITE;
826 color_bg_modeline = COLOR_BLACK;
827 color_fg_highlight = COLOR_BLACK;
828 color_bg_highlight = COLOR_YELLOW;
830 setlocale(LC_ALL, "");
832 strcpy(output_filename, "");
834 while (!rest_are_files &&
835 (c = getopt_long(argc, argv, "o:s:x:vwmqf:ibzdeat:l:c:-h",
836 long_options, NULL)) != -1) {
841 strncpy(output_filename, optarg, BUFFER_SIZE);
845 pattern_separator = optarg[0];
849 label_separator = optarg[0];
853 output_to_vt_buffer = 1;
881 remove_duplicates = 1;
894 title = safe_malloc((strlen(optarg) + 1) * sizeof(char));
895 strcpy(title, optarg);
899 nb_lines_max = string_to_positive_integer(optarg);
904 color_fg_modeline = string_to_positive_integer(s);
905 while(*s && *s != ',') s++; if(*s == ',') { s++; }
906 color_bg_modeline = string_to_positive_integer(s);
907 while(*s && *s != ',') s++; if(*s == ',') { s++; }
908 color_fg_highlight = string_to_positive_integer(s);
909 while(*s && *s != ',') s++; if(*s == ',') { s++; }
910 color_bg_highlight = string_to_positive_integer(s);
927 if(show_help || error) {
937 lines = safe_malloc(nb_lines_max * sizeof(char *));
941 if(remove_duplicates) {
942 hash_table = new_hash_table(nb_lines_max * 10);
947 while(optind < argc) {
948 read_file(hash_table,
950 nb_lines_max, &nb_lines, lines);
955 free_hash_table(hash_table);
958 /* Now remove the null strings */
961 for(k = 0; k < nb_lines; k++) {
963 lines[n++] = lines[k];
970 for(l = 0; l < nb_lines / 2; l++) {
971 char *s = lines[nb_lines - 1 - l];
972 lines[nb_lines - 1 - l] = lines[l];
977 /* Build the labels from the strings, take only the part before the
978 label_separator and transform control characters to printable
981 labels = safe_malloc(nb_lines * sizeof(char *));
983 for(l = 0; l < nb_lines; l++) {
989 while(*t && *t != label_separator) {
994 labels[l] = safe_malloc((e + 1) * sizeof(char));
997 while(*t && *t != label_separator) {
999 while(*u) { *s++ = *u++; }
1006 cursor_position = 0;
1008 /* Here we start to display with curse */
1013 intrflush(stdscr, FALSE);
1015 /* So that the arrow keys work */
1016 keypad(stdscr, TRUE);
1018 attr_error = A_STANDOUT;
1019 attr_modeline = A_REVERSE;
1020 attr_focus_line = A_STANDOUT;
1022 if(with_colors && has_colors()) {
1026 if(color_fg_modeline < 0 || color_fg_modeline >= COLORS ||
1027 color_bg_modeline < 0 || color_bg_modeline >= COLORS ||
1028 color_fg_highlight < 0 || color_bg_highlight >= COLORS ||
1029 color_bg_highlight < 0 || color_bg_highlight >= COLORS) {
1032 fprintf(stderr, "Selector: Color numbers have to be between 0 and %d.\n",
1037 init_pair(1, color_fg_modeline, color_bg_modeline);
1038 attr_modeline = COLOR_PAIR(1);
1040 init_pair(2, color_fg_highlight, color_bg_highlight);
1041 attr_focus_line = COLOR_PAIR(2);
1043 init_pair(3, COLOR_WHITE, COLOR_RED);
1044 attr_error = COLOR_PAIR(3);
1048 current_focus_line = 0;
1049 displayed_focus_line = 0;
1051 update_screen(¤t_focus_line, &displayed_focus_line,
1053 nb_lines, labels, cursor_position, pattern);
1060 if(key >= ' ' && key <= '~') { /* Insert character */
1061 insert_char(pattern, &cursor_position, key);
1064 else if(key == KEY_BACKSPACE ||
1065 key == '\010' || /* ^H */
1066 key == '\177') { /* ^? */
1067 backspace_char(pattern, &cursor_position);
1070 else if(key == KEY_DC ||
1071 key == '\004') { /* ^D */
1072 delete_char(pattern, &cursor_position);
1075 else if(key == KEY_HOME) {
1076 current_focus_line = 0;
1079 else if(key == KEY_END) {
1080 current_focus_line = nb_lines - 1;
1083 else if(key == KEY_NPAGE) {
1087 else if(key == KEY_PPAGE) {
1091 else if(key == KEY_DOWN ||
1092 key == '\016') { /* ^N */
1096 else if(key == KEY_UP ||
1097 key == '\020') { /* ^P */
1101 else if(key == KEY_LEFT ||
1102 key == '\002') { /* ^B */
1103 if(cursor_position > 0) cursor_position--;
1104 else error_feedback();
1107 else if(key == KEY_RIGHT ||
1108 key == '\006') { /* ^F */
1109 if(pattern[cursor_position]) cursor_position++;
1110 else error_feedback();
1113 else if(key == '\001') { /* ^A */
1114 cursor_position = 0;
1117 else if(key == '\005') { /* ^E */
1118 cursor_position = strlen(pattern);
1121 else if(key == '\022') { /* ^R */
1122 use_regexp = !use_regexp;
1125 else if(key == '\011') { /* ^I */
1126 case_sensitive = !case_sensitive;
1129 else if(key == '\025') { /* ^U */
1130 kill_before_cursor(pattern, &cursor_position);
1133 else if(key == '\013') { /* ^K */
1134 kill_after_cursor(pattern, &cursor_position);
1137 else if(key == '\014') { /* ^L */
1138 /* I suspect that we may sometime mess up the display, so ^L is
1139 here to force a full refresh */
1143 update_screen(¤t_focus_line, &displayed_focus_line,
1145 nb_lines, labels, cursor_position, pattern);
1147 } while(key != '\007' && /* ^G */
1148 key != '\033' && /* ^[ (escape) */
1155 /* Here we come back to standard display */
1157 if((key == KEY_ENTER || key == '\n')) {
1161 if(displayed_focus_line >= 0 && displayed_focus_line < nb_lines) {
1162 t = lines[displayed_focus_line];
1163 if(label_separator) {
1164 while(*t && *t != label_separator) t++;
1171 if(output_to_vt_buffer && t) {
1172 inject_into_tty_buffer(t, add_control_qs);
1175 if(output_filename[0]) {
1176 FILE *out = fopen(output_filename, "w");
1184 "Selector: Can not open %s for writing.\n",
1192 printf("Aborted.\n");
1195 for(l = 0; l < nb_lines; l++) {