about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorRory Bradford <roryrjb@gmail.com>2020-08-13 09:07:05 +0100
committerRory Bradford <roryrjb@gmail.com>2020-08-13 09:07:05 +0100
commit4d872677b173d5380e5ab63b17e45d876cc3e864 (patch)
treee4c1dd9a40350e7a4f566539c5036ec15e865153
parent71abce4e0725251ee9484e85fa58e325d1cd6407 (diff)
downloadrf-4d872677b173d5380e5ab63b17e45d876cc3e864.tar.gz
Add config parser
Signed-off-by: Rory Bradford <roryrjb@gmail.com>
-rw-r--r--Makefile7
-rw-r--r--config.c82
-rw-r--r--config.default29
-rw-r--r--config.h14
-rw-r--r--rf.18
-rw-r--r--rf.c136
-rw-r--r--rfconfig.541
7 files changed, 294 insertions, 23 deletions
diff --git a/Makefile b/Makefile
index 14a80e9..42b5b79 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@
 
 BIN = rf
 VERSION = 0.0.5
-OBJS = rf.o ignore.o
+OBJS = rf.o ignore.o config.o
 PREFIX = /usr/local
 CC = cc
 CFLAGS = -std=c99 -pedantic -O2 \
@@ -12,12 +12,12 @@ CFLAGS = -std=c99 -pedantic -O2 \
 	  -DVERSION='"$(VERSION)"' \
 	  -DNAME='"$(BIN)"'
 
-build: $(BIN)
+all: $(BIN)
 
 $(BIN): $(OBJS)
 	$(CC) $(CFLAGS) -o $(BIN) $(OBJS)
 
-install: build
+install: $(BIN)
 	mkdir -p \
 		$(DESTDIR)$(PREFIX)/bin \
 		$(DESTDIR)$(PREFIX)/man/man1 \
@@ -25,6 +25,7 @@ install: build
 	install -m755 $(BIN) $(DESTDIR)$(PREFIX)/bin/
 	install -m444 rf.1 $(DESTDIR)$(PREFIX)/man/man1/
 	install -m444 rfignore.5 $(DESTDIR)$(PREFIX)/man/man5/
+	install -m444 rfconfig.5 $(DESTDIR)$(PREFIX)/man/man5/
 
 clean:
 	rm -vf $(BIN) *.o
diff --git a/config.c b/config.c
new file mode 100644
index 0000000..52e1b5e
--- /dev/null
+++ b/config.c
@@ -0,0 +1,82 @@
+#define _XOPEN_SOURCE 700
+
+#include "config.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+char config_key[KV_STRING_LIMIT] = {'\0'};
+char config_value[KV_STRING_LIMIT] = {'\0'};
+
+int config_get(size_t *llen, FILE *fp) {
+	int i = 0;
+	int k = 0;
+	int v = 0;
+	int len = 0;
+	int in_key = 1;
+	char *line = NULL;
+
+	memset(config_key, '\0', KV_STRING_LIMIT);
+	memset(config_value, '\0', KV_STRING_LIMIT);
+
+	if (getline(&line, llen, fp) < 0) {
+		free(line);
+		return -1;
+	}
+
+	len = strlen(line);
+
+	switch (line[0]) {
+	case '=':
+	case '#':
+		free(line);
+		return 0;
+
+	default:
+		if (isspace(line[0])) {
+			free(line);
+			return 0;
+		}
+	}
+
+	for (i = 0; i < len; i++) {
+		char c = line[i];
+
+		switch (c) {
+		case '=':
+			if (in_key == 1) {
+				in_key = 0;
+				break;
+			}
+
+			/* fallthrough */
+		default:
+			if (c != '\n') {
+				if (isspace(c)) {
+					/* if previous char was '=' then skip */
+					if (i > 0 && line[i - 1] == '=') {
+						break;
+					}
+
+					/* if next char is '=' then skip */
+					if (i + 1 < len && line[i + 1] == '=') {
+						break;
+					}
+				}
+
+				if (in_key) {
+					config_key[k++] = c;
+				} else {
+					config_value[v++] = c;
+				}
+			}
+
+			break;
+		}
+	}
+
+	free(line);
+
+	return 0;
+}
diff --git a/config.default b/config.default
new file mode 100644
index 0000000..b1f5a8e
--- /dev/null
+++ b/config.default
@@ -0,0 +1,29 @@
+# vim: ft=config
+# this is a default config file for rf
+# the options as presented here should match the default behaviour
+# of rf and should give you an overview of what is configurable
+
+# read symlinks (true or false)
+symlinks = false
+
+# match whole path by default (true or false)
+wholename = false
+
+# limit number of results (a positive integer)
+limit = 0
+
+# optionally specify an alternative wildcard character
+# (a single ascii character)
+# allows you to do something like:
+#   rf %.jpg
+# with a wildcard set to '%' so you don't have to worry about
+# shell expansion with the normal '*' character in certain situations
+#
+# wildcard =
+
+# by default if there are no matches just exit with 0 exit code
+# uncomment 'unmatched error' below to exit with a non-zero exit code
+# allowing you to do something like this:
+#   rf {pattern} || fallback
+#
+# unmatched error
diff --git a/config.h b/config.h
new file mode 100644
index 0000000..22eeef6
--- /dev/null
+++ b/config.h
@@ -0,0 +1,14 @@
+#ifndef RF_CONFIG_H
+#define RF_CONFIG_H
+
+#include <ctype.h>
+#include <stdio.h>
+
+#define KV_STRING_LIMIT 1024
+
+extern char config_key[KV_STRING_LIMIT];
+extern char config_value[KV_STRING_LIMIT];
+
+int config_get(size_t *len, FILE *fp);
+
+#endif
diff --git a/rf.1 b/rf.1
index 9f58417..a600e69 100644
--- a/rf.1
+++ b/rf.1
@@ -6,6 +6,7 @@ rf \- a tiny and simple file finder
 .SH SYNOPSIS
 .SY rf
 .OP \-d directory
+.OP \-c config
 .OP \-lsvw
 .OP pattern ...
 
@@ -19,6 +20,10 @@ substring) patterns, while respecting any ignore rules in \fBrfignore\fR files.
 Search from specified directory, otherwise defaults to current working directory.
 .
 .TP
+.BI \-c config
+Override config file location.
+.
+.TP
 .BI \-l count
 Limit to specified matches count.
 .
@@ -35,7 +40,8 @@ Invert matches.
 Match wholename, including path.
 
 .SH SEE ALSO
-.BR rfignore (5)
+.BR rfignore (5),
+.BR rfconfig (5)
 
 .SH COPYRIGHT
 Copyright \(co 2019, 2020 Rory Bradford <roryrjb@gmail.com>.
diff --git a/rf.c b/rf.c
index 6fc4981..d2d18aa 100644
--- a/rf.c
+++ b/rf.c
@@ -15,6 +15,7 @@
 #include <sys/types.h>
 #include <unistd.h>
 
+#include "config.h"
 #include "ignore.h"
 
 extern char *__progname;
@@ -24,22 +25,23 @@ struct ignores *config_ignores;
 struct ignores *local_ignores;
 
 struct switches {
-	int substring;
-	int wholename;
+	int count;
 	int invert;
 	int limit;
-	int count;
-	int unlink;
-	char *cmd;
+	int substring;
+	int wholename;
 };
 
+int read_links = 0;
+
 static void usage(char *error) {
 	if (error != NULL) {
 		fprintf(stderr, "Error: %s\n\n", error);
 	}
 
-	fprintf(
-		stderr, "usage: %s [-d directory] [-lsvw] pattern ...\n", __progname);
+	fprintf(stderr,
+		"usage: %s [-d directory] [-c config] [-lsvw] pattern ...\n",
+		__progname);
 }
 
 static int is_child(char *dirname) {
@@ -50,7 +52,11 @@ static int is_child(char *dirname) {
 	return 1;
 }
 
-static int excluded(char *name) {
+static int excluded(const char *name) {
+	if (!fnmatch("/proc/*", name, 0)) {
+		return 1;
+	}
+
 	if (global_ignores != NULL) {
 		for (int i = 0; i < global_ignores->size; i++) {
 			int res = fnmatch(global_ignores->list[i], name, 0);
@@ -85,8 +91,8 @@ static int excluded(char *name) {
 }
 
 /* return 1 if breaking early (e.g. reaching limit) otherwise return 0 */
-static int recurse_find(char **patterns, int *pattern_count, char *dirname,
-	struct switches *switches) {
+static int recurse_find(char **patterns, int *pattern_count,
+	const char *dirname, struct switches *switches) {
 	DIR *dir;
 
 	char path[MAXPATHLEN] = {'\0'};
@@ -111,8 +117,9 @@ static int recurse_find(char **patterns, int *pattern_count, char *dirname,
 
 			struct stat entry_stat;
 
-			if (stat(full_path, &entry_stat)) {
+			if ((read_links ? stat : lstat)(full_path, &entry_stat)) {
 				perror("stat");
+				exit(EXIT_FAILURE);
 				continue;
 			}
 
@@ -177,13 +184,37 @@ static int recurse_find(char **patterns, int *pattern_count, char *dirname,
 
 int main(int argc, char **argv) {
 	struct switches switches = {
-		.substring = 0, .invert = 0, .wholename = 0, .limit = 0, .count = 0};
+		.count = 0,
+		.invert = 0,
+		.limit = 0,
+		.substring = 0,
+		.wholename = 0,
+	};
 
 	int ch;
 	char *remainder;
+	size_t len = 0;
+	const char *root = ".";
+	FILE *fp;
+	char cwd[MAXPATHLEN];
+	int unmatched_error = 0;
+	char wildcard = 0;
+	char *override_config_file = NULL;
 
-	while ((ch = getopt(argc, argv, "l:svw")) > -1) {
+	if (getcwd(cwd, MAXPATHLEN) == NULL) {
+		perror("getcwd");
+		exit(EXIT_FAILURE);
+	}
+
+	char *xdg_config_home = getenv("XDG_CONFIG_HOME");
+	char *home = getenv("HOME");
+
+	while ((ch = getopt(argc, argv, "c:d:l:svw")) > -1) {
 		switch (ch) {
+		case 'c':
+			override_config_file = optarg;
+			break;
+
 		case 'd':
 			root = optarg;
 			break;
@@ -212,6 +243,7 @@ int main(int argc, char **argv) {
 					usage("Invalid limit.");
 					exit(EXIT_FAILURE);
 				}
+
 				switches.limit = limit;
 			} while (0);
 
@@ -219,12 +251,66 @@ int main(int argc, char **argv) {
 		}
 	}
 
-	char *home = getenv("HOME");
-	char *xdg_config_home = getenv("XDG_CONFIG_HOME");
-	char cwd[MAXPATHLEN];
+	char config_file[xdg_config_home
+						 ? (strlen(xdg_config_home) + strlen("ignore") + 3)
+						 : (strlen(cwd) + strlen(".rfignore") + 1)];
 
-	if (getcwd(cwd, MAXPATHLEN) == NULL) {
-		perror("getcwd");
+	if (xdg_config_home) {
+		sprintf(config_file, "%s/rf/%s", xdg_config_home, "config");
+	} else {
+		sprintf(config_file, "%s/.config/rf/%s", home, "config");
+	}
+
+	fp = fopen(override_config_file ? override_config_file : config_file, "r");
+
+	if (fp != NULL) {
+		while ((config_get(&len, fp)) != -1) {
+			if (strlen(config_key) && strlen(config_value)) {
+				if (strcmp(config_key, "symlinks") == 0) {
+					if (strcmp(config_value, "true") == 0) {
+						read_links = 1;
+					} else if (strcmp(config_value, "false") == 0) {
+						read_links = 0;
+					} else {
+						fprintf(stderr,
+							"'%s' is not a valid value for property: %s.\n",
+							config_value, config_key);
+						exit(EXIT_FAILURE);
+					}
+				} else if (strcmp(config_key, "wholename") == 0) {
+					if (strcmp(config_value, "true") == 0) {
+						switches.wholename = 1;
+					} else if (strcmp(config_value, "false") == 0) {
+						/* default */
+					} else {
+						fprintf(stderr,
+							"'%s' is not a valid value for property: %s.\n",
+							config_value, config_key);
+						exit(EXIT_FAILURE);
+					}
+				} else if (strcmp(config_key, "limit") == 0) {
+					int limit = strtol(config_value, &remainder, 10);
+
+					if (limit < 0) {
+						fprintf(stderr, "Warning: Invalid limit, ignoring.");
+					} else {
+						switches.limit = limit;
+					}
+				} else if (strcmp(config_key, "unmatched error") == 0) {
+					unmatched_error = 1;
+				} else if (strcmp(config_key, "wildcard") == 0) {
+					wildcard = config_value[0];
+				}
+			} else if (strlen(config_key)) {
+				fprintf(stderr,
+					"Warning: Ignoring empty config property '%s'.\n",
+					config_key);
+			}
+		}
+
+		fclose(fp);
+	} else if (override_config_file) {
+		perror("fopen");
 		exit(EXIT_FAILURE);
 	}
 
@@ -256,6 +342,14 @@ int main(int argc, char **argv) {
 
 		while (optind < argc) {
 			patterns[i++] = argv[optind++];
+
+			if (wildcard) {
+				for (size_t j = 0; j < strlen(patterns[i - 1]); j++) {
+					if (patterns[i - 1][j] == wildcard) {
+						patterns[i - 1][j] = '*';
+					}
+				}
+			}
 		}
 
 		if (recurse_find(patterns, &pattern_count, root, &switches)) {
@@ -266,9 +360,13 @@ int main(int argc, char **argv) {
 		free_ignores(global_ignores);
 		free_ignores(config_ignores);
 		free_ignores(local_ignores);
+
+		if (unmatched_error && switches.count == 0) {
+			exit(EXIT_FAILURE);
+		}
 	} else {
 		usage(NULL);
 	}
 
-	return 0;
+	exit(EXIT_SUCCESS);
 }
diff --git a/rfconfig.5 b/rfconfig.5
new file mode 100644
index 0000000..8ad7ab0
--- /dev/null
+++ b/rfconfig.5
@@ -0,0 +1,41 @@
+.TH rfconfig 5
+
+.SH NAME
+rfconfig \- configure rf behaviour
+
+.SH SYNOPSIS
+$XDG_CONFIG_HOME/rf/config
+
+.SH DESCRIPTION
+An \fBrfconfig\fR file is used to override default behaviour and configure options.
+
+.SH FORMAT
+The config file format is very simple. Comments are only available for a whole
+line and start with #. Any lines that begin with a space are ignored.
+Each valid config line consists of a key value pair with the key and value
+separated by an = character and optionally a single space either side. Additional
+spaces will become part of either the key and/or value.
+
+.SH OPTIONS
+.TP
+.B symlinks
+Read symlinks (value: "true" or "false")
+.
+.TP
+.B wholename
+Match whole path (value "true" or "false")
+.
+.TP
+.B limit
+Limit amount of results (value: a positive integer)
+.
+.TP
+.B wildcard
+Define a custom wildcard character, used in place of * (value: a single ascii character)
+.
+.TP
+.B unmatched error
+Exit with non-zero exit code if there were no matches
+
+.SH COPYRIGHT
+Copyright \(co 2019, 2020 Rory Bradford <roryrjb@gmail.com>.