Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Search in and/or output only selected parts of lines #185

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Ruby 2.6
uses: actions/setup-ruby@v1
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.6.x
- name: Install dependencies and compile
run: |
gem install bundler
make
cd test/acceptance && bundle install --jobs 4 --retry 3
ruby-version: '2.7'
working-directory: test/acceptance
bundler-cache: true
- name: Run acceptance tests
run: make acceptance
79 changes: 70 additions & 9 deletions src/choices.c
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ 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 *));
if (c->field)
c->nfields = safe_realloc(c->nfields, new_capacity * sizeof(unsigned int));
c->capacity = new_capacity;
}

Expand All @@ -100,20 +102,25 @@ static void choices_reset_search(choices_t *c) {

void choices_init(choices_t *c, options_t *options) {
c->strings = NULL;
c->nfields = NULL;
c->results = NULL;

c->buffer_size = 0;
c->buffer = NULL;

c->capacity = c->size = 0;
choices_resize(c, INITIAL_CHOICE_CAPACITY);

if (options->workers) {
c->worker_count = options->workers;
} else {
c->worker_count = (int)sysconf(_SC_NPROCESSORS_ONLN);
}

c->delimiter = options->delimiter;
c->field = options->field;
c->output_field = options->output_field;

choices_resize(c, INITIAL_CHOICE_CAPACITY);
choices_reset_search(c);
}

Expand All @@ -126,19 +133,42 @@ void choices_destroy(choices_t *c) {
c->strings = NULL;
c->capacity = c->size = 0;

if (c->field) {
free(c->nfields);
c->nfields = NULL;
}

free(c->results);
c->results = NULL;
c->available = c->selection = 0;
}

void choices_add(choices_t *c, const char *choice) {
void choices_add(choices_t *c, char *line) {
char *choice = line;
unsigned int fields = 1;

if (c->field) {
char *field = line;

while ((field = strchr(field, c->delimiter)) != NULL) {
*field++ = '\0';
if (++fields == c->field)
choice = field;
}

if (fields < c->field || fields < c->output_field)
return;
}

/* Previous search is now invalid */
choices_reset_search(c);

if (c->size == c->capacity) {
if (c->size == c->capacity)
choices_resize(c, c->capacity * 2);
}
c->strings[c->size++] = choice;
c->strings[c->size] = choice;
if (c->field)
c->nfields[c->size] = fields;
++c->size;
}

size_t choices_available(choices_t *c) {
Expand Down Expand Up @@ -231,7 +261,7 @@ 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].str = i;
result->list[result->size].score = match(job->search, c->strings[i]);
result->size++;
}
Expand Down Expand Up @@ -301,10 +331,41 @@ void choices_search(choices_t *c, const char *search) {
}

const char *choices_get(choices_t *c, size_t n) {
if (n < c->available) {
return c->results[n].str;
} else {
if (n >= c->available)
return NULL;

return c->strings[c->results[n].str];
}

const char *choices_get_result(choices_t *c, size_t n) {
char *ptr = (char *)choices_get(c, n);
if (!ptr || !c->field)
return ptr;

/* Find the first field */
for (unsigned int i = c->field; i; --i) {
do {
--ptr;
} while (*ptr && ptr != c->buffer);
}

char *line = (!*ptr ? ptr + 1 : ptr);
ptr = line;

if (c->output_field) {
/* Find the output field */
for (unsigned int i = 1; i != c->output_field; ++i) {
while (*ptr++);
}
return ptr;
} else {
/* Put back all delimiters */
unsigned int nfields = c->nfields[c->results[n].str];
for (unsigned int i = 1; i != nfields; ++i) {
for (;*ptr; ++ptr);
*ptr = c->delimiter;
}
return line;
}
}

Expand Down
9 changes: 7 additions & 2 deletions src/choices.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

struct scored_result {
score_t score;
const char *str;
size_t str;
};

typedef struct {
Expand All @@ -19,21 +19,26 @@ typedef struct {
size_t size;

const char **strings;
unsigned int *nfields;
struct scored_result *results;

size_t available;
size_t selection;

unsigned int worker_count;
char delimiter;
unsigned int field;
unsigned int output_field;
} choices_t;

void choices_init(choices_t *c, options_t *options);
void choices_fread(choices_t *c, FILE *file, char input_delimiter);
void choices_destroy(choices_t *c);
void choices_add(choices_t *c, const char *choice);
void choices_add(choices_t *c, char *line);
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_result(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);
Expand Down
1 change: 1 addition & 0 deletions src/config.def.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
#define DEFAULT_NUM_LINES 10
#define DEFAULT_WORKERS 0
#define DEFAULT_SHOW_INFO 0
#define DEFAULT_DELIMITER ':'
46 changes: 44 additions & 2 deletions src/options.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ static const char *usage_str =
" -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"
" -d, --delimiter=DELIM Use DELIM to split the line to fields (default ':')\n"
" -f, --field=NUM Use field NUM for searching (default is the whole line)\n"
" -F, --output-field=NUM Use field NUM for output (default is the whole line)\n"
" -h, --help Display this help and exit\n"
" -v, --version Output version information and exit\n";

Expand All @@ -37,7 +40,10 @@ static struct option longopts[] = {{"show-matches", required_argument, NULL, 'e'
{"version", no_argument, NULL, 'v'},
{"benchmark", optional_argument, NULL, 'b'},
{"workers", required_argument, NULL, 'j'},
{"show-info", no_argument, NULL, 'i'},
{"show-info", no_argument, NULL, 'i'},
{"delimiter", required_argument, NULL, 'd'},
{"field", required_argument, NULL, 'f'},
{"output-field", required_argument, NULL, 'F'},
{"help", no_argument, NULL, 'h'},
{NULL, 0, NULL, 0}};

Expand All @@ -54,13 +60,16 @@ void options_init(options_t *options) {
options->workers = DEFAULT_WORKERS;
options->input_delimiter = '\n';
options->show_info = DEFAULT_SHOW_INFO;
options->delimiter = DEFAULT_DELIMITER;
options->field = 0;
options->output_field = 0;
}

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]);
Expand Down Expand Up @@ -99,6 +108,32 @@ void options_parse(options_t *options, int argc, char *argv[]) {
exit(EXIT_FAILURE);
}
break;
case 'd':
if (sscanf(optarg, "%c", &options->delimiter) != 1) {
usage(argv[0]);
exit(EXIT_FAILURE);
}
break;
case 'f': {
unsigned int f;
if (sscanf(optarg, "%u", &f) != 1 || f < 1) {
fprintf(stderr, "Invalid format for --field: %s\n", optarg);
fprintf(stderr, "Must be integer in range 1..\n");
usage(argv[0]);
exit(EXIT_FAILURE);
}
options->field = f;
} break;
case 'F': {
unsigned int f;
if (sscanf(optarg, "%u", &f) != 1 || f < 1) {
fprintf(stderr, "Invalid format for --field: %s\n", optarg);
fprintf(stderr, "Must be integer in range 1..\n");
usage(argv[0]);
exit(EXIT_FAILURE);
}
options->output_field = f;
} break;
case 'l': {
int l;
if (!strcmp(optarg, "max")) {
Expand All @@ -120,6 +155,13 @@ void options_parse(options_t *options, int argc, char *argv[]) {
exit(EXIT_SUCCESS);
}
}

if (options->output_field && !options->field) {
fprintf(stderr, "Must specify --field with --output-field too.\n");
usage(argv[0]);
exit(EXIT_FAILURE);
}

if (optind != argc) {
usage(argv[0]);
exit(EXIT_FAILURE);
Expand Down
3 changes: 3 additions & 0 deletions src/options.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ typedef struct {
unsigned int workers;
char input_delimiter;
int show_info;
char delimiter;
unsigned int field;
unsigned int output_field;
} options_t;

void options_init(options_t *options);
Expand Down
7 changes: 3 additions & 4 deletions src/tty_interface.c
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,9 @@ 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);
if (selection) {
/* output the selected result */
printf("%s\n", selection);
const char *result = choices_get_result(state->choices, state->choices->selection);
if (result) {
printf("%s\n", result);
} else {
/* No match, output the query instead */
printf("%s\n", state->search);
Expand Down
15 changes: 0 additions & 15 deletions test/acceptance/Gemfile.lock

This file was deleted.

27 changes: 27 additions & 0 deletions test/acceptance/acceptance_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,29 @@ def test_long_strings
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"
Expand All @@ -469,6 +492,10 @@ def test_help
-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
Expand Down