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>
47 #define VERSION "1.1.1"
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 str_to_positive_integers(char *string, int *values, int nb) {
108 int current_value, gotone;
118 if(*s >= '0' && *s <= '9') {
119 current_value = current_value * 10 + (int) (*s - '0');
121 } else if(*s == ',' || *s == '\0') {
124 values[n++] = current_value;
130 "Selector: Missing value in `%s'.\n", string);
138 "Selector: Too many values in `%s'.\n", string);
143 "Selector: Empty integer value in `%s'.\n", string);
148 "Selector: Syntax error in `%s'.\n", string);
155 void error_feedback() {
163 void print_help(FILE *out) {
165 fprintf(out, "Selector version %s (%s)\n", VERSION, UNAME);
166 fprintf(out, "Written by Francois Fleuret <francois@fleuret.org>.\n");
168 fprintf(out, "Usage: selector [options] [<filename1> [<filename2> ...]]\n");
170 fprintf(out, " -h show this help\n");
171 fprintf(out, " -v inject the selected line in the tty\n");
172 fprintf(out, " -w quote control characters with ^Qs when using -v\n");
173 fprintf(out, " -d remove duplicated lines\n");
174 fprintf(out, " -b remove the bash history line prefix\n");
175 fprintf(out, " -z remove the zsh history line prefix\n");
176 fprintf(out, " -i invert the order of lines\n");
177 fprintf(out, " -e start in regexp mode\n");
178 fprintf(out, " -a start in case sensitive mode\n");
179 fprintf(out, " -m monochrome mode\n");
180 fprintf(out, " -q make a flash instead of a beep on an edition error\n");
181 fprintf(out, " -- all following arguments are filenames\n");
182 fprintf(out, " -t <title>\n");
183 fprintf(out, " add a title in the modeline\n");
184 fprintf(out, " -c <fg modeline> <bg modeline> <fg highlight> <bg highlight>\n");
185 fprintf(out, " set the display colors\n");
186 fprintf(out, " -o <output filename>\n");
187 fprintf(out, " set a file to write the selected line to\n");
188 fprintf(out, " -s <pattern separator>\n");
189 fprintf(out, " set the symbol to separate substrings in the pattern\n");
190 fprintf(out, " -x <label separator>\n");
191 fprintf(out, " set the symbol to terminate the label\n");
192 fprintf(out, " -l <max number of lines>\n");
193 fprintf(out, " set the maximum number of lines to take into account\n");
197 /*********************************************************************/
199 /* A quick and dirty hash table */
201 /* The table itself stores indexes of the strings taken in a char**
202 table. When a string is added, if it was already in the table, the
203 new index replaces the previous one. */
205 struct hash_table_t {
210 struct hash_table_t *new_hash_table(int size) {
212 struct hash_table_t *hash_table;
214 hash_table = safe_malloc(sizeof(struct hash_table_t));
216 hash_table->size = size;
217 hash_table->entries = safe_malloc(hash_table->size * sizeof(int));
219 for(k = 0; k < hash_table->size; k++) {
220 hash_table->entries[k] = -1;
226 void free_hash_table(struct hash_table_t *hash_table) {
227 free(hash_table->entries);
231 /* Adds new_string in the table, associated to new_index. If this
232 string was not already in the table, returns -1. Otherwise, returns
233 the previous index it had. */
235 int add_and_get_previous_index(struct hash_table_t *hash_table,
236 const char *new_string, int new_index,
239 unsigned int code = 0;
242 /* This is my recipe. I checked, it seems to work (as long as
243 hash_table->size is not a multiple of 387433 that should be
246 for(k = 0; new_string[k]; k++) {
247 code = code * 387433 + (unsigned int) (new_string[k]);
250 code = code % hash_table->size;
252 while(hash_table->entries[code] >= 0) {
253 /* There is a string with that code */
254 if(strcmp(new_string, strings[hash_table->entries[code]]) == 0) {
255 /* It is the same string, we keep a copy of the stored index */
256 int result = hash_table->entries[code];
257 /* Put the new one */
258 hash_table->entries[code] = new_index;
259 /* And return the previous one */
262 /* This collision was not the same string, let's move to the next
264 code = (code + 1) % hash_table->size;
267 /* This string was not already in there, store the index in the
268 table and return -1 */
270 hash_table->entries[code] = new_index;
274 /*********************************************************************
275 A matcher matches either with a collection of substrings, or with a
283 char *splitted_patterns, **patterns;
286 int match(char *string, matcher_t *matcher) {
288 if(matcher->nb_patterns >= 0) {
289 if(matcher->case_sensitive) {
290 for(n = 0; n < matcher->nb_patterns; n++) {
291 if(strstr(string, matcher->patterns[n]) == 0) return 0;
294 for(n = 0; n < matcher->nb_patterns; n++) {
295 if(strcasestr(string, matcher->patterns[n]) == 0) return 0;
300 return regexec(&matcher->preg, string, 0, 0, 0) == 0;
304 void free_matcher(matcher_t *matcher) {
305 if(matcher->nb_patterns < 0) {
306 if(!matcher->regexp_error) regfree(&matcher->preg);
308 free(matcher->splitted_patterns);
309 free(matcher->patterns);
313 void initialize_matcher(int use_regexp, int case_sensitive,
314 matcher_t *matcher, const char *pattern) {
316 char *t, *last_pattern_start;
320 matcher->nb_patterns = -1;
321 matcher->regexp_error = regcomp(&matcher->preg, pattern,
322 case_sensitive ? 0 : REG_ICASE);
324 matcher->regexp_error = 0;
325 matcher->nb_patterns = 1;
326 matcher->case_sensitive = case_sensitive;
328 for(s = pattern; *s; s++) {
329 if(*s == pattern_separator) {
330 matcher->nb_patterns++;
334 matcher->splitted_patterns =
335 safe_malloc((strlen(pattern) + 1) * sizeof(char));
338 safe_malloc(matcher->nb_patterns * sizeof(char *));
340 strcpy(matcher->splitted_patterns, pattern);
343 last_pattern_start = matcher->splitted_patterns;
344 for(t = matcher->splitted_patterns; n < matcher->nb_patterns; t++) {
345 if(*t == pattern_separator || *t == '\0') {
347 matcher->patterns[n++] = last_pattern_start;
348 last_pattern_start = t + 1;
354 /*********************************************************************
357 void delete_char(char *buffer, int *position) {
358 if(buffer[*position]) {
360 while(c < BUFFER_SIZE && buffer[c]) {
361 buffer[c] = buffer[c+1];
364 } else error_feedback();
367 void backspace_char(char *buffer, int *position) {
369 if(buffer[*position]) {
370 int c = *position - 1;
372 buffer[c] = buffer[c+1];
376 buffer[*position - 1] = '\0';
380 } else error_feedback();
383 void insert_char(char *buffer, int *position, char character) {
384 if(strlen(buffer) < BUFFER_SIZE - 1) {
386 char t = buffer[c], u;
395 buffer[(*position)++] = character;
396 } else error_feedback();
399 void kill_before_cursor(char *buffer, int *position) {
401 while(buffer[*position + s]) {
402 buffer[s] = buffer[*position + s];
409 void kill_after_cursor(char *buffer, int *position) {
410 buffer[*position] = '\0';
413 /*********************************************************************/
415 int previous_visible(int current_line, int nb_lines, char **lines,
416 matcher_t *matcher) {
417 int line = current_line - 1;
418 while(line >= 0 && !match(lines[line], matcher)) line--;
422 int next_visible(int current_line, int nb_lines, char **lines,
423 matcher_t *matcher) {
424 int line = current_line + 1;
425 while(line < nb_lines && !match(lines[line], matcher)) line++;
433 /*********************************************************************/
435 /* The line highlighted is the first one matching the matcher in that
436 order: (1) current_focus_line after motion, if it does not match,
437 then (2) the first with a greater index, if none matches, then (3)
438 the first with a lesser index.
440 The index of the line actually shown highlighted is written in
441 displayed_focus_line (it can be -1 if no line at all matches the
444 If there is a motion and a line is actually shown highlighted, its
445 value is written in current_focus_line. */
447 void update_screen(int *current_focus_line, int *displayed_focus_line,
449 int nb_lines, char **lines,
453 char buffer[BUFFER_SIZE];
456 int console_width, console_height;
457 int nb_printed_lines = 0;
460 initialize_matcher(use_regexp, case_sensitive, &matcher, pattern);
462 console_width = getmaxx(stdscr);
463 console_height = getmaxy(stdscr);
465 use_default_colors();
467 /* Add an empty line where we will print the modeline at the end */
471 /* If the regexp is erroneous, print a message saying so */
473 if(matcher.regexp_error) {
475 addnstr("Regexp syntax error", console_width);
479 /* Else, and we do have lines to select from, find a visible line. */
481 else if(nb_lines > 0) {
483 if(match(lines[*current_focus_line], &matcher)) {
484 new_focus_line = *current_focus_line;
486 new_focus_line = next_visible(*current_focus_line, nb_lines, lines,
488 if(new_focus_line < 0) {
489 new_focus_line = previous_visible(*current_focus_line, nb_lines, lines,
494 /* If we found a visible line and we should move, let's move */
496 if(new_focus_line >= 0 && motion != 0) {
497 int l = new_focus_line;
499 /* We want to go down, let's find the first visible line below */
500 for(m = 0; l >= 0 && m < motion; m++) {
501 l = next_visible(l, nb_lines, lines, &matcher);
507 /* We want to go up, let's find the first visible line above */
508 for(m = 0; l >= 0 && m < -motion; m++) {
509 l = previous_visible(l, nb_lines, lines, &matcher);
517 /* Here new_focus_line is either a line number matching the
520 if(new_focus_line >= 0) {
522 int first_line = new_focus_line, last_line = new_focus_line;
525 /* We find the first and last lines to show, so that the total
526 of visible lines between them (them included) is
529 while(nb_match < console_height-1 &&
530 (first_line > 0 || last_line < nb_lines - 1)) {
534 while(first_line > 0 && !match(lines[first_line], &matcher)) {
537 if(match(lines[first_line], &matcher)) {
542 if(nb_match < console_height - 1 && last_line < nb_lines - 1) {
544 while(last_line < nb_lines - 1 && !match(lines[last_line], &matcher)) {
548 if(match(lines[last_line], &matcher)) {
554 /* Now we display them */
556 for(l = first_line; l <= last_line; l++) {
557 if(match(lines[l], &matcher)) {
560 while(lines[l][k] && k < BUFFER_SIZE - 2 && k < console_width - 2) {
561 buffer[k] = lines[l][k];
565 /* We fill the rest of the line with blanks if this is the
568 if(l == new_focus_line) {
569 while(k < console_width) {
579 /* Highlight the highlighted line ... */
581 if(l == new_focus_line) {
582 attron(attr_focus_line);
583 addnstr(buffer, console_width);
584 attroff(attr_focus_line);
586 addnstr(buffer, console_width);
593 /* If we are on a focused line and we moved, this become the new
597 *current_focus_line = new_focus_line;
601 *displayed_focus_line = new_focus_line;
603 if(nb_printed_lines == 0) {
605 addnstr("No selection", console_width);
610 /* Else, print a message saying that there are no lines to select from */
614 addnstr("Empty choice", console_width);
620 /* Draw the modeline */
624 attron(attr_modeline);
626 for(k = 0; k < console_width; k++) buffer[k] = ' ';
627 buffer[console_width] = '\0';
628 addnstr(buffer, console_width);
632 /* There must be a more elegant way of moving the cursor at a
633 location met during display */
640 cursor_x += strlen(title) + 1;
643 sprintf(buffer, "%d/%d ", nb_printed_lines, nb_lines);
645 cursor_x += strlen(buffer);
647 addnstr(pattern, cursor_position);
648 cursor_x += cursor_position;
650 if(pattern[cursor_position]) {
651 addstr(pattern + cursor_position);
656 /* Add a few info about the mode we are in (regexp and/or case
659 if(use_regexp || case_sensitive) {
676 attroff(attr_modeline);
681 free_matcher(&matcher);
684 /*********************************************************************/
686 void store_line(struct hash_table_t *hash_table,
687 const char *new_line,
688 int *nb_lines, char **lines) {
691 /* Remove the zsh history prefix */
693 if(zsh_history && *new_line == ':') {
694 while(*new_line && *new_line != ';') new_line++;
695 if(*new_line == ';') new_line++;
698 /* Remove the bash history prefix */
701 while(*new_line == ' ') new_line++;
702 while(*new_line >= '0' && *new_line <= '9') new_line++;
703 while(*new_line == ' ') new_line++;
706 /* Check for duplicates with the hash table and insert the line in
707 the list if necessary */
710 dup = add_and_get_previous_index(hash_table,
711 new_line, *nb_lines, lines);
717 lines[*nb_lines] = safe_malloc((strlen(new_line) + 1) * sizeof(char));
718 strcpy(lines[*nb_lines], new_line);
720 /* The string was already in there, so we do not allocate a new
721 string but use the pointer to the first occurence of it */
722 lines[*nb_lines] = lines[dup];
729 void read_file(struct hash_table_t *hash_table,
730 const char *input_filename,
731 int nb_lines_max, int *nb_lines, char **lines) {
733 char raw_line[BUFFER_SIZE];
734 int start, end, eol, k;
737 file = fopen(input_filename, "r");
740 fprintf(stderr, "Selector: Can not open `%s'.\n", input_filename);
747 while(*nb_lines < nb_lines_max && (end > start || !feof(file))) {
750 /* Look for the end of a line in what is already in the buffer */
751 while(eol < end && raw_line[eol] != '\n') eol++;
753 /* if we did not find the of a line, move what has not been
754 processed and is in the buffer to the beginning of the buffer,
755 fill the buffer with new data from the file, and look for the
758 for(k = 0; k < end - start; k++) {
759 raw_line[k] = raw_line[k + start];
764 end += fread(raw_line + end, sizeof(char), BUFFER_SIZE - end, file);
765 while(eol < end && raw_line[eol] != '\n') eol++;
768 /* The end of the line is the buffer size, which means the line is
771 if(eol == BUFFER_SIZE) {
772 raw_line[BUFFER_SIZE - 1] = '\0';
773 fprintf(stderr, "Selector: Line too long (max is %d characters):\n",
775 fprintf(stderr, raw_line);
776 fprintf(stderr, "\n");
780 /* If we got a line, we replace the carriage return by a \0 to
783 raw_line[eol] = '\0';
785 store_line(hash_table, raw_line + start,
794 /*********************************************************************/
796 static struct option long_options[] = {
797 { "output-file", 1, 0, 'o' },
798 { "pattern-separator", 1, 0, 's' },
799 { "label-separator", 1, 0, 'x' },
800 { "inject-in-tty", no_argument, 0, 'v' },
801 { "add-control-qs", no_argument, 0, 'w' },
802 { "monochrome", no_argument, 0, 'm' },
803 { "no-beep", no_argument, 0, 'q' },
804 { "revert-order", no_argument, 0, 'i' },
805 { "remove-bash-prefix", no_argument, 0, 'b' },
806 { "remove-zsh-prefix", no_argument, 0, 'z' },
807 { "remove-duplicates", no_argument, 0, 'd' },
808 { "regexp", no_argument, 0, 'e' },
809 { "case-sensitive", no_argument, 0, 'a' },
810 { "title", 1, 0, 't' },
811 { "number-of-lines", 1, 0, 'l' },
812 { "colors", 1, 0, 'c' },
813 { "rest-are-files", no_argument, 0, '-' },
814 { "help", no_argument, 0, 'h' },
818 int main(int argc, char **argv) {
820 char output_filename[BUFFER_SIZE];
821 char pattern[BUFFER_SIZE];
824 int error = 0, show_help = 0;
825 int rest_are_files = 0;
827 int current_focus_line, displayed_focus_line;
830 int color_fg_modeline, color_bg_modeline;
831 int color_fg_highlight, color_bg_highlight;
833 char **lines, **labels;
835 struct hash_table_t *hash_table;
837 if(!isatty(STDIN_FILENO)) {
838 fprintf(stderr, "Selector: The standard input is not a tty.\n");
842 color_fg_modeline = COLOR_WHITE;
843 color_bg_modeline = COLOR_BLACK;
844 color_fg_highlight = COLOR_BLACK;
845 color_bg_highlight = COLOR_YELLOW;
847 setlocale(LC_ALL, "");
849 strcpy(output_filename, "");
851 while (!rest_are_files &&
852 (c = getopt_long(argc, argv, "o:s:x:vwmqf:ibzdeat:l:c:-h",
853 long_options, NULL)) != -1) {
858 strncpy(output_filename, optarg, BUFFER_SIZE);
862 pattern_separator = optarg[0];
866 label_separator = optarg[0];
870 output_to_vt_buffer = 1;
898 remove_duplicates = 1;
911 title = safe_malloc((strlen(optarg) + 1) * sizeof(char));
912 strcpy(title, optarg);
916 str_to_positive_integers(optarg, &nb_lines_max, 1);
920 str_to_positive_integers(optarg, colors, 4);
921 color_fg_modeline = colors[0];
922 color_bg_modeline = colors[1];
923 color_fg_highlight = colors[2];
924 color_bg_highlight = colors[3];
941 if(show_help || error) {
951 lines = safe_malloc(nb_lines_max * sizeof(char *));
955 if(remove_duplicates) {
956 hash_table = new_hash_table(nb_lines_max * 10);
961 while(optind < argc) {
962 read_file(hash_table,
964 nb_lines_max, &nb_lines, lines);
969 free_hash_table(hash_table);
972 /* Now remove the null strings */
975 for(k = 0; k < nb_lines; k++) {
977 lines[n++] = lines[k];
984 for(l = 0; l < nb_lines / 2; l++) {
985 char *s = lines[nb_lines - 1 - l];
986 lines[nb_lines - 1 - l] = lines[l];
991 /* Build the labels from the strings, take only the part before the
992 label_separator and transform control characters to printable
995 labels = safe_malloc(nb_lines * sizeof(char *));
997 for(l = 0; l < nb_lines; l++) {
1003 while(*t && *t != label_separator) {
1008 labels[l] = safe_malloc((e + 1) * sizeof(char));
1011 while(*t && *t != label_separator) {
1013 while(*u) { *s++ = *u++; }
1020 cursor_position = 0;
1022 /* Here we start to display with curse */
1027 intrflush(stdscr, FALSE);
1029 /* So that the arrow keys work */
1030 keypad(stdscr, TRUE);
1032 attr_error = A_STANDOUT;
1033 attr_modeline = A_REVERSE;
1034 attr_focus_line = A_STANDOUT;
1036 if(with_colors && has_colors()) {
1040 if(color_fg_modeline < 0 || color_fg_modeline >= COLORS ||
1041 color_bg_modeline < 0 || color_bg_modeline >= COLORS ||
1042 color_fg_highlight < 0 || color_bg_highlight >= COLORS ||
1043 color_bg_highlight < 0 || color_bg_highlight >= COLORS) {
1046 fprintf(stderr, "Selector: Color numbers have to be between 0 and %d.\n",
1051 init_pair(1, color_fg_modeline, color_bg_modeline);
1052 attr_modeline = COLOR_PAIR(1);
1054 init_pair(2, color_fg_highlight, color_bg_highlight);
1055 attr_focus_line = COLOR_PAIR(2);
1057 init_pair(3, COLOR_WHITE, COLOR_RED);
1058 attr_error = COLOR_PAIR(3);
1062 current_focus_line = 0;
1063 displayed_focus_line = 0;
1065 update_screen(¤t_focus_line, &displayed_focus_line,
1067 nb_lines, labels, cursor_position, pattern);
1074 if(key >= ' ' && key <= '~') { /* Insert character */
1075 insert_char(pattern, &cursor_position, key);
1078 else if(key == KEY_BACKSPACE ||
1079 key == '\010' || /* ^H */
1080 key == '\177') { /* ^? */
1081 backspace_char(pattern, &cursor_position);
1084 else if(key == KEY_DC ||
1085 key == '\004') { /* ^D */
1086 delete_char(pattern, &cursor_position);
1089 else if(key == KEY_HOME) {
1090 current_focus_line = 0;
1093 else if(key == KEY_END) {
1094 current_focus_line = nb_lines - 1;
1097 else if(key == KEY_NPAGE) {
1101 else if(key == KEY_PPAGE) {
1105 else if(key == KEY_DOWN ||
1106 key == '\016') { /* ^N */
1110 else if(key == KEY_UP ||
1111 key == '\020') { /* ^P */
1115 else if(key == KEY_LEFT ||
1116 key == '\002') { /* ^B */
1117 if(cursor_position > 0) cursor_position--;
1118 else error_feedback();
1121 else if(key == KEY_RIGHT ||
1122 key == '\006') { /* ^F */
1123 if(pattern[cursor_position]) cursor_position++;
1124 else error_feedback();
1127 else if(key == '\001') { /* ^A */
1128 cursor_position = 0;
1131 else if(key == '\005') { /* ^E */
1132 cursor_position = strlen(pattern);
1135 else if(key == '\022') { /* ^R */
1136 use_regexp = !use_regexp;
1139 else if(key == '\011') { /* ^I */
1140 case_sensitive = !case_sensitive;
1143 else if(key == '\025') { /* ^U */
1144 kill_before_cursor(pattern, &cursor_position);
1147 else if(key == '\013') { /* ^K */
1148 kill_after_cursor(pattern, &cursor_position);
1151 else if(key == '\014') { /* ^L */
1152 /* I suspect that we may sometime mess up the display, so ^L is
1153 here to force a full refresh */
1157 update_screen(¤t_focus_line, &displayed_focus_line,
1159 nb_lines, labels, cursor_position, pattern);
1161 } while(key != '\007' && /* ^G */
1162 key != '\033' && /* ^[ (escape) */
1169 /* Here we come back to standard display */
1171 if((key == KEY_ENTER || key == '\n')) {
1175 if(displayed_focus_line >= 0 && displayed_focus_line < nb_lines) {
1176 t = lines[displayed_focus_line];
1177 if(label_separator) {
1178 while(*t && *t != label_separator) t++;
1185 if(output_to_vt_buffer && t) {
1186 inject_into_tty_buffer(t, add_control_qs);
1189 if(output_filename[0]) {
1190 FILE *out = fopen(output_filename, "w");
1198 "Selector: Can not open %s for writing.\n",
1206 printf("Aborted.\n");
1209 for(l = 0; l < nb_lines; l++) {