From f59d5d1b62b1c2b2f505acc535c798e7c47974df Mon Sep 17 00:00:00 2001 From: Anton Khirnov Date: Tue, 24 Nov 2020 11:33:20 +0100 Subject: Add support for restricting search/output to specific fields. --- fzy.1 | 29 ++++++++ src/choices.c | 140 +++++++++++++++++++++++++++++++------ src/choices.h | 18 ++++- src/common.h | 2 + src/config.def.h | 1 + src/fzy.c | 7 +- src/options.c | 125 +++++++++++++++++++++++++++++---- src/options.h | 16 +++++ src/tty_interface.c | 9 +-- test/acceptance/acceptance_test.rb | 26 +++++++ test/test_choices.c | 16 ++--- 11 files changed, 340 insertions(+), 49 deletions(-) create mode 100644 src/common.h diff --git a/fzy.1 b/fzy.1 index 807b01b..e6eb3a1 100644 --- a/fzy.1 +++ b/fzy.1 @@ -45,6 +45,22 @@ Non-interactive mode. Print the matches in sorted order for QUERY to stdout. Read input delimited by ASCII NUL characters. . .TP +.BR \-f ", " \-\-search-fields =\fISELECTOR\fR +Restrict search to these fields in the input lines. See the \fIFIELD +SELECTORS\fR section for the \fISELECTOR\fR syntax. +. +.TP +.BR \-F ", " \-\-output-fields =\fISELECTOR\fR +Output these fields from the input lines. See the \fIFIELD SELECTORS\fR section +for the \fISELECTOR\fR syntax. +. +.TP +.BR \-d ", " \-\-delimiter =\fIDELIM\fR +Use \fIDELIM\fR to split input lines into fields. This is used for the +\fB\-f\fR/\fB\-F\fR options. \fIDELIM\fR is a sequence of delimiter characters, +defaulting to space+TAB. +. +.TP .BR \-h ", " \-\-help Usage help. . @@ -79,6 +95,19 @@ Delete the word before the cursor .BR Ctrl+u Delete the entire line . +.SH FIELD SELECTORS +A field selector is a comma-separated list of zero or more ranges. Each range is +one of: +.IP \fIN\fR 8 +the field with index \fIN\fR +.IP [\fIN\fR]:[\fIM\fR] +all fields from index \fIN\fR to index \fIM\fR (inclusive). If \fIN\fR is +omitted, it defaults to 0. If \fIM\fR is omitted, it defaults to -1. +.PP +Indices are zero-based, i.e. first field is 0, second is 1 etc. Negative +indices count from the end of the line, i.e. -1 is the last field, -2 is the +second to last, etc. +. .SH USAGE EXAMPLES . .TP diff --git a/src/choices.c b/src/choices.c index fe2f80b..998fb4b 100644 --- a/src/choices.c +++ b/src/choices.c @@ -5,6 +5,7 @@ #include #include +#include "common.h" #include "options.h" #include "choices.h" #include "match.h" @@ -20,11 +21,10 @@ static int cmpchoice(const void *_idx1, const void *_idx2) { const struct scored_result *b = _idx2; if (a->score == b->score) { - /* To ensure a stable sort, we must also sort by the string - * pointers. We can do this since we know all the strings are - * from a contiguous memory segment (buffer in choices_t). + /* To ensure a stable sort, we must also sort by the line + * indices. */ - if (a->str < b->str) { + if (a->idx < b->idx) { return -1; } else { return 1; @@ -46,6 +46,66 @@ static void *safe_realloc(void *buffer, size_t size) { return buffer; } +static char *line_split(const char *line, const char *delim, + const FieldSelector *fs) +{ + int nb_fields; + struct { + int start; + int len; + } *fields = NULL; + + char *ret = NULL; + size_t offset, ret_len; + const char *p; + + /* split line into fields */ + p = line; + nb_fields = 0; + while (*p) { + p += strspn(p, delim); + if (!*p) + break; + + fields = safe_realloc(fields, (nb_fields + 1) * sizeof(*fields)); + + fields[nb_fields].start = p - line; + fields[nb_fields].len = strcspn(p, delim); + p += fields[nb_fields].len; + nb_fields++; + } + + /* count output length */ + ret_len = offset = 0; + for (size_t i = 0; i < fs->nb_ranges; i++) { + size_t range_len; + + int start = fs->ranges[i].start; + int end = fs->ranges[i].end; + + if (start < 0) + start = MAX(nb_fields + start, 0); + start = MIN(nb_fields - 1, start); + + if (end < 0) + end = MAX(nb_fields + end, 0); + end = MIN(nb_fields - 1, end); + + if (end < start) + continue; + + range_len = fields[end].start + fields[end].len - fields[start].start; + + ret = safe_realloc(ret, ret_len + range_len + 1); + + strncpy(ret + offset, line + fields[start].start, range_len); + offset += range_len; + ret[offset] = 0; + } + + return ret; +} + void choices_fread(choices_t *c, FILE *file, char input_delimiter) { /* Save current position for parsing later */ size_t buffer_start = c->buffer_size; @@ -88,7 +148,11 @@ void choices_fread(choices_t *c, FILE *file, char input_delimiter) { } static void choices_resize(choices_t *c, size_t new_capacity) { - c->strings = safe_realloc(c->strings, new_capacity * sizeof(const char *)); + c->input_items = safe_realloc(c->input_items, + new_capacity * sizeof(*c->input_items)); + memset(c->input_items + c->capacity, 0, + (new_capacity - c->capacity) * sizeof(*c->input_items)); + c->capacity = new_capacity; } @@ -99,13 +163,18 @@ static void choices_reset_search(choices_t *c) { } void choices_init(choices_t *c, options_t *options) { - c->strings = NULL; + c->input_items = NULL; c->results = NULL; c->buffer_size = 0; c->buffer = NULL; c->capacity = c->size = 0; + + c->delimiters = options->delimiters; + c->search_fields = &options->search_fields; + c->output_fields = &options->output_fields; + choices_resize(c, INITIAL_CHOICE_CAPACITY); if (options->workers) { @@ -122,8 +191,11 @@ void choices_destroy(choices_t *c) { c->buffer = NULL; c->buffer_size = 0; - free(c->strings); - c->strings = NULL; + for (size_t i = 0; i < c->size; i++) + free(c->input_items[i].allocated); + + free(c->input_items); + c->input_items = NULL; c->capacity = c->size = 0; free(c->results); @@ -131,14 +203,26 @@ void choices_destroy(choices_t *c) { c->available = c->selection = 0; } -void choices_add(choices_t *c, const char *choice) { - /* Previous search is now invalid */ - choices_reset_search(c); +void choices_add(choices_t *c, const char *line) +{ + InputItem *it; if (c->size == c->capacity) { choices_resize(c, c->capacity * 2); } - c->strings[c->size++] = choice; + it = &c->input_items[c->size++]; + + it->input_line = line; + it->search_buf = it->input_line; + + /* extract the fields to be searched, if requested */ + if (c->search_fields->nb_ranges) { + it->allocated = line_split(line, c->delimiters, c->search_fields); + it->search_buf = it->allocated; + } + + /* Previous search is now invalid */ + choices_reset_search(c); } size_t choices_available(choices_t *c) { @@ -230,9 +314,12 @@ static void *choices_search_worker(void *data) { } for(size_t i = start; i < end; i++) { - if (has_match(job->search, c->strings[i])) { - result->list[result->size].str = c->strings[i]; - result->list[result->size].score = match(job->search, c->strings[i]); + InputItem *it = &c->input_items[i]; + const char *str = it->search_buf; + + if (has_match(job->search, str)) { + result->list[result->size].idx = i; + result->list[result->size].score = match(job->search, str); result->size++; } } @@ -300,12 +387,23 @@ void choices_search(choices_t *c, const char *search) { free(job); } -const char *choices_get(choices_t *c, size_t n) { - if (n < c->available) { - return c->results[n].str; - } else { - return NULL; - } +const char *choices_get_search(choices_t *c, size_t n) { + if (n < c->available) + return c->input_items[c->results[n].idx].search_buf; + return NULL; +} + +char *choices_get_output(choices_t *c, size_t n) +{ + const char *line; + + if (n >= c->available) + return NULL; + + line = c->input_items[c->results[n].idx].input_line; + if (c->output_fields->nb_ranges) + return line_split(line, c->delimiters, c->output_fields); + return strdup(line); } score_t choices_getscore(choices_t *c, size_t n) { diff --git a/src/choices.h b/src/choices.h index 925478e..70ef13f 100644 --- a/src/choices.h +++ b/src/choices.h @@ -8,9 +8,16 @@ struct scored_result { score_t score; - const char *str; + size_t idx; }; +typedef struct InputItem { + const char *input_line; + const char *search_buf; + + char *allocated; +} InputItem; + typedef struct { char *buffer; size_t buffer_size; @@ -18,13 +25,17 @@ typedef struct { size_t capacity; size_t size; - const char **strings; + InputItem *input_items; struct scored_result *results; size_t available; size_t selection; unsigned int worker_count; + + const char *delimiters; + const FieldSelector *search_fields; + const FieldSelector *output_fields; } choices_t; void choices_init(choices_t *c, options_t *options); @@ -33,7 +44,8 @@ void choices_destroy(choices_t *c); void choices_add(choices_t *c, const char *choice); size_t choices_available(choices_t *c); void choices_search(choices_t *c, const char *search); -const char *choices_get(choices_t *c, size_t n); +const char *choices_get_search(choices_t *c, size_t n); +char *choices_get_output(choices_t *c, size_t n); score_t choices_getscore(choices_t *c, size_t n); void choices_prev(choices_t *c); void choices_next(choices_t *c); diff --git a/src/common.h b/src/common.h new file mode 100644 index 0000000..7395adb --- /dev/null +++ b/src/common.h @@ -0,0 +1,2 @@ +#define MIN(x, y) ((x) <= (y) ? (x) : (y)) +#define MAX(x, y) ((x) >= (y) ? (x) : (y)) diff --git a/src/config.def.h b/src/config.def.h index fcdcc03..5b33f81 100644 --- a/src/config.def.h +++ b/src/config.def.h @@ -17,3 +17,4 @@ #define DEFAULT_NUM_LINES 10 #define DEFAULT_WORKERS 0 #define DEFAULT_SHOW_INFO 0 +#define DEFAULT_DELIMITERS " " diff --git a/src/fzy.c b/src/fzy.c index 967a1fc..001aa55 100644 --- a/src/fzy.c +++ b/src/fzy.c @@ -34,9 +34,14 @@ int main(int argc, char *argv[]) { choices_fread(&choices, stdin, options.input_delimiter); choices_search(&choices, options.filter); for (size_t i = 0; i < choices_available(&choices); i++) { + char *output; + if (options.show_scores) printf("%f\t", choices_getscore(&choices, i)); - printf("%s\n", choices_get(&choices, i)); + + output = choices_get_output(&choices, i); + printf("%s\n", output); + free(output); } } else { /* interactive */ diff --git a/src/options.c b/src/options.c index e35402f..e06b421 100644 --- a/src/options.c +++ b/src/options.c @@ -1,3 +1,5 @@ +#include +#include #include #include #include @@ -11,17 +13,20 @@ static const char *usage_str = "" "Usage: fzy [OPTION]...\n" - " -l, --lines=LINES Specify how many lines of results to show (default 10)\n" - " -p, --prompt=PROMPT Input prompt (default '> ')\n" - " -q, --query=QUERY Use QUERY as the initial search string\n" - " -e, --show-matches=QUERY Output the sorted matches of QUERY\n" - " -t, --tty=TTY Specify file to use as TTY device (default /dev/tty)\n" - " -s, --show-scores Show the scores of each match\n" - " -0, --read-null Read input delimited by ASCII NUL characters\n" - " -j, --workers NUM Use NUM workers for searching. (default is # of CPUs)\n" - " -i, --show-info Show selection info line\n" - " -h, --help Display this help and exit\n" - " -v, --version Output version information and exit\n"; + " -l, --lines=LINES Specify how many lines of results to show (default 10)\n" + " -p, --prompt=PROMPT Input prompt (default '> ')\n" + " -q, --query=QUERY Use QUERY as the initial search string\n" + " -e, --show-matches=QUERY Output the sorted matches of QUERY\n" + " -t, --tty=TTY Specify file to use as TTY device (default /dev/tty)\n" + " -s, --show-scores Show the scores of each match\n" + " -0, --read-null Read input delimited by ASCII NUL characters\n" + " -j, --workers NUM Use NUM workers for searching. (default is # of CPUs)\n" + " -i, --show-info Show selection info line\n" + " -f, --search-fields=SELECTOR Use these fields for searching. Default is the whole line.\n" + " -F, --output-fields=SELECTOR Use these fields for output. Default is the whole line)\n" + " -d, --delimiters=DELIM Use these delimiters to split input lines into fields. Default is ''.\n" + " -h, --help Display this help and exit\n" + " -v, --version Output version information and exit\n"; static void usage(const char *argv0) { fprintf(stderr, usage_str, argv0); @@ -38,11 +43,87 @@ static struct option longopts[] = {{"show-matches", required_argument, NULL, 'e' {"benchmark", optional_argument, NULL, 'b'}, {"workers", required_argument, NULL, 'j'}, {"show-info", no_argument, NULL, 'i'}, + {"search-fields", required_argument, NULL, 'f'}, + {"output-fields", required_argument, NULL, 'F'}, + {"delimiter", required_argument, NULL, 'd'}, {"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, 0}}; +static void field_selector_uninit(FieldSelector *fs) +{ + if (!fs) + return; + + free(fs->ranges); + fs->ranges = NULL; + fs->nb_ranges = 0; +} + +static int field_selector_parse(FieldSelector *fs, char *str) +{ + size_t nb_ranges; + int cur_range; + + field_selector_uninit(fs); + + if (!str || !*str) + return 0; + + nb_ranges = 1; + for (const char *p = str; *p; p++) { + if (*p == ',') + nb_ranges++; + } + + fs->ranges = calloc(nb_ranges, sizeof(*fs->ranges)); + if (!fs->ranges) + return -ENOMEM; + fs->nb_ranges = nb_ranges; + + cur_range = 0; + while (*str) { + int start, end; + + start = 0; + if (*str == '-' || isdigit(*str)) + start = strtol(str, &str, 0); + + if (*str == ':') { + str++; + end = (*str == '-' || isdigit(*str)) ? + strtol(str, &str, 0) : -1; + } else + end = start; + + if (*str) { + if (*str != ',') { + field_selector_uninit(fs); + return -EINVAL; + } + + str++; + } + + fs->ranges[cur_range].start = start; + fs->ranges[cur_range].end = end; + cur_range++; + } + + return 0; +} + +void options_uninit(options_t *options) +{ + field_selector_uninit(&options->search_fields); + field_selector_uninit(&options->output_fields); + + memset(options, 0, sizeof(*options)); +} + void options_init(options_t *options) { /* set defaults */ + memset(options, 0, sizeof(*options)); + options->benchmark = 0; options->filter = NULL; options->init_search = NULL; @@ -54,13 +135,14 @@ void options_init(options_t *options) { options->workers = DEFAULT_WORKERS; options->input_delimiter = '\n'; options->show_info = DEFAULT_SHOW_INFO; + options->delimiters = DEFAULT_DELIMITERS; } void options_parse(options_t *options, int argc, char *argv[]) { options_init(options); int c; - while ((c = getopt_long(argc, argv, "vhs0e:q:l:t:p:j:i", longopts, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "vhs0e:q:l:t:p:j:id:f:F:", longopts, NULL)) != -1) { switch (c) { case 'v': printf("%s " VERSION " © 2014-2018 John Hawthorn\n", argv[0]); @@ -114,6 +196,25 @@ void options_parse(options_t *options, int argc, char *argv[]) { case 'i': options->show_info = 1; break; + case 'd': + options->delimiters = optarg; + break; + case 'f': { + int ret = field_selector_parse(&options->search_fields, optarg); + if (ret < 0) { + fprintf(stderr, "Invalid format for --search-fields: %s\n", optarg); + usage(argv[0]); + exit(EXIT_FAILURE); + } + } break; + case 'F': { + int ret = field_selector_parse(&options->output_fields, optarg); + if (ret < 0) { + fprintf(stderr, "Invalid format for --output-field: %s\n", optarg); + usage(argv[0]); + exit(EXIT_FAILURE); + } + } break; case 'h': default: usage(argv[0]); diff --git a/src/options.h b/src/options.h index 4be4cb6..e8419b1 100644 --- a/src/options.h +++ b/src/options.h @@ -1,6 +1,16 @@ #ifndef OPTIONS_H #define OPTIONS_H OPTIONS_H +typedef struct FieldRange { + int start; + int end; +} FieldRange; + +typedef struct FieldSelector { + FieldRange *ranges; + size_t nb_ranges; +} FieldSelector; + typedef struct { int benchmark; const char *filter; @@ -13,9 +23,15 @@ typedef struct { unsigned int workers; char input_delimiter; int show_info; + + const char *delimiters; + + FieldSelector search_fields; + FieldSelector output_fields; } options_t; void options_init(options_t *options); void options_parse(options_t *options, int argc, char *argv[]); +void options_uninit(options_t *options); #endif diff --git a/src/tty_interface.c b/src/tty_interface.c index 343dde8..305c0eb 100644 --- a/src/tty_interface.c +++ b/src/tty_interface.c @@ -103,7 +103,7 @@ static void draw(tty_interface_t *state) { for (size_t i = start; i < start + num_lines; i++) { tty_printf(tty, "\n"); tty_clearline(tty); - const char *choice = choices_get(choices, i); + const char *choice = choices_get_search(choices, i); if (choice) { draw_match(state, choice, i == choices->selection); } @@ -140,10 +140,11 @@ static void action_emit(tty_interface_t *state) { /* ttyout should be flushed before outputting on stdout */ tty_close(state->tty); - const char *selection = choices_get(state->choices, state->choices->selection); + char *selection = choices_get_output(state->choices, state->choices->selection); if (selection) { /* output the selected result */ printf("%s\n", selection); + free(selection); } else { /* No match, output the query instead */ printf("%s\n", state->search); @@ -237,9 +238,9 @@ static void action_pagedown(tty_interface_t *state) { static void action_autocomplete(tty_interface_t *state) { update_state(state); - const char *current_selection = choices_get(state->choices, state->choices->selection); + const char *current_selection = choices_get_search(state->choices, state->choices->selection); if (current_selection) { - strncpy(state->search, choices_get(state->choices, state->choices->selection), SEARCH_SIZE_MAX); + strncpy(state->search, choices_get_search(state->choices, state->choices->selection), SEARCH_SIZE_MAX); state->cursor = strlen(state->search); } } diff --git a/test/acceptance/acceptance_test.rb b/test/acceptance/acceptance_test.rb index 13af803..cbc6d45 100644 --- a/test/acceptance/acceptance_test.rb +++ b/test/acceptance/acceptance_test.rb @@ -447,6 +447,29 @@ class FzyTest < Minitest::Test TTY end + def test_field + @tty = interactive_fzy(input: %w[1/foo 2/bar], args: "-d/ -f2 -F1") + @tty.assert_matches(">\nfoo\nbar") + + @tty.send_keys("foo\r") + @tty.assert_matches "1" # the first field + end + + def test_field_input_only + @tty = interactive_fzy(input: %w[1:foo 2:bar], args: "-f2") + @tty.assert_matches ">\nfoo\nbar" + + @tty.send_keys("bar\r") + @tty.assert_matches "2:bar" # the whole line + + end + + def test_field_ignored_line + # not enough fields for -f or -F + @tty = interactive_fzy(input: %w[1:foo:x 2:baz 3 4:bar:y], args: "-f2 -F3") + @tty.assert_matches ">\nfoo\nbar" + end + def test_show_info @tty = interactive_fzy(input: %w[foo bar baz], args: "-i") @tty.assert_matches ">\n[3/3]\nfoo\nbar\nbaz" @@ -469,6 +492,9 @@ Usage: fzy [OPTION]... -0, --read-null Read input delimited by ASCII NUL characters -j, --workers NUM Use NUM workers for searching. (default is # of CPUs) -i, --show-info Show selection info line + -d, --delimiter=DELIM Use DELIM to split the line to fields (default ':') + -f, --field=NUM Use field NUM for searching (default is the whole line) + -F, --output-field=NUM Use field NUM for output (default is the whole line) -h, --help Display this help and exit -v, --version Output version information and exit TTY diff --git a/test/test_choices.c b/test/test_choices.c index d86bc12..b198d16 100644 --- a/test/test_choices.c +++ b/test/test_choices.c @@ -56,8 +56,8 @@ TEST test_choices_1() { choices_next(&choices); ASSERT_SIZE_T_EQ(0, choices.selection); - ASSERT(!strcmp(choices_get(&choices, 0), "tags")); - ASSERT_EQ(NULL, choices_get(&choices, 1)); + ASSERT(!strcmp(choices_get_search(&choices, 0), "tags")); + ASSERT_EQ(NULL, choices_get_search(&choices, 1)); PASS(); } @@ -85,7 +85,7 @@ TEST test_choices_2() { choices_search(&choices, "te"); ASSERT_SIZE_T_EQ(1, choices.available); ASSERT_SIZE_T_EQ(0, choices.selection); - ASSERT_STR_EQ("test", choices_get(&choices, 0)); + ASSERT_STR_EQ("test", choices_get_search(&choices, 0)); choices_next(&choices); ASSERT_SIZE_T_EQ(0, choices.selection); @@ -102,8 +102,8 @@ TEST test_choices_2() { choices_search(&choices, "ts"); ASSERT_SIZE_T_EQ(2, choices.available); ASSERT_SIZE_T_EQ(0, choices.selection); - ASSERT_STR_EQ("test", choices_get(&choices, 0)); - ASSERT_STR_EQ("tags", choices_get(&choices, 1)); + ASSERT_STR_EQ("test", choices_get_search(&choices, 0)); + ASSERT_STR_EQ("tags", choices_get_search(&choices, 1)); PASS(); } @@ -114,14 +114,14 @@ TEST test_choices_without_search() { ASSERT_SIZE_T_EQ(0, choices.available); ASSERT_SIZE_T_EQ(0, choices.selection); ASSERT_SIZE_T_EQ(0, choices.size); - ASSERT_EQ(NULL, choices_get(&choices, 0)); + ASSERT_EQ(NULL, choices_get_search(&choices, 0)); choices_add(&choices, "test"); ASSERT_SIZE_T_EQ(0, choices.available); ASSERT_SIZE_T_EQ(0, choices.selection); ASSERT_SIZE_T_EQ(1, choices.size); - ASSERT_EQ(NULL, choices_get(&choices, 0)); + ASSERT_EQ(NULL, choices_get_search(&choices, 0)); PASS(); } @@ -148,7 +148,7 @@ TEST test_choices_large_input() { /* Must match `seq 0 99999 | grep '.*1.*2.*' | wc -l` */ ASSERT_SIZE_T_EQ(8146, choices.available); - ASSERT_STR_EQ("12", choices_get(&choices, 0)); + ASSERT_STR_EQ("12", choices_get_search(&choices, 0)); for(int i = 0; i < N; i++) { free(strings[i]); -- cgit v1.2.3