[pacman-dev,v2] Merge expac into src/pacman
diff mbox

Message ID 20180710175821.31989-1-dreisner@archlinux.org
State New
Headers show

Commit Message

Dave Reisner July 10, 2018, 5:58 p.m. UTC
This introduces code which has long been maintained at:

https://github.com/falconindy/expac

From the README:

expac is a data extraction tool for alpm databases. It features
printf-like flexibility and aims to be used as a simple tool for other
pacman based utilities which don't link against the library. It uses
pacman.conf as a config file for locating and loading your local and
sync databases.
---
v2:

* use alpm_pkgfrom_t instead of homegrown enum
* silently skip output when strftime fails
* split input from stdin on newlines only

 doc/Makefile.am        |   7 +-
 doc/expac.1.asciidoc   | 179 +++++++++
 src/pacman/.gitignore  |   2 +
 src/pacman/Makefile.am |   8 +-
 src/pacman/expac.c     | 858 +++++++++++++++++++++++++++++++++++++++++
 5 files changed, 1051 insertions(+), 3 deletions(-)
 create mode 100644 doc/expac.1.asciidoc
 create mode 100644 src/pacman/expac.c

Comments

Allan McRae July 19, 2018, 2:20 a.m. UTC | #1
On 11/07/18 03:58, Dave Reisner wrote:
> This introduces code which has long been maintained at:
> 
> https://github.com/falconindy/expac
> 
>>From the README:
> 
> expac is a data extraction tool for alpm databases. It features
> printf-like flexibility and aims to be used as a simple tool for other
> pacman based utilities which don't link against the library. It uses
> pacman.conf as a config file for locating and loading your local and
> sync databases.

Where are the plans to use this in the pacman codebase?  I assume use in
makepkg?  I need such a justification given we just booted out every
other utility except pacman-conf, which is used in several of our tools
(bash/zsh completion, pacman-key, pacman-db-upgrade) and removed many bugs.

The only place I know where it will be "useful" is replacing the awk
statement for generating .BUILDINFO in makepkg.  But that is not fixing
a bug.

A

Patch
diff mbox

diff --git a/doc/Makefile.am b/doc/Makefile.am
index 2ac38cba..38f7077b 100644
--- a/doc/Makefile.am
+++ b/doc/Makefile.am
@@ -16,7 +16,8 @@  MANPAGES = \
 	makepkg.conf.5 \
 	pacman.conf.5 \
 	libalpm.3 \
-	BUILDINFO.5
+	BUILDINFO.5 \
+	expac.1
 
 DOXYGEN_MANS = $(wildcard man3/*.3)
 
@@ -32,7 +33,8 @@  HTML_MANPAGES = \
 	PKGBUILD.5.html \
 	makepkg.conf.5.html \
 	pacman.conf.5.html \
-	libalpm.3.html
+	libalpm.3.html \
+	expac.1.html
 
 HTML_OTHER = \
 	index.html \
@@ -61,6 +63,7 @@  EXTRA_DIST = \
 	pacman.conf.5.asciidoc \
 	BUILDINFO.5.asciidoc \
 	libalpm.3.asciidoc \
+	expac.1.asciidoc \
 	footer.asciidoc \
 	index.asciidoc \
 	submitting-patches.asciidoc \
diff --git a/doc/expac.1.asciidoc b/doc/expac.1.asciidoc
new file mode 100644
index 00000000..f5ec3adf
--- /dev/null
+++ b/doc/expac.1.asciidoc
@@ -0,0 +1,179 @@ 
+expac(1)
+=========
+
+Name
+----
+expac - alpm data extraction utility
+
+Synopsis
+--------
+'expac' <format> <targets...> [options]
+
+Description
+-----------
+expac is a data extraction tool for alpm databases. It features printf-like
+flexibility and aims to be used as a simple tool for other pacman based
+utilities which don't link against the library. It uses pacman.conf as a config
+file for locating and loading your local and sync databases.
+
+Invoking expac consists of supplying a format string, which is generally
+described by one to many of the formatting tokens (see the 'FORMATTING'
+section), any relevant options and zero to many targets. The format string
+'must' be the first non-option argument. Targets can be a simple package name,
+a query string (in the case of a search), or in repo/package syntax when the
+-sync option is supplied.
+
+
+Options
+-------
+*-Q, \--query*::
+	Search the local database for provided targets. This is the default behavior.
+
+*-S, \--sync*::
+	Search the sync databases for provided targets.
+
+*-s, --search*::
+	Search for packages matching the strings specified by targets. This is a
+	boolean AND query and regex is allowed.
+
+*-g, --group*::
+	Return packages matching the specified targets as package groups.
+
+*\--config* <file>::
+	Read from <file> for alpm initialization instead of </etc/pacman.conf>.
+
+*-H, \--humansize* <size>::
+	Format package sizes in SI units according to <size>. Valid options are:
++
+B, K, M, G, T, P, E, Z, Y
+
+*-1, \--readone*::
+	Stop searching after the first result. This only has an effect on -S operations
+	without -s.
+
+*-d, \--delim <string>*::
+	Separate each package with the specified <string>. The default value is a
+	newline character.
+
+*-l, \--listdelim* <string>::
+	Separate each list item with the specified <string>. Lists are any interpreted
+	sequence specified with a capital letter. The default value is two spaces.
+
+*-p, \--file*::
+	Interpret targets as paths to local files.
+
+*-t, \--timefmt* <format>::
+	Output time described by the specified <format>. This string is passed directly
+	to linkman:strftime[3]. The default format is %c.
+
+*-v, \--verbose*::
+	Output more. `Package not found' errors will be shown, and empty field values
+	will display as 'None'.
+
+*-h, \--help*::
+
+Display the help message.
+
+*-V, \--version*::
+
+Display version information.
+
+Formatting
+----------
+
+The format argument allows the following interpreted sequences:
+
+  %B    backup files
+
+  %C    conflicts with (no version strings)
+
+  %D    depends on
+
+  %E    depends on (no version strings)
+
+  %F    files (only with -Q)
+
+  %G    groups
+
+  %H    conflicts with
+
+  %L    licenses
+
+  %N    required by
+
+  %O    optional deps
+
+  %o    optional deps (no descriptions)
+
+  %P    provides
+
+  %R    replaces (no version strings)
+
+  %T    replaces
+
+  %S    provides (no version strings)
+
+  %a    architecture
+
+  %b    build date
+
+  %d    description
+
+  %e    package base
+
+  %f    filename (only with -S)
+
+  %g    base64 encoded PGP signature (only with -S)
+
+  %h    sha256sum
+
+  %V    package validation method
+
+  %i    has install scriptlet (only with -Q)
+
+  %k    download size (only with -S)
+
+  %l    install date (only with -Q)
+
+  %m    install size
+
+  %M    modified backup files (only with -Q)
+
+  %n    package name
+
+  %p    packager name
+
+  %r    repo
+
+  %s    md5sum
+
+  %u    project URL
+
+  %v    version
+
+  %w    install reason (only with -Q)
+
+  %!    result number (auto-incremented counter, starts at 0)
+
+  %%    literal %
+
+Note that for any lowercase tokens aside from %m and %k, full printf support is
+allowed, e.g. %-20n. This does not apply to any list based, date, or numerical
+output.
+
+Standard backslash escape sequences are supported, as per linkman:printf[1].
+
+Examples
+--------
+
+expac -Ss \'%r/%n %v\n    %d\' <search terms>::
+	Emulate pacman\'s search function.
+
+expac --timefmt=%s \'%b\t%n\' | sort -n | head -10::
+	List the oldest 10 installed packages (by build date).
+
+See Also
+--------
+linkman:libalpm[3], linkman:pacman.conf[5]
+
+include::footer.asciidoc[]
diff --git a/src/pacman/.gitignore b/src/pacman/.gitignore
index 9889c35e..78b78159 100644
--- a/src/pacman/.gitignore
+++ b/src/pacman/.gitignore
@@ -5,3 +5,5 @@  pacman
 pacman.exe
 pacman-conf
 pacman-conf.exe
+expac
+expac.exe
diff --git a/src/pacman/Makefile.am b/src/pacman/Makefile.am
index 15cf20ce..deb386ab 100644
--- a/src/pacman/Makefile.am
+++ b/src/pacman/Makefile.am
@@ -21,7 +21,7 @@  libbasic_la_SOURCES = \
 libbasic_la_LIBADD = \
 	$(top_builddir)/lib/libalpm/.libs/libalpm.la
 
-bin_PROGRAMS = pacman pacman-conf
+bin_PROGRAMS = pacman pacman-conf expac
 
 AM_CPPFLAGS = \
 	-imacros $(top_builddir)/config.h \
@@ -66,3 +66,9 @@  pacman_conf_SOURCES = pacman-conf.c
 pacman_conf_LDADD = \
 	libbasic.la \
 	$(top_builddir)/lib/libalpm/.libs/libalpm.la
+
+expac_SOURCES = expac.c
+
+expac_LDADD = \
+	libbasic.la \
+	$(top_builddir)/lib/libalpm/.libs/libalpm.la
diff --git a/src/pacman/expac.c b/src/pacman/expac.c
new file mode 100644
index 00000000..7d993cf6
--- /dev/null
+++ b/src/pacman/expac.c
@@ -0,0 +1,858 @@ 
+/*
+ *  expac.c
+ *
+ *  Copyright (c) 2018 Pacman Development Team <pacman-dev@archlinux.org>
+ *  Copyright (c) 2010-2018 by Dave Reisner <d@falconindy.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <getopt.h>
+#include <glob.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include <alpm.h>
+
+#include "conf.h"
+#include "util.h"
+
+#define DEFAULT_DELIM        "\n"
+#define DEFAULT_LISTDELIM    "  "
+#define DEFAULT_TIMEFMT      "%c"
+#define SIZE_TOKENS          "BKMGTPEZY\0"
+
+#if defined(GIT_VERSION)
+#undef PACKAGE_VERSION
+#define PACKAGE_VERSION GIT_VERSION
+#endif
+
+static char const digits[] = "0123456789";
+static char const printf_flags[] = "'-+ #0I";
+
+typedef enum search_what_t {
+	SEARCH_EXACT,
+	SEARCH_GROUPS,
+	SEARCH_REGEX,
+} search_what_t;
+
+typedef struct expac_t {
+	config_t *config;
+} expac_t;
+
+bool opt_readone = false;
+bool opt_verbose = false;
+char opt_humansize = 'B';
+alpm_pkgfrom_t opt_corpus = ALPM_PKG_FROM_LOCALDB;
+search_what_t opt_what = SEARCH_EXACT;
+const char *opt_format = NULL;
+const char *opt_timefmt = DEFAULT_TIMEFMT;
+const char *opt_listdelim = DEFAULT_LISTDELIM;
+const char *opt_delim = DEFAULT_DELIM;
+const char *opt_config_file = CONFFILE;
+int opt_pkgcounter = 0;
+
+typedef const char *(*extractfn)(void*);
+
+static int is_valid_size_unit(char *u)
+{
+	return u[0] != '\0' && u[1] == '\0' &&
+		memchr(SIZE_TOKENS, *u, strlen(SIZE_TOKENS)) != NULL;
+}
+
+static const char *alpm_backup_get_name(alpm_backup_t *bkup)
+{
+	return bkup->name;
+}
+
+static char *size_to_string(off_t pkgsize)
+{
+	static char out[64];
+
+	if(opt_humansize == 'B') {
+		snprintf(out, sizeof(out), "%jd", (intmax_t)pkgsize);
+	} else {
+		snprintf(out, sizeof(out), "%.2f %ciB", humanize_size(pkgsize, opt_humansize, 0, NULL), opt_humansize);
+	}
+
+	return out;
+}
+
+static char *format_optdep(alpm_depend_t *optdep)
+{
+	char *out = NULL;
+
+	if(asprintf(&out, "%s: %s", optdep->name, optdep->desc) < 0) {
+		return NULL;
+	}
+
+	return out;
+}
+
+static const char *alpm_dep_get_name(alpm_depend_t *dep)
+{
+	return dep->name;
+}
+
+static void usage(void)
+{
+	fprintf(stderr, "expac %s\n"
+			"Usage: expac [options] <format> target...\n\n", PACKAGE_VERSION);
+	fprintf(stderr,
+			" Options:\n"
+			"  -Q, --query               search local DB (default)\n"
+			"  -S, --sync                search sync DBs\n"
+			"  -s, --search              search for matching regex\n"
+			"  -g, --group               return packages matching targets as groups\n"
+			"  -H, --humansize <size>    format package sizes in SI units (default: bytes)\n"
+			"  -1, --readone             return only the first result of a sync search\n\n"
+			"  -d, --delim <string>      separator used between packages (default: \"\\n\")\n"
+			"  -l, --listdelim <string>  separator used between list elements (default: \"  \")\n"
+			"  -p, --file                query local files instead of the DB\n"
+			"  -t, --timefmt <fmt>       date format passed to strftime (default: \"%%c\")\n"
+			"      --config <file>       read from <file> for alpm initialization (default: /etc/pacman.conf)\n\n"
+			"  -v, --verbose             be more verbose\n\n"
+			"  -V, --version             display version information\n"
+			"  -h, --help                display this help information\n\n"
+			"For more details see expac(1).\n");
+}
+
+static int parse_options(int *argc, char **argv[])
+{
+	static struct option opts[] = {
+		{"readone",   no_argument,        0, '1'},
+		{"delim",     required_argument,  0, 'd'},
+		{"listdelim", required_argument,  0, 'l'},
+		{"group",     required_argument,  0, 'g'},
+		{"help",      no_argument,        0, 'h'},
+		{"file",      no_argument,        0, 'p'},
+		{"humansize", required_argument,  0, 'H'},
+		{"query",     no_argument,        0, 'Q'},
+		{"sync",      no_argument,        0, 'S'},
+		{"search",    no_argument,        0, 's'},
+		{"timefmt",   required_argument,  0, 't'},
+		{"verbose",   no_argument,        0, 'v'},
+		{"version",   no_argument,        0, 'V'},
+		{"config",    required_argument,  0, 128},
+		{0, 0, 0, 0}
+	};
+
+	for(;;) {
+		int opt;
+
+		opt = getopt_long(*argc, *argv, "1l:d:gH:hf:pQSst:Vv", opts, NULL);
+		if(opt < 0) {
+			break;
+		}
+
+		switch (opt) {
+			case 'S':
+				opt_corpus = ALPM_PKG_FROM_SYNCDB;
+				break;
+			case 'Q':
+				opt_corpus = ALPM_PKG_FROM_LOCALDB;
+				break;
+			case '1':
+				opt_readone = true;
+				break;
+			case 'd':
+				opt_delim = optarg;
+				break;
+			case 'g':
+				opt_what = SEARCH_GROUPS;
+				break;
+			case 'l':
+				opt_listdelim = optarg;
+				break;
+			case 'H':
+				if(!is_valid_size_unit(optarg)) {
+					fprintf(stderr, "error: invalid SI size formatter: %s\n", optarg);
+					return -1;
+				}
+				opt_humansize = *optarg;
+				break;
+			case 'h':
+				usage();
+				exit(0);
+			case 'p':
+				opt_corpus = ALPM_PKG_FROM_FILE;
+				break;
+			case 's':
+				opt_what = SEARCH_REGEX;
+				break;
+			case 't':
+				opt_timefmt = optarg;
+				break;
+			case 'v':
+				opt_verbose = true;
+				break;
+			case 128:
+				opt_config_file = optarg;
+				break;
+			case 'V':
+				printf("expac v%s\n", PACKAGE_VERSION);
+				break;
+
+			case '?':
+				return -EINVAL;
+			default:
+				return -EINVAL;
+		}
+	}
+
+	if(optind < *argc) {
+		opt_format = (*argv)[optind++];
+	} else {
+		fprintf(stderr, "error: missing format string (use -h for help)\n");
+		return -EINVAL;
+	}
+
+	*argc -= optind;
+	*argv += optind;
+
+	return 0;
+}
+
+static int print_escaped(const char *delim)
+{
+	const char *f;
+	int out = 0;
+
+	for(f = delim; *f != '\0'; f++) {
+		if(*f == '\\') {
+			switch (*++f) {
+				case '\\':
+					fputc('\\', stdout);
+					break;
+				case '"':
+					fputc('\"', stdout);
+					break;
+				case 'a':
+					fputc('\a', stdout);
+					break;
+				case 'b':
+					fputc('\b', stdout);
+					break;
+				case 'e': /* \e is nonstandard */
+					fputc('\033', stdout);
+					break;
+				case 'n':
+					fputc('\n', stdout);
+					break;
+				case 'r':
+					fputc('\r', stdout);
+					break;
+				case 't':
+					fputc('\t', stdout);
+					break;
+				case 'v':
+					fputc('\v', stdout);
+					break;
+				case '0':
+					fputc('\0', stdout);
+					break;
+				default:
+					fputc(*f, stdout);
+					break;
+			}
+			++out;
+		} else {
+			fputc(*f, stdout);
+			++out;
+		}
+	}
+
+	return out;
+}
+
+static int print_list(alpm_list_t *list, extractfn fn)
+{
+	alpm_list_t *i;
+	int out = 0;
+
+	if(!list) {
+		if(opt_verbose) {
+			out += printf("None");
+		}
+		return out;
+	}
+
+	i = list;
+	for(;;) {
+		const char *item = fn ? fn(i->data) : i->data;
+		if(item == NULL) {
+			continue;
+		}
+
+		out += printf("%s", item);
+
+		if((i = i->next)) {
+			out += print_escaped(opt_listdelim);
+		} else {
+			break;
+		}
+	}
+
+	return out;
+}
+
+static int print_allocated_list(alpm_list_t *list, extractfn fn)
+{
+	int out = print_list(list, fn);
+	alpm_list_free(list);
+	return out;
+}
+
+static int print_time(time_t timestamp) {
+	char buffer[64];
+	int out = 0;
+
+	if(!timestamp) {
+		if(opt_verbose) {
+			out += printf("None");
+		}
+		return out;
+	}
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wformat-nonliteral"
+	/* no overflow here, strftime prints a max of 64 including null */
+	if(strftime(buffer, sizeof(buffer), opt_timefmt, localtime(&timestamp)) > 0) {
+		out += printf("%s", buffer);
+	}
+#pragma GCC diagnostic pop
+
+	return out;
+}
+
+static int print_filelist(alpm_filelist_t *filelist)
+{
+	int out = 0;
+	size_t i;
+
+	for(i = 0; i < filelist->count; i++) {
+		out += printf("%s", (filelist->files + i)->name);
+		if(i < filelist->count - 1) {
+			out += print_escaped(opt_listdelim);
+		}
+	}
+
+	return out;
+}
+
+static bool backup_file_is_modified(const alpm_backup_t *backup_file)
+{
+	char fullpath[PATH_MAX];
+	char *md5sum = NULL;
+	bool modified;
+
+	snprintf(fullpath, sizeof(fullpath), "%s%s", config->rootdir, backup_file->name);
+
+	md5sum = alpm_compute_md5sum(fullpath);
+	if(md5sum == NULL) {
+		return false;
+	}
+
+	modified = strcmp(md5sum, backup_file->hash) != 0;
+	free(md5sum);
+
+	return modified;
+}
+
+static alpm_list_t *get_modified_files(alpm_pkg_t *pkg)
+{
+	alpm_list_t *i, *modified_files = NULL;
+
+	for(i = alpm_pkg_get_backup(pkg); i; i = i->next) {
+		const alpm_backup_t *backup = i->data;
+		if(backup->hash && backup_file_is_modified(backup)) {
+			modified_files = alpm_list_add(modified_files, backup->name);
+		}
+	}
+
+	return modified_files;
+}
+
+static alpm_list_t *get_validation_method(alpm_pkg_t *pkg)
+{
+	alpm_list_t *validation = NULL;
+
+	alpm_pkgvalidation_t v = alpm_pkg_get_validation(pkg);
+
+	if(v == ALPM_PKG_VALIDATION_UNKNOWN) {
+		return alpm_list_add(validation, (void*)"Unknown");
+	}
+
+	if(v & ALPM_PKG_VALIDATION_NONE) {
+		return alpm_list_add(validation, (void*)"None");
+	}
+
+	if(v & ALPM_PKG_VALIDATION_MD5SUM) {
+		validation = alpm_list_add(validation, (void*)"MD5 Sum");
+	}
+	if(v & ALPM_PKG_VALIDATION_SHA256SUM) {
+		validation = alpm_list_add(validation, (void*)"SHA256 Sum");
+	}
+	if(v & ALPM_PKG_VALIDATION_SIGNATURE) {
+		validation = alpm_list_add(validation, (void*)"Signature");
+	}
+
+	return validation;
+}
+
+static void print_pkg(alpm_pkg_t *pkg, const char *format)
+{
+	const char *f, *end;
+	int out = 0;
+
+	end = format + strlen(format);
+
+	for(f = format; f < end; f++) {
+		if(*f == '%') {
+			char fmt[64] = {0};
+			int l = 1;
+
+			l += strspn(f + l, printf_flags);
+			l += strspn(f + l, digits);
+			memcpy(fmt, f, l);
+			fmt[l] = 's';
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wformat-nonliteral"
+			f += l;
+			switch (*f) {
+				/* simple attributes */
+				case 'f': /* filename */
+					out += printf(fmt, alpm_pkg_get_filename(pkg));
+					break;
+				case 'e': /* package base */
+					out += printf(fmt, alpm_pkg_get_base(pkg));
+					break;
+				case 'n': /* package name */
+					out += printf(fmt, alpm_pkg_get_name(pkg));
+					break;
+				case 'v': /* version */
+					out += printf(fmt, alpm_pkg_get_version(pkg));
+					break;
+				case 'd': /* description */
+					out += printf(fmt, alpm_pkg_get_desc(pkg));
+					break;
+				case 'u': /* project url */
+					out += printf(fmt, alpm_pkg_get_url(pkg));
+					break;
+				case 'p': /* packager name */
+					out += printf(fmt, alpm_pkg_get_packager(pkg));
+					break;
+				case 's': /* md5sum */
+					out += printf(fmt, alpm_pkg_get_md5sum(pkg));
+					break;
+				case 'a': /* architecture */
+					out += printf(fmt, alpm_pkg_get_arch(pkg));
+					break;
+				case 'i': /* has install scriptlet? */
+					out += printf(fmt, alpm_pkg_has_scriptlet(pkg) ? "yes" : "no");
+					break;
+				case 'r': /* repo */
+					out += printf(fmt, alpm_db_get_name(alpm_pkg_get_db(pkg)));
+					break;
+				case 'w': /* install reason */
+					out += printf(fmt, alpm_pkg_get_reason(pkg) ? "dependency" : "explicit");
+					break;
+				case '!': /* result number */
+					fmt[strlen(fmt)-1] = 'd';
+					out += printf(fmt, opt_pkgcounter++);
+					break;
+				case 'g': /* base64 gpg sig */
+					out += printf(fmt, alpm_pkg_get_base64_sig(pkg));
+					break;
+				case 'h': /* sha256sum */
+					out += printf(fmt, alpm_pkg_get_sha256sum(pkg));
+					break;
+
+				/* times */
+				case 'b': /* build date */
+					out += print_time(alpm_pkg_get_builddate(pkg));
+					break;
+				case 'l': /* install date */
+					out += print_time(alpm_pkg_get_installdate(pkg));
+					break;
+
+				/* sizes */
+				case 'k': /* download size */
+					out += printf(fmt, size_to_string(alpm_pkg_get_size(pkg)));
+					break;
+				case 'm': /* install size */
+					out += printf(fmt, size_to_string(alpm_pkg_get_isize(pkg)));
+					break;
+
+				/* lists */
+				case 'F': /* files */
+					out += print_filelist(alpm_pkg_get_files(pkg));
+					break;
+				case 'N': /* requiredby */
+					out += print_list(alpm_pkg_compute_requiredby(pkg), NULL);
+					break;
+				case 'L': /* licenses */
+					out += print_list(alpm_pkg_get_licenses(pkg), NULL);
+					break;
+				case 'G': /* groups */
+					out += print_list(alpm_pkg_get_groups(pkg), NULL);
+					break;
+				case 'E': /* depends (shortdeps) */
+					out += print_list(alpm_pkg_get_depends(pkg), (extractfn)alpm_dep_get_name);
+					break;
+				case 'D': /* depends */
+					out += print_list(alpm_pkg_get_depends(pkg), (extractfn)alpm_dep_compute_string);
+					break;
+				case 'O': /* optdepends */
+					out += print_list(alpm_pkg_get_optdepends(pkg), (extractfn)format_optdep);
+					break;
+				case 'o': /* optdepends (shortdeps) */
+					out += print_list(alpm_pkg_get_optdepends(pkg), (extractfn)alpm_dep_get_name);
+					break;
+				case 'H': /* conflicts */
+					out += print_list(alpm_pkg_get_conflicts(pkg), (extractfn)alpm_dep_compute_string);
+					break;
+				case 'C': /* conflicts (shortdeps) */
+					out += print_list(alpm_pkg_get_conflicts(pkg), (extractfn)alpm_dep_get_name);
+					break;
+				case 'S': /* provides (shortdeps) */
+					out += print_list(alpm_pkg_get_provides(pkg), (extractfn)alpm_dep_get_name);
+					break;
+				case 'P': /* provides */
+					out += print_list(alpm_pkg_get_provides(pkg), (extractfn)alpm_dep_compute_string);
+					break;
+				case 'R': /* replaces (shortdeps) */
+					out += print_list(alpm_pkg_get_replaces(pkg), (extractfn)alpm_dep_get_name);
+					break;
+				case 'T': /* replaces */
+					out += print_list(alpm_pkg_get_replaces(pkg), (extractfn)alpm_dep_compute_string);
+					break;
+				case 'B': /* backup */
+					out += print_list(alpm_pkg_get_backup(pkg), (extractfn)alpm_backup_get_name);
+					break;
+				case 'V': /* package validation */
+					out += print_allocated_list(get_validation_method(pkg), NULL);
+					break;
+				case 'M': /* modified */
+					out += print_allocated_list(get_modified_files(pkg), NULL);
+					break;
+				case '%':
+					fputc('%', stdout);
+					out++;
+					break;
+				default:
+					fputc('?', stdout);
+					out++;
+					break;
+			}
+		} else if(*f == '\\') {
+			char esc[3] = { f[0], f[1], '\0' };
+			out += print_escaped(esc);
+			++f;
+		} else {
+			fputc(*f, stdout);
+			out++;
+		}
+	}
+#pragma GCC diagnostic pop
+
+	/* only print a delimeter if any package data was outputted */
+	if(out > 0) {
+		print_escaped(opt_delim);
+	}
+}
+
+static alpm_list_t *all_packages(alpm_list_t *dbs)
+{
+	alpm_list_t *i, *packages = NULL;
+
+	for(i = dbs; i; i = i->next) {
+		packages = alpm_list_join(packages, alpm_list_copy(alpm_db_get_pkgcache(i->data)));
+	}
+
+	return packages;
+}
+
+static alpm_list_t *search_packages(alpm_list_t *dbs, alpm_list_t *targets)
+{
+	alpm_list_t *i, *packages = NULL;
+
+	for(i = dbs; i; i = i->next) {
+		packages = alpm_list_join(packages, alpm_db_search(i->data, targets));
+	}
+
+	return packages;
+}
+
+static alpm_list_t *search_groups(alpm_list_t *dbs, alpm_list_t *groupnames)
+{
+	alpm_list_t *i, *j, *packages = NULL;
+
+	for(i = groupnames; i; i = i->next) {
+		for(j = dbs; j; j = j->next) {
+			alpm_group_t *grp = alpm_db_get_group(j->data, i->data);
+			if(grp != NULL) {
+				packages = alpm_list_join(packages, alpm_list_copy(grp->packages));
+			}
+		}
+	}
+
+	return packages;
+}
+
+static alpm_list_t *search_exact(alpm_list_t *dblist, alpm_list_t *targets)
+{
+	alpm_list_t *results = NULL;
+
+	/* resolve each target individually from the repo pool */
+	for(alpm_list_t *t = targets; t; t = t->next) {
+		char *pkgname, *reponame;
+		alpm_list_t *r;
+		int found = 0;
+
+		pkgname = reponame = t->data;
+		if(strchr(pkgname, '/')) {
+			strsep(&pkgname, "/");
+		} else {
+			reponame = NULL;
+		}
+
+		for(r = dblist; r; r = r->next) {
+			alpm_db_t *repo = r->data;
+			alpm_pkg_t *pkg;
+
+			if(reponame && strcmp(reponame, alpm_db_get_name(repo)) != 0) {
+				continue;
+			}
+
+			pkg = alpm_db_get_pkg(repo, pkgname);
+			if(pkg == NULL) {
+				continue;
+			}
+
+			found = 1;
+			results = alpm_list_add(results, pkg);
+			if(opt_readone) {
+				break;
+			}
+		}
+
+		if(!found && opt_verbose) {
+			fprintf(stderr, "error: package `%s' not found\n", pkgname);
+		}
+	}
+
+	return results;
+}
+
+static alpm_list_t *resolve_targets(alpm_list_t *dblist, alpm_list_t *targets)
+{
+	if(targets == NULL) {
+		return all_packages(dblist);
+	}
+
+	if(opt_what == SEARCH_REGEX) {
+		return search_packages(dblist, targets);
+	}
+
+	if(opt_what == SEARCH_GROUPS) {
+		return search_groups(dblist, targets);
+	}
+
+	return search_exact(dblist, targets);
+}
+
+static void expac_free(expac_t *expac)
+{
+	free(expac);
+	config_free(config);
+}
+
+static int expac_new(expac_t **expac, const char *config_file)
+{
+	expac_t *e;
+
+	config = config_new();
+
+	e = calloc(1, sizeof(*e));
+	if(e == NULL) {
+		return -ENOMEM;
+	}
+
+	if(parseconfig(config_file) != 0) {
+		fprintf(stderr, "error parsing '%s'\n", config_file);
+		return -EINVAL;
+	}
+
+	e->config = config;
+	*expac = e;
+
+	return 0;
+}
+
+static alpm_list_t *expac_search_files(expac_t *expac, alpm_list_t *targets)
+{
+	alpm_list_t *i, *r = NULL;
+
+	for(i = targets; i; i = i->next) {
+		const char *path = i->data;
+		alpm_pkg_t *pkg;
+
+		if(alpm_pkg_load(expac->config->handle, path, 0, 0, &pkg) != 0) {
+			fprintf(stderr, "error: %s: %s\n", path,
+					alpm_strerror(alpm_errno(expac->config->handle)));
+			continue;
+		}
+
+		r = alpm_list_add(r, pkg);
+	}
+
+	return r;
+}
+
+static alpm_list_t *expac_search_local(expac_t *expac, alpm_list_t *targets)
+{
+	alpm_list_t *dblist, *r;
+
+	dblist = alpm_list_add(NULL, alpm_get_localdb(expac->config->handle));
+	r = resolve_targets(dblist, targets);
+	alpm_list_free(dblist);
+
+	return r;
+}
+
+static alpm_list_t *expac_search_sync(expac_t *expac, alpm_list_t *targets)
+{
+	return resolve_targets(alpm_get_syncdbs(expac->config->handle), targets);
+}
+
+static alpm_list_t *expac_search(
+		expac_t *expac, alpm_pkgfrom_t corpus, alpm_list_t *targets)
+{
+	switch (corpus) {
+	case ALPM_PKG_FROM_LOCALDB:
+		return expac_search_local(expac, targets);
+	case ALPM_PKG_FROM_SYNCDB:
+		return expac_search_sync(expac, targets);
+	case ALPM_PKG_FROM_FILE:
+		return expac_search_files(expac, targets);
+	}
+
+	/* should be unreachable */
+	return NULL;
+}
+
+static int read_targets_from_stdin(alpm_list_t **targets)
+{
+	char *line;
+	size_t line_size = 0;
+	ssize_t nread;
+	int added = 0;
+
+	while((nread = getline(&line, &line_size, stdin)) != -1) {
+		if(line[nread - 1] == '\n') {
+			/* remove trailing newline */
+			line[nread - 1] = '\0';
+		}
+		if(line[0] == '\0') {
+			/* skip empty lines */
+			continue;
+		}
+		if(!alpm_list_append_strdup(targets, line)) {
+			return -ENOMEM;
+		}
+
+		added++;
+	}
+
+	return added;
+}
+
+static int process_targets(int argc, char **argv, alpm_list_t **targets)
+{
+	int allow_stdin;
+
+	allow_stdin = !isatty(STDIN_FILENO);
+
+	for(int i = 0; i < argc; ++i) {
+		if(allow_stdin && strcmp(argv[i], "-") == 0) {
+			int k;
+
+			k = read_targets_from_stdin(targets);
+			if(k < 0) {
+				fprintf(stderr, "error: failed to read targets from stdin: %s\n", strerror(-k));
+				return k;
+			}
+
+			if(k == 0) {
+				fputs("error: argument '-' specified with empty stdin\n", stderr);
+				return -1;
+			}
+
+			allow_stdin = 0;
+		} else {
+			alpm_list_append_strdup(targets, argv[i]);
+		}
+	}
+
+	return 0;
+}
+
+int main(int argc, char *argv[])
+{
+	alpm_list_t *results = NULL, *targets = NULL;
+	expac_t *expac = NULL;
+	int r;
+
+	r = parse_options(&argc, &argv);
+	if(r < 0) {
+		return 1;
+	}
+
+	r = process_targets(argc, argv, &targets);
+	if(r < 0) {
+		return 1;
+	}
+
+	r = expac_new(&expac, opt_config_file);
+	if(r < 0) {
+		return 1;
+	}
+
+	results = expac_search(expac, opt_corpus, targets);
+	if(results == NULL) {
+		return 1;
+	}
+
+	for(alpm_list_t *i = results; i; i = i->next) {
+		print_pkg(i->data, opt_format);
+	}
+
+	alpm_list_free_inner(targets, free);
+	alpm_list_free(targets);
+	alpm_list_free(results);
+	expac_free(expac);
+
+	return 0;
+}
+
+/* vim: set ts=2 sw=2 noet: */